diff --git a/modules/net-vpc/README.md b/modules/net-vpc/README.md
index 62d298d69..2968c54db 100644
--- a/modules/net-vpc/README.md
+++ b/modules/net-vpc/README.md
@@ -23,6 +23,7 @@ This module allows creation and management of VPC networks including subnetworks
- [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)
+ - [IPv6-Only and IP Collections](#ipv6-only-and-ip-collections)
- [Variables](#variables)
- [Outputs](#outputs)
@@ -712,6 +713,47 @@ module "vpc" {
}
# tftest modules=1 resources=6 inventory=ipv6.yaml e2e
```
+
+### IPv6-Only and IP Collections
+
+An IPv6-only subnetwork can be specified by setting `ipv6_only` to `true` and
+setting `ip_cidr_range` to `null`. An IP Collection may be specified with
+`ip_collection` and a
+[reference](https://cloud.google.com/compute/docs/reference/rest/v1/subnetworks/insert)
+to a collection source, like a PublicDelegatedPrefix (PDP) for BYOIPv6. The PDP
+must be a sub-PDP in `EXTERNAL_IPV6_SUBNETWORK_CREATION` mode.
+
+```hcl
+module "vpc" {
+ source = "./fabric/modules/net-vpc"
+ project_id = var.project_id
+ name = "my-network"
+ ipv6_config = {
+ enable_ula_internal = true
+ }
+ subnets = [
+ {
+ ip_cidr_range = null
+ name = "test-v6only"
+ region = "europe-west1"
+ ipv6 = {
+ ipv6_only = true
+ }
+ },
+ {
+ ip_cidr_range = null
+ name = "test-v6only"
+ region = "europe-west3"
+ ipv6 = {
+ access_type = "EXTERNAL"
+ ipv6_only = true
+ }
+ ip_collection = "https://www.googleapis.com/compute/v1/projects/project-id/regions/europe-west3/publicDelegatedPrefixes/test-sub-pdp"
+ }
+ ]
+}
+# tftest modules=1 resources=6 inventory=ipv6_only.yaml e2e
+```
## Variables
@@ -736,11 +778,11 @@ module "vpc" {
| [routing_mode](variables.tf#L234) | The network routing mode (default 'GLOBAL'). | string | | "GLOBAL" |
| [shared_vpc_host](variables.tf#L244) | Enable shared VPC for this project. | bool | | false |
| [shared_vpc_service_projects](variables.tf#L250) | Shared VPC service projects to register with this host. | list(string) | | [] |
-| [subnets](variables.tf#L256) | Subnet configuration. | list(object({…})) | | [] |
-| [subnets_private_nat](variables.tf#L303) | List of private NAT subnets. | list(object({…})) | | [] |
-| [subnets_proxy_only](variables.tf#L315) | 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#L349) | List of subnets for Private Service Connect service producers. | list(object({…})) | | [] |
-| [vpc_create](variables.tf#L381) | Create VPC. When set to false, uses a data source to reference existing VPC. | bool | | true |
+| [subnets](variables.tf#L256) | Subnet configuration. | list(object({…})) | | [] |
+| [subnets_private_nat](variables.tf#L305) | List of private NAT subnets. | list(object({…})) | | [] |
+| [subnets_proxy_only](variables.tf#L317) | 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#L351) | List of subnets for Private Service Connect service producers. | list(object({…})) | | [] |
+| [vpc_create](variables.tf#L383) | Create VPC. When set to false, uses a data source to reference existing VPC. | bool | | true |
## Outputs
diff --git a/modules/net-vpc/schemas/subnet.schema.json b/modules/net-vpc/schemas/subnet.schema.json
index f8cb7d55d..48f8093c6 100644
--- a/modules/net-vpc/schemas/subnet.schema.json
+++ b/modules/net-vpc/schemas/subnet.schema.json
@@ -56,9 +56,15 @@
"properties": {
"access_type": {
"type": "string"
+ },
+ "ipv6_only": {
+ "type": "boolean"
}
}
},
+ "ip_collection": {
+ "type": "string"
+ },
"name": {
"type": "string"
},
diff --git a/modules/net-vpc/schemas/subnet.schema.md b/modules/net-vpc/schemas/subnet.schema.md
index 66286ef8e..0022471c6 100644
--- a/modules/net-vpc/schemas/subnet.schema.md
+++ b/modules/net-vpc/schemas/subnet.schema.md
@@ -23,6 +23,8 @@
- **ipv6**: *object*
*additional properties: false*
- **access_type**: *string*
+ - +**ipv6_only**: *boolean*
+- ⁺**ip_collection**: *string*
- **name**: *string*
- ⁺**region**: *string*
- **psc**: *boolean*
diff --git a/modules/net-vpc/subnets.tf b/modules/net-vpc/subnets.tf
index a75f5b7b9..6f14de997 100644
--- a/modules/net-vpc/subnets.tf
+++ b/modules/net-vpc/subnets.tf
@@ -43,7 +43,9 @@ locals {
ip_cidr_range = v.ip_cidr_range
ipv6 = !can(v.ipv6) ? null : {
access_type = try(v.ipv6.access_type, "INTERNAL")
+ ipv6_only = try(v.ipv6.ipv6_only, false)
}
+ ip_collection = try(v.ip_collection, null)
name = try(v.name, k)
region = v.region_computed
secondary_ip_ranges = try(v.secondary_ip_ranges, null)
@@ -139,13 +141,21 @@ locals {
}
resource "google_compute_subnetwork" "subnetwork" {
- provider = google-beta
- for_each = local.subnets
- project = var.project_id
- network = local.network.name
- name = each.value.name
- region = each.value.region
- ip_cidr_range = each.value.ip_cidr_range
+ provider = google-beta
+ for_each = local.subnets
+ project = var.project_id
+ network = local.network.name
+ name = each.value.name
+ region = each.value.region
+ ip_cidr_range = (
+ try(each.value.ipv6, null) != null
+ ? (
+ try(each.value.ipv6.ipv6_only, false)
+ ? null
+ : each.value.ip_cidr_range
+ )
+ : each.value.ip_cidr_range
+ )
allow_subnet_cidr_routes_overlap = each.value.allow_subnet_cidr_routes_overlap
description = (
each.value.description == null
@@ -154,12 +164,19 @@ resource "google_compute_subnetwork" "subnetwork" {
)
private_ip_google_access = each.value.enable_private_access
stack_type = (
- try(each.value.ipv6, null) != null ? "IPV4_IPV6" : null
+ try(each.value.ipv6, null) != null
+ ? (
+ try(each.value.ipv6.ipv6_only, false)
+ ? "IPV6_ONLY"
+ : "IPV4_IPV6"
+ )
+ : null
)
ipv6_access_type = (
try(each.value.ipv6, null) != null ? each.value.ipv6.access_type : null
)
private_ipv6_google_access = try(each.value.ipv6.enable_private_access, null)
+ ip_collection = each.value.ip_collection
send_secondary_ip_range_if_empty = true
dynamic "secondary_ip_range" {
diff --git a/modules/net-vpc/variables.tf b/modules/net-vpc/variables.tf
index 39e6674eb..ebbe9655c 100644
--- a/modules/net-vpc/variables.tf
+++ b/modules/net-vpc/variables.tf
@@ -274,7 +274,9 @@ variable "subnets" {
access_type = optional(string, "INTERNAL")
# this field is marked for internal use in the API documentation
# enable_private_access = optional(string)
+ ipv6_only = optional(bool, false)
}))
+ ip_collection = optional(string, null)
secondary_ip_ranges = optional(map(string))
iam = optional(map(list(string)), {})
iam_bindings = optional(map(object({
diff --git a/tests/modules/net_vpc/examples/ipv6_only.yaml b/tests/modules/net_vpc/examples/ipv6_only.yaml
new file mode 100644
index 000000000..c6001ebd1
--- /dev/null
+++ b/tests/modules/net_vpc/examples/ipv6_only.yaml
@@ -0,0 +1,82 @@
+# 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.
+
+values:
+ module.vpc.google_compute_network.network[0]:
+ auto_create_subnetworks: false
+ delete_default_routes_on_create: false
+ description: Terraform-managed.
+ enable_ula_internal_ipv6: true
+ # internal_ipv6_range: fd20:6b2:27e5:0:0:0:0:0/48
+ name: my-network
+ network_firewall_policy_enforcement_order: AFTER_CLASSIC_FIREWALL
+ project: project-id
+ routing_mode: GLOBAL
+ timeouts: null
+ module.vpc.google_compute_route.gateway["private-googleapis"]:
+ description: Terraform-managed.
+ dest_range: 199.36.153.8/30
+ name: my-network-private-googleapis
+ next_hop_gateway: default-internet-gateway
+ next_hop_ilb: null
+ next_hop_instance: null
+ next_hop_vpn_tunnel: null
+ priority: 1000
+ project: project-id
+ tags: null
+ timeouts: null
+ module.vpc.google_compute_route.gateway["restricted-googleapis"]:
+ description: Terraform-managed.
+ dest_range: 199.36.153.4/30
+ name: my-network-restricted-googleapis
+ next_hop_gateway: default-internet-gateway
+ next_hop_ilb: null
+ next_hop_instance: null
+ next_hop_vpn_tunnel: null
+ priority: 1000
+ project: project-id
+ tags: null
+ timeouts: null
+ module.vpc.google_compute_subnetwork.subnetwork["europe-west1/test-v6only"]:
+ description: Terraform-managed.
+ ipv6_access_type: INTERNAL
+ log_config: []
+ name: test-v6only
+ private_ip_google_access: true
+ project: project-id
+ region: europe-west1
+ role: null
+ stack_type: IPV6_ONLY
+ timeouts: null
+ module.vpc.google_compute_subnetwork.subnetwork["europe-west3/test-v6only"]:
+ description: Terraform-managed.
+ ipv6_access_type: EXTERNAL
+ log_config: []
+ name: test-v6only
+ private_ip_google_access: true
+ project: project-id
+ region: europe-west3
+ role: null
+ stack_type: IPV6_ONLY
+ timeouts: null
+ ip_collection: "https://www.googleapis.com/compute/v1/projects/project-id/regions/europe-west3/publicDelegatedPrefixes/test-sub-pdp"
+
+counts:
+ google_compute_network: 1
+ google_compute_route: 3
+ google_compute_subnetwork: 2
+ modules: 1
+ resources: 6
+
+outputs: {}