diff --git a/build/int.cloudbuild.yaml b/build/int.cloudbuild.yaml index aa513346..8a144395 100644 --- a/build/int.cloudbuild.yaml +++ b/build/int.cloudbuild.yaml @@ -179,6 +179,27 @@ steps: - verify backend-with-iap name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' args: ['/bin/bash', '-c', 'cft test run TestLbBackendServiceIap --stage teardown --verbose'] + # backend-with-psc-negs example +- id: init backend-with-psc-negs + waitFor: + - teardown backend-with-iap + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestLbBackendServiceWithPscNeg --stage init --verbose'] +- id: apply backend-with-psc-negs + waitFor: + - init backend-with-psc-negs + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestLbBackendServiceWithPscNeg --stage apply --verbose'] +- id: verify backend-with-psc-negs + waitFor: + - apply backend-with-psc-negs + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestLbBackendServiceWithPscNeg --stage verify --verbose'] +- id: teardown backend-with-psc-negs + waitFor: + - verify backend-with-psc-negs + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestLbBackendServiceWithPscNeg --stage teardown --verbose'] tags: - 'ci' - 'integration' diff --git a/examples/backend-with-psc-negs/main.tf b/examples/backend-with-psc-negs/main.tf new file mode 100644 index 00000000..a2467a76 --- /dev/null +++ b/examples/backend-with-psc-negs/main.tf @@ -0,0 +1,149 @@ +/** + * 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. + */ + + +module "producer-network" { + source = "terraform-google-modules/network/google//modules/vpc" + version = "~> 10.0.0" + project_id = var.project_id + network_name = "producer-network" + auto_create_subnetworks = false +} + +module "producer-subnet" { + source = "terraform-google-modules/network/google//modules/subnets" + version = "~> 10.0.0" + + subnets = [ + { + subnet_name = "producer-subnet-a" + subnet_ip = "10.1.2.0/24" + subnet_region = "us-central1" + }, + { + subnet_name = "producer-subnet-b" + subnet_ip = "10.1.3.0/24" + subnet_region = "us-central1" + purpose = "PRIVATE_SERVICE_CONNECT" + } + ] + + network_name = module.producer-network.network_name + project_id = var.project_id + depends_on = [module.producer-network] +} + +module "gce-ilb" { + source = "GoogleCloudPlatform/lb-internal/google" + version = "~> 6.0" + project = var.project_id + region = "us-central1" + name = "group2-ilb" + ports = ["80"] + + source_tags = ["allow-group1"] + target_tags = ["allow-group2"] + + global_access = true + + network = module.producer-network.network_name + subnetwork = module.producer-subnet.subnets["us-central1/producer-subnet-a"].name + + health_check = { + type = "http" + check_interval_sec = 1 + healthy_threshold = 4 + timeout_sec = 1 + unhealthy_threshold = 5 + response = "" + proxy_header = "NONE" + port = 80 + port_name = "health-check-port" + request = "" + request_path = "/" + host = "1.2.3.4" + enable_log = false + } + + backends = [] + depends_on = [module.producer-subnet] + +} + +resource "google_compute_service_attachment" "minimal_sa" { + project = var.project_id + name = "sa" + region = "us-central1" + enable_proxy_protocol = false + connection_preference = "ACCEPT_AUTOMATIC" + nat_subnets = [module.producer-subnet.subnets["us-central1/producer-subnet-b"].name] + target_service = module.gce-ilb.forwarding_rule + + depends_on = [module.gce-ilb] +} + + +module "psc-neg-network" { + source = "terraform-google-modules/network/google//modules/vpc" + version = "~> 10.0.0" + project_id = var.project_id + network_name = "psc-neg-network" + auto_create_subnetworks = false +} + +module "psc-neg-subnet" { + source = "terraform-google-modules/network/google//modules/subnets" + version = "~> 10.0.0" + + subnets = [ + { + subnet_name = "psc-neg-subnet-a" + subnet_ip = "10.1.2.0/24" + subnet_region = "us-central1" + } + ] + + network_name = module.psc-neg-network.network_name + project_id = var.project_id + depends_on = [module.psc-neg-network] +} + +module "lb-backend-psc-neg" { + source = "terraform-google-modules/lb-http/google//modules/backend" + version = "~> 12.0" + + project_id = var.project_id + name = "backend-with-psc-negs" + psc_neg_backends = [{ + name = "test-psc-1" + region = "us-central1" + psc_target_service = google_compute_service_attachment.minimal_sa.self_link + network = module.psc-neg-network.network_name + subnetwork = module.psc-neg-subnet.subnets["us-central1/psc-neg-subnet-a"].name + producer_port = "80" + }] + + depends_on = [google_compute_service_attachment.minimal_sa] +} + +module "lb-frontend" { + source = "terraform-google-modules/lb-http/google//modules/frontend" + version = "~> 12.0" + + project_id = var.project_id + name = "global-lb-fe-psc-neg" + url_map_input = module.lb-backend-psc-neg.backend_service_info +} diff --git a/examples/backend-with-psc-negs/outputs.tf b/examples/backend-with-psc-negs/outputs.tf new file mode 100644 index 00000000..4873b70f --- /dev/null +++ b/examples/backend-with-psc-negs/outputs.tf @@ -0,0 +1,26 @@ +/** + * 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 "project_id" { + value = var.project_id + description = "Project ID of the service" +} + +output "psc_negs" { + value = module.lb-backend-psc-neg.psc_negs + description = "Psc Neg created for this load balancer" +} diff --git a/examples/backend-with-psc-negs/variables.tf b/examples/backend-with-psc-negs/variables.tf new file mode 100644 index 00000000..419e3a19 --- /dev/null +++ b/examples/backend-with-psc-negs/variables.tf @@ -0,0 +1,19 @@ +/** + * 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 "project_id" { + type = string +} diff --git a/metadata.yaml b/metadata.yaml index c0dbcf80..e8b31d77 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -42,6 +42,8 @@ spec: examples: - name: backend-with-iap location: examples/backend-with-iap + - name: backend-with-psc-negs + location: examples/backend-with-psc-negs - name: cdn-policy location: examples/cdn-policy - name: certificate-map @@ -338,13 +340,13 @@ spec: roles: - level: Project roles: + - roles/run.admin - roles/iam.serviceAccountUser - roles/certificatemanager.owner - roles/vpcaccess.admin - roles/iam.serviceAccountAdmin - roles/storage.admin - roles/compute.admin - - roles/run.admin services: - certificatemanager.googleapis.com - cloudresourcemanager.googleapis.com diff --git a/modules/backend/README.md b/modules/backend/README.md index 409758bc..a1be2d9f 100644 --- a/modules/backend/README.md +++ b/modules/backend/README.md @@ -31,6 +31,7 @@ This module creates `google_compute_backend_service` resource and its dependenci | port\_name | Name of backend port. The same name should appear in the instance groups referenced by this service. Required when the load balancing scheme is EXTERNAL. | `string` | `"http"` | no | | project\_id | The project to deploy to, if not set the default provider project is used. | `string` | n/a | yes | | protocol | The protocol this BackendService uses to communicate with backends. | `string` | `"HTTP"` | no | +| psc\_neg\_backends | The list of Private Service Connect backends which serve the traffic. |
list(object({
name = string
region = string
psc_target_service = string
network = string
subnetwork = string
producer_port = optional(string)
}))
| `[]` | no | | security\_policy | The resource URL for the security policy to associate with the backend service | `string` | `null` | no | | serverless\_neg\_backends | The list of serverless backend which serves the traffic. |
list(object({
region = string
type = string // cloud-run, cloud-function, and app-engine
service_name = string
service_version = optional(string)
}))
| `[]` | no | | session\_affinity | Type of session affinity to use. Possible values are: NONE, CLIENT\_IP, CLIENT\_IP\_PORT\_PROTO, CLIENT\_IP\_PROTO, GENERATED\_COOKIE, HEADER\_FIELD, HTTP\_COOKIE, STRONG\_COOKIE\_AFFINITY. | `string` | `null` | no | @@ -44,5 +45,6 @@ This module creates `google_compute_backend_service` resource and its dependenci |------|-------------| | apphub\_service\_uri | Service URI in CAIS style to be used by Apphub. | | backend\_service\_info | Host, path and backend service mapping | +| psc\_negs | Private Service Connect backends that were created for this backend service | diff --git a/modules/backend/main.tf b/modules/backend/main.tf index 5487fdc3..e18c2816 100644 --- a/modules/backend/main.tf +++ b/modules/backend/main.tf @@ -17,6 +17,7 @@ locals { is_backend_bucket = var.backend_bucket_name != null && var.backend_bucket_name != "" serverless_neg_backends = local.is_backend_bucket ? [] : var.serverless_neg_backends + psc_neg_backends = local.is_backend_bucket ? [] : var.psc_neg_backends iap_access_members = var.iap_config.enable ? coalesce(var.iap_config.iap_members, []) : [] } @@ -72,6 +73,13 @@ resource "google_compute_backend_service" "default" { } } + dynamic "backend" { + for_each = toset(var.psc_neg_backends) + content { + group = google_compute_region_network_endpoint_group.psc_negs[backend.value.name].id + } + } + dynamic "log_config" { for_each = var.log_config.enable ? [1] : [] content { @@ -200,6 +208,29 @@ resource "google_compute_region_network_endpoint_group" "serverless_negs" { } } +resource "google_compute_region_network_endpoint_group" "psc_negs" { + for_each = { for psc_neg_backend in local.psc_neg_backends : + psc_neg_backend.name => psc_neg_backend + } + + provider = google-beta + project = var.project_id + name = each.key + network_endpoint_type = "PRIVATE_SERVICE_CONNECT" + region = each.value.region + psc_target_service = each.value.psc_target_service + network = each.value.network + subnetwork = each.value.subnetwork + + psc_data { + producer_port = try(each.value.producer_port, null) + } + + lifecycle { + create_before_destroy = true + } +} + resource "google_compute_health_check" "default" { provider = google-beta count = var.health_check != null ? 1 : 0 diff --git a/modules/backend/metadata.yaml b/modules/backend/metadata.yaml index 55b47c49..caab3369 100644 --- a/modules/backend/metadata.yaml +++ b/modules/backend/metadata.yaml @@ -34,6 +34,8 @@ spec: examples: - name: backend-with-iap location: examples/backend-with-iap + - name: backend-with-psc-negs + location: examples/backend-with-psc-negs - name: cdn-policy location: examples/cdn-policy - name: certificate-map @@ -180,6 +182,18 @@ spec: version: ">= 0.13" spec: outputExpr: "{\"region\": location, \"service_name\": service_name, \"type\": \"cloud-run\", \"service_version\": \"\"}" + - name: psc_neg_backends + description: The list of Private Service Connect backends which serve the traffic. + varType: |- + list(object({ + name = string + region = string + psc_target_service = string + network = string + subnetwork = string + producer_port = optional(string) + })) + defaultValue: [] - name: backend_bucket_name description: The name of GCS bucket which serves the traffic. varType: string @@ -330,17 +344,19 @@ spec: - backend_service: string host: string path: string + - name: psc_negs + description: Private Service Connect backends that were created for this backend service requirements: roles: - level: Project roles: - - roles/iam.serviceAccountUser - - roles/iam.serviceAccountAdmin - roles/compute.admin - roles/storage.admin - roles/run.admin - roles/compute.networkAdmin - roles/iap.admin + - roles/iam.serviceAccountUser + - roles/iam.serviceAccountAdmin services: - cloudresourcemanager.googleapis.com - compute.googleapis.com diff --git a/modules/backend/outputs.tf b/modules/backend/outputs.tf index a8dab938..df0adbb3 100644 --- a/modules/backend/outputs.tf +++ b/modules/backend/outputs.tf @@ -43,3 +43,10 @@ output "apphub_service_uri" { ) description = "Service URI in CAIS style to be used by Apphub." } + +output "psc_negs" { + value = !local.is_backend_bucket ? [ + for neg_key, neg in google_compute_region_network_endpoint_group.psc_negs : neg.self_link + ] : [] + description = "Private Service Connect backends that were created for this backend service" +} diff --git a/modules/backend/variables.tf b/modules/backend/variables.tf index f825bda1..6be4cfad 100644 --- a/modules/backend/variables.tf +++ b/modules/backend/variables.tf @@ -147,6 +147,19 @@ variable "serverless_neg_backends" { } } +variable "psc_neg_backends" { + description = "The list of Private Service Connect backends which serve the traffic." + type = list(object({ + name = string + region = string + psc_target_service = string + network = string + subnetwork = string + producer_port = optional(string) + })) + default = [] +} + variable "backend_bucket_name" { description = "The name of GCS bucket which serves the traffic." type = string @@ -293,3 +306,10 @@ variable "firewall_source_ranges" { type = list(string) default = ["10.127.0.0/23"] } + +check "backend_neg_type_exclusive" { + assert { + condition = length(var.serverless_neg_backends) == 0 || length(var.psc_neg_backends) == 0 + error_message = "The 'serverless_neg_backends' and 'psc_neg_backends' variables are mutually exclusive. Please specify only one." + } +} diff --git a/modules/dynamic_backends/metadata.yaml b/modules/dynamic_backends/metadata.yaml index a68e0bcc..8867c87d 100644 --- a/modules/dynamic_backends/metadata.yaml +++ b/modules/dynamic_backends/metadata.yaml @@ -34,6 +34,8 @@ spec: examples: - name: backend-with-iap location: examples/backend-with-iap + - name: backend-with-psc-negs + location: examples/backend-with-psc-negs - name: cdn-policy location: examples/cdn-policy - name: certificate-map @@ -330,13 +332,13 @@ spec: roles: - level: Project roles: - - roles/vpcaccess.admin - - roles/iam.serviceAccountAdmin - roles/storage.admin - roles/compute.admin - roles/run.admin - roles/iam.serviceAccountUser - roles/certificatemanager.owner + - roles/vpcaccess.admin + - roles/iam.serviceAccountAdmin services: - certificatemanager.googleapis.com - cloudresourcemanager.googleapis.com diff --git a/modules/frontend/metadata.yaml b/modules/frontend/metadata.yaml index 2ca4cc96..007f41d1 100644 --- a/modules/frontend/metadata.yaml +++ b/modules/frontend/metadata.yaml @@ -34,6 +34,8 @@ spec: examples: - name: backend-with-iap location: examples/backend-with-iap + - name: backend-with-psc-negs + location: examples/backend-with-psc-negs - name: cdn-policy location: examples/cdn-policy - name: certificate-map diff --git a/modules/serverless_negs/metadata.yaml b/modules/serverless_negs/metadata.yaml index 14f886c4..70c2b339 100644 --- a/modules/serverless_negs/metadata.yaml +++ b/modules/serverless_negs/metadata.yaml @@ -34,6 +34,8 @@ spec: examples: - name: backend-with-iap location: examples/backend-with-iap + - name: backend-with-psc-negs + location: examples/backend-with-psc-negs - name: cdn-policy location: examples/cdn-policy - name: certificate-map @@ -294,13 +296,13 @@ spec: roles: - level: Project roles: - - roles/iam.serviceAccountUser - roles/certificatemanager.owner - roles/vpcaccess.admin - roles/iam.serviceAccountAdmin - roles/storage.admin - roles/compute.admin - roles/run.admin + - roles/iam.serviceAccountUser services: - certificatemanager.googleapis.com - cloudresourcemanager.googleapis.com diff --git a/test/integration/backend-with-psc-negs/backend_with_psc_negs_test.go b/test/integration/backend-with-psc-negs/backend_with_psc_negs_test.go new file mode 100644 index 00000000..66a84e4e --- /dev/null +++ b/test/integration/backend-with-psc-negs/backend_with_psc_negs_test.go @@ -0,0 +1,42 @@ +// 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. + +package backend_with_psc_negs + +import ( + "testing" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" + "github.com/stretchr/testify/assert" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" +) + +func TestLbBackendServiceWithPscNeg(t *testing.T) { + backendServiceWithPscNegs := tft.NewTFBlueprintTest(t) + + backendServiceWithPscNegs.DefineVerify(func(assert *assert.Assertions) { + projectID := backendServiceWithPscNegs.GetTFSetupStringOutput("project_id") + pscNegURIs := backendServiceWithPscNegs.GetStringOutputList("psc_negs") + + serviceName := "backend-with-psc-negs" + + backendServiceDescribeCmd := gcloud.Run(t, "compute backend-services describe", gcloud.WithCommonArgs([]string{serviceName, "--project", projectID, "--global", "--format", "json"})) + + backends := backendServiceDescribeCmd.Get("backends").Array() + assert.Len(backends, 1, "should have 1 PSC NEG backends attached") + assert.Len(pscNegURIs, 1, "should return 1 PSC NEG as output") + }) + backendServiceWithPscNegs.Test() +} +