From 1e82683b15ed1605efd6b1d072818f78c205cf43 Mon Sep 17 00:00:00 2001 From: Julio Castillo Date: Fri, 23 Jan 2026 13:51:00 +0100 Subject: [PATCH] Add service connection policies to net-vpc (#3667) --- modules/net-vpc/README.md | 70 ++++++++++++++----- modules/net-vpc/main.tf | 22 ++++++ modules/net-vpc/outputs.tf | 10 +++ modules/net-vpc/variables.tf | 42 +++++++++++ .../examples/service-connection-policies.yaml | 69 ++++++++++++++++++ 5 files changed, 196 insertions(+), 17 deletions(-) create mode 100644 tests/modules/net_vpc/examples/service-connection-policies.yaml diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md index 9a15c01a2..ae38c8105 100644 --- a/modules/net-vpc/README.md +++ b/modules/net-vpc/README.md @@ -20,6 +20,7 @@ This module allows creation and management of VPC networks including subnetworks - [Subnet Factory](#subnet-factory) - [Custom Routes](#custom-routes) - [Policy Based Routes](#policy-based-routes) + - [Service Connection Policies](#service-connection-policies) - [Private Google Access routes](#private-google-access-routes) - [Allow Firewall Policy to be evaluated before Firewall Rules](#allow-firewall-policy-to-be-evaluated-before-firewall-rules) - [IPv6](#ipv6) @@ -664,6 +665,38 @@ module "vpc" { # tftest modules=1 resources=3 inventory=pbr.yaml ``` +### Service Connection Policies + +[Service Connection Policies](https://cloud.google.com/vpc/docs/about-service-connection-policies) can be configured through the `service_connection_policies` variable. + +```hcl +module "vpc" { + source = "./fabric/modules/net-vpc" + project_id = var.project_id + name = "vpc" + subnets = [ + { + ip_cidr_range = "10.0.16.0/24" + name = "subnet" + region = "europe-southwest1" + } + ] + service_connection_policies = { + my-policy = { + location = "europe-southwest1" + service_class = "gcp-vertexai" + limit = 150 + psc_config = { + subnetworks = ["europe-southwest1/subnet"] + producer_instance_location = "CUSTOM_RESOURCE_HIERARCHY_LEVELS" + nodes = ["folders/123456789"] + } + } + } +} +# tftest inventory=service-connection-policies.yaml +``` + ### Private Google Access routes By default the VPC module creates IPv4 routes for the [Private Google Access ranges](https://cloud.google.com/vpc/docs/configure-private-google-access#config-routing). This behavior can be controlled through the `create_googleapis_routes` variable: @@ -946,13 +979,14 @@ secondary_ip_ranges: | [psa_configs](variables.tf#L266) | The Private Service Access configuration. | list(object({…})) | | [] | | [routes](variables.tf#L298) | Network routes, keyed by name. | map(object({…})) | | {} | | [routing_mode](variables.tf#L319) | The network routing mode (default 'GLOBAL'). | string | | "GLOBAL" | -| [shared_vpc_host](variables.tf#L329) | Enable shared VPC for this project. | bool | | false | -| [shared_vpc_service_projects](variables.tf#L335) | Shared VPC service projects to register with this host. | list(string) | | [] | -| [subnets](variables.tf#L341) | Subnet configuration. | list(object({…})) | | [] | -| [subnets_private_nat](variables.tf#L421) | List of private NAT subnets. | list(object({…})) | | [] | -| [subnets_proxy_only](variables.tf#L433) | List of proxy-only subnets for Regional HTTPS or Internal HTTPS load balancers. Note: Only one proxy-only subnet for each VPC network in each region can be active. | list(object({…})) | | [] | -| [subnets_psc](variables.tf#L467) | List of subnets for Private Service Connect service producers. | list(object({…})) | | [] | -| [vpc_reuse](variables.tf#L507) | Reuse existing VPC if not null. If the network_id number is not passed in, a data source is used. | object({…}) | | null | +| [service_connection_policies](variables.tf#L329) | Service connection policies, keyed by name. | map(object({…})) | | {} | +| [shared_vpc_host](variables.tf#L371) | Enable shared VPC for this project. | bool | | false | +| [shared_vpc_service_projects](variables.tf#L377) | Shared VPC service projects to register with this host. | list(string) | | [] | +| [subnets](variables.tf#L383) | Subnet configuration. | list(object({…})) | | [] | +| [subnets_private_nat](variables.tf#L463) | List of private NAT subnets. | list(object({…})) | | [] | +| [subnets_proxy_only](variables.tf#L475) | List of proxy-only subnets for Regional HTTPS or Internal HTTPS load balancers. Note: Only one proxy-only subnet for each VPC network in each region can be active. | list(object({…})) | | [] | +| [subnets_psc](variables.tf#L509) | List of subnets for Private Service Connect service producers. | list(object({…})) | | [] | +| [vpc_reuse](variables.tf#L549) | Reuse existing VPC if not null. If the network_id number is not passed in, a data source is used. | object({…}) | | null | ## Outputs @@ -969,14 +1003,16 @@ secondary_ip_ranges: | [network_id](outputs.tf#L83) | Numeric network id. | | | [project_id](outputs.tf#L95) | Project ID containing the network. Use this when you need to create resources *after* the VPC is fully set up (e.g. subnets created, shared VPC service projects attached, Private Service Networking configured). | | | [self_link](outputs.tf#L108) | Network self link. | | -| [subnet_ids](outputs.tf#L120) | Map of subnet IDs keyed by name. | | -| [subnet_ips](outputs.tf#L129) | Map of subnet address ranges keyed by name. | | -| [subnet_ipv6_external_prefixes](outputs.tf#L136) | Map of subnet external IPv6 prefixes keyed by name. | | -| [subnet_regions](outputs.tf#L144) | Map of subnet regions keyed by name. | | -| [subnet_secondary_ranges](outputs.tf#L151) | Map of subnet secondary ranges keyed by name. | | -| [subnet_self_links](outputs.tf#L162) | Map of subnet self links keyed by name. | | -| [subnets](outputs.tf#L171) | Subnet resources. | | -| [subnets_private_nat](outputs.tf#L180) | Private NAT subnet resources. | | -| [subnets_proxy_only](outputs.tf#L185) | L7 ILB or L7 Regional LB subnet resources. | | -| [subnets_psc](outputs.tf#L190) | Private Service Connect subnet resources. | | +| [service_connection_policies](outputs.tf#L120) | Service connection policy resources. | | +| [service_connection_policy_ids](outputs.tf#L125) | Service connection policy IDs. | | +| [subnet_ids](outputs.tf#L130) | Map of subnet IDs keyed by name. | | +| [subnet_ips](outputs.tf#L139) | Map of subnet address ranges keyed by name. | | +| [subnet_ipv6_external_prefixes](outputs.tf#L146) | Map of subnet external IPv6 prefixes keyed by name. | | +| [subnet_regions](outputs.tf#L154) | Map of subnet regions keyed by name. | | +| [subnet_secondary_ranges](outputs.tf#L161) | Map of subnet secondary ranges keyed by name. | | +| [subnet_self_links](outputs.tf#L172) | Map of subnet self links keyed by name. | | +| [subnets](outputs.tf#L181) | Subnet resources. | | +| [subnets_private_nat](outputs.tf#L190) | Private NAT subnet resources. | | +| [subnets_proxy_only](outputs.tf#L195) | L7 ILB or L7 Regional LB subnet resources. | | +| [subnets_psc](outputs.tf#L200) | Private Service Connect subnet resources. | | diff --git a/modules/net-vpc/main.tf b/modules/net-vpc/main.tf index 5248c941a..d36b3f686 100644 --- a/modules/net-vpc/main.tf +++ b/modules/net-vpc/main.tf @@ -183,3 +183,25 @@ resource "google_dns_policy" "default" { } } } + +resource "google_network_connectivity_service_connection_policy" "service_connection_policy" { + for_each = var.service_connection_policies + project = local.project_id + name = each.key + network = local.network.id + description = each.value.description + service_class = each.value.service_class + labels = each.value.labels + location = lookup( + local.ctx.locations, each.value.location, each.value.location + ) + psc_config { + subnetworks = [ + for s in each.value.psc_config.subnetworks : + try(local.all_subnets[s].id, s) + ] + limit = each.value.psc_config.limit + producer_instance_location = each.value.psc_config.producer_instance_location + allowed_google_producers_resource_hierarchy_level = each.value.psc_config.nodes + } +} diff --git a/modules/net-vpc/outputs.tf b/modules/net-vpc/outputs.tf index bcb4c27da..63acad8e4 100644 --- a/modules/net-vpc/outputs.tf +++ b/modules/net-vpc/outputs.tf @@ -117,6 +117,16 @@ output "self_link" { ] } +output "service_connection_policies" { + description = "Service connection policy resources." + value = google_network_connectivity_service_connection_policy.service_connection_policy +} + +output "service_connection_policy_ids" { + description = "Service connection policy IDs." + value = { for k, v in google_network_connectivity_service_connection_policy.service_connection_policy : k => v.id } +} + output "subnet_ids" { description = "Map of subnet IDs keyed by name." value = { for k, v in google_compute_subnetwork.subnetwork : k => v.id } diff --git a/modules/net-vpc/variables.tf b/modules/net-vpc/variables.tf index 1e4b4d1da..224f4d7ed 100644 --- a/modules/net-vpc/variables.tf +++ b/modules/net-vpc/variables.tf @@ -326,6 +326,48 @@ variable "routing_mode" { } } +variable "service_connection_policies" { + description = "Service connection policies, keyed by name." + type = map(object({ + location = string + service_class = string + description = optional(string) + labels = optional(map(string)) + psc_config = object({ + subnetworks = list(string) + limit = optional(number) + producer_instance_location = optional(string) + # maps to allowed_google_producers_resource_hierarchy_level + nodes = optional(list(string)) + }) + })) + nullable = false + default = {} + validation { + condition = alltrue([ + for k, v in var.service_connection_policies : + v.psc_config.producer_instance_location == null || v.psc_config.producer_instance_location == "CUSTOM_RESOURCE_HIERARCHY_LEVELS" + ]) + error_message = "Producer instance location must be null or CUSTOM_RESOURCE_HIERARCHY_LEVELS." + } + validation { + condition = alltrue([ + for k, v in var.service_connection_policies : + v.psc_config.nodes == null || v.psc_config.producer_instance_location == "CUSTOM_RESOURCE_HIERARCHY_LEVELS" + ]) + error_message = "Nodes can only be set if producer instance location is CUSTOM_RESOURCE_HIERARCHY_LEVELS." + } + validation { + condition = alltrue(flatten([ + for k, v in var.service_connection_policies : [ + for n in coalesce(v.psc_config.nodes, []) : + can(regex("^(projects|folders|organizations)/[a-zA-Z0-9-]+$", n)) + ] + ])) + error_message = "Nodes must be in the format 'projects/id', 'folders/id', or 'organizations/id'." + } +} + variable "shared_vpc_host" { description = "Enable shared VPC for this project." type = bool diff --git a/tests/modules/net_vpc/examples/service-connection-policies.yaml b/tests/modules/net_vpc/examples/service-connection-policies.yaml new file mode 100644 index 000000000..3a71bc3c9 --- /dev/null +++ b/tests/modules/net_vpc/examples/service-connection-policies.yaml @@ -0,0 +1,69 @@ +# Copyright 2026 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. + +values: + module.vpc.google_compute_network.network[0]: + auto_create_subnetworks: false + delete_bgp_always_compare_med: false + delete_default_routes_on_create: false + description: Terraform-managed. + enable_ula_internal_ipv6: null + name: vpc + network_firewall_policy_enforcement_order: AFTER_CLASSIC_FIREWALL + network_profile: null + params: [] + project: project-id + routing_mode: GLOBAL + timeouts: null + module.vpc.google_compute_subnetwork.subnetwork["europe-southwest1/subnet"]: + description: Terraform-managed. + ip_cidr_range: 10.0.16.0/24 + ip_collection: null + ipv6_access_type: null + log_config: [] + name: subnet + network: vpc + params: [] + private_ip_google_access: true + project: project-id + region: europe-southwest1 + reserved_internal_range: null + resolve_subnet_mask: null + role: null + send_secondary_ip_range_if_empty: true + timeouts: null + module.vpc.google_network_connectivity_service_connection_policy.service_connection_policy["my-policy"]: + description: null + effective_labels: + goog-terraform-provisioned: 'true' + labels: null + location: europe-southwest1 + name: my-policy + project: project-id + psc_config: + - allowed_google_producers_resource_hierarchy_level: + - folders/123456789 + limit: null + producer_instance_location: CUSTOM_RESOURCE_HIERARCHY_LEVELS + service_class: gcp-vertexai + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + +counts: + google_compute_network: 1 + google_compute_subnetwork: 1 + google_network_connectivity_service_connection_policy: 1 + modules: 1 + resources: 6