Factory based FAST Networking stage (#3435)

New factory based networking stage, shipping with a single dataset (peering) to keep the PR size somewhat manageable.
This commit is contained in:
Simone Ruffilli
2025-10-23 14:17:44 +02:00
committed by GitHub
parent cc6570f77c
commit 23f8326665
58 changed files with 9503 additions and 1 deletions

View File

@@ -54,6 +54,7 @@ fast
├── 0-bootstrap ├── 0-bootstrap
├── 1-resman ├── 1-resman
├── 1-vpcsc ├── 1-vpcsc
├── 2-networking
├── 2-networking-legacy-a-simple ├── 2-networking-legacy-a-simple
├── 2-networking-legacy-b-nva ├── 2-networking-legacy-b-nva
├── 2-networking-legacy-c-separate-envs ├── 2-networking-legacy-c-separate-envs

View File

@@ -0,0 +1,5 @@
FAST_STAGE_DESCRIPTION="networking"
FAST_STAGE_LEVEL=2
FAST_STAGE_NAME=networking
FAST_STAGE_DEPS="0-globals 0-org-setup"
FAST_STAGE_OPTIONAL="2-networking-ngfw"

View File

@@ -0,0 +1,338 @@
# Shared Networking Resources
<!-- BEGIN TOC -->
- [Quickstart](#quickstart)
- [Select and configure a factory dataset](#select-and-configure-a-factory-dataset)
- [Defaults file](#defaults-file)
- [Terraform variables configuration](#terraform-variables-configuration)
- [Linking FAST output files](#linking-fast-output-files)
- [Terraform init/apply cycle](#terraform-initapply-cycle)
- [Default Dataset: Hub and Spoke with VPC Peering](#default-dataset-hub-and-spoke-with-vpc-peering)
- [Design overview and choices](#design-overview-and-choices)
- [Networking projects](#networking-projects)
- [VPCs](#vpcs)
- [DNS](#dns)
- [Firewall Policies](#firewall-policies)
- [Cloud NAT and Routers](#cloud-nat-and-routers)
- [VPC Connectivity](#vpc-connectivity)
- [Context-based interpolation](#context-based-interpolation)
- [Files](#files)
- [Variables](#variables)
- [Outputs](#outputs)
<!-- END TOC -->
This stage sets up the networking infrastructure for the organization. It provides a flexible, factory-based approach to defining and managing networking resources, including:
- VPCs
- Subnets
- VPC Firewall rules
- Routes
- DNS policies
- DNS
- Forwarding, Public, Private Zones
- Response Policy Rules
- Firewall policies
- VPC connectivity
- Network Connectivity Center (NCC)
- Network Virtual Appliances (NVAs)
- VPC Peering
- VPN
Several common networking patterns are available as "[datasets](datasets/)" that can be easily used as is or adapted as needed.
## Quickstart
This stage is designed to be applied after stage 0, but like any other FAST stage, it can also be used in isolation, provided the required prerequisites are met. This section details the FAST usage, where prerequisites are already in place.
The high-level flow for running this stage is:
- check the **factory data set** as configured in `var.factories_config` and do any necessary edits to match your configuration (VPCs, Subnetting, DNS zones, Onprem connectivity, etc.)
- review the **defaults.yaml file** for your chosen dataset
- define a simple tfvars file if your dataset is in a non-standard path, and/or you want local output files (recommended for initial setups)
- bring in or link the prerequisite files generated by the previous stage
- run `terraform init` and `apply`
### Select and configure a factory dataset
The default dataset describes multiple different networking patterns.
It currently implements the following:
- **Hub and spoke (w/ VPC Peering)**: Environment-based VPCs interconnected through VPC peering, resulting in full isolation between spokes ([dataset](./datasets/hub-and-spokes-peerings/))
### Defaults file
Configurations defaults are stored in the `defaults.yaml` file in the selected dataset. Relocating the defaults file is good practice to avoid accidental changes from upstream, this is done via the `factories_config.default` variable attribute.
Once a suitable place has been found for the file, edit it to match the desired configuration. Several pieces of information coming from the previous stage (prefix, billing account, etc.) are pre-populated in the project defaults so they don't need to be explicitly set.
### Terraform variables configuration
A tfvars file allows you to control paths for the project factories data and to enable local output files generation. This example shows how to override all the factory paths and how to enable output files.
```hcl
factories_config = {
defaults = "/some/path/data/defaults.yaml"
dns = "/some/path/data/dns/zones"
dns-response-policies = "/some/path/data/dns/response-policies"
firewall-policies = "/some/path/data/firewall-policies"
folders = "/some/path/data/folders"
interconnect = "/some/path/data/interconnect"
ncc-hubs = "/some/path/data/ncc-hubs"
nvas = "/some/path/data/nvas"
projects = "/some/path/data/projects"
vpcs = "/some/path/data/vpcs"
}
outputs_location = "~/fast-config"
```
### Linking FAST output files
If you enabled local output files in the previous stage, run this command replacing the example path with the one for your output files.
```bash
../fast-links.sh ~/fast-config
# File linking commands for networking stage
# provider file
ln -s ~/fast-config/providers/2-networking-providers.tf ./
# input files from other stages
ln -s ~/fast-config/tfvars/0-globals.auto.tfvars.json ./
ln -s ~/fast-config/tfvars/0-org-setup.auto.tfvars.json ./
# conventional location for this stage terraform.tfvars (manually managed)
ln -s ~/fast-config/2-networking.auto.tfvars ./
```
If you have no local output files, check the previous state's outputs for the name of your GCS outputs bucket and replace it in the example below.
```bash
../fast-links.sh gs://myprefix-prod-iac-org-0-iac-outputs
# File linking commands for networking stage
# provider file
gcloud storage cp gs://myprefix-prod-iac-org-0-iac-outputs/providers/2-networking-providers.tf ./
# input files from other stages
gcloud storage cp gs://myprefix-prod-iac-org-0-iac-outputs/tfvars/0-globals.auto.tfvars.json ./
gcloud storage cp gs://myprefix-prod-iac-org-0-iac-outputs/tfvars/0-org-setup.auto.tfvars.json ./
# conventional location for this stage terraform.tfvars (manually managed)
gcloud storage cp gs://myprefix-prod-iac-org-0-iac-outputs/2-networking.auto.tfvars ./
```
Once you have one of the above outputs, copy/paste it in your terminal from within this stage's folder.
Note that the last command in both outputs is optional: this is our recommended best practice to centrally store the tfvars file you created for this stage. If this convention works for you, move the tfvars file created in the previous steps to the path shown in the output, then run the command.
### Terraform init/apply cycle
Once everything is set up, simply run the usual `init`/`apply` cycle.
```bash
terraform init
terraform apply
```
## Default Dataset: Hub and Spoke with VPC Peering
This stage includes a default dataset that implements a **Hub and Spoke** topology using [VPC Network Peering](https://cloud.google.com/vpc/docs/vpc-peering). This model is ideal for achieving network isolation between different environments (e.g., development and production) while centralizing shared services and connectivity in a hub network.
The architecture consists of:
- A **hub VPC** (`hub`) for centralized resources, such as DNS and connectivity to on-premises networks (VPN).
- Two **spoke VPCs** (`dev` and `prod`) for different environments.
- **VPC Peering** connections between the hub and each spoke. Because peering is not transitive, the spokes are isolated from each other.
- Each VPC is deployed in its own dedicated **Shared VPC host project**, allowing for clear separation of resources and IAM policies.
For more details on this dataset, refer to its [dedicated README file](./datasets/hub-and-spokes-peerings/README.md).
## Design overview and choices
The following diagram shows the canonical paths for the different factory configurations within a dataset.
```tree
.
├── dns
│ ├── response-policies # Response Policy Rules for DNS.
│ └── zones # DNS zones (private, forwarding, peering).
├── firewall-policies # Hierarchical firewall policies.
├── ncc-hubs # NCC configurations.
├── projects # Project definitions.
└── vpcs
└── [vpc-name] # Each subfolder represents a VPC.
├── .config.yaml # Main VPC configuration, peerings, NAT.
├── firewall-rules # VPC-level firewall rules.
├── subnets # Subnet definitions.
└── vpns # VPN configurations.
```
### Networking projects
A [project factory](../../../modules/project-factory/) is embedded in this stage ([factory-projects.tf](./factory-projects.tf)) to create the projects that will host networking resources.
The project factory is configured via YAML files in the `projects` directory of your chosen dataset. Each file defines a project, its parent folder, the services to be enabled, and IAM bindings. In the default dataset, three projects are created: `net-core-0` for the hub, `net-dev-0` for the development spoke, and `net-prod-0` for the production spoke. All are configured as Shared VPC hosts.
Note that the stage doesn't force you to create projects; factories also allow you to use externally-managed projects by specifying their project ID.
### VPCs
The VPC factory allows for the definition of an arbitrary number of VPCs, along with their subnets, routes, firewall rules, and connectivity settings, all through YAML files.
VPCs are defined in `.config.yaml` files within the `vpcs/[vpc-name]` directory of a dataset. This file contains the VPC's main configuration. Subnets, firewall rules, and VPNs are defined in subdirectories within each VPC's folder.
### DNS
The DNS factory manages Cloud DNS zones and Response Policy Rules. DNS zones are by default defined within the `dns/zones` directory of your chosen dataset. The factory supports private, peering, and forwarding zones.
In the default dataset, DNS is centralized in the `net-core-0` (hub) project. It hosts:
- A **forwarding zone** to resolve on-premises domains.
- A **private zone** for the cloud environment (e.g., `test.`).
- **Peering zones** to make its DNS resolution available to the spoke VPCs.
The spoke VPCs have their own private zones for subdomains (e.g., `dev.test.`) and use the hub for all other DNS lookups.
### Firewall Policies
This stage supports both VPC-level firewall rules and hierarchical firewall policies.
- **Hierarchical Firewall Policies** are defined in the `firewall-policies` directory. They are attached to folders or the organization and are useful for enforcing baseline security rules across multiple projects (e.g., allowing health checks and IAP access).
- **VPC Firewall Rules** are defined within each VPC's configuration directory (`vpcs/[vpc-name]/firewall-rules`). These are used for more specific rules tailored to a particular VPC.
### Cloud NAT and Routers
- **Cloud NAT:** The NAT factory manages Cloud NAT gateways, allowing instances without external IP addresses to access the internet. NAT is configured within each VPC's `.config.yaml` file.
For example:
```yaml
# [...]
nat_config:
nat-ew8:
region: $locations:primary
# [...]
```
- **Cloud Routers:** The `factory-routers.tf` file manages Cloud Routers, which are used with Cloud VPN and Cloud Interconnect to exchange routes with on-premises networks. Routers are configured within each VPC's `.config.yaml` file.
```yaml
# [...]
routers:
vpn-router:
region: $locations:primary
asn: 64514
# [...]
```
### VPC Connectivity
This stage supports multiple ways to connect VPCs:
- **VPC Peering:** Managed via the `peering_config` section in a VPC's `.config.yaml` file.
- **VPNs:** High-availability VPNs are defined in the `vpcs/[vpc-name]/vpns` directory.
- **Network Connectivity Center (NCC):** Managed via the `ncc_config` section in a VPC's `.config.yaml` file.
## Context-based interpolation
Interpolation allows referring to resources that are either created at runtime or externally managed via short aliases.
This feature has two main benefits:
- Being able to refer to resource IDs which are not known before creation (e.g., a VPC's self-link).
- Making YAML configuration files more readable and portable by using mnemonic keys instead of hardcoded values.
The two VPC snippets below show an extended example:
```yaml
# Hub VPC configuration
project_id: $project_ids:net-core-0
name: hub
peering_config:
to-prod:
peer_network: $networks:prod
```
```yaml
# Prod spoke VPC configuration
project_id: $project_ids:net-prod-0
name: prod
peering_config:
to-hub:
peer_network: $networks:hub
```
Interpolations leverage contexts from two separate sources: resources managed by the different factories (projects, folders, vpcs, etc.) and user-defined resource IDs passed in via the `context` variable.
Context replacements use the `$` prefix and are accessible via namespaces that match the attributes in the context variable.
Context variables are accessed by keys that match the YAML file name for resources declared in individual files (projects, folders, custom roles, etc.), or the key in the YAML map where the resource is declared for other resources (service accounts, buckets, etc.). In case of objects that are logically part of another resource (e.g. NCC Groups part of NCC Hubs, Routers part of a VPC), the key will be composite.
Assuming keys of the form `my_folder`, `my_project`, `my_vpc`, etc. this is an example of referencing the actual IDs via interpolation in YAML files.
- `$addresses:my_vpc/my_address`
- `$folder_ids:my_folder`
- `$locations:my_region`
- `$ncc_groups:my_hub/my_group`
- `$ncc_hubs:my_hub`
- `$networks:my_vpc`
- `$project_ids:my_project`
- `$routers:my_vpc/my_vpn`
- `$vpn_gateways:my_vpc/my_gateway`
Internally created resources are mapped to context namespaces, and use specific prefixes to express the relationship with their container.
<!-- TFDOC OPTS files:1 -->
<!-- BEGIN TFDOC -->
## Files
| name | description | modules | resources |
|---|---|---|---|
| [factory-cloudnat.tf](./factory-cloudnat.tf) | Cloud NAT factory. | <code>net-cloudnat</code> | |
| [factory-dns.tf](./factory-dns.tf) | DNS zones and RPZ factory. | <code>dns</code> · <code>dns-response-policy</code> | |
| [factory-firewall-policies.tf](./factory-firewall-policies.tf) | Firewall policies factory. | <code>net-firewall-policy</code> | |
| [factory-ncc.tf](./factory-ncc.tf) | NCC Hubs and Groups factory | | <code>google_network_connectivity_group</code> · <code>google_network_connectivity_hub</code> · <code>google_network_connectivity_spoke</code> |
| [factory-nva.tf](./factory-nva.tf) | NVA factory | <code>compute-vm</code> · <code>net-lb-int</code> | <code>google_compute_instance_group</code> |
| [factory-peering.tf](./factory-peering.tf) | VPC Peering factory. | | <code>google_compute_network_peering</code> |
| [factory-projects.tf](./factory-projects.tf) | Projects factory. | <code>project-factory</code> | |
| [factory-routers.tf](./factory-routers.tf) | Routers factory. | | <code>google_compute_router</code> |
| [factory-vpcs.tf](./factory-vpcs.tf) | VPC and firewall rules factory. | <code>net-vpc</code> · <code>net-vpc-firewall</code> | |
| [factory-vpns.tf](./factory-vpns.tf) | VPNs factory. | <code>net-vpn-ha</code> | <code>google_compute_ha_vpn_gateway</code> |
| [main.tf](./main.tf) | Module-level locals and resources. | | |
| [outputs.tf](./outputs.tf) | Module outputs. | | <code>google_storage_bucket_object</code> · <code>local_file</code> |
| [variables-fast.tf](./variables-fast.tf) | None | | |
| [variables.tf](./variables.tf) | Module variables. | | |
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [automation](variables-fast.tf#L17) | Automation resources created by the bootstrap stage. | <code title="object&#40;&#123;&#10; outputs_bucket &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [billing_account](variables-fast.tf#L26) | Billing account id. | <code title="object&#40;&#123;&#10; id &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [organization](variables-fast.tf#L58) | Organization details. | <code title="object&#40;&#123;&#10; id &#61; number&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [prefix](variables-fast.tf#L67) | Prefix used for resources that need unique names. Use a maximum of 9 chars for organizations, and 11 chars for tenants. | <code>string</code> | ✓ | |
| [context](variables.tf#L17) | Context-specific interpolations. | <code title="object&#40;&#123;&#10; custom_roles &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; folder_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; iam_principals &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; locations &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; project_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; tag_keys &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; tag_values &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [custom_roles](variables-fast.tf#L34) | Custom roles defined at the org level, in key => id format. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [factories_config](variables.tf#L32) | Configuration for the resource factories or external data. | <code title="object&#40;&#123;&#10; defaults &#61; optional&#40;string, &#34;datasets&#47;hub-and-spokes-peerings&#47;defaults.yaml&#34;&#41;&#10; dns &#61; optional&#40;string, &#34;datasets&#47;hub-and-spokes-peerings&#47;dns&#47;zones&#34;&#41;&#10; dns-response-policies &#61; optional&#40;string, &#34;datasets&#47;hub-and-spokes-peerings&#47;dns&#47;response-policies&#34;&#41;&#10; firewall-policies &#61; optional&#40;string, &#34;datasets&#47;hub-and-spokes-peerings&#47;firewall-policies&#34;&#41;&#10; folders &#61; optional&#40;string, &#34;datasets&#47;hub-and-spokes-peerings&#47;folders&#34;&#41;&#10; ncc-hubs &#61; optional&#40;string, &#34;datasets&#47;hub-and-spokes-peerings&#47;ncc-hubs&#34;&#41;&#10; nvas &#61; optional&#40;string, &#34;datasets&#47;hub-and-spokes-peerings&#47;nvas&#34;&#41;&#10; projects &#61; optional&#40;string, &#34;datasets&#47;hub-and-spokes-peerings&#47;projects&#34;&#41;&#10; vpcs &#61; optional&#40;string, &#34;datasets&#47;hub-and-spokes-peerings&#47;vpcs&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [folder_ids](variables-fast.tf#L42) | Folders created in the bootstrap stage. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [iam_principals](variables-fast.tf#L50) | IAM-format principals. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [outputs_location](variables.tf#L49) | Path where tfvars files for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> |
| [project_ids](variables-fast.tf#L77) | Projects created in the bootstrap stage. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [service_accounts](variables-fast.tf#L85) | Service accounts created in the bootstrap stage. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [tag_keys](variables-fast.tf#L93) | FAST-managed resource manager tag keys. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [tag_values](variables-fast.tf#L101) | FAST-managed resource manager tag values. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [universe](variables.tf#L55) | GCP universe where to deploy projects. The prefix will be prepended to the project id. | <code title="object&#40;&#123;&#10; domain &#61; string&#10; prefix &#61; string&#10; forced_jit_service_identities &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; unavailable_services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; unavailable_service_identities &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [host_project_ids](outputs.tf#L65) | Project IDs. | |
| [host_project_numbers](outputs.tf#L70) | Project numbers. | |
| [subnet_proxy_only_self_links](outputs.tf#L75) | Subnet proxy-only self-links. | |
| [subnet_psc_self_links](outputs.tf#L80) | Subnet PSC self-links. | |
| [subnet_self_links](outputs.tf#L85) | Subnet self-links. | |
| [vpc_self_links](outputs.tf#L90) | VPC self-links. | |
<!-- END TFDOC -->

View File

@@ -0,0 +1,104 @@
#cloud-config
write_files:
- path: /var/run/nva/update-pbrs.sh
permissions: "0755"
content: |
#!/bin/bash
# Continuous update of PBRs for an interface.
set -e
IF_INDEX=$1
METADATA_URL="http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces"
METADATA_HEADER="Metadata-Flavor: Google"
FORWARDED_IPS=$(curl -s -H "$${METADATA_HEADER}" "$${METADATA_URL}/$${IF_INDEX}/forwarded-ips/")
IF_NAME="eth$${IF_INDEX}"
TABLE_ID=$((110 + IF_INDEX))
echo "Starting."
GATEWAY=$(curl -s -H "$${METADATA_HEADER}" "$${METADATA_URL}/$${IF_INDEX}/gateway")
if [ -z "$${GATEWAY}" ]; then
echo "Could not retrieve gateway. Exiting."
exit 1
fi
echo "Initial PBR configuration using table $${TABLE_ID}."
ip route add default via $${GATEWAY} dev $${IF_NAME} proto static onlink table $${TABLE_ID} || true
while true; do
echo "Checking for PBR updates."
FORWARDED_IP_IDS=$(curl -s -H "$${METADATA_HEADER}" "$${METADATA_URL}/$${IF_INDEX}/forwarded-ips/")
FORWARDED_IPS=""
for ID in $${FORWARDED_IP_IDS}; do
IP=$(curl -s -H "$${METADATA_HEADER}" "$${METADATA_URL}/$${IF_INDEX}/forwarded-ips/$${ID}")
FORWARDED_IPS="$${FORWARDED_IPS} $${IP}"
done
CURRENT_RULE_IPS=$(ip rule show | grep "lookup $${TABLE_ID}" | awk -F 'from ' '/from/ {split($2, a, " "); print a[1]}' | sort -u || true)
for FW_IP in $${FORWARDED_IPS}; do
if ! echo "$${CURRENT_RULE_IPS}" | grep -q "^$${FW_IP}$"; then
echo "Adding PBR rules for forwarded IP $${FW_IP}."
ip rule add from $${FW_IP} to 35.191.0.0/16 lookup $${TABLE_ID}
ip rule add from $${FW_IP} to 130.211.0.0/22 lookup $${TABLE_ID}
fi
done
for RULE_IP in $${CURRENT_RULE_IPS}; do
if ! echo "$${FORWARDED_IPS}" | grep -q "^$${RULE_IP}$"; then
echo "Removing PBR rules for forwarded IP $${RULE_IP}."
ip rule del from $${RULE_IP} to 35.191.0.0/16 lookup $${TABLE_ID} || true
ip rule del from $${RULE_IP} to 130.211.0.0/22 lookup $${TABLE_ID} || true
fi
done
sleep 5
done
runcmd:
- |
#!/bin/bash
set -ex
echo "Starting NVA network configuration..."
echo "Enabling IP forwarding."
echo "net.ipv4.ip_forward = 1" > /etc/sysctl.d/90-ip-forwarding.conf
sysctl -p /etc/sysctl.d/90-ip-forwarding.conf
iptables -I FORWARD -j ACCEPT
METADATA_URL="http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces"
METADATA_HEADER="Metadata-Flavor: Google"
INTERFACE_INDEXES=$(curl -s -H "$${METADATA_HEADER}" "$${METADATA_URL}/" | sed 's:/$::')
for i in $${INTERFACE_INDEXES}; do
IF_NAME="eth$${i}"
echo "Configuring interface $${IF_NAME}..."
GATEWAY=$(curl -s -H "$${METADATA_HEADER}" "$${METADATA_URL}/$${i}/gateway")
if [ -z "$${GATEWAY}" ]; then
echo "Could not retrieve gateway for $${IF_NAME}. Skipping."
continue
fi
echo "Gateway for $${IF_NAME} is $${GATEWAY}."
%{ for nic_index, nic in nva_nics_config ~}
if [ "$${i}" == "${nic_index}" ]; then
%{ for route in nic.routes ~}
echo "Adding route for ${route} via $${GATEWAY} on $${IF_NAME}"
ip route add ${route} via $${GATEWAY} dev $${IF_NAME} proto static
%{ endfor ~}
%{ if try(nic.masquerade, false) ~}
echo "Enabling NAT (Masquerade) on $${IF_NAME}."
iptables -t nat -A POSTROUTING -o $${IF_NAME} -j MASQUERADE
%{ endif ~}
fi
%{ endfor ~}
echo "Starting continuous PBR update for eth$${i} in the background."
nohup /var/run/nva/update-pbrs.sh $i >> /var/log/nva-pbr-update.echo 2>&1 &
done
echo "NVA network configuration complete."

View File

@@ -0,0 +1,114 @@
# Hub and spoke with VPC Peerings
This stage sets up the shared network infrastructure environment, and leverages [VPC Network Peering](https://cloud.google.com/vpc/docs/vpc-peering) to implement a Hub and Spoke topology with a hub and two spokes.
Leveraging the lack of transitivity when using VPC peerings, the spoke VPCs have no routing line-of-sight with each other, which is ideal when implementing isolated environment. However anything that transit through the hub will be able to reach destinations in both spokes.
Transitivity between spokes could be achieved by adding a (potentially one-legged) network virtual appliance acting as a router.
The following diagram illustrates the high-level design, and should be used as a reference for the following sections.
<p align="center">
<img src="diagram.svg" alt="Peerings diagram">
</br>Peering diagram
</p>
### VPC design
The hub VPC hosts external connectivity (by default VPN tunnels), and centralises the DNS configuration.
The default dataset ships two different VPCs, mapping to hypotetical environments (dev and prod). Each VPC is created into its own project, and each project is configured as a Shared VPC host, so that network-related resources and access configurations via IAM are kept separate for each VPC.
The design easily lends itself to implementing additional environments, or adopting a different logical mapping for spokes (e.g. one spoke for each company entity, etc.).
### IP ranges, subnetting, routing
Minimizing the number of routes (and subnets) in use on the cloud environment is an important consideration, as it simplifies management and avoids hitting [Cloud Router](https://cloud.google.com/network-connectivity/docs/router/quotas) and [VPC](https://cloud.google.com/vpc/docs/quota) quotas and limits. For this reason, we recommend careful planning of the IP space used in your cloud environment, to be able to use large IP CIDR blocks in routes whenever possible.
This stage uses a dedicated /16 block (which should of course be sized to your needs) for each region in each VPC, and subnets created in each VPC should derive their ranges from the relevant block.
The Prod Spoke VPC also define and reserve - as an example - two "special" CIDR ranges dedicated to [PSA (Private Service Access)](https://cloud.google.com/vpc/docs/private-services-access) and [Internal Application Load Balancers (L7 LBs)](https://cloud.google.com/load-balancing/docs/l7-internal).
Routes in GCP are either automatically created for VPC subnets, manually created via static routes, programmed by the NCC hub or dynamically programmed by [Cloud Routers](https://cloud.google.com/network-connectivity/docs/router#docs) via BGP sessions, which can be configured to advertise VPC ranges, and/or custom ranges via custom advertisements.
Furthermore:
- routes between multiple subnets within the same VPC are automatically programmed by GCP
- each spoke exchanges routes with the HUB
- on-premises is connected to the hub VPC and dynamically exchanges BGP routes with GCP using HA VPN. The HA VPN tunnels program routes in the hub VPC which then exports them to the spoke VPCs.
### Peering Configuration
VPC peering is controlled by each VPC `.config` file, as per the example below.
[vpcs/hub/.config.yaml](./vpcs/hub/.config.yaml)
```yaml
# [...]
peering_config:
to-prod:
peer_network: $networks:prod
to-dev:
peer_network: $networks:dev
# [...]
```
[vpcs/prod/.config.yaml](./vpcs/prod/.config.yaml)
```yaml
# [...]
peering_config:
to-hub:
peer_network: $networks:hub
# [...]
```
For more informations about cross referencing resources, please check the [main README.md file](../../README.md)
### Internet egress
Cloud NAT provides the simplest path for internet egress. This setup uses Cloud NAT, which is enabled by default on the primary region on every VPC.
e.g. in [vpcs/prod/.config.yaml](./vpcs/prod/.config.yaml)
```yaml
# [...]
nat_config:
nat-primary:
region: $locations:primary
# [...]
```
Several other scenarios are possible through ad-hoc implementations, with varying degrees of complexity:
- a forward proxy (including [SWP](https://cloud.google.com/secure-web-proxy/docs/overview)), with optional URL filters
- a default route to on-prem to leverage existing egress infrastructure
- a full-fledged perimeter firewall to control egress and implement additional security features like IPS
### VPC and Hierarchical Firewall
The GCP Firewall is a stateful, distributed feature that allows the creation of L4 policies, either via VPC-level rules or more recently via hierarchical policies applied on the resource hierarchy (organization, folders).
The current setup adopts both firewall types, and uses [hierarchical rules on the Networking folder](./firewall-policies/networking-policy.yaml) for common ingress rules, e.g. from health check or IAP forwarders ranges, and [VPC rules](./vpcs/prod/firewall-rules) for the environment or workload-level ingress.
### DNS
This dataset implements a centralized DNS architecture that handles resolution between GCP and on-premises environments.
- **Cloud to on-prem:** A [forwarding zone](./dns/zones/net-core-0/fwd-root.yaml) for the `onprem.` domain is configured in the hub VPC. It forwards DNS queries for on-premises resources to the on-premises DNS resolvers.
- **On-prem to cloud:** An [inbound DNS policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) allows on-premises systems to resolve resources in GCP.
DNS configuration is centralized in the hub project (`net-core-0`) and shared with the spokes using DNS peering:
- The **hub** hosts:
- A top-level private zone for the cloud environment (e.g., `test.`).
- The forwarding zone to on-premises.
- The **spokes** (`net-dev-0`, `net-prod-0`) host private zones for their specific subdomains (e.g., `dev.test.`, `prod.test.`). These zones are visible to the hub.
- A **peering zone** for the `.` (root) domain is configured in the spokes, pointing to the hub. This delegates all DNS resolution from the spokes to the hub, creating a centralized model.
- **Private Google Access** is enabled via [DNS Response Policies](https://cloud.google.com/dns/docs/zones/manage-response-policies#create-response-policy-rule) for most of the [supported domains](https://cloud.google.com/vpc/docs/configure-private-google-access#domain-options).
To complete the configuration, on-premises DNS servers should be configured to forward queries for your cloud domain (e.g., `test.`) to the GCP inbound policy's IP addresses. Additionally, the `35.199.192.0/19` range (used by the inbound forwarder) should be routed over the VPN tunnels from on-premises.
### VPNs
Connectivity to on-prem is implemented with HA VPN ([`net-vpn`](../../../../../modules/net-vpn-ha/)) and defined in [`onprem.yaml`](./vpcs/hub/vpns/onprem.yaml). The file provisionally implements a single logical connection between onprem and the hub on the primary region through 2 IPSec tunnels.

View File

@@ -0,0 +1,39 @@
# 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.
context:
cidr_ranges_sets:
healthchecks:
- 35.191.0.0/16
- 130.211.0.0/22
- 209.85.152.0/22
- 209.85.204.0/22
rfc1918:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
locations:
primary: europe-west8
secondary: europe-west12
iam_principals: {}
projects:
defaults:
locations:
storage: eu
vpcs:
auto_create_subnetworks: false
delete_default_route_on_create: true
mtu: 1500

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 281 KiB

View File

@@ -0,0 +1,156 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../../schemas/dns-response-policy-rules.schema.json
project_id: $project_ids:net-core-0
networks:
- $networks:hub
- $networks:prod
- $networks:dev
rules:
accounts:
dns_name: "accounts.google.com."
behavior: bypassResponsePolicy
aiplatform-notebook-cloud-all:
dns_name: "*.aiplatform-notebook.cloud.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
aiplatform-notebook-gu-all:
dns_name: "*.aiplatform-notebook.googleusercontent.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
appengine:
dns_name: "appengine.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
appspot-all:
dns_name: "*.appspot.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
backupdr-cloud:
dns_name: "backupdr.cloud.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
backupdr-cloud-all:
dns_name: "*.backupdr.cloud.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
backupdr-gu:
dns_name: "backupdr.googleusercontent.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
backupdr-gu-all:
dns_name: "*.backupdr.googleusercontent.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
cloudfunctions:
dns_name: "*.cloudfunctions.net."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
cloudproxy:
dns_name: "*.cloudproxy.app."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
composer-cloud-all:
dns_name: "*.composer.cloud.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
composer-gu-all:
dns_name: "*.composer.googleusercontent.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
datafusion-all:
dns_name: "*.datafusion.cloud.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
datafusion-gu-all:
dns_name: "*.datafusion.googleusercontent.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
dataproc:
dns_name: "dataproc.cloud.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
dataproc-all:
dns_name: "*.dataproc.cloud.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
dataproc-gu:
dns_name: "dataproc.googleusercontent.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
dataproc-gu-all:
dns_name: "*.dataproc.googleusercontent.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
dl:
dns_name: "dl.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
gcr:
dns_name: "gcr.io."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
gcr-all:
dns_name: "*.gcr.io."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
gke-all:
dns_name: "*.gke.goog."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
googleapis-all:
dns_name: "*.googleapis.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
googleapis-private:
dns_name: "private.googleapis.com."
local_data:
A:
rrdatas:
- 199.36.153.8
- 199.36.153.9
- 199.36.153.10
- 199.36.153.11
AAAA:
rrdatas:
- "2600:2d00:2:2000::"
googleapis-restricted:
dns_name: "restricted.googleapis.com."
local_data:
A:
rrdatas:
- 199.36.153.4
- 199.36.153.5
- 199.36.153.6
- 199.36.153.7
AAAA:
rrdatas:
- "2600:2d00:2:1000::"
gstatic-all:
dns_name: "*.gstatic.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
kernels-gu:
dns_name: "kernels.googleusercontent.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
kernels-gu-all:
dns_name: "*.kernels.googleusercontent.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
ltsapis-all:
dns_name: "*.ltsapis.goog."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
notebooks:
dns_name: "notebooks.cloud.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
notebooks-all:
dns_name: "*.notebooks.cloud.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
notebooks-gu-all:
dns_name: "*.notebooks.googleusercontent.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
packages-cloud:
dns_name: "packages.cloud.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
packages-cloud-all:
dns_name: "*.packages.cloud.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
pkgdev:
dns_name: "pkg.dev."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
pkgdev-all:
dns_name: "*.pkg.dev."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
pkigoog:
dns_name: "pki.goog."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
pkigoog-all:
dns_name: "*.pki.goog."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
run-all:
dns_name: "*.run.app."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
source:
dns_name: "source.developers.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }
storage:
dns_name: "storage.cloud.google.com."
local_data: { CNAME: { rrdatas: ["private.googleapis.com."] } }

View File

@@ -0,0 +1,14 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../../../schemas/dns.schema.json
project_id: $project_ids:net-core-0
domain: onprem.
forwarding:
forwarders:
"8.8.8.8": default
"1.1.1.1": default
client_networks:
- $networks:hub

View File

@@ -0,0 +1,13 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../../../schemas/dns.schema.json
project_id: $project_ids:net-core-0
domain: .
peering:
peer_network: $networks:hub
client_networks:
- $networks:prod
- $networks:dev

View File

@@ -0,0 +1,14 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../../../schemas/dns.schema.json
project_id: $project_ids:net-core-0
domain: test.
private:
client_networks:
- $networks:hub
recordsets:
"A localhost":
records: ["127.0.0.1"]

View File

@@ -0,0 +1,15 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../../../schemas/dns.schema.json
project_id: $project_ids:net-dev-0
domain: dev.test.
private:
client_networks:
- $networks:hub
- $networks:dev
recordsets:
"A localhost":
records: ["127.0.0.1"]

View File

@@ -0,0 +1,15 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../../../schemas/dns.schema.json
project_id: $project_ids:net-prod-0
domain: prod.test.
private:
client_networks:
- $networks:hub
- $networks:prod
recordsets:
"A localhost":
records: ["127.0.0.1"]

View File

@@ -0,0 +1,55 @@
# skip boilerplate check
# yaml-language-server: $schema=../../../schemas/firewall-policy.schema.json
parent_id: $folder_ids:networking
attachments:
networking: $folder_ids:networking
name: network-policies
ingress_rules:
allow-healthchecks:
description: Enable SSH, HTTP and HTTPS healthchecks
priority: 1001
match:
source_ranges:
- $cidr_ranges_sets:healthchecks
layer4_configs:
- protocol: tcp
ports: ["22", "80", "443"]
allow-ssh-from-iap:
description: Enable SSH from IAP
priority: 1002
enable_logging: true
match:
source_ranges:
- 35.235.240.0/20
layer4_configs:
- protocol: tcp
ports: ["22"]
allow-icmp:
description: Enable ICMP
priority: 1003
match:
source_ranges:
- 0.0.0.0/0
layer4_configs:
- protocol: icmp
allow-nat-ranges:
description: Enable NAT ranges for VPC serverless connector
priority: 1004
match:
source_ranges:
- 107.178.230.64/26
- 35.199.224.0/19
egress_rules:
deny-example-ip:
description: Allow internal traffic within the VPC
priority: 2000
match:
destination_ranges:
- 1.2.3.4/32
layer4_configs:
- protocol: all

View File

@@ -0,0 +1,19 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../schemas/project.schema.json
name: prod-net-core-0
parent: $folder_ids:networking
services:
- container.googleapis.com
- compute.googleapis.com
- dns.googleapis.com
- iap.googleapis.com
- networkmanagement.googleapis.com
- networksecurity.googleapis.com
- servicenetworking.googleapis.com
- stackdriver.googleapis.com
- vpcaccess.googleapis.com
shared_vpc_host_config:
enabled: true

View File

@@ -0,0 +1,20 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../schemas/project.schema.json
name: dev-net-dev-0
parent: $folder_ids:networking/dev
services:
- container.googleapis.com
- compute.googleapis.com
- dns.googleapis.com
- iap.googleapis.com
- networkmanagement.googleapis.com
- networksecurity.googleapis.com
- servicenetworking.googleapis.com
- stackdriver.googleapis.com
- vpcaccess.googleapis.com
shared_vpc_host_config:
enabled: true

View File

@@ -0,0 +1,20 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../schemas/project.schema.json
name: prod-net-prod-0
parent: $folder_ids:networking/prod
services:
- container.googleapis.com
- compute.googleapis.com
- dns.googleapis.com
- iap.googleapis.com
- networkmanagement.googleapis.com
- networksecurity.googleapis.com
- servicenetworking.googleapis.com
- stackdriver.googleapis.com
- vpcaccess.googleapis.com
shared_vpc_host_config:
enabled: true

View File

@@ -0,0 +1,16 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../../schemas/vpc.schema.json
project_id: $project_ids:net-dev-0
name: dev
delete_default_routes_on_create: false
mtu: 1500
nat_config:
nat-ew8:
region: $locations:primary
peering_config:
to-hub:
peer_network: $networks:hub

View File

@@ -0,0 +1,13 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../../../schemas/firewall-rules.schema.json
ingress:
ingress-default-dev-deny:
description: "Deny and log any unmatched ingress traffic."
deny: true
priority: 65535
enable_logging:
include_metadata: false

View File

@@ -0,0 +1,8 @@
# skip boilerplate check
# yaml-language-server: $schema=../../../../../schemas/subnet.schema.json
name: dev-default
region: $locations:secondary
ip_cidr_range: 10.73.0.0/24
description: Default europe-west12 subnet for dev

View File

@@ -0,0 +1,27 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../../schemas/vpc.schema.json
project_id: $project_ids:net-core-0
name: hub
delete_default_routes_on_create: false
nat_config:
nat-ew8:
region: $locations:primary
routers:
vpn-router:
region: $locations:primary
asn: 64514
routes:
gateway:
dest_range: "8.8.8.8/32"
priority: 100
next_hop_type: "gateway"
next_hop: "default-internet-gateway"
peering_config:
to-prod:
peer_network: $networks:prod
to-dev:
peer_network: $networks:dev

View File

@@ -0,0 +1,13 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../../../schemas/firewall-rules.schema.json
ingress:
ingress-default-landing-deny:
description: "Deny and log any unmatched ingress traffic."
deny: true
priority: 65535
enable_logging:
include_metadata: false

View File

@@ -0,0 +1,8 @@
# skip boilerplate check
# yaml-language-server: $schema=../../../../../schemas/subnet.schema.json
name: hub-default
region: $locations:secondary
ip_cidr_range: 10.71.0.0/24
description: Default europe-west12 subnet for hub

View File

@@ -0,0 +1,34 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../../../schemas/vpn.schema.json
name: to-onprem
region: $locations:primary
peer_gateways:
default:
external:
redundancy_type: SINGLE_IP_INTERNALLY_REDUNDANT
interfaces:
- 8.8.8.8
router_config:
create: false
name: $routers:hub/vpn-router
tunnels:
remote-0:
bgp_peer:
address: 169.254.128.1
asn: 64513
bgp_session_range: "169.254.128.2/30"
peer_external_gateway_interface: 0
shared_secret: "mySecret"
vpn_gateway_interface: 0
remote-1:
bgp_peer:
address: 169.254.128.5
asn: 64513
bgp_session_range: "169.254.128.6/30"
peer_external_gateway_interface: 0
shared_secret: "mySecret"
vpn_gateway_interface: 1

View File

@@ -0,0 +1,28 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../../schemas/vpc.schema.json
project_id: $project_ids:net-prod-0
name: prod
delete_default_routes_on_create: false
mtu: 1500
nat_config:
nat-ew8:
region: $locations:primary
peering_config:
to-hub:
peer_network: $networks:hub
psa_configs:
- ranges:
psa: 10.72.224.0/24
export_routes: true
import_routes: true
peered_domains:
- "test."
subnets_proxy_only:
- ip_cidr_range: 10.72.240.0/24
region: $locations:primary
name: primary-region-proxy-only
active: true

View File

@@ -0,0 +1,13 @@
# skip boilerplate check
---
# start of document (---) avoids errors if the file only contains comments
# yaml-language-server: $schema=../../../../../schemas/firewall-rules.schema.json
ingress:
ingress-default-prod-deny:
description: "Deny and log any unmatched ingress traffic."
deny: true
priority: 65535
enable_logging:
include_metadata: false

View File

@@ -0,0 +1,8 @@
# skip boilerplate check
# yaml-language-server: $schema=../../../../../schemas/subnet.schema.json
name: prod-default
region: $locations:primary
ip_cidr_range: 10.72.0.0/24
description: Default europe-west12 subnet for prod

View File

@@ -0,0 +1,65 @@
/**
* 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.
*/
# tfdoc:file:description Cloud NAT factory.
locals {
nat_configs = merge(flatten([
for vpc_key, vpc_config in local.vpcs : [
for nat_key, nat_config in try(vpc_config.nat_config, {}) : {
"${vpc_key}/${nat_key}" = merge(nat_config, {
name = replace("${vpc_key}/${nat_key}", "/", "-")
project_id = vpc_config.project_id
addresses = try(nat_config.addresses, [])
config_port_allocation = try(nat_config.config_port_allocation, {})
config_source_subnetworks = try(nat_config.config_source_subnetworks, {})
config_timeouts = try(nat_config.config_timeouts, {})
endpoint_types = try(nat_config.endpoint_types, null)
logging_filter = try(nat_config.logging_filter, null)
router_asn = try(nat_config.router_asn, null)
router_create = try(nat_config.router_create, true)
router_network = module.vpcs[vpc_key].self_link
rules = try(nat_config.rules, [])
type = try(nat_config.type, "PUBLIC")
})
}
]
])...)
}
module "nat" {
source = "../../../modules/net-cloudnat"
for_each = local.nat_configs
project_id = each.value.project_id
name = each.value.name
addresses = each.value.addresses
config_port_allocation = each.value.config_port_allocation
config_source_subnetworks = each.value.config_source_subnetworks
config_timeouts = each.value.config_timeouts
endpoint_types = each.value.endpoint_types
logging_filter = each.value.logging_filter
region = each.value.region
router_asn = each.value.router_asn
router_create = each.value.router_create
router_network = each.value.router_network
rules = each.value.rules
type = each.value.type
context = merge(local.ctx, {
project_ids = local.ctx_projects.project_ids
vpc_self_links = local.ctx_vpcs.self_links
locations = local.ctx.locations
})
}

View File

@@ -0,0 +1,126 @@
/**
* 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.
*/
# tfdoc:file:description DNS zones and RPZ factory.
locals {
_dns_path = try(pathexpand(var.factories_config.dns), null)
_dns_files = try(fileset(local._dns_path, "**/*.yaml"), [])
_dns_preprocess = [
for f in local._dns_files : merge(yamldecode(file("${coalesce(local._dns_path, "-")}/${f}")), {
key = replace(f, ".yaml", "")
})
]
dns_zones = {
for zone_config in local._dns_preprocess : "${zone_config.key}" => merge(
zone_config,
{
project_id = zone_config.project_id
name = replace(zone_config.key, "/", "-")
description = try(zone_config.description, "Terraform-managed.")
force_destroy = try(zone_config.force_destroy, null)
iam = try(zone_config.iam, null)
recordsets = try(zone_config.recordsets, null)
},
{
zone_config = merge(
{ domain = try(zone_config.domain, null) },
contains(keys(try(zone_config, {})), "private")
? {
private = {
service_directory_namespace = try(
zone_config.private.service_directory_namespace, null
)
client_networks = zone_config.private.client_networks
}
}
: {},
contains(keys(try(zone_config, {})), "peering")
? {
peering = {
peer_network = zone_config.peering.peer_network
client_networks = zone_config.peering.client_networks
}
}
: {},
contains(keys(try(zone_config, {})), "forwarding")
? {
forwarding = {
forwarders = try(zone_config.forwarding.forwarders, {}),
client_networks = zone_config.forwarding.client_networks
}
}
: {}
)
}
)
}
# DNS response policies
_dns_response_policies_path = try(
pathexpand(var.factories_config.dns-response-policies), null
)
_dns_response_policies_files = try(
fileset(local._dns_response_policies_path, "**/*.yaml"), []
)
_dns_response_policies_preprocess = [
for f in local._dns_response_policies_files :
merge(
yamldecode(file("${coalesce(local._dns_response_policies_path, "-")}/${f}")),
{
key = replace(f, ".yaml", "")
}
)
]
dns_response_policies = {
for policy_config in local._dns_response_policies_preprocess : "${policy_config.key}" => {
project_id = policy_config.project_id
name = policy_config.key
networks = policy_config.networks
rules = policy_config.rules
}
}
}
module "dns-zones" {
source = "../../../modules/dns"
for_each = local.dns_zones
project_id = each.value.project_id
name = each.value.name
description = each.value.description
force_destroy = each.value.force_destroy
iam = each.value.iam
zone_config = each.value.zone_config
recordsets = each.value.recordsets
context = {
project_ids = local.ctx_projects.project_ids
networks = local.ctx_vpcs.self_links
}
depends_on = [module.vpcs]
}
module "dns-response-policies" {
source = "../../../modules/dns-response-policy"
for_each = local.dns_response_policies
project_id = each.value.project_id
name = each.value.name
networks = { for n in each.value.networks : n => n }
rules = each.value.rules
context = {
project_ids = local.ctx_projects.project_ids
networks = local.ctx_vpcs.self_links
}
depends_on = [module.vpcs]
}

View File

@@ -0,0 +1,53 @@
/**
* 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.
*/
# tfdoc:file:description Firewall policies factory.
locals {
_firewall_policies_path = try(
pathexpand(var.factories_config.firewall-policies), null
)
_firewall_policies_files = local._firewall_policies_path == null ? [] : fileset(
local._firewall_policies_path, "**/*.yaml"
)
_firewall_policies_data = {
for f in local._firewall_policies_files : replace(f, ".yaml", "") => yamldecode(
file("${local._firewall_policies_path}/${f}")
)
}
firewall_policies = {
for k, v in local._firewall_policies_data : try(v.name, k) => merge(v, {
parent = v.parent_id
attachments = try(v.attachments, {})
ingress_rules = try(v.ingress_rules, {})
egress_rules = try(v.egress_rules, {})
})
}
}
module "firewall_policies" {
source = "../../../modules/net-firewall-policy"
for_each = local.firewall_policies
attachments = each.value.attachments
name = each.key
parent_id = each.value.parent
egress_rules = each.value.egress_rules
ingress_rules = each.value.ingress_rules
context = {
folder_ids = local.ctx_folders
cidr_ranges_sets = local.ctx.cidr_ranges_sets
}
}

View File

@@ -0,0 +1,190 @@
/**
* 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.
*/
# tfdoc:file:description NCC Hubs and Groups factory
locals {
_ncc_path = try(pathexpand(var.factories_config.ncc-hubs), null)
_ncc_files = try(fileset(local._ncc_path, "**/*.yaml"), [])
_ncc_preprocess = [
for f in local._ncc_files : yamldecode(
file("${coalesce(local._ncc_path, "-")}/${f}")
)
]
# Since NCC groups depend on NCC hubs, two different lookup maps avoid circular dependencies.
ctx_ncc_groups = {
for k, v in google_network_connectivity_group.default : k => v.id
}
ctx_ncc_hubs = {
for k, v in google_network_connectivity_hub.default : k => v.id
}
ncc_hubs = {
for k, v in local._ncc_preprocess : v.name => {
project_id = v.project_id
description = try(v.description, "Terraform-managed")
export_psc = try(v.export_psc, true)
preset_topology = try(v.preset_topology, "MESH")
}
}
ncc_groups = merge(flatten([
for k, v in local._ncc_preprocess : {
for gk, gv in try(v.groups, {}) : "${v.name}/${gk}" =>
{
name = gk
project_id = v.project_id
hub = v.name
description = try(gv.description, "Terraform-managed")
labels = try(gv.labels, {})
auto_accept = try(gv.auto_accept, [])
}
}
])...)
ncc_vpc_spokes = {
for vpc_key, vpc_config in local.vpcs :
"${vpc_key}/${replace(vpc_config.ncc_config.hub, "$ncc_hubs:", "")}" => merge(
vpc_config.ncc_config,
{
project_id = vpc_config.project_id
vpc_self_link = vpc_key
labels = try(vpc_config.ncc_config.labels, {})
hub = vpc_config.ncc_config.hub
description = try(vpc_config.ncc_config.description, "Terraform-managed")
exclude_export_ranges = try(vpc_config.ncc_config.exclude_export_ranges, null)
include_export_ranges = try(vpc_config.ncc_config.include_export_ranges, null)
group = try(vpc_config.ncc_config.group, null)
}
) if try(vpc_config.ncc_config != null, false)
}
ncc_vpn_spokes = {
for vpn_key, vpn_config in local.vpns :
"${vpn_key}/${replace(vpn_config.ncc_spoke_config.hub, "$ncc_hubs:", "")}" => merge(
vpn_config.ncc_spoke_config,
{
name = replace("${vpn_key}/${vpn_config.ncc_spoke_config.hub}", "$ncc_hubs:", "") # TODO: eww
project_id = vpn_config.project_id
hub = vpn_config.ncc_spoke_config.hub
location = vpn_config.region
description = lookup(vpn_config.ncc_spoke_config, "description", "Terraform-managed.")
labels = lookup(vpn_config.ncc_spoke_config, "labels", {})
tunnel_self_link = [for t, _ in vpn_config.tunnels : module.vpn-ha[vpn_key].tunnel_self_links[t]]
}
) if try(vpn_config.ncc_spoke_config != null, false)
}
}
resource "google_network_connectivity_hub" "default" {
for_each = local.ncc_hubs
project = lookup(
local.ctx_projects.project_ids,
replace(each.value.project_id, "$project_ids:", ""),
each.value.project_id
)
name = each.key
description = each.value.description
export_psc = each.value.export_psc
preset_topology = each.value.preset_topology
}
resource "google_network_connectivity_group" "default" {
for_each = local.ncc_groups
project = lookup(
local.ctx_projects.project_ids,
replace(each.value.project_id, "$project_ids:", ""),
each.value.project_id
)
name = each.value.name
hub = lookup(
local.ctx_ncc_hubs, replace(each.value.hub, "$ncc_hubs:", ""), each.value.hub
)
labels = each.value.labels
description = each.value.description
dynamic "auto_accept" {
for_each = try(each.value.auto_accept != null, false) ? [""] : []
content {
auto_accept_projects = [
for project_key in try(each.value.auto_accept, []) : lookup(
local.ctx_projects.project_ids,
replace(project_key, "$project_ids:", ""),
project_key
)
]
}
}
depends_on = [google_network_connectivity_hub.default]
}
resource "google_network_connectivity_spoke" "vpcs" {
for_each = local.ncc_vpc_spokes
project = lookup(
local.ctx_projects.project_ids,
replace(each.value.project_id, "$project_ids:", ""),
each.value.project_id
)
name = replace(each.key, "/", "-")
location = "global"
description = each.value.description
labels = each.value.labels
hub = lookup(
local.ctx_ncc_hubs,
replace(each.value.hub, "$ncc_hubs:", ""),
each.value.hub
)
linked_vpc_network {
uri = lookup(
local.ctx_vpcs.self_links,
each.value.vpc_self_link,
each.value.vpc_self_link
)
exclude_export_ranges = each.value.exclude_export_ranges
include_export_ranges = each.value.include_export_ranges
}
group = each.value.group == null ? null : lookup(
local.ctx_ncc_groups,
replace(each.value.group, "$ncc_groups:", ""),
each.value.group
)
depends_on = [google_network_connectivity_hub.default]
}
resource "google_network_connectivity_spoke" "tunnels" {
for_each = local.ncc_vpn_spokes
project = lookup(
local.ctx_projects.project_ids,
replace(each.value.project_id, "$project_ids:", ""),
each.value.project_id
)
name = replace(each.key, "/", "-")
location = lookup(
local.ctx.locations,
replace(each.value.location, "$locations:", ""),
each.value.location
)
description = each.value.description
labels = each.value.labels
hub = lookup(
local.ctx_ncc_hubs,
replace(each.value.hub, "$ncc_hubs:", ""),
each.value.hub
)
linked_vpn_tunnels {
uris = each.value.tunnel_self_link
site_to_site_data_transfer = true
include_import_ranges = ["ALL_IPV4_RANGES"]
}
depends_on = [module.vpn-ha]
}

View File

@@ -0,0 +1,179 @@
/**
* 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.
*/
# tfdoc:file:description NVA factory
locals {
_nva_path = try(pathexpand(var.factories_config.nvas), null)
_nva_files = try(fileset(local._nva_path, "**/*.yaml"), [])
_nva_configs = [
for f in local._nva_files : merge(
yamldecode(file("${coalesce(local._nva_path, "-")}/${f}")),
{ filename = replace(f, ".yaml", "") }
)
]
ctx_nva = {
ilb_addresses = {
for k, v in module.ilb : k => v.forwarding_rule_addresses[""]
}
}
nva_configs = {
for k, v in local._nva_configs : try(v.name, k) => merge(v, {
attachments = [for a in try(v.attachments, []) : merge(a, {
routes = try(a.routes, [])
create_ilb = try(a.create_ilb, true)
})]
})
}
nva_instances = merge(flatten([
for nva_key, nva_def in local.nva_configs : [
for group_key, group_value in nva_def.instance_groups : [
for i in range(try(group_value.auto_create_instances, 0)) : {
"${nva_def.name}-${group_key}-${i}" = {
group_zone = group_key
zone = "${nva_def.region}-${group_key}"
project_id = nva_def.project_id
image = try(
nva_def.image, "projects/debian-cloud/global/images/family/debian-12"
)
instance_type = try(
nva_def.instance_type, "e2-standard-4"
)
metadata = coalesce(
try(nva_def.metadata, null),
{
user-data = templatefile(
"${path.module}/assets/nva-startup-script.yaml.tpl",
{ nva_nics_config = nva_def.attachments }
)
}
)
attachments = nva_def.attachments
tags = try(nva_def.tags, ["nva"])
options = try(nva_def.options, null)
}
}
]
]
])...)
nva_instance_groups = merge([
for nva_def in local.nva_configs : {
for group_key, group_value in try(nva_def.instance_groups, {}) :
"${nva_def.name}-${group_key}" => {
nva_config = nva_def.name
zone_key = group_key
name = "nva-${nva_def.name}-${group_key}"
project_id = nva_def.project_id
zone = "${nva_def.region}-${group_key}"
network = try(nva_def.attachments[0].network, null)
instances = toset(concat(
[
for i in range(try(group_value.auto_create_instances, 0)) :
module.nva-instance["${nva_def.name}-${group_key}-${i}"].self_link
],
flatten([
for v in try(group_value.attach_instances, {}) : values(v)
])
))
}
}
]...)
nva_ilbs = merge(flatten([
for nva_def in local.nva_configs : [
for i, attachment in nva_def.attachments : {
"${replace(attachment.network, "$networks:", "")}/${nva_def.name}" = {
name = "ilb-${nva_def.name}-${i}"
nva_config = nva_def.name
project_id = nva_def.project_id
region = nva_def.region
vpc_config = {
network = attachment.network
subnetwork = attachment.subnet
}
health_check = try(nva_def.health_check, null)
}
} if attachment.create_ilb == true
]
])...)
}
module "nva-instance" {
for_each = local.nva_instances
source = "../../../modules/compute-vm"
project_id = each.value.project_id
name = "nva-${each.key}"
zone = each.value.zone
instance_type = each.value.instance_type
tags = each.value.tags
can_ip_forward = true
network_interfaces = [for k, v in each.value.attachments :
{
network = v.network
subnetwork = v.subnet
nat = false
addresses = null
}
]
boot_disk = {
initialize_params = {
image = each.value.image
google-logging-enabled = true
type = "pd-ssd"
size = 10 # TODO: make configurable?
}
}
metadata = each.value.metadata
context = {
project_ids = local.ctx_projects.project_ids
vpcs = local.ctx_vpcs.self_links
subnets = local.ctx_vpcs.subnets_by_vpc
}
}
resource "google_compute_instance_group" "nva" {
for_each = local.nva_instance_groups
project = lookup(
local.ctx_projects.project_ids,
replace(each.value.project_id, "$project_ids:", ""),
each.value.project_id
)
zone = each.value.zone
name = each.value.name
#network = lookup(local.ctx_vpcs.self_links, replace(each.value.network, "$networks:", ""), each.value.network)
instances = each.value.instances
depends_on = [module.nva-instance]
}
module "ilb" {
source = "../../../modules/net-lb-int"
for_each = local.nva_ilbs
project_id = each.value.project_id
region = each.value.region
name = replace("ilb-${each.key}", "/", "-")
vpc_config = each.value.vpc_config
backends = [
for k, v in local.nva_instance_groups : {
group = google_compute_instance_group.nva[k].id
} if v.nva_config == each.value.nva_config
]
health_check_config = each.value.health_check
context = {
project_ids = local.ctx_projects.project_ids
vpcs = local.ctx_vpcs.self_links
subnets = local.ctx_vpcs.subnets_by_vpc
}
depends_on = [module.nva-instance]
}

View File

@@ -0,0 +1,64 @@
/**
* 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.
*/
# tfdoc:file:description VPC Peering factory.
locals {
peering_configs = merge(flatten([
for vpc_key, vpc_config in local.vpcs : {
for peering_key, peering_configs in try(vpc_config.peering_config, {}) :
"${vpc_key}/${peering_key}" => merge(peering_configs, {
project = vpc_config.project_id
name = replace("${vpc_key}/${peering_key}", "/", "-")
local_network = vpc_key
peer_network = peering_configs.peer_network
export_custom_routes = try(
peering_configs.routes_config.export, true
)
import_custom_routes = try(
peering_configs.routes_config.import, true
)
export_subnet_routes_with_public_ip = try(
peering_configs.routes_config.public_export, null
)
import_subnet_routes_with_public_ip = try(
peering_configs.routes_config.public_import, null
)
stack_type = try(
peering_configs.stack_type, null
)
})
}
])...)
}
resource "google_compute_network_peering" "default" {
for_each = local.peering_configs
name = each.value.name
network = lookup(local.ctx_vpcs.self_links,
replace(each.value.local_network, "$networks:", ""),
each.value.local_network
)
peer_network = lookup(local.ctx_vpcs.self_links,
replace(each.value.peer_network, "$networks:", ""),
each.value.peer_network
)
export_custom_routes = each.value.export_custom_routes
import_custom_routes = each.value.import_custom_routes
export_subnet_routes_with_public_ip = each.value.export_subnet_routes_with_public_ip
import_subnet_routes_with_public_ip = each.value.import_subnet_routes_with_public_ip
stack_type = each.value.stack_type
}

View File

@@ -0,0 +1,55 @@
/**
* 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.
*/
# tfdoc:file:description Projects factory.
locals {
ctx_folders = merge(local.ctx.folder_ids, module.projects.folder_ids, {
organization = "organizations/${var.organization.id}"
})
ctx_projects = {
project_ids = merge(local.ctx.project_ids, module.projects.project_ids)
}
project_defaults = {
defaults = merge(
{
billing_account = var.billing_account.id
prefix = var.prefix
},
lookup(var.folder_ids, local.defaults.folder_name, null) == null ? {} : {
parent = lookup(var.folder_ids, local.defaults.folder_name, null)
},
try(local._defaults.projects.defaults, {})
)
overrides = try(local._defaults.projects.overrides, {})
}
}
moved {
from = module.factory
to = module.projects
}
module "projects" {
source = "../../../modules/project-factory"
data_defaults = local.project_defaults.defaults
data_overrides = local.project_defaults.overrides
context = local.ctx
factories_config = {
folders = var.factories_config.folders
projects = var.factories_config.projects
}
}

View File

@@ -0,0 +1,74 @@
/**
* 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.
*/
# tfdoc:file:description Routers factory.
locals {
ctx_routers = {
ids = { for k, v in google_compute_router.default : k => v.id }
names = { for k, v in google_compute_router.default : k => v.name }
}
router_configs = merge(flatten([
for vpc_key, vpc_config in local.vpcs : [
for router_key, router_config in try(vpc_config.routers, {}) : {
"${vpc_key}/${router_key}" = merge(router_config, {
name = replace("${vpc_key}/${router_key}", "/", "-")
vpc_self_link = vpc_key
project_id = vpc_config.project_id
custom_advertise = try(router_config.custom_advertise, {})
advertise_mode = try(router_config.custom_advertise != null, false) ? "CUSTOM" : "DEFAULT"
advertised_groups = try(router_config.custom_advertise.all_subnets, false) ? ["ALL_SUBNETS"] : []
keepalive = try(router_config.keepalive, null)
asn = try(router_config.asn, null)
})
}
]
])...)
}
resource "google_compute_router" "default" {
for_each = local.router_configs
name = replace(each.key, "/", "-")
project = lookup(
local.ctx_projects.project_ids,
replace(each.value.project_id, "$project_ids:", ""),
each.value.project_id
)
region = lookup(
local.ctx.locations,
replace(each.value.region, "$locations:", ""),
each.value.region
)
network = lookup(
local.ctx_vpcs.self_links,
each.value.vpc_self_link,
each.value.vpc_self_link
)
bgp {
advertise_mode = each.value.advertise_mode
advertised_groups = each.value.advertised_groups
dynamic "advertised_ip_ranges" {
for_each = try(each.value.custom_advertise.ip_ranges, {})
iterator = range
content {
range = range.key
description = range.value
}
}
keepalive_interval = each.value.keepalive
asn = each.value.asn
}
}

View File

@@ -0,0 +1,147 @@
/**
* 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.
*/
# tfdoc:file:description VPC and firewall rules factory.
locals {
_vpcs_path = try(
pathexpand(var.factories_config.vpcs), null
)
_vpcs_files = try(
fileset(local._vpcs_path, "**/.config.yaml"),
[]
)
_vpcs_preprocess = [
for f in local._vpcs_files : merge(
yamldecode(file("${coalesce(local._vpcs_path, "-")}/${f}")),
{
factory_dirname = dirname(f)
factory_basepath = "${local._vpcs_path}/${dirname(f)}"
}
)
]
_vpcs = {
for v in local._vpcs_preprocess : v.factory_dirname => v
}
ctx_vpcs = {
ids = { for k, v in module.vpcs : k => v.id }
names = { for k, v in module.vpcs : k => v.name }
self_links = { for k, v in module.vpcs : k => v.self_link }
subnets_by_vpc = merge([
for vpc_key, vpc in module.vpcs : {
for subnet_key, subnet_self_link in vpc.subnet_self_links :
"${vpc_key}/${subnet_key}" => subnet_self_link
}
]...)
}
vpcs = {
for k, v in local._vpcs : k => merge(
local.defaults.vpcs, v,
{
project_id = v.project_id
description = try(v.description, "Terraform managed")
create_googleapis_routes = try(v.create_googleapis_routes, {})
dns_policy = try(v.dns_policy, {})
firewall_policy_enforcement_order = try(v.firewall_policy_enforcement_order, "AFTER_CLASSIC_FIREWALL")
ipv6_config = try(v.ipv6_config, {})
name = v.name
network_attachments = try(v.network_attachments, {})
policy_based_routes = try(v.policy_based_routes, {})
psa_configs = try(v.psa_configs, [])
routes = try(v.routes, {})
routing_mode = try(v.routing_mode, "GLOBAL")
subnets_private_nat = try(v.subnets_private_nat, [])
subnets_proxy_only = try(v.subnets_proxy_only, [])
subnets_psc = try(v.subnets_psc, [])
subnets = try(v.subnets, [])
subnets_factory_config = {
subnets_folder = "${v.factory_basepath}/subnets"
}
firewall_factory_config = {
rules_folder = "${v.factory_basepath}/firewall-rules"
}
peering_config = try(v.peering_config, {})
vpn_config = try(v.vpn_config, {})
}
)
}
vpc_defaults = try(local._defaults.vpcs.defaults, {})
}
module "vpcs" {
source = "../../../modules/net-vpc"
for_each = local.vpcs
project_id = each.value.project_id
name = each.value.name
auto_create_subnetworks = each.value.auto_create_subnetworks
create_googleapis_routes = each.value.create_googleapis_routes
delete_default_routes_on_create = each.value.delete_default_routes_on_create
description = each.value.description
dns_policy = each.value.dns_policy
factories_config = each.value.subnets_factory_config
firewall_policy_enforcement_order = each.value.firewall_policy_enforcement_order
ipv6_config = each.value.ipv6_config
mtu = each.value.mtu
network_attachments = each.value.network_attachments
policy_based_routes = each.value.policy_based_routes
psa_configs = each.value.psa_configs
routing_mode = each.value.routing_mode
subnets = each.value.subnets
subnets_private_nat = each.value.subnets_private_nat
subnets_proxy_only = each.value.subnets_proxy_only
subnets_psc = each.value.subnets_psc
context = {
project_ids = local.ctx_projects.project_ids
locations = local.ctx.locations
}
depends_on = [module.projects]
}
module "vpc_routes" {
source = "../../../modules/net-vpc"
for_each = local.vpcs
vpc_reuse = {
use_data_source = false
attributes = { network_id = module.vpcs[each.key].network_id }
}
project_id = each.value.project_id
name = each.value.name
routes = each.value.routes
context = {
project_ids = local.ctx_projects.project_ids
locations = local.ctx.locations
addresses = local.ctx_nva.ilb_addresses
}
depends_on = [
module.projects,
module.vpcs
]
}
module "firewall" {
source = "../../../modules/net-vpc-firewall"
for_each = {
for k, v in local.vpcs : k => v if v.firewall_factory_config != null
}
project_id = each.value.project_id
network = each.value.name
factories_config = each.value.firewall_factory_config
default_rules_config = { disabled = true }
context = {
project_ids = local.ctx_projects.project_ids
}
depends_on = [module.vpcs]
}

View File

@@ -0,0 +1,94 @@
/**
* 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.
*/
# tfdoc:file:description VPNs factory.
locals {
_vpns_files = try(
fileset(local._vpcs_path, "**/vpns/*.yaml"),
[]
)
_vpns_preprocess = [
for f in local._vpns_files : merge(
yamldecode(file("${coalesce(local._vpcs_path, "-")}/${f}")),
{
factory_basepath = dirname(dirname(f))
}
)
]
ctx_gateways = { for k, v in google_compute_ha_vpn_gateway.default : k => v.id }
vpns = {
for v in local._vpns_preprocess : "${v.factory_basepath}/${v.name}" => merge(v, {
vpc_name = v.factory_basepath
# TODO: discuss - this is pushing context at any cost, as project could be easily resolved
# as module.vpcs[v.factory_basepath].project_id
project_id = local.vpcs[v.factory_basepath].project_id
router_config = try(v.router_config, {})
region = try(v.region, local.defaults.vpcs.region)
peer_gateways = try(v.peer_gateways, {})
tunnels = try(v.tunnels, {})
})
}
}
resource "google_compute_ha_vpn_gateway" "default" {
for_each = local.vpns
project = lookup(
merge(local.ctx.project_ids, module.projects.project_ids),
replace(each.value.project_id, "$project_ids:", ""),
each.value.project_id
)
region = lookup(
local.ctx.locations,
replace(each.value.region, "$locations:", ""),
each.value.region
)
network = lookup(
local.ctx_vpcs.names, each.value.vpc_name, each.value.vpc_name
)
name = replace(each.key, "/", "-")
stack_type = try(each.value.stack_type, null)
depends_on = [module.vpcs]
}
module "vpn-ha" {
source = "../../../modules/net-vpn-ha"
for_each = local.vpns
project_id = each.value.project_id
name = replace(each.key, "/", "-")
network = each.value.vpc_name
region = each.value.region
router_config = each.value.router_config
tunnels = each.value.tunnels
vpn_gateway = google_compute_ha_vpn_gateway.default[each.key].id
vpn_gateway_create = null
peer_gateways = {
for k, gw in each.value.peer_gateways : k => {
for gw_type, value in gw : gw_type => (
gw_type == "gcp"
? try(google_compute_ha_vpn_gateway.default[value].id, value)
: value
)
}
}
context = {
gateways = local.ctx_gateways
locations = local.ctx.locations
network = local.ctx_vpcs.names
project_ids = local.ctx_projects.project_ids
routers = local.ctx_routers.names
}
}

View File

@@ -0,0 +1,15 @@
# 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
#
# https://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.
# FAST release: v45.0.0

View File

@@ -0,0 +1,56 @@
/**
* 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.
*/
locals {
paths = {
for k, v in var.factories_config : k => try(pathexpand(v), null)
}
# fail if we have no valid defaults
_defaults = yamldecode(file(local.paths.defaults))
ctx = merge(var.context, {
cidr_ranges_sets = try(local._defaults.context.cidr_ranges_sets, {})
folder_ids = merge(
var.folder_ids, var.context.folder_ids
)
iam_principals = merge(
var.iam_principals,
{
for k, v in var.service_accounts :
"service_accounts/${k}" => "serviceAccount:${v}"
},
var.context.iam_principals,
try(local._defaults.context.iam_principals, {})
)
locations = merge(
var.context.locations,
try(local._defaults.context.locations, {})
)
project_ids = merge(var.project_ids, var.context.project_ids)
tag_keys = merge(var.tag_keys, var.context.tag_keys)
tag_values = merge(var.tag_values, var.context.tag_values)
})
defaults = {
folder_name = try(local._defaults.global.folder_id, "networking")
stage_name = try(local._defaults.global.stage_name, "2-networking")
vpcs = try(local._defaults.vpcs, {})
}
output_files = {
local_path = try(local._defaults.output_files.local_path, null)
storage_bucket = try(local._defaults.output_files.storage_bucket, null)
providers = try(local._defaults.output_files.providers, {})
}
}

View File

@@ -0,0 +1,93 @@
/**
* 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.
*/
locals {
tfvars = {
host_project_ids = module.projects.project_ids
host_project_numbers = module.projects.project_numbers
subnet_self_links = {
for vpc_key, vpc in module.vpcs : vpc_key => vpc.subnet_ids
}
subnet_proxy_only_self_links = {
for vpc_key, vpc in module.vpcs : vpc_key => {
for subnet_key, subnet in vpc.subnets_proxy_only : subnet_key => subnet.id
}
}
subnet_psc_self_links = {
for vpc_key, vpc in module.vpcs : vpc_key => {
for subnet_key, subnet in vpc.subnets_psc : subnet_key => subnet.id
}
}
vpc_self_links = {
for vpc_key, vpc in module.vpcs : vpc_key => vpc.id
}
}
}
# generate tfvars file for subsequent stages
resource "local_file" "tfvars" {
for_each = var.outputs_location == null ? {} : { 1 = 1 }
file_permission = "0644"
filename = "${try(pathexpand(var.outputs_location), "")}tfvars/2-networking.auto.tfvars.json"
content = jsonencode(local.tfvars)
}
resource "google_storage_bucket_object" "tfvars" {
for_each = try(var.automation.outputs_bucket, null) == null ? {} : { 1 = 1 }
bucket = var.automation.outputs_bucket
name = "tfvars/2-networking.auto.tfvars.json"
content = jsonencode(local.tfvars)
}
resource "google_storage_bucket_object" "version" {
for_each = try(var.automation.outputs_bucket, null) == null || !fileexists("fast_version.txt") ? {} : { 1 = 1 }
bucket = var.automation.outputs_bucket
name = "versions/2-networking-version.txt"
source = "fast_version.txt"
}
# outputs
output "host_project_ids" {
description = "Project IDs."
value = local.tfvars.host_project_ids
}
output "host_project_numbers" {
description = "Project numbers."
value = local.tfvars.host_project_numbers
}
output "subnet_proxy_only_self_links" {
description = "Subnet proxy-only self-links."
value = local.tfvars.subnet_proxy_only_self_links
}
output "subnet_psc_self_links" {
description = "Subnet PSC self-links."
value = local.tfvars.subnet_psc_self_links
}
output "subnet_self_links" {
description = "Subnet self-links."
value = local.tfvars.subnet_self_links
}
output "vpc_self_links" {
description = "VPC self-links."
value = local.tfvars.vpc_self_links
}

View File

@@ -0,0 +1,56 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "DNS Response Policy Rules Factory",
"type": "object",
"additionalProperties": false,
"properties": {
"project_id": {
"type": "string"
},
"networks": {
"type": "array",
"items": {
"type": "string"
}
},
"rules": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"dns_name": {
"type": "string"
},
"behavior": {
"type": "string"
},
"local_data": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^(?:A|AAAA|CAA|CNAME|DNSKEY|DS|HTTPS|IPSECVPNKEY|MX|NAPTR|NS|PTR|SOA|SPF|SRV|SSHFP|SVCB|TLSA|TXT)$": {
"type": "object",
"additionalProperties": false,
"properties": {
"ttl": {
"type": "number"
},
"rrdatas": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,183 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "DNS Zone configuration",
"description": "Schema for DNS zone YAML configuration files.",
"type": "object",
"additionalProperties": false,
"properties": {
"project_id": {
"type": "string",
"description": "Project ID for the DNS zone."
},
"description": {
"type": "string",
"description": "Optional description for the DNS zone."
},
"force_destroy": {
"type": "boolean",
"description": "Whether to force deletion of the zone."
},
"domain": {
"type": "string",
"description": "Domain name for the zone (e.g. 'example.com.')."
},
"iam": {
"$ref": "#/$defs/iam"
},
"recordsets": {
"$ref": "#/$defs/recordsets"
},
"private": {
"$ref": "#/$defs/private_zone"
},
"peering": {
"$ref": "#/$defs/peering_zone"
},
"forwarding": {
"$ref": "#/$defs/forwarding_zone"
}
},
"required": [
"project_id"
],
"oneOf": [
{
"title": "Private Zone",
"required": [
"private"
],
"properties": {
"peering": {
"not": {}
},
"forwarding": {
"not": {}
}
}
},
{
"title": "Peering Zone",
"required": [
"peering"
],
"properties": {
"private": {
"not": {}
},
"forwarding": {
"not": {}
}
}
},
{
"title": "Forwarding Zone",
"required": [
"forwarding"
],
"properties": {
"private": {
"not": {}
},
"peering": {
"not": {}
}
}
}
],
"$defs": {
"iam": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^(?:roles/|\\$custom_roles:)": {
"type": "array",
"items": {
"type": "string",
"pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:||\\$iam_principals:[a-z0-9_-]+)"
}
}
}
},
"recordsets": {
"type": "object",
"description": "Map of DNS recordsets.",
"additionalProperties": {
"type": "object",
"properties": {
"records": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"records"
]
}
},
"private_zone": {
"description": "Private zone specific configuration.",
"type": "object",
"additionalProperties": false,
"properties": {
"service_directory_namespace": {
"type": "string"
},
"client_networks": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"client_networks"
]
},
"peering_zone": {
"description": "Peering zone specific configuration.",
"type": "object",
"additionalProperties": false,
"properties": {
"peer_network": {
"type": "string"
},
"client_networks": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"peer_network",
"client_networks"
]
},
"forwarding_zone": {
"description": "Forwarding zone specific configuration.",
"type": "object",
"additionalProperties": false,
"properties": {
"forwarders": {
"type": "object",
"patternProperties": {
"^.*$": {
"type": "string"
}
}
},
"client_networks": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"client_networks"
]
}
}
}

View File

@@ -0,0 +1,163 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Network Firewall Policy",
"type": "object",
"additionalProperties": false,
"properties": {
"parent_id": {
"type": "string",
"description": "The parent folder or organization of the firewall policy."
},
"attachments": {
"type": "object",
"description": "Folders or organizations where the policy is attached.",
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "string"
}
},
"additionalProperties": false
},
"name": {
"type": "string",
"description": "The name of the firewall policy."
},
"ingress_rules": {
"$ref": "#/$defs/rules",
"description": "A map of ingress firewall rules."
},
"egress_rules": {
"$ref": "#/$defs/rules",
"description": "A map of egress firewall rules."
}
},
"$defs": {
"rules": {
"title": "Firewall Rules",
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"$ref": "#/$defs/rule"
}
}
},
"rule": {
"type": "object",
"additionalProperties": false,
"required": [
"priority"
],
"properties": {
"priority": {
"type": "number"
},
"action": {
"type": "string",
"enum": [
"allow",
"deny",
"goto_next",
"apply_security_profile_group"
]
},
"description": {
"type": "string"
},
"disabled": {
"type": "boolean"
},
"enable_logging": {
"type": "boolean"
},
"security_profile_group": {
"type": "string"
},
"target_resources": {
"type": "array",
"items": {
"type": "string"
}
},
"target_service_accounts": {
"type": "array",
"items": {
"type": "string"
}
},
"target_tags": {
"type": "array",
"items": {
"type": "string"
}
},
"tls_inspect": {
"type": "boolean"
},
"match": {
"type": "object",
"additionalProperties": false,
"properties": {
"address_groups": {
"type": "array",
"items": {
"type": "string"
}
},
"fqdns": {
"type": "array",
"items": {
"type": "string"
}
},
"region_codes": {
"type": "array",
"items": {
"type": "string"
}
},
"threat_intelligences": {
"type": "array",
"items": {
"type": "string"
}
},
"destination_ranges": {
"type": "array",
"items": {
"type": "string"
}
},
"source_ranges": {
"type": "array",
"items": {
"type": "string"
}
},
"source_tags": {
"type": "array",
"items": {
"type": "string"
}
},
"layer4_configs": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"protocol": {
"type": "string"
},
"ports": {
"type": "array"
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,104 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Firewall Rules",
"type": "object",
"additionalProperties": false,
"properties": {
"egress": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"$ref": "#/$defs/rule"
}
}
},
"ingress": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"$ref": "#/$defs/rule"
}
}
}
},
"$defs": {
"rule": {
"type": "object",
"additionalProperties": false,
"properties": {
"deny": {
"type": "boolean"
},
"description": {
"type": "string"
},
"destination_ranges": {
"type": "array",
"items": {
"type": "string"
}
},
"disabled": {
"type": "boolean"
},
"enable_logging": {
"type": "object",
"additionalProperties": false,
"properties": {
"include_metadata": {
"type": "boolean"
}
}
},
"priority": {
"type": "number"
},
"source_ranges": {
"type": "array",
"items": {
"type": "string"
}
},
"sources": {
"type": "array",
"items": {
"type": "string"
}
},
"targets": {
"type": "array",
"items": {
"type": "string"
}
},
"use_service_accounts": {
"type": "boolean"
},
"rules": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"protocol": {
"type": "string"
},
"ports": {
"type": "array",
"items": {
"type": [
"integer",
"string"
],
"pattern": "^[0-9]+(?:-[0-9]+)?$"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,555 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Folder",
"type": "object",
"additionalProperties": false,
"properties": {
"automation": {
"type": "object",
"additionalProperties": false,
"required": [
"project"
],
"properties": {
"prefix": {
"type": "string"
},
"project": {
"type": "string"
},
"bucket": {
"$ref": "#/$defs/bucket"
},
"service_accounts": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"description": {
"type": "string"
},
"iam": {
"$ref": "#/$defs/iam"
},
"iam_bindings": {
"$ref": "#/$defs/iam_bindings"
},
"iam_bindings_additive": {
"$ref": "#/$defs/iam_bindings_additive"
},
"iam_billing_roles": {
"$ref": "#/$defs/iam_billing_roles"
},
"iam_folder_roles": {
"$ref": "#/$defs/iam_folder_roles"
},
"iam_organization_roles": {
"$ref": "#/$defs/iam_organization_roles"
},
"iam_project_roles": {
"$ref": "#/$defs/iam_project_roles"
},
"iam_sa_roles": {
"$ref": "#/$defs/iam_sa_roles"
},
"iam_storage_roles": {
"$ref": "#/$defs/iam_storage_roles"
}
}
}
}
}
}
},
"factories_config": {
"type": "object",
"additionalProperties": false,
"properties": {
"org_policies": {
"type": "string"
},
"pam_entitlements": {
"type": "string"
},
"scc_sha_custom_modules": {
"type": "string"
}
}
},
"iam": {
"$ref": "#/$defs/iam"
},
"iam_bindings": {
"$ref": "#/$defs/iam_bindings"
},
"iam_bindings_additive": {
"$ref": "#/$defs/iam_bindings_additive"
},
"iam_by_principals": {
"$ref": "#/$defs/iam_by_principals"
},
"name": {
"type": "string"
},
"org_policies": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z]+\\.": {
"type": "object",
"properties": {
"inherit_from_parent": {
"type": "boolean"
},
"reset": {
"type": "boolean"
},
"rules": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"allow": {
"type": "object",
"additionalProperties": false,
"properties": {
"all": {
"type": "boolean"
},
"values": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"deny": {
"type": "object",
"additionalProperties": false,
"properties": {
"all": {
"type": "boolean"
},
"values": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"enforce": {
"type": "boolean"
},
"condition": {
"type": "object",
"additionalProperties": false,
"properties": {
"description": {
"type": "string"
},
"expression": {
"type": "string"
},
"location": {
"type": "string"
},
"title": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
"pam_entitlements": {
"$ref": "#/$defs/pam_entitlements"
},
"parent": {
"type": "string",
"pattern": "^(?:folders/[0-9]+|organizations/[0-9]+|\\$folder_ids:[a-z0-9_-]+)$"
},
"tag_bindings": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"type": "string"
}
}
}
},
"$defs": {
"bucket": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"iam": {
"$ref": "#/$defs/iam"
},
"iam_bindings": {
"$ref": "#/$defs/iam_bindings"
},
"iam_bindings_additive": {
"$ref": "#/$defs/iam_bindings_additive"
},
"force_destroy": {
"type": "boolean"
},
"labels": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"location": {
"type": "string"
},
"managed_folders": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-zA-Z0-9][a-zA-Z0-9_/-]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"force_destroy": {
"type": "boolean"
},
"iam": {
"$ref": "#/$defs/iam"
},
"iam_bindings": {
"$ref": "#/$defs/iam_bindings"
},
"iam_bindings_additive": {
"$ref": "#/$defs/iam_bindings_additive"
}
}
}
}
},
"prefix": {
"type": "string"
},
"storage_class": {
"type": "string"
},
"uniform_bucket_level_access": {
"type": "boolean"
},
"versioning": {
"type": "boolean"
}
}
},
"iam": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^(?:roles/|\\$custom_roles:)": {
"type": "array",
"items": {
"type": "string",
"pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|\\$iam_principals:)"
}
}
}
},
"iam_bindings": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"members": {
"type": "array",
"items": {
"type": "string",
"pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|\\$iam_principals:)"
}
},
"role": {
"type": "string",
"pattern": "^(?:roles/|\\$custom_roles:)"
},
"condition": {
"type": "object",
"additionalProperties": false,
"required": [
"expression",
"title"
],
"properties": {
"expression": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
}
}
}
}
}
}
},
"iam_bindings_additive": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"member": {
"type": "string",
"pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|\\$iam_principals:)"
},
"role": {
"type": "string",
"pattern": "^(?:roles/|\\$custom_roles:)"
},
"condition": {
"type": "object",
"additionalProperties": false,
"required": [
"expression",
"title"
],
"properties": {
"expression": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
}
}
}
}
}
}
},
"iam_by_principals": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|\\$iam_principals:)": {
"type": "array",
"items": {
"type": "string",
"pattern": "^(?:roles/|\\$custom_roles:)"
}
}
}
},
"iam_billing_roles": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"iam_folder_roles": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"iam_organization_roles": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"iam_project_roles": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"iam_sa_roles": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"iam_storage_roles": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"pam_entitlements": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z][a-z0-9-]{0,61}[a-z0-9]$": {
"type": "object",
"properties": {
"max_request_duration": {
"type": "string"
},
"eligible_users": {
"type": "array",
"items": {
"type": "string"
}
},
"privileged_access": {
"type": "array",
"items": {
"type": "object",
"properties": {
"role": {
"type": "string"
},
"condition": {
"type": "string"
}
},
"required": [
"role"
],
"additionalProperties": false
}
},
"requester_justification_config": {
"type": "object",
"properties": {
"not_mandatory": {
"type": "boolean"
},
"unstructured": {
"type": "boolean"
}
},
"additionalProperties": false
},
"manual_approvals": {
"type": "object",
"properties": {
"require_approver_justification": {
"type": "boolean"
},
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {
"approvers": {
"type": "array",
"items": {
"type": "string"
}
},
"approvals_needed": {
"type": "number"
},
"approver_email_recipients": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"approvers"
],
"additionalProperties": false
}
}
},
"required": [
"require_approver_justification",
"steps"
],
"additionalProperties": false
},
"additional_notification_targets": {
"type": "object",
"properties": {
"admin_email_recipients": {
"type": "array",
"items": {
"type": "string"
}
},
"requester_email_recipients": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
}
},
"required": [
"max_request_duration",
"eligible_users",
"privileged_access"
],
"additionalProperties": false
}
}
}
}
}

View File

@@ -0,0 +1,69 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "NCC Hub Configuration",
"description": "Schema for an NCC Hub YAML configuration file.",
"type": "object",
"additionalProperties": false,
"required": [
"name",
"project_id"
],
"properties": {
"name": {
"type": "string",
"description": "The name of the NCC hub."
},
"project_id": {
"type": "string",
"description": "The project ID where the hub will be created."
},
"description": {
"type": "string",
"description": "An optional description for the hub."
},
"export_psc": {
"type": "boolean",
"description": "Whether to export PSC routes to other spokes."
},
"preset_topology": {
"type": "string",
"description": "The preset topology for the hub."
},
"groups": {
"$ref": "#/$defs/groups"
}
},
"$defs": {
"groups": {
"type": "object",
"description": "A map of NCC groups to be created in the hub.",
"additionalProperties": false,
"patternProperties": {
"^[a-zA-Z0-9_-]+$": {
"$ref": "#/$defs/group"
}
}
},
"group": {
"type": "object",
"additionalProperties": false,
"properties": {
"description": {
"type": "string",
"description": "An optional description for the group."
},
"labels": {
"type": "object",
"description": "Labels to apply to the group."
},
"auto_accept": {
"type": "array",
"description": "A list of project IDs to auto-accept for group membership.",
"items": {
"type": "string"
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,231 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Subnet",
"type": "object",
"additionalProperties": false,
"required": [
"region"
],
"anyOf": [
{"required": ["ip_cidr_range"]},
{"required": ["reserved_internal_range"]},
{"required": ["ip_collection"]},
{
"allOf": [
{"not": {"required": ["ip_cidr_range"]}},
{"not": {"required": ["reserved_internal_range"]}},
{"not": {"required": ["ip_collection"]}},
{"properties": {"ipv6": {"properties": {"ipv6_only": {"const": true}}}}, "required": ["ipv6"]}
]
}
],
"properties": {
"active": {
"type": "boolean"
},
"description": {
"type": "string"
},
"enable_private_access": {
"type": "boolean"
},
"allow_subnet_cidr_routes_overlap": {
"type": "boolean"
},
"flow_logs_config": {
"type": "object",
"additionalProperties": false,
"properties": {
"aggregation_interval": {
"type": "string"
},
"filter_expression": {
"type": "string"
},
"flow_sampling": {
"type": "number"
},
"metadata": {
"type": "string"
},
"metadata_fields": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"global": {
"type": "boolean"
},
"ip_cidr_range": {
"type": "string"
},
"reserved_internal_range": {
"type": "string",
"description": "Name of the internal range to use for this subnet. Mutually exclusive with ip_cidr_range and ip_collection."
},
"ipv6": {
"type": "object",
"additionalProperties": false,
"properties": {
"access_type": {
"type": "string"
},
"ipv6_only": {
"type": "boolean"
}
}
},
"ip_collection": {
"type": "string"
},
"name": {
"type": "string"
},
"region": {
"type": "string"
},
"psc": {
"type": "boolean"
},
"proxy_only": {
"type": "boolean"
},
"secondary_ip_ranges": {
"type": "object",
"additionalProperties": {
"oneOf": [
{
"type": "string",
"description": "IP CIDR range for backward compatibility"
},
{
"type": "object",
"additionalProperties": false,
"anyOf": [
{"required": ["ip_cidr_range"]},
{"required": ["reserved_internal_range"]}
],
"properties": {
"ip_cidr_range": {
"type": "string",
"description": "IP CIDR range for this secondary range"
},
"reserved_internal_range": {
"type": "string",
"description": "Name of the internal range to use for this secondary range"
}
}
}
]
}
},
"iam": {
"$ref": "#/$defs/iam"
},
"iam_bindings": {
"$ref": "#/$defs/iam_bindings"
},
"iam_bindings_additive": {
"$ref": "#/$defs/iam_bindings_additive"
}
},
"$defs": {
"iam": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^roles/": {
"type": "array",
"items": {
"type": "string",
"pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|ro|rw)"
}
}
}
},
"iam_bindings": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"members": {
"type": "array",
"items": {
"type": "string",
"pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|ro|rw)"
}
},
"role": {
"type": "string",
"pattern": "^roles/"
},
"condition": {
"type": "object",
"additionalProperties": false,
"required": [
"expression",
"title"
],
"properties": {
"expression": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
}
}
}
}
}
}
},
"iam_bindings_additive": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9_-]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"member": {
"type": "string",
"pattern": "^(?:domain:|group:|serviceAccount:|user:|principal:|principalSet:|ro|rw)"
},
"role": {
"type": "string",
"pattern": "^roles/"
},
"condition": {
"type": "object",
"additionalProperties": false,
"required": [
"expression",
"title"
],
"properties": {
"expression": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,427 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "VPC Configuration",
"description": "Schema for a VPC .config.yaml file.",
"type": "object",
"additionalProperties": false,
"required": [
"name",
"project_id"
],
"properties": {
"project_id": {
"type": "string"
},
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"auto_create_subnetworks": {
"type": "boolean"
},
"delete_default_routes_on_create": {
"type": "boolean"
},
"mtu": {
"type": "number"
},
"routing_mode": {
"type": "string",
"enum": [
"GLOBAL",
"REGIONAL"
]
},
"firewall_policy_enforcement_order": {
"type": "string",
"enum": [
"BEFORE_CLASSIC_FIREWALL",
"AFTER_CLASSIC_FIREWALL"
]
},
"create_googleapis_routes": {
"$ref": "#/$defs/create_googleapis_routes"
},
"dns_policy": {
"$ref": "#/$defs/dns_policy"
},
"ipv6_config": {
"$ref": "#/$defs/ipv6_config"
},
"network_attachments": {
"$ref": "#/$defs/network_attachments"
},
"policy_based_routes": {
"$ref": "#/$defs/policy_based_routes"
},
"routes": {
"$ref": "#/$defs/routes"
},
"routers": {
"$ref": "#/$defs/routers"
},
"peering_config": {
"$ref": "#/$defs/peering_config"
},
"psa_configs": {
"type": "array",
"items": {
"$ref": "#/$defs/psa_config"
}
},
"subnets": {
"type": "array",
"items": {
"$ref": "#/$defs/subnet"
}
},
"subnets_private_nat": {
"type": "array",
"items": {
"$ref": "#/$defs/simple_subnet"
}
},
"subnets_proxy_only": {
"type": "array",
"items": {
"$ref": "#/$defs/proxy_only_subnet"
}
},
"subnets_psc": {
"type": "array",
"items": {
"$ref": "#/$defs/simple_subnet"
}
},
"nat_config": {
"$ref": "#/$defs/nat_config"
},
"ncc_config": {
"$ref": "#/$defs/ncc_config"
}
},
"$defs": {
"create_googleapis_routes": {
"type": "object",
"properties": {
"directpath": {
"type": "boolean"
},
"directpath-6": {
"type": "boolean"
},
"private": {
"type": "boolean"
},
"private-6": {
"type": "boolean"
},
"restricted": {
"type": "boolean"
},
"restricted-6": {
"type": "boolean"
}
}
},
"dns_policy": {
"type": "object",
"properties": {
"inbound": {
"type": "boolean"
},
"logging": {
"type": "boolean"
},
"outbound": {
"type": "object",
"properties": {
"private_ns": {
"type": "array",
"items": {
"type": "string"
}
},
"public_ns": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"ipv6_config": {
"type": "object",
"properties": {
"enable_ula_internal": {
"type": "boolean"
},
"internal_range": {
"type": "string"
}
}
},
"nat_config": {
"type": "object",
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "object",
"required": [
"region"
],
"properties": {
"region": {
"type": "string"
}
}
}
}
},
"ncc_config": {
"type": "object",
"required": [
"hub"
],
"properties": {
"hub": {
"type": "string"
},
"group": {
"type": "string"
}
}
},
"network_attachments": {
"type": "object",
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "object",
"properties": {
"subnet": {
"type": "string"
},
"automatic_connection": {
"type": "boolean"
},
"description": {
"type": "string"
},
"producer_accept_lists": {
"type": "array",
"items": {
"type": "string"
}
},
"producer_reject_lists": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"peering_config": {
"type": "object",
"properties": {
"peer_vpc_self_link": {
"type": "string"
},
"create_remote_peer": {
"type": "boolean"
},
"export_routes": {
"type": "boolean"
},
"import_routes": {
"type": "boolean"
}
}
},
"policy_based_routes": {
"type": "object",
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "object"
}
}
},
"psa_config": {
"type": "object",
"properties": {
"deletion_policy": {
"type": "string"
},
"ranges": {
"type": "object",
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "string"
}
}
},
"export_routes": {
"type": "boolean"
},
"import_routes": {
"type": "boolean"
},
"peered_domains": {
"type": "array",
"items": {
"type": "string"
}
},
"range_prefix": {
"type": "string"
},
"service_producer": {
"type": "string"
}
}
},
"routes": {
"type": "object",
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "object",
"required": [
"dest_range",
"next_hop_type",
"next_hop"
],
"properties": {
"description": {
"type": "string"
},
"dest_range": {
"type": "string"
},
"next_hop_type": {
"type": "string"
},
"next_hop": {
"type": "string"
},
"priority": {
"type": "number"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"routers": {
"type": "object",
"description": "A map of Cloud Routers to create in this VPC.",
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "object",
"additionalProperties": false,
"required": [
"region",
"asn"
],
"properties": {
"region": {
"type": "string"
},
"asn": {
"type": "number"
}
}
}
}
},
"simple_subnet": {
"type": "object",
"required": [
"name",
"ip_cidr_range",
"region"
],
"properties": {
"name": {
"type": "string"
},
"ip_cidr_range": {
"type": "string"
},
"region": {
"type": "string"
},
"description": {
"type": "string"
}
}
},
"subnet": {
"type": "object",
"required": [
"name",
"region"
],
"properties": {
"name": {
"type": "string"
},
"ip_cidr_range": {
"type": "string"
},
"region": {
"type": "string"
},
"description": {
"type": "string"
},
"enable_private_access": {
"type": "boolean"
},
"allow_subnet_cidr_routes_overlap": {
"type": "boolean"
},
"reserved_internal_range": {
"type": "string"
}
}
},
"proxy_only_subnet": {
"type": "object",
"required": [
"name",
"ip_cidr_range",
"region"
],
"properties": {
"name": {
"type": "string"
},
"ip_cidr_range": {
"type": "string"
},
"region": {
"type": "string"
},
"description": {
"type": "string"
},
"active": {
"type": "boolean"
},
"global": {
"type": "boolean"
}
}
}
}
}

View File

@@ -0,0 +1,164 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "VPN Configuration",
"description": "Schema for a VPN YAML configuration file.",
"type": "object",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"region": {
"type": "string"
},
"stack_type": {
"type": "string",
"enum": [
"IPV4_ONLY",
"IPV4_IPV6"
]
},
"peer_gateways": {
"$ref": "#/$defs/peer_gateways"
},
"router_config": {
"$ref": "#/$defs/router_config"
},
"tunnels": {
"$ref": "#/$defs/tunnels"
},
"ncc_spoke_config": {
"$ref": "#/$defs/ncc_spoke_config"
}
},
"$defs": {
"peer_gateways": {
"type": "object",
"patternProperties": {
"^[a-z0-9-]+$": {
"$ref": "#/$defs/peer_gateway"
}
}
},
"peer_gateway": {
"type": "object",
"oneOf": [
{
"required": [
"external"
],
"properties": {
"external": {
"type": "object",
"properties": {
"redundancy_type": {
"type": "string"
},
"interfaces": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"gcp": false
}
},
{
"required": [
"gcp"
],
"properties": {
"gcp": {
"type": "string"
},
"external": false
}
}
]
},
"router_config": {
"type": "object",
"properties": {
"asn": {
"type": "number"
},
"create": {
"type": "boolean"
},
"name": {
"type": "string"
}
}
},
"tunnels": {
"type": "object",
"patternProperties": {
"^[a-z0-9-]+$": {
"$ref": "#/$defs/tunnel"
}
}
},
"tunnel": {
"type": "object",
"properties": {
"bgp_peer": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"asn": {
"type": "number"
}
}
},
"bgp_session_range": {
"type": "string"
},
"peer_external_gateway_interface": {
"type": "number"
},
"shared_secret": {
"type": "string"
},
"vpn_gateway_interface": {
"type": "number"
}
}
},
"ncc_spoke_config": {
"type": "object",
"properties": {
"hub": {
"type": "string"
},
"description": {
"type": "string"
},
"labels": {
"type": "object"
},
"exclude_export_ranges": {
"type": "array",
"items": {
"type": "string"
}
},
"include_export_ranges": {
"type": "array",
"items": {
"type": "string"
}
},
"group": {
"type": "string"
}
}
}
}
}

View File

@@ -0,0 +1,107 @@
/**
* Copyright 2024 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 "automation" {
# tfdoc:variable:source 0-org-setup
description = "Automation resources created by the bootstrap stage."
type = object({
outputs_bucket = string
})
nullable = false
}
variable "billing_account" {
# tfdoc:variable:source 0-org-setup
description = "Billing account id."
type = object({
id = string
})
}
variable "custom_roles" {
# tfdoc:variable:source 0-org-setup
description = "Custom roles defined at the org level, in key => id format."
type = map(string)
nullable = false
default = {}
}
variable "folder_ids" {
# tfdoc:variable:source 0-org-setup
description = "Folders created in the bootstrap stage."
type = map(string)
nullable = false
default = {}
}
variable "iam_principals" {
# tfdoc:variable:source 0-org-setup
description = "IAM-format principals."
type = map(string)
nullable = false
default = {}
}
variable "organization" {
# tfdoc:variable:source 0-org-setup
description = "Organization details."
type = object({
id = number
})
nullable = false
}
variable "prefix" {
# tfdoc:variable:source 0-org-setup
description = "Prefix used for resources that need unique names. Use a maximum of 9 chars for organizations, and 11 chars for tenants."
type = string
validation {
condition = try(length(var.prefix), 0) < 12
error_message = "Use a maximum of 9 chars for organizations, and 11 chars for tenants."
}
}
variable "project_ids" {
# tfdoc:variable:source 0-org-setup
description = "Projects created in the bootstrap stage."
type = map(string)
nullable = false
default = {}
}
variable "service_accounts" {
# tfdoc:variable:source 0-org-setup
description = "Service accounts created in the bootstrap stage."
type = map(string)
nullable = false
default = {}
}
variable "tag_keys" {
# tfdoc:variable:source 0-org-setup
description = "FAST-managed resource manager tag keys."
type = map(string)
nullable = false
default = {}
}
variable "tag_values" {
# tfdoc:variable:source 0-org-setup
description = "FAST-managed resource manager tag values."
type = map(string)
nullable = false
default = {}
}

View File

@@ -0,0 +1,66 @@
/**
* 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 "context" {
description = "Context-specific interpolations."
type = object({
custom_roles = optional(map(string), {})
folder_ids = optional(map(string), {})
iam_principals = optional(map(string), {})
locations = optional(map(string), {})
project_ids = optional(map(string), {})
tag_keys = optional(map(string), {})
tag_values = optional(map(string), {})
})
default = {}
nullable = false
}
variable "factories_config" {
description = "Configuration for the resource factories or external data."
type = object({
defaults = optional(string, "datasets/hub-and-spokes-peerings/defaults.yaml")
dns = optional(string, "datasets/hub-and-spokes-peerings/dns/zones")
dns-response-policies = optional(string, "datasets/hub-and-spokes-peerings/dns/response-policies")
firewall-policies = optional(string, "datasets/hub-and-spokes-peerings/firewall-policies")
folders = optional(string, "datasets/hub-and-spokes-peerings/folders")
ncc-hubs = optional(string, "datasets/hub-and-spokes-peerings/ncc-hubs")
nvas = optional(string, "datasets/hub-and-spokes-peerings/nvas")
projects = optional(string, "datasets/hub-and-spokes-peerings/projects")
vpcs = optional(string, "datasets/hub-and-spokes-peerings/vpcs")
})
nullable = false
default = {}
}
variable "outputs_location" {
description = "Path where tfvars files for the following stages are written. Leave empty to disable."
type = string
default = null
}
variable "universe" {
# tfdoc:variable:source 0-org-setup
description = "GCP universe where to deploy projects. The prefix will be prepended to the project id."
type = object({
domain = string
prefix = string
forced_jit_service_identities = optional(list(string), [])
unavailable_services = optional(list(string), [])
unavailable_service_identities = optional(list(string), [])
})
default = null
}

View File

@@ -39,7 +39,7 @@ To destroy a previous FAST deployment follow the instructions detailed in [clean
- [Security](2-security/README.md) - [Security](2-security/README.md)
Manages centralized security configurations in a separate stage, and is typically owned by the security team. This stage implements VPC Security Controls via separate perimeters for environments and central services, and creates projects to host centralized KMS keys used by the whole organization. It's meant to be easily extended to include other security-related resources which are required, like Secret Manager.\ Manages centralized security configurations in a separate stage, and is typically owned by the security team. This stage implements VPC Security Controls via separate perimeters for environments and central services, and creates projects to host centralized KMS keys used by the whole organization. It's meant to be easily extended to include other security-related resources which are required, like Secret Manager.\
Exports: KMS key ids, CA ids Exports: KMS key ids, CA ids
- Networking ([Peering/VPN](2-networking-legacy-a-simple/README.md)/[NVA (w/ optional BGP support)](2-networking-legacy-b-nva/README.md)/[Separate environments](2-networking-legacy-c-separate-envs/README.md)) - Networking ([Networking factory](2-networking/README.md))/Networking ([Peering/VPN](2-networking-legacy-a-simple/README.md)/[NVA (w/ optional BGP support)](2-networking-legacy-b-nva/README.md)/[Separate environments](2-networking-legacy-c-separate-envs/README.md))
Manages centralized network resources in a separate stage, and is typically owned by the networking team. This stage implements a hub-and-spoke design, and includes connectivity via VPN to on-premises, and YAML-based factories for firewall rules (hierarchical and VPC-level) and subnets. It's currently available in four flavors: [spokes connected via VPC peering/VPN](2-networking-legacy-a-simple/README.md), [spokes connected via appliances (w/ optional BGP support)](2-networking-legacy-b-nva/README.md) and [separated network environments](2-networking-legacy-c-separate-envs/README.md).\ Manages centralized network resources in a separate stage, and is typically owned by the networking team. This stage implements a hub-and-spoke design, and includes connectivity via VPN to on-premises, and YAML-based factories for firewall rules (hierarchical and VPC-level) and subnets. It's currently available in four flavors: [spokes connected via VPC peering/VPN](2-networking-legacy-a-simple/README.md), [spokes connected via appliances (w/ optional BGP support)](2-networking-legacy-b-nva/README.md) and [separated network environments](2-networking-legacy-c-separate-envs/README.md).\
Exports: host project ids and numbers, vpc self links Exports: host project ids and numbers, vpc self links
- [Project Factory](./2-project-factory/) - [Project Factory](./2-project-factory/)

View File

@@ -0,0 +1,38 @@
automation = {
outputs_bucket = "test"
}
billing_account = {
id = "000000-111111-222222"
}
factories_config = {
defaults = "datasets/hub-and-spokes-peerings/defaults.yaml"
dns = "datasets/hub-and-spokes-peerings/dns/zones"
dns-response-policies = "datasets/hub-and-spokes-peerings/dns/response-policies"
firewall-policies = "datasets/hub-and-spokes-peerings/firewall-policies"
folders = "datasets/hub-and-spokes-peerings/folders"
interconnect = "datasets/hub-and-spokes-peerings/interconnect"
ncc-hubs = "datasets/hub-and-spokes-peerings/ncc-hubs"
nvas = "datasets/hub-and-spokes-peerings/nvas"
projects = "datasets/hub-and-spokes-peerings/projects"
vpcs = "datasets/hub-and-spokes-peerings/vpcs"
}
folder_ids = {
"networking" = "folders/12345678"
"networking/prod" = "folders/23456789"
"networking/dev" = "folders/34567890"
}
organization = {
domain = "fast.example.com"
id = 123456789012
customer_id = "C00000000"
}
prefix = "fast"
service_accounts = {
"iac-0/iac-pf-rw" = "iac-pf-rw@test.iam.gserviceaccount.com"
"iac-0/iac-pf-ro" = "iac-pf-ro@test.iam.gserviceaccount.com"
}
tag_values = {
"environment/development" = "tagValues/12345"
"environment/production" = "tagValues/12346"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
# 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: fast/stages/2-networking
tests:
peerings:

View File

@@ -84,6 +84,7 @@ duplicates = [
"modules/net-firewall-policy/schemas/firewall-policy-rules.schema.json", "modules/net-firewall-policy/schemas/firewall-policy-rules.schema.json",
], ],
[ [
"fast/stages/2-networking/schemas/firewall-rules.schema.json",
"fast/stages/2-networking-legacy-a-simple/schemas/firewall-rules.schema.json", "fast/stages/2-networking-legacy-a-simple/schemas/firewall-rules.schema.json",
"fast/stages/2-networking-legacy-c-separate-envs/schemas/firewall-rules.schema.json", "fast/stages/2-networking-legacy-c-separate-envs/schemas/firewall-rules.schema.json",
"fast/stages/2-networking-legacy-b-nva/schemas/firewall-rules.schema.json", "fast/stages/2-networking-legacy-b-nva/schemas/firewall-rules.schema.json",
@@ -91,6 +92,7 @@ duplicates = [
], ],
[ [
"fast/stages/2-project-factory/schemas/folder.schema.json", "fast/stages/2-project-factory/schemas/folder.schema.json",
"fast/stages/2-networking/schemas/folder.schema.json",
"fast/stages/0-org-setup/schemas/folder.schema.json", "fast/stages/0-org-setup/schemas/folder.schema.json",
"modules/project-factory/schemas/folder.schema.json", "modules/project-factory/schemas/folder.schema.json",
], ],
@@ -114,6 +116,7 @@ duplicates = [
"modules/vpc-sc/schemas/perimeter.schema.json", "modules/vpc-sc/schemas/perimeter.schema.json",
], ],
[ [
"fast/stages/2-networking/schemas/project.schema.json",
"fast/stages/2-project-factory/schemas/project.schema.json", "fast/stages/2-project-factory/schemas/project.schema.json",
"fast/stages/0-org-setup/schemas/project.schema.json", "fast/stages/0-org-setup/schemas/project.schema.json",
"fast/stages/2-security/schemas/project.schema.json", "fast/stages/2-security/schemas/project.schema.json",
@@ -125,6 +128,7 @@ duplicates = [
"modules/organization/schemas/scc-sha-custom-modules.schema.json", "modules/organization/schemas/scc-sha-custom-modules.schema.json",
], ],
[ [
"fast/stages/2-networking/schemas/subnet.schema.json",
"fast/stages/2-networking-legacy-a-simple/schemas/subnet.schema.json", "fast/stages/2-networking-legacy-a-simple/schemas/subnet.schema.json",
"fast/stages/2-networking-legacy-c-separate-envs/schemas/subnet.schema.json", "fast/stages/2-networking-legacy-c-separate-envs/schemas/subnet.schema.json",
"fast/stages/2-networking-legacy-b-nva/schemas/subnet.schema.json", "fast/stages/2-networking-legacy-b-nva/schemas/subnet.schema.json",