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()
+}
+