diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..84b279c Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md index 9bc070e..9ac9322 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Sample modules are intended to demonstrate the most common network topologies fo * [X Basic](samples/x-basic) for a basic Apigee X setup with the raw instance endpoints exposed as internal IP addresses. * [X with external L7 LB](samples/x-l7xlb) for an Apigee X setup that is exposed via a global external L7 load balancer. * [X with external L7 LB and northbound PSC](samples/x-nb-psc-xlb) for an Apigee X setup that uses a global external L7 load balancer and a Private Service Connect (PSC) Network Endpoint Group (NEG) to connect to an Apigee instance's service attachment. +* [X with external L7 LB, MIG and northbound PSC](samples/x-nb-psc-mig-l7xlb) for an Apigee X setup that uses a global external L7 load balancer-->Managed Instance Group-->Private Service Connect (PSC) Endpoint to connect to an Apigee instance's service attachment to leverage active health check with PSC. * [X with southbound PSC](samples/x-sb-psc) for an Apigee X setup that uses Private Service Connect (PSC) to connect to a backend service in another VPC. * [X with internal L4 LB and mTLS](samples/x-ilb-mtls) for a basic Apigee X setup plus exposure via regional L4 load balancer and envoy proxy to terminate mTLS. * [X with external L4 LB and mTLS](samples/x-l4xlb-mtls) for a basic Apigee X setup plus exposure via global external L4 load balancer and envoy proxy to terminate mTLS. diff --git a/samples/.DS_Store b/samples/.DS_Store new file mode 100644 index 0000000..d46715f Binary files /dev/null and b/samples/.DS_Store differ diff --git a/samples/x-nb-psc-mig-l7xlb/.DS_Store b/samples/x-nb-psc-mig-l7xlb/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/samples/x-nb-psc-mig-l7xlb/.DS_Store differ diff --git a/samples/x-nb-psc-mig-l7xlb/README.md b/samples/x-nb-psc-mig-l7xlb/README.md new file mode 100644 index 0000000..477b9cf --- /dev/null +++ b/samples/x-nb-psc-mig-l7xlb/README.md @@ -0,0 +1,111 @@ +# Apigee X exposed in Multiple GCP Regions with PSC and MIG External HTTPS Load Balancer + +End-to-end sample terraform code to provision Apigee X exposed by External Load Balancer with PSC and MIG. + +If you plan to use LB-->PSC-NEG for Apigee northbound network routing, follow the instructions in this document to configure active health check. +At this time, PSC-NEG does not support active health check monitoring as mentioned [here](https://cloud.google.com/load-balancing/docs/negs#psc-neg). +To work around this limitation of PSC, you can modify the Apigee installation configuration to use a managed instance group (MIG), which does provide active health check capability. Refer the solution guide [here](https://cloud.google.com/apigee/docs/api-platform/system-administration/health-check-mig-workaround) and the network diagram below : + +

+ Apigee X Shared VPC Multi Region Sample Architecture +

+ + + + +## Setup Instructions + +Set the project ID where you want your Apigee Organization to be deployed to: + +```sh +PROJECT_ID=my-project-id +``` + +```sh +cd samples/... # Sample from above +cp ./x-demo.tfvars ./my-config.tfvars +``` + +Decide on a [backend](https://www.terraform.io/language/settings/backends) and create the necessary config. To use a backend on Google Cloud Storage (GCS) use: + +```sh +gsutil mb "gs://$PROJECT_ID-tf" + +cat <terraform.tf +terraform { + backend "gcs" { + bucket = "$PROJECT_ID-tf" + prefix = "terraform/state" + } +} +EOF +``` + +Validate your config: + +```sh +terraform init +terraform plan --var-file=./my-config.tfvars -var "project_id=$PROJECT_ID" +``` + +and provision everything (takes roughly 25min): + +```sh +terraform apply --var-file=./my-config.tfvars -var "project_id=$PROJECT_ID" +``` + + + +## Providers + +| Name | Version | +|------|---------| +| [google](#provider\_google) | n/a | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [apigee-x-bridge-mig](#module\_apigee-x-bridge-mig) | ../../modules/apigee-x-bridge-mig | n/a | +| [apigee-x-core](#module\_apigee-x-core) | ../../modules/apigee-x-core | n/a | +| [mig-l7xlb](#module\_mig-l7xlb) | ../../modules/mig-l7xlb | n/a | +| [project](#module\_project) | github.com/terraform-google-modules/cloud-foundation-fabric//modules/project | v28.0.0 | +| [vpc](#module\_vpc) | github.com/terraform-google-modules/cloud-foundation-fabric//modules/net-vpc | v28.0.0 | + +## Resources + +| Name | Type | +|------|------| +| [google_compute_address.psc_endpoint_address](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_address) | resource | +| [google_compute_forwarding_rule.psc_ilb_consumer](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_forwarding_rule) | resource | +| [google_compute_global_address.external_address](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_global_address) | resource | +| [google_compute_managed_ssl_certificate.google_cert](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_managed_ssl_certificate) | resource | +| [google_compute_address.int_psc_ips](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/compute_address) | data source | +| [google_compute_global_address.my_lb_external_address](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/compute_global_address) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [apigee\_envgroups](#input\_apigee\_envgroups) | Apigee Environment Groups. |
map(object({
hostnames = list(string)
}))
| `null` | no | +| [apigee\_environments](#input\_apigee\_environments) | Apigee Environments. |
map(object({
display_name = optional(string)
description = optional(string)
node_config = optional(object({
min_node_count = optional(number)
max_node_count = optional(number)
}))
iam = optional(map(list(string)))
envgroups = list(string)
type = optional(string)
}))
| `null` | no | +| [apigee\_instances](#input\_apigee\_instances) | Apigee Instances (only one instance for EVAL orgs). |
map(object({
region = string
ip_range = string
environments = list(string)
}))
| `null` | no | +| [ax\_region](#input\_ax\_region) | GCP region for storing Apigee analytics data. | `string` | n/a | yes | +| [billing\_account](#input\_billing\_account) | Billing account ID. | `string` | n/a | yes | +| [billing\_type](#input\_billing\_type) | Billing type of the Apigee organization. | `string` | `null` | no | +| [exposure\_subnets](#input\_exposure\_subnets) | Subnets for exposing Apigee services |
list(object({
name = string
ip_cidr_range = string
region = string
instance = string
secondary_ip_range = map(string)
}))
| `[]` | no | +| [lb\_name](#input\_lb\_name) | Name of the load balancer. | `string` | n/a | yes | +| [peering\_range](#input\_peering\_range) | Peering CIDR range | `string` | n/a | yes | +| [project\_create](#input\_project\_create) | Create project. When set to false, uses a data source to reference existing project. | `bool` | `false` | no | +| [project\_id](#input\_project\_id) | Project ID. | `string` | n/a | yes | +| [project\_parent](#input\_project\_parent) | Parent folder or organization in 'folders/folder\_id' or 'organizations/org\_id' format. | `string` | n/a | yes | +| [project\_services](#input\_project\_services) | List of services to enable in the project. | `list(string)` |
[
"apigee.googleapis.com",
"cloudkms.googleapis.com",
"compute.googleapis.com",
"servicenetworking.googleapis.com"
]
| no | +| [psc\_subnets](#input\_psc\_subnets) | Subnets for psc endpoints |
list(object({
name = string
ip_cidr_range = string
region = string
instance = string
secondary_ip_range = map(string)
}))
| `[]` | no | +| [ssl\_crt\_domains](#input\_ssl\_crt\_domains) | Domains for the managed SSL certificate. | `list(string)` | n/a | yes | +| [support\_range1](#input\_support\_range1) | Support CIDR range of length /28 (required by Apigee for troubleshooting purposes). | `string` | n/a | yes | +| [vpc\_name](#input\_vpc\_name) | Project ID. | `string` | n/a | yes | + +## Outputs + +No outputs. + diff --git a/samples/x-nb-psc-mig-l7xlb/main.tf b/samples/x-nb-psc-mig-l7xlb/main.tf new file mode 100644 index 0000000..3675f70 --- /dev/null +++ b/samples/x-nb-psc-mig-l7xlb/main.tf @@ -0,0 +1,141 @@ +/** + * Copyright 2024 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. + */ + +locals { + subnet_region_name = { for subnet in var.exposure_subnets : + subnet.region => "${subnet.region}/${subnet.name}" + } + psc_subnets = { for psc in var.psc_subnets : + psc.name => psc + } + region_psc_map = {for psc in var.psc_subnets : + psc.region => psc.name + } + +} + +module "project" { + source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/project?ref=v28.0.0" + name = var.project_id + parent = var.project_parent + billing_account = var.billing_account + project_create = var.project_create + services = var.project_services +} + +module "vpc" { + source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/net-vpc?ref=v28.0.0" + project_id = module.project.project_id + name = var.vpc_name + subnets = [ + for subnet in concat(var.exposure_subnets, var.psc_subnets) : + { + "name" = subnet.name + "region" = subnet.region + "secondary_ip_ranges" = subnet.secondary_ip_range + "ip_cidr_range" = subnet.ip_cidr_range + } + ] + psa_config = { + ranges = { + apigee-range = var.peering_range + apigee-support-range1 = var.support_range1 + + #add_more_support_ranges_per_instance_region + } + } +} + +module "apigee-x-core" { + source = "../../modules/apigee-x-core" + project_id = module.project.project_id + billing_type = var.billing_type + ax_region = var.ax_region + apigee_environments = var.apigee_environments + apigee_envgroups = var.apigee_envgroups + apigee_instances = var.apigee_instances + network = module.vpc.network.id +} + + +resource "google_compute_address" "psc_endpoint_address" { + for_each = local.psc_subnets + name = "psc-ip-${each.value.region}" + project = module.project.project_id + address_type = "INTERNAL" + subnetwork = module.vpc.subnet_self_links["${each.value.region}/${each.value.name}"] + region = each.value.region +} + +resource "google_compute_forwarding_rule" "psc_ilb_consumer" { + for_each = local.psc_subnets + name = "psc-ea-${each.value.region}" + project = module.project.project_id + region = each.value.region + target = module.apigee-x-core.instance_service_attachments[each.value.region] + load_balancing_scheme = "" + network = module.vpc.network.id + ip_address = google_compute_address.psc_endpoint_address[each.value.name].id + depends_on = [ + google_compute_address.psc_endpoint_address, + module.apigee-x-core + ] +} + +data "google_compute_address" "int_psc_ips" { + for_each = google_compute_address.psc_endpoint_address + name = each.value.name + region = each.value.region + project = module.project.project_id +} + +module "apigee-x-bridge-mig" { + for_each = local.subnet_region_name + source = "../../modules/apigee-x-bridge-mig" + project_id = module.project.project_id + network = module.vpc.network.id + region = each.key + subnet = module.vpc.subnet_self_links[local.subnet_region_name[each.key]] + endpoint_ip = data.google_compute_address.int_psc_ips[local.region_psc_map[each.key]].address +} + +resource "google_compute_global_address" "external_address" { + name = "lb-${var.lb_name}-ip" + project = module.project.project_id + address_type = "EXTERNAL" +} + +data "google_compute_global_address" "my_lb_external_address" { + name = google_compute_global_address.external_address.name + project = module.project.project_id +} + +resource "google_compute_managed_ssl_certificate" "google_cert" { + project = var.project_id + name = "ssl-cert" + managed { + domains = var.ssl_crt_domains + } +} + +module "mig-l7xlb" { + source = "../../modules/mig-l7xlb" + project_id = module.project.project_id + name = var.lb_name + backend_migs = [for _, mig in module.apigee-x-bridge-mig : mig.instance_group] + ssl_certificate = [google_compute_managed_ssl_certificate.google_cert.id] + external_ip = data.google_compute_global_address.my_lb_external_address.address +} \ No newline at end of file diff --git a/samples/x-nb-psc-mig-l7xlb/sample-architecture.png b/samples/x-nb-psc-mig-l7xlb/sample-architecture.png new file mode 100644 index 0000000..368cd24 Binary files /dev/null and b/samples/x-nb-psc-mig-l7xlb/sample-architecture.png differ diff --git a/samples/x-nb-psc-mig-l7xlb/variables.tf b/samples/x-nb-psc-mig-l7xlb/variables.tf new file mode 100644 index 0000000..0a1315b --- /dev/null +++ b/samples/x-nb-psc-mig-l7xlb/variables.tf @@ -0,0 +1,147 @@ +/** + * Copyright 2024 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 "project_id" { + description = "Project ID." + type = string +} + +variable "vpc_name" { + description = "Project ID." + type = string +} + +variable "project_parent" { + description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." + type = string + validation { + condition = var.project_parent == null || can(regex("(organizations|folders)/[0-9]+", var.project_parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "billing_account" { + description = "Billing account ID." + type = string +} + +variable "billing_type" { + description = "Billing type of the Apigee organization." + type = string + default = null +} + +variable "project_create" { + description = "Create project. When set to false, uses a data source to reference existing project." + type = bool + default = false +} + +variable "project_services" { + description = "List of services to enable in the project." + type = list(string) + default = [ + "apigee.googleapis.com", + "cloudkms.googleapis.com", + "compute.googleapis.com", + "servicenetworking.googleapis.com" + ] +} + +variable "ax_region" { + description = "GCP region for storing Apigee analytics data." + type = string +} + +variable "apigee_environments" { + description = "Apigee Environments." + type = map(object({ + display_name = optional(string) + description = optional(string) + node_config = optional(object({ + min_node_count = optional(number) + max_node_count = optional(number) + })) + iam = optional(map(list(string))) + envgroups = list(string) + type = optional(string) + })) + default = null +} + +variable "apigee_envgroups" { + description = "Apigee Environment Groups." + type = map(object({ + hostnames = list(string) + })) + default = null +} + +variable "apigee_instances" { + description = "Apigee Instances (only one instance for EVAL orgs)." + type = map(object({ + region = string + ip_range = string + environments = list(string) + })) + default = null +} + +variable "exposure_subnets" { + description = "Subnets for exposing Apigee services" + type = list(object({ + name = string + ip_cidr_range = string + region = string + instance = string + secondary_ip_range = map(string) + })) + default = [] +} + +variable "psc_subnets" { + description = "Subnets for psc endpoints" + type = list(object({ + name = string + ip_cidr_range = string + region = string + instance = string + secondary_ip_range = map(string) + })) + default = [] +} + +variable "lb_name" { + description = "Name of the load balancer." + type = string +} + + +variable "ssl_crt_domains" { + description = "Domains for the managed SSL certificate." + type = list(string) +} + +variable "peering_range" { + description = "Peering CIDR range" + type = string +} + +variable "support_range1" { + description = "Support CIDR range of length /28 (required by Apigee for troubleshooting purposes)." + type = string +} + diff --git a/samples/x-nb-psc-mig-l7xlb/x-demo.tfvars b/samples/x-nb-psc-mig-l7xlb/x-demo.tfvars new file mode 100644 index 0000000..ffc8e62 --- /dev/null +++ b/samples/x-nb-psc-mig-l7xlb/x-demo.tfvars @@ -0,0 +1,80 @@ +/** + * Copyright 2023 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. + */ + + +ax_region = "europe-west1" +billing_account = "" +billing_type = "EVAL" +project_parent = "organizations/406283053755" + +vpc_name = "apigeexvpc" +peering_range = "10.1.0.0/16" +support_range1 = "10.2.0.0/28" + +lb_name = "apigeexlb" +ssl_crt_domains = ["xyz.com"] + + + +# Apigee configurations +apigee_envgroups = { + "testgroup" = { hostnames = ["example.xyz.com"] } + # Add more environment groups if needed +} + +apigee_instances = { + "euw1-instance" = { + region = "europe-west1" + ip_range = "10.1.4.0/22" + environments = ["test"] + }, + +} + +apigee_environments = { + "test" = { + display_name = "TEST" + description = "" + iam = null + envgroups = ["testgroup"] + + } + # Add more environments if needed +} + + +exposure_subnets = [ + { + name = "apigee-exposure-1" + ip_cidr_range = "10.100.0.0/24" + region = "europe-west1" + instance = "euw1-instance" + secondary_ip_range = null + }, + +] + +psc_subnets = [ + { + name = "psc-subnet-1" + ip_cidr_range = "10.100.255.240/29" + region = "europe-west1" + instance = "euw1-instance" + secondary_ip_range = null + }, + +] + diff --git a/tests/.DS_Store b/tests/.DS_Store new file mode 100644 index 0000000..4d85fc6 Binary files /dev/null and b/tests/.DS_Store differ diff --git a/tests/samples/test_nb_psc_mig_l7xlb.py b/tests/samples/test_nb_psc_mig_l7xlb.py new file mode 100644 index 0000000..e57c884 --- /dev/null +++ b/tests/samples/test_nb_psc_mig_l7xlb.py @@ -0,0 +1,83 @@ +# Copyright 2024 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. + + +import os +import pytest +from .utils import * + +FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "../../samples/x-nb-psc-mig-l7xlb") + + +@pytest.fixture(scope="module") +def resources(recursive_plan_runner): + _, resources = recursive_plan_runner( + FIXTURES_DIR, + tf_var_file=os.path.join(FIXTURES_DIR, "x-demo.tfvars"), + project_id="testonly", + project_create="true" + ) + return resources + + +def test_resource_count(resources): + "Test total number of resources created." + assert len(resources) == 46 + + +def test_apigee_instance(resources): + "Test Apigee Instance Resource" + assert_instance(resources, "europe-west1", "10.1.4.0/22") + + + +def test_apigee_instance_attachment(resources): + "Test Apigee Instance Attachments." + assert_instance_attachment(resources, ["europe-west1-test"]) + + +def test_envgroup_attachment(resources): + "Test Apigee Envgroup Attachments." + assert_envgroup_attachment(resources, ["test"]) + + +def test_envgroup(resources): + "Test env group." + assert_envgroup_name(resources, "testgroup") + + +def test_instance_bidge_location_parity(resources): + "Test that the instance and bridge VM are in the same location" + instance = [ + r["values"] for r in resources if r["type"] == "google_apigee_instance" + ][0] + instance_group_mgr = [ + r["values"] + for r in resources + if r["type"] == "google_compute_region_instance_group_manager" + ][0] + assert instance["location"] == instance_group_mgr["region"] + +def test_same_region_psc(resources): + "Test that Apigee instance and the PSC are in the same region." + instances = [ + r for r in resources if r["type"] == "google_apigee_instance" + ] + psc = [ + r for r in resources if r["type"] == "google_compute_forwarding_rule" + ] + assert len(instances) == 1 + assert len(psc) == 1 + assert instances[0]["values"]["location"] == psc[0]["values"]["region"] +