diff --git a/.gitignore b/.gitignore
index fec30fd52..718277092 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,3 +58,6 @@ blueprints/gke/autopilot/bundle/monitoring/kustomization.yaml
blueprints/gke/autopilot/bundle/locust/kustomization.yaml
blueprints/gke/autopilot/bundle.tar.gz
blueprints/gke/patterns/batch/job-*.yaml
+modules/apigee/recipe-apigee-swp/bundle.zip
+modules/apigee/recipe-apigee-swp/deploy-apiproxy.sh
+
diff --git a/modules/apigee/README.md b/modules/apigee/README.md
index d209a6a81..45229c5bc 100644
--- a/modules/apigee/README.md
+++ b/modules/apigee/README.md
@@ -18,6 +18,7 @@ This module simplifies the creation of a Apigee resources (organization, environ
- [New endpoint attachment](#new-endpoint-attachment)
- [Apigee add-ons](#apigee-add-ons)
- [IAM](#iam)
+- [Recipes](#recipes)
- [Variables](#variables)
- [Outputs](#outputs)
@@ -355,6 +356,10 @@ module "apigee" {
# tftest modules=1 resources=10
```
+## Recipes
+
+- [Apigee X with Secure Web Proxy](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/blob/master/modules/apigee/recipe-apigee-swp)
+
## Variables
| name | description | type | required | default |
diff --git a/modules/apigee/recipe-apigee-swp/README.md b/modules/apigee/recipe-apigee-swp/README.md
new file mode 100644
index 000000000..121d54532
--- /dev/null
+++ b/modules/apigee/recipe-apigee-swp/README.md
@@ -0,0 +1,57 @@
+# Apigee X with Secure Web Proxy
+
+This recipe demonstrates how to configure Apigee X with Secure Web Proxy (SWP). This is a common solution when you need your Apigee X runtime to connect to numerous on-premises backends, but prefer to avoid establishing VPC peering between the Apigee X Google-managed VPC and the VPC where hybrid connectivity and advertising Apigee X runtime IP ranges to the on-premises network.
+
+The diagram below depicts the architecture deployed:
+
+
+
+In this recipe the SWP gateway has been co-located with Apigee X in the same project for ease of deployment. It's important to note that the SWP gateway's deployment is flexible and can be independently placed in a different project. Our current setup uses a privately accessible VM as the backend target for SWP. In a real-world scenario, with hybrid connectivity configured in the SWP gateway's VPC, the backend could alternatively be an on-premises host.
+
+Once the terraform configuration is applied you can verify that all is working by running the following:
+
+* Deploy a sample proxy to Apigee X
+
+ ./deploy-apiproxy.sh
+
+* Make a request to the proxy
+
+ curl -v <API_URL>/test
+
+ Note: The API_URL is returned as a terraform output
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [analytics_region](variables.tf#L27) | Region. | string | ✓ | |
+| [instance_region](variables.tf#L32) | Region. | string | ✓ | |
+| [network_config](variables.tf#L37) | Network configuration. | object({…}) | ✓ | |
+| [project_id](variables.tf#L46) | Project ID. | string | ✓ | |
+| [_testing](variables.tf#L17) | Populate this variable to avoid triggering the data source. | object({…}) | | null |
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [api_url](outputs.tf#L17) | API url. | |
+
+## Test
+
+```hcl
+module "recipe_apigee_swp" {
+ source = "./fabric/modules/apigee/recipe-apigee-swp"
+ project_id = "project-1"
+ _testing = {
+ name = "project-1"
+ number = 1234567890
+ }
+ instance_region = "europe-west1"
+ analytics_region = "europe-west1"
+ network_config = {
+ subnet_ip_cidr_range = "10.16.0.0/24"
+ subnet_psc_ip_cidr_range = "10.16.1.0/24"
+ subnet_proxy_only_ip_cidr_range = "10.16.2.0/24"
+ }
+}
+# tftest modules=10 resources=43
diff --git a/modules/apigee/recipe-apigee-swp/bundle/apiproxy/proxies/default.xml b/modules/apigee/recipe-apigee-swp/bundle/apiproxy/proxies/default.xml
new file mode 100644
index 000000000..30859c52a
--- /dev/null
+++ b/modules/apigee/recipe-apigee-swp/bundle/apiproxy/proxies/default.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ /test
+
+
+ default
+
+
\ No newline at end of file
diff --git a/modules/apigee/recipe-apigee-swp/bundle/apiproxy/targets/default.xml b/modules/apigee/recipe-apigee-swp/bundle/apiproxy/targets/default.xml
new file mode 100644
index 000000000..dca7a16cc
--- /dev/null
+++ b/modules/apigee/recipe-apigee-swp/bundle/apiproxy/targets/default.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ http://10.16.0.4
+
+
\ No newline at end of file
diff --git a/modules/apigee/recipe-apigee-swp/bundle/apiproxy/test.xml b/modules/apigee/recipe-apigee-swp/bundle/apiproxy/test.xml
new file mode 100644
index 000000000..47ab47f1b
--- /dev/null
+++ b/modules/apigee/recipe-apigee-swp/bundle/apiproxy/test.xml
@@ -0,0 +1,19 @@
+
+
+
+ test
+
\ No newline at end of file
diff --git a/modules/apigee/recipe-apigee-swp/diagram.png b/modules/apigee/recipe-apigee-swp/diagram.png
new file mode 100644
index 000000000..e50005cc5
Binary files /dev/null and b/modules/apigee/recipe-apigee-swp/diagram.png differ
diff --git a/modules/apigee/recipe-apigee-swp/main.tf b/modules/apigee/recipe-apigee-swp/main.tf
new file mode 100644
index 000000000..c1f8d0bc4
--- /dev/null
+++ b/modules/apigee/recipe-apigee-swp/main.tf
@@ -0,0 +1,257 @@
+/**
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+# Added this because there is a bug on the provider
+
+provider "google" {
+ region = var.instance_region
+}
+
+locals {
+ hostname = "${module.addresses.global_addresses.apigee.address}.nip.io"
+ environment = "dev"
+ envgroup = "apis"
+ environments = {
+ (local.environment) = {
+ envgroups = [local.envgroup]
+ forward_proxy_uri = "http://${module.apigee.endpoint_attachment_hosts["swp"]}:8080"
+ }
+ }
+ instances = {
+ (var.instance_region) = {
+ environments = keys(local.environments)
+ } }
+}
+
+
+module "project" {
+ source = "../../../modules/project"
+ name = var.project_id
+ project_reuse = {
+ use_data_source = var._testing == null
+ project_attributes = var._testing
+ }
+ services = [
+ "apigee.googleapis.com",
+ "compute.googleapis.com",
+ "networksecurity.googleapis.com",
+ "networkservices.googleapis.com",
+ ]
+}
+
+module "vpc" {
+ source = "../../../modules/net-vpc"
+ project_id = module.project.id
+ name = "vpc"
+ subnets = [
+ {
+ ip_cidr_range = var.network_config.subnet_ip_cidr_range
+ name = "subnet-${var.instance_region}"
+ region = var.instance_region
+ }
+ ]
+ subnets_psc = [
+ {
+ ip_cidr_range = var.network_config.subnet_psc_ip_cidr_range
+ name = "subnet-psc-${var.instance_region}"
+ region = var.instance_region
+ }
+ ]
+ subnets_proxy_only = [
+ {
+ ip_cidr_range = var.network_config.subnet_proxy_only_ip_cidr_range
+ name = "subnet-proxy-only-${var.instance_region}"
+ region = var.instance_region
+ active = true
+ }
+ ]
+}
+
+module "firewall" {
+ source = "../../../modules/net-vpc-firewall"
+ project_id = module.project.id
+ network = module.vpc.name
+ default_rules_config = {
+ disabled = true
+ }
+ ingress_rules = {
+ allow-ingress-http = {
+ description = "Allow ingress to http servers."
+ targets = ["http-server"]
+ rules = [{ protocol = "tcp", ports = [80] }]
+ source_ranges = [var.network_config.subnet_proxy_only_ip_cidr_range]
+ }
+ }
+}
+
+module "apigee" {
+ source = "../../../modules/apigee"
+ project_id = module.project.project_id
+ organization = {
+ analytics_region = var.analytics_region
+ billing_type = "EVALUATION"
+ runtime_type = "CLOUD"
+ retention = "MINIMUM"
+ disable_vpc_peering = true
+ }
+ envgroups = {
+ "apis" = [local.hostname]
+ }
+ environments = local.environments
+ instances = local.instances
+ endpoint_attachments = {
+ swp = {
+ region = var.instance_region
+ service_attachment = module.swp.service_attachment
+ }
+ }
+}
+
+module "ext_lb" {
+ source = "../../../modules/net-lb-app-ext"
+ name = "glb"
+ project_id = module.project.id
+ forwarding_rules_config = {
+ "" = {
+ address = (
+ module.addresses.global_addresses.apigee.address
+ )
+ }
+ }
+ protocol = "HTTPS"
+ use_classic_version = false
+ backend_service_configs = {
+ default = {
+ backends = [for k, v in module.apigee.instances : { backend = "neg-${k}" }]
+ protocol = "HTTPS"
+ health_checks = []
+ }
+ }
+ neg_configs = {
+ for k, v in module.apigee.instances :
+ "neg-${k}" => { psc = {
+ region = k
+ target_service = v.service_attachment
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnets_psc["${var.instance_region}/subnet-psc-${var.instance_region}"].self_link
+ }
+ }
+ }
+ ssl_certificates = {
+ managed_configs = {
+ default = {
+ domains = [local.hostname]
+ }
+ }
+ }
+}
+
+module "swp" {
+ source = "../../../modules/net-swp"
+ project_id = module.project.id
+ region = var.instance_region
+ name = "gateway"
+ network = module.vpc.id
+ subnetwork = module.vpc.subnet_self_links["${var.instance_region}/subnet-${var.instance_region}"]
+ gateway_config = {
+ addresses = [module.addresses.internal_addresses["gateway"].address]
+ ports = [8080]
+ }
+ service_attachment = {
+ nat_subnets = [module.vpc.subnets_psc["${var.instance_region}/subnet-psc-${var.instance_region}"].self_link]
+ automatic_connection = true
+ }
+ policy_rules = {
+ allowed-hosts = {
+ priority = 1000
+ allow = true
+ session_matcher = "host() == '${module.nginx_vm.internal_ip}'"
+ }
+ }
+}
+
+module "addresses" {
+ source = "../../../modules/net-address"
+ project_id = module.project.project_id
+ internal_addresses = {
+ gateway = {
+ region = var.instance_region
+ subnetwork = module.vpc.subnet_self_links["${var.instance_region}/subnet-${var.instance_region}"]
+ }
+ }
+ global_addresses = {
+ apigee = {}
+ }
+}
+
+module "nginx_vm" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project.project_id
+ zone = "${var.instance_region}-b"
+ name = "nginx"
+ network_interfaces = [{
+ network = module.vpc.self_link
+ subnetwork = module.vpc.subnet_self_links["${var.instance_region}/subnet-${var.instance_region}"]
+ }]
+ metadata = {
+ startup-script = <<-EOF
+ #! /bin/bash
+ apt-get update
+ apt-get install -y nginx
+ EOF
+ }
+ service_account = {
+ auto_create = true
+ }
+ tags = [
+ "http-server"
+ ]
+}
+
+resource "local_file" "target_endpoint_file" {
+ content = templatefile("${path.module}/templates/targets/default.xml.tpl", {
+ ip_address = module.nginx_vm.internal_ip
+ })
+ filename = "${path.module}/bundle/apiproxy/targets/default.xml"
+ file_permission = "0644"
+}
+
+# tflint-ignore: terraform_unused_declarations
+data "archive_file" "bundle" {
+ type = "zip"
+ source_dir = "${path.module}/bundle"
+ output_path = "${path.module}/bundle.zip"
+ depends_on = [
+ local_file.target_endpoint_file
+ ]
+}
+
+resource "local_file" "deploy_apiproxy_file" {
+ content = templatefile("${path.module}/templates/deploy-apiproxy.sh.tpl", {
+ organization = module.apigee.org_name
+ environment = local.environment
+ })
+ filename = "${path.module}/deploy-apiproxy.sh"
+ file_permission = "0755"
+}
+
+module "nat" {
+ source = "../../../modules/net-cloudnat"
+ project_id = module.project.project_id
+ region = var.instance_region
+ name = "nat-${var.instance_region}"
+ router_network = module.vpc.self_link
+}
diff --git a/modules/apigee/recipe-apigee-swp/outputs.tf b/modules/apigee/recipe-apigee-swp/outputs.tf
new file mode 100644
index 000000000..951c1165c
--- /dev/null
+++ b/modules/apigee/recipe-apigee-swp/outputs.tf
@@ -0,0 +1,20 @@
+/**
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+output "api_url" {
+ description = "API url."
+ value = "https://${local.hostname}/test"
+}
diff --git a/modules/apigee/recipe-apigee-swp/templates/deploy-apiproxy.sh.tpl b/modules/apigee/recipe-apigee-swp/templates/deploy-apiproxy.sh.tpl
new file mode 100644
index 000000000..576bf69de
--- /dev/null
+++ b/modules/apigee/recipe-apigee-swp/templates/deploy-apiproxy.sh.tpl
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+ORGANIZATION=${organization}
+ENVIRONMENT=${environment}
+
+export TOKEN=$(gcloud auth print-access-token)
+
+curl -v -X POST \
+-H "Authorization: Bearer $TOKEN" \
+-H "Content-Type:application/octet-stream" \
+-T 'bundle.zip' \
+"https://apigee.googleapis.com/v1/organizations/$ORGANIZATION/apis?name=test&action=import"
+
+curl -v -X POST \
+-H "Authorization: Bearer $TOKEN" \
+"https://apigee.googleapis.com/v1/organizations/$ORGANIZATION/environments/$ENVIRONMENT/apis/test/revisions/1/deployments"
+
+curl -v \
+-H "Authorization: Bearer $TOKEN" \
+"https://apigee.googleapis.com/v1/organizations/$ORGANIZATION/environments/$ENVIRONMENT/apis/test/revisions/1/deployments"
\ No newline at end of file
diff --git a/modules/apigee/recipe-apigee-swp/templates/targets/default.xml.tpl b/modules/apigee/recipe-apigee-swp/templates/targets/default.xml.tpl
new file mode 100644
index 000000000..44744009e
--- /dev/null
+++ b/modules/apigee/recipe-apigee-swp/templates/targets/default.xml.tpl
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ http://${ip_address}
+
+
\ No newline at end of file
diff --git a/modules/apigee/recipe-apigee-swp/variables.tf b/modules/apigee/recipe-apigee-swp/variables.tf
new file mode 100644
index 000000000..e8599f039
--- /dev/null
+++ b/modules/apigee/recipe-apigee-swp/variables.tf
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+variable "_testing" {
+ description = "Populate this variable to avoid triggering the data source."
+ type = object({
+ name = string
+ number = number
+ services_enabled = optional(list(string), [])
+ })
+ default = null
+}
+
+variable "analytics_region" {
+ description = "Region."
+ type = string
+}
+
+variable "instance_region" {
+ description = "Region."
+ type = string
+}
+
+variable "network_config" {
+ description = "Network configuration."
+ type = object({
+ subnet_ip_cidr_range = string
+ subnet_psc_ip_cidr_range = string
+ subnet_proxy_only_ip_cidr_range = string
+ })
+}
+
+variable "project_id" {
+ description = "Project ID."
+ type = string
+}
\ No newline at end of file