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: + +![Architecture](./diagram.png) + +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