diff --git a/fast/stages/2-networking/README.md b/fast/stages/2-networking/README.md
index 0ea3bbeb5..7e9d98b79 100644
--- a/fast/stages/2-networking/README.md
+++ b/fast/stages/2-networking/README.md
@@ -367,7 +367,7 @@ Internally created resources are mapped to context namespaces, and use specific
| [factory-peering.tf](./factory-peering.tf) | VPC Peering factory. | | google_compute_network_peering |
| [factory-projects.tf](./factory-projects.tf) | Projects factory. | project-factory | |
| [factory-routers.tf](./factory-routers.tf) | Routers factory. | | google_compute_router |
-| [factory-vlan-attachments.tf](./factory-vlan-attachments.tf) | VLAN attachments factory. | net-vlan-attachment | |
+| [factory-vlan-attachments.tf](./factory-vlan-attachments.tf) | VLAN attachments factory. | net-vlan-attachment | google_compute_interconnect_attachment_group |
| [factory-vpcs.tf](./factory-vpcs.tf) | VPC and firewall rules factory. | net-vpc · net-vpc-factory | |
| [factory-vpns.tf](./factory-vpns.tf) | VPNs factory. | net-vpn-ha | google_compute_ha_vpn_gateway |
| [main.tf](./main.tf) | Module-level locals and resources. | | |
diff --git a/fast/stages/2-networking/factory-vlan-attachments.tf b/fast/stages/2-networking/factory-vlan-attachments.tf
index d29198a94..7174823f7 100644
--- a/fast/stages/2-networking/factory-vlan-attachments.tf
+++ b/fast/stages/2-networking/factory-vlan-attachments.tf
@@ -66,6 +66,87 @@ locals {
mtu = try(v.mtu, local.vpcs[v.vpc_key].mtu, local.vpc_defaults.mtu, 1500)
})
}
+
+ _attachment_groups_files = try(
+ merge([
+ for vpc_key, vpc in local.vpcs : {
+ for f in try(fileset(
+ try(
+ startswith(vpc.factories_config.attachment_groups, "/") || startswith(vpc.factories_config.attachment_groups, ".") ? vpc.factories_config.attachment_groups :
+ "${vpc.factory_basepath}/${vpc.factories_config.attachment_groups}",
+ "${vpc.factory_basepath}/attachment-groups"
+ ),
+ "**/*.yaml"
+ ), []) :
+ "${vpc_key}-${replace(f, ".yaml", "")}" => {
+ vpc_key = vpc_key
+ filename = f
+ path = try(
+ startswith(vpc.factories_config.attachment_groups, "/") || startswith(vpc.factories_config.attachment_groups, ".")
+ ? "${vpc.factories_config.attachment_groups}/${f}"
+ : "${vpc.factory_basepath}/${vpc.factories_config.attachment_groups}/${f}",
+ "${vpc.factory_basepath}/attachment-groups/${f}"
+ )
+ }
+ }
+ ]...),
+ {}
+ )
+ _attachment_groups_preprocess = {
+ for k, v in local._attachment_groups_files : k => merge(
+ {
+ project_id = local.vpcs[v.vpc_key].project_id
+ },
+ try(yamldecode(file(v.path)), {}),
+ {
+ key = k
+ vpc_key = v.vpc_key
+ }
+ )
+ }
+ attachment_groups = {
+ for k, v in local._attachment_groups_preprocess : k => merge(v, {
+ name = try(v.name, k)
+ intent = try(v.intent, { availability_sla = "NO_SLA" })
+ })
+ }
+
+ ctx_attachment_groups = {
+ for k, v in local.attachment_groups : "${v.vpc_key}/${v.name}" => k
+ }
+
+ ctx_vlan_attachments = {
+ for k, v in local.vlan_attachments : "${v.vpc_key}/${try(v.name, k)}" => k
+ }
+
+ # Gathers all members for each attachment group. Membership can be defined
+ # in two ways:
+ # 1. From the VLAN attachment's config via the `attachment_group` attribute.
+ # 2. From the attachment group's config via the `attachments` map.
+ _attachment_groups_attachments = {
+ for g_key, g_config in local.attachment_groups : g_key =>
+ concat(
+ [
+ for a_key, a_config in local.vlan_attachments : {
+ name = try(a_config.name, a_config.key)
+ attachment = module.vlan-attachments[a_key].id
+ }
+ if try(
+ lookup(local.ctx_attachment_groups, replace(a_config.attachment_group, "$attachment_groups:", ""), a_config.attachment_group),
+ null
+ ) == g_key || (try(a_config.attachment_group, null) == g_config.name && a_config.vpc_key == g_config.vpc_key)
+ ],
+ [
+ for a in values(try(g_config.attachments, {})) : {
+ name = a.name
+ attachment = try(
+ module.vlan-attachments[lookup(local.ctx_vlan_attachments, replace(a.attachment, "$vlan_attachments:", ""), a.attachment)].id,
+ a.attachment
+ )
+ }
+ ]
+ )
+ }
}
module "vlan-attachments" {
@@ -95,3 +176,28 @@ module "vlan-attachments" {
}
depends_on = [module.vpc-factory]
}
+
+resource "google_compute_interconnect_attachment_group" "default" {
+ for_each = local.attachment_groups
+ project = lookup(
+ local.ctx_projects.project_ids,
+ replace(each.value.project_id, "$project_ids:", ""),
+ each.value.project_id
+ )
+ name = each.value.name
+ description = try(each.value.description, "Terraform-managed.")
+
+ intent {
+ availability_sla = try(each.value.intent.availability_sla, "NO_SLA")
+ }
+
+ dynamic "attachments" {
+ for_each = local._attachment_groups_attachments[each.key]
+ content {
+ name = attachments.value.name
+ attachment = attachments.value.attachment
+ }
+ }
+
+ depends_on = [module.vlan-attachments]
+}
diff --git a/fast/stages/2-networking/schemas/attachment-groups.schema.json b/fast/stages/2-networking/schemas/attachment-groups.schema.json
new file mode 100644
index 000000000..f6ede53e1
--- /dev/null
+++ b/fast/stages/2-networking/schemas/attachment-groups.schema.json
@@ -0,0 +1,56 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/blob/master/fast/stages/2-networking/schemas/attachment-groups.schema.json",
+ "title": "Attachment Groups schema",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "project_id": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "intent": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "availability_sla": {
+ "type": "string",
+ "enum": [
+ "NO_SLA",
+ "PRODUCTION_NON_CRITICAL",
+ "PRODUCTION_CRITICAL",
+ "AVAILABILITY_SLA_UNSPECIFIED"
+ ],
+ "default": "NO_SLA"
+ }
+ }
+ },
+ "attachments": {
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-zA-Z0-9-_]+$": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "name",
+ "attachment"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "attachment": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/fast/stages/2-networking/schemas/attachment-groups.schema.md b/fast/stages/2-networking/schemas/attachment-groups.schema.md
new file mode 100644
index 000000000..504e47118
--- /dev/null
+++ b/fast/stages/2-networking/schemas/attachment-groups.schema.md
@@ -0,0 +1,23 @@
+# Attachment Groups schema
+
+
+
+## Properties
+
+*additional properties: false*
+
+- **name**: *string*
+- **project_id**: *string*
+- **description**: *string*
+- **intent**: *object*
+
*additional properties: false*
+ - **availability_sla**: *string*
+
*default: NO_SLA*, *enum: ['NO_SLA', 'PRODUCTION_NON_CRITICAL', 'PRODUCTION_CRITICAL', 'AVAILABILITY_SLA_UNSPECIFIED']*
+- **attachments**: *object*
+
*additional properties: false*
+ - **`^[a-zA-Z0-9-_]+$`**: *object*
+
*additional properties: false*
+ - ⁺**name**: *string*
+ - ⁺**attachment**: *string*
+
+## Definitions
diff --git a/fast/stages/2-networking/schemas/vlan-attachments.schema.json b/fast/stages/2-networking/schemas/vlan-attachments.schema.json
index 2b4f74e59..8cf51587d 100644
--- a/fast/stages/2-networking/schemas/vlan-attachments.schema.json
+++ b/fast/stages/2-networking/schemas/vlan-attachments.schema.json
@@ -13,6 +13,9 @@
"type": "boolean",
"default": true
},
+ "attachment_group": {
+ "type": "string"
+ },
"dedicated_interconnect_config": {
"type": "object",
"additionalProperties": false,
diff --git a/fast/stages/2-networking/schemas/vlan-attachments.schema.md b/fast/stages/2-networking/schemas/vlan-attachments.schema.md
index a39117016..63bb710e8 100644
--- a/fast/stages/2-networking/schemas/vlan-attachments.schema.md
+++ b/fast/stages/2-networking/schemas/vlan-attachments.schema.md
@@ -7,6 +7,7 @@
*additional properties: false*
- **admin_enabled**: *boolean*
+- **attachment_group**: *string*
- **dedicated_interconnect_config**: *object*
*additional properties: false*
- **bandwidth**: *string*
diff --git a/fast/stages/2-networking/schemas/vpc.schema.json b/fast/stages/2-networking/schemas/vpc.schema.json
index 311ededb9..59289339a 100644
--- a/fast/stages/2-networking/schemas/vpc.schema.json
+++ b/fast/stages/2-networking/schemas/vpc.schema.json
@@ -25,6 +25,9 @@
"firewall_rules": {
"type": "string"
},
+ "attachment_groups": {
+ "type": "string"
+ },
"subnets": {
"type": "string"
},
diff --git a/fast/stages/2-networking/schemas/vpc.schema.md b/fast/stages/2-networking/schemas/vpc.schema.md
index d35f0eeef..990e7fa8f 100644
--- a/fast/stages/2-networking/schemas/vpc.schema.md
+++ b/fast/stages/2-networking/schemas/vpc.schema.md
@@ -12,6 +12,7 @@
- **factories_config**: *object*
*additional properties: false*
- **firewall_rules**: *string*
+ - **attachment_groups**: *string*
- **subnets**: *string*
- **vlan_attachments**: *string*
- **vpns**: *string*
diff --git a/tests/fast/stages/s2_networking/data-testvlan/vpcs/hub/attachment-groups/test-group.yaml b/tests/fast/stages/s2_networking/data-testvlan/vpcs/hub/attachment-groups/test-group.yaml
new file mode 100644
index 000000000..ff6cedf82
--- /dev/null
+++ b/tests/fast/stages/s2_networking/data-testvlan/vpcs/hub/attachment-groups/test-group.yaml
@@ -0,0 +1,19 @@
+# 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.
+
+# yaml-language-server: $schema=../../../../../schemas/attachment-groups.schema.json
+
+name: test-group
+intent:
+ availability_sla: PRODUCTION_NON_CRITICAL
diff --git a/tests/fast/stages/s2_networking/data-testvlan/vpcs/hub/vlan-attachments/onprem-0.yaml b/tests/fast/stages/s2_networking/data-testvlan/vpcs/hub/vlan-attachments/onprem-0.yaml
index 77eed92e3..5ce067651 100644
--- a/tests/fast/stages/s2_networking/data-testvlan/vpcs/hub/vlan-attachments/onprem-0.yaml
+++ b/tests/fast/stages/s2_networking/data-testvlan/vpcs/hub/vlan-attachments/onprem-0.yaml
@@ -15,6 +15,7 @@
# yaml-language-server: $schema=../../../../../schemas/vlan-attachments.schema.json
name: to-onprem-vlan-0
+attachment_group: $attachment_groups:hub/test-group
region: $locations:primary
router_config:
create: false
diff --git a/tests/fast/stages/s2_networking/data-testvlan/vpcs/hub/vlan-attachments/onprem-1.yaml b/tests/fast/stages/s2_networking/data-testvlan/vpcs/hub/vlan-attachments/onprem-1.yaml
index 78d6ba43b..e6d1d7b7e 100644
--- a/tests/fast/stages/s2_networking/data-testvlan/vpcs/hub/vlan-attachments/onprem-1.yaml
+++ b/tests/fast/stages/s2_networking/data-testvlan/vpcs/hub/vlan-attachments/onprem-1.yaml
@@ -15,6 +15,7 @@
# yaml-language-server: $schema=../../../../../schemas/vlan-attachments.schema.json
name: to-onprem-vlan-1
+attachment_group: $attachment_groups:hub/test-group
region: $locations:primary
router_config:
create: false
diff --git a/tests/fast/stages/s2_networking/vlan_attachments.yaml b/tests/fast/stages/s2_networking/vlan_attachments.yaml
index c4da065e3..7b17afeab 100644
--- a/tests/fast/stages/s2_networking/vlan_attachments.yaml
+++ b/tests/fast/stages/s2_networking/vlan_attachments.yaml
@@ -21,12 +21,24 @@ values:
labels: null
name: hub-to-onprem
network: hub-0
+ params: []
project: fast-prod-net-core-0
region: europe-west1
stack_type: IPV4_ONLY
terraform_labels:
goog-terraform-provisioned: 'true'
timeouts: null
+ google_compute_interconnect_attachment_group.default["hub-test-group"]:
+ attachments:
+ - name: to-onprem-vlan-0
+ - name: to-onprem-vlan-1
+ description: Terraform-managed.
+ intent:
+ - availability_sla: PRODUCTION_NON_CRITICAL
+ interconnect_group: null
+ name: test-group
+ project: fast-prod-net-core-0
+ timeouts: null
google_compute_router.default["hub/hybrid-connectivity-router"]:
bgp:
- advertise_mode: DEFAULT
@@ -38,6 +50,7 @@ values:
encrypted_interconnect_router: null
md5_authentication_keys: []
name: hub-hybrid-connectivity-router
+ params: []
project: fast-prod-net-core-0
region: europe-west1
timeouts: null
@@ -78,7 +91,10 @@ values:
linked_router_appliance_instances: []
linked_vpc_network: []
linked_vpn_tunnels:
- - include_import_ranges:
+ - exclude_export_ranges: null
+ exclude_import_ranges: null
+ include_export_ranges: null
+ include_import_ranges:
- ALL_IPV4_RANGES
site_to_site_data_transfer: true
location: europe-west1
@@ -93,7 +109,10 @@ values:
goog-terraform-provisioned: 'true'
labels: null
linked_interconnect_attachments:
- - include_import_ranges:
+ - exclude_export_ranges: null
+ exclude_import_ranges: null
+ include_export_ranges: null
+ include_import_ranges:
- ALL_IPV4_RANGES
site_to_site_data_transfer: true
linked_producer_vpc_network: []
@@ -112,7 +131,10 @@ values:
goog-terraform-provisioned: 'true'
labels: null
linked_interconnect_attachments:
- - include_import_ranges:
+ - exclude_export_ranges: null
+ exclude_import_ranges: null
+ include_export_ranges: null
+ include_import_ranges:
- ALL_IPV4_RANGES
site_to_site_data_transfer: true
linked_producer_vpc_network: []
@@ -143,27 +165,11 @@ values:
source: null
temporary_hold: null
timeouts: null
- google_storage_bucket_object.version[0]:
- bucket: test
- cache_control: null
- content_disposition: null
- content_encoding: null
- content_language: null
- contexts: []
- customer_encryption: []
- deletion_policy: null
- detect_md5hash: null
- event_based_hold: null
- force_empty_content_type: null
- metadata: null
- name: versions/2-networking-version.txt
- retention: []
- source: fast_version.txt
- temporary_hold: null
- timeouts: null
module.projects.module.projects-iam["net-core-0"].google_compute_shared_vpc_host_project.shared_vpc_host[0]:
project: fast-prod-net-core-0
timeouts: null
+ module.projects.module.projects["net-core-0"].data.google_logging_project_settings.logging_sa[0]:
+ project: fast-prod-net-core-0
module.projects.module.projects["net-core-0"].google_project.project[0]:
auto_create_network: false
billing_account: 000000-111111-222222
@@ -195,6 +201,10 @@ values:
condition: []
project: fast-prod-net-core-0
role: roles/container.defaultNodeServiceAgent
+ module.projects.module.projects["net-core-0"].google_project_iam_member.service_agents["monitoring-notification"]:
+ condition: []
+ project: fast-prod-net-core-0
+ role: roles/monitoring.notificationServiceAgent
module.projects.module.projects["net-core-0"].google_project_iam_member.service_agents["networkmanagement"]:
condition: []
project: fast-prod-net-core-0
@@ -231,6 +241,18 @@ values:
project: fast-prod-net-core-0
service: iap.googleapis.com
timeouts: null
+ module.projects.module.projects["net-core-0"].google_project_service.project_services["logging.googleapis.com"]:
+ disable_dependent_services: false
+ disable_on_destroy: false
+ project: fast-prod-net-core-0
+ service: logging.googleapis.com
+ timeouts: null
+ module.projects.module.projects["net-core-0"].google_project_service.project_services["monitoring.googleapis.com"]:
+ disable_dependent_services: false
+ disable_on_destroy: false
+ project: fast-prod-net-core-0
+ service: monitoring.googleapis.com
+ timeouts: null
module.projects.module.projects["net-core-0"].google_project_service.project_services["networkmanagement.googleapis.com"]:
disable_dependent_services: false
disable_on_destroy: false
@@ -249,18 +271,6 @@ values:
project: fast-prod-net-core-0
service: servicenetworking.googleapis.com
timeouts: null
- module.projects.module.projects["net-core-0"].google_project_service.project_services["logging.googleapis.com"]:
- disable_dependent_services: false
- disable_on_destroy: false
- project: fast-prod-net-core-0
- service: logging.googleapis.com
- timeouts: null
- module.projects.module.projects["net-core-0"].google_project_service.project_services["monitoring.googleapis.com"]:
- disable_dependent_services: false
- disable_on_destroy: false
- project: fast-prod-net-core-0
- service: monitoring.googleapis.com
- timeouts: null
module.projects.module.projects["net-core-0"].google_project_service.project_services["vpcaccess.googleapis.com"]:
disable_dependent_services: false
disable_on_destroy: false
@@ -279,6 +289,10 @@ values:
project: fast-prod-net-core-0
service: iap.googleapis.com
timeouts: null
+ module.projects.module.projects["net-core-0"].google_project_service_identity.default["monitoring.googleapis.com"]:
+ project: fast-prod-net-core-0
+ service: monitoring.googleapis.com
+ timeouts: null
module.projects.module.projects["net-core-0"].google_project_service_identity.default["networkmanagement.googleapis.com"]:
project: fast-prod-net-core-0
service: networkmanagement.googleapis.com
@@ -322,6 +336,7 @@ values:
labels: null
mtu: '1500'
name: to-onprem-vlan-0
+ params: []
project: fast-prod-net-core-0
region: europe-west1
router: hub-hybrid-connectivity-router
@@ -387,6 +402,7 @@ values:
labels: null
mtu: '1500'
name: to-onprem-vlan-1
+ params: []
project: fast-prod-net-core-0
region: europe-west1
router: hub-hybrid-connectivity-router
@@ -444,6 +460,7 @@ values:
name: hub-0
network_firewall_policy_enforcement_order: AFTER_CLASSIC_FIREWALL
network_profile: null
+ params: []
project: fast-prod-net-core-0
routing_mode: GLOBAL
timeouts: null
@@ -456,6 +473,7 @@ values:
next_hop_ilb: null
next_hop_instance: null
next_hop_vpn_tunnel: null
+ params: []
priority: 1000
project: fast-prod-net-core-0
tags: null
@@ -469,6 +487,7 @@ values:
next_hop_ilb: null
next_hop_instance: null
next_hop_vpn_tunnel: null
+ params: []
priority: 1000
project: fast-prod-net-core-0
tags: null
@@ -482,6 +501,7 @@ values:
next_hop_ilb: null
next_hop_instance: null
next_hop_vpn_tunnel: null
+ params: []
priority: 1000
project: fast-prod-net-core-0
tags: null
@@ -494,6 +514,7 @@ values:
log_config: []
name: hub-default
network: hub-0
+ params: []
private_ip_google_access: true
project: fast-prod-net-core-0
region: europe-west1
@@ -511,6 +532,7 @@ values:
next_hop_ilb: null
next_hop_instance: null
next_hop_vpn_tunnel: null
+ params: []
priority: 1000
project: fast-prod-net-core-0
tags: null
@@ -525,6 +547,7 @@ values:
ipv6_address: null
labels: null
name: hub-to-onprem-default
+ params: []
project: fast-prod-net-core-0
redundancy_type: SINGLE_IP_INTERNALLY_REDUNDANT
terraform_labels:
@@ -606,6 +629,7 @@ values:
ike_version: 2
labels: null
name: hub-to-onprem-remote-0
+ params: []
peer_external_gateway_interface: 0
peer_gcp_gateway: null
project: fast-prod-net-core-0
@@ -627,6 +651,7 @@ values:
ike_version: 2
labels: null
name: hub-to-onprem-remote-1
+ params: []
peer_external_gateway_interface: 0
peer_gcp_gateway: null
project: fast-prod-net-core-0
@@ -657,6 +682,7 @@ counts:
google_compute_external_vpn_gateway: 1
google_compute_ha_vpn_gateway: 1
google_compute_interconnect_attachment: 2
+ google_compute_interconnect_attachment_group: 1
google_compute_network: 1
google_compute_route: 4
google_compute_router: 1
@@ -665,6 +691,7 @@ counts:
google_compute_shared_vpc_host_project: 1
google_compute_subnetwork: 1
google_compute_vpn_tunnel: 2
+ google_logging_project_settings: 1
google_network_connectivity_group: 1
google_network_connectivity_hub: 1
google_network_connectivity_spoke: 3
@@ -675,7 +702,7 @@ counts:
google_storage_bucket_object: 2
modules: 9
random_id: 5
- resources: 64
+ resources: 65
terraform_data: 2
outputs:
@@ -691,3 +718,4 @@ outputs:
hub: {}
subnet_self_links: __missing__
vpc_self_links: __missing__
+