From a9cba47ce8679010d3d3201efb33ed3c6aac60c4 Mon Sep 17 00:00:00 2001 From: Luca Prete Date: Tue, 4 Apr 2023 20:41:04 +0200 Subject: [PATCH] Add FAST stage 2-networking-e-nva-bgp (NVA+NCC) Co-authored-by: Luca Prete Co-authored-by: Simone Bruzzechesse Co-authored-by: Simone Ruffilli --- fast/stages/2-networking-a-peering/README.md | 4 +- fast/stages/2-networking-b-vpn/README.md | 4 +- fast/stages/2-networking-c-nva/README.md | 2 +- .../2-networking-d-separate-envs/README.md | 2 +- fast/stages/2-networking-e-nva-bgp/README.md | 540 ++++++++++++++++++ .../data/bgp-config.tftpl | 76 +++ .../2-networking-e-nva-bgp/data/cidrs.yaml | 27 + .../data/dashboards/firewall_insights.json | 68 +++ .../data/dashboards/vpn.json | 248 ++++++++ .../data/firewall-rules/dev/rules.yaml | 21 + .../firewall-rules/landing-trusted/rules.yaml | 31 + .../landing-untrusted/rules.yaml | 31 + .../data/hierarchical-policy-rules.yaml | 49 ++ .../subnets/dev/dev-dataplatform-ew1.yaml | 8 + .../data/subnets/dev/dev-default-ew1.yaml | 5 + .../data/subnets/dev/dev-default-ew4.yaml | 5 + .../landing-trusted-default-ew1.yaml | 5 + .../landing-trusted-default-ew4.yaml | 5 + .../landing-untrusted-default-ew1.yaml | 5 + .../landing-untrusted-default-ew4.yaml | 5 + .../data/subnets/prod/prod-default-ew1.yaml | 5 + .../data/subnets/prod/prod-default-ew4.yaml | 5 + .../stages/2-networking-e-nva-bgp/diagram.png | Bin 0 -> 81573 bytes .../stages/2-networking-e-nva-bgp/diagram.svg | 1 + fast/stages/2-networking-e-nva-bgp/dns-dev.tf | 53 ++ .../2-networking-e-nva-bgp/dns-landing.tf | 155 +++++ .../stages/2-networking-e-nva-bgp/dns-prod.tf | 53 ++ fast/stages/2-networking-e-nva-bgp/landing.tf | 145 +++++ fast/stages/2-networking-e-nva-bgp/main.tf | 57 ++ .../2-networking-e-nva-bgp/monitoring.tf | 32 ++ fast/stages/2-networking-e-nva-bgp/ncc.tf | 108 ++++ fast/stages/2-networking-e-nva-bgp/nva.tf | 187 ++++++ fast/stages/2-networking-e-nva-bgp/outputs.tf | 91 +++ fast/stages/2-networking-e-nva-bgp/regions.tf | 46 ++ .../2-networking-e-nva-bgp/spoke-dev.tf | 112 ++++ .../2-networking-e-nva-bgp/spoke-prod.tf | 110 ++++ .../2-networking-e-nva-bgp/test-resources.tf | 245 ++++++++ .../2-networking-e-nva-bgp/variables.tf | 283 +++++++++ .../2-networking-e-nva-bgp/vpn-onprem.tf | 56 ++ fast/stages/3-project-factory/dev/README.md | 2 +- fast/stages/COMPANION.md | 3 +- fast/stages/README.md | 4 +- .../s2_networking_e_nva_bgp/common.tfvars | 106 ++++ .../s2_networking_e_nva_bgp/stage.tfvars | 0 .../stages/s2_networking_e_nva_bgp/stage.yaml | 17 + .../s2_networking_e_nva_bgp/tftest.yaml | 20 + 46 files changed, 3027 insertions(+), 10 deletions(-) create mode 100644 fast/stages/2-networking-e-nva-bgp/README.md create mode 100644 fast/stages/2-networking-e-nva-bgp/data/bgp-config.tftpl create mode 100644 fast/stages/2-networking-e-nva-bgp/data/cidrs.yaml create mode 100644 fast/stages/2-networking-e-nva-bgp/data/dashboards/firewall_insights.json create mode 100644 fast/stages/2-networking-e-nva-bgp/data/dashboards/vpn.json create mode 100644 fast/stages/2-networking-e-nva-bgp/data/firewall-rules/dev/rules.yaml create mode 100644 fast/stages/2-networking-e-nva-bgp/data/firewall-rules/landing-trusted/rules.yaml create mode 100644 fast/stages/2-networking-e-nva-bgp/data/firewall-rules/landing-untrusted/rules.yaml create mode 100644 fast/stages/2-networking-e-nva-bgp/data/hierarchical-policy-rules.yaml create mode 100644 fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-dataplatform-ew1.yaml create mode 100644 fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-default-ew1.yaml create mode 100644 fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-default-ew4.yaml create mode 100644 fast/stages/2-networking-e-nva-bgp/data/subnets/landing-trusted/landing-trusted-default-ew1.yaml create mode 100644 fast/stages/2-networking-e-nva-bgp/data/subnets/landing-trusted/landing-trusted-default-ew4.yaml create mode 100644 fast/stages/2-networking-e-nva-bgp/data/subnets/landing-untrusted/landing-untrusted-default-ew1.yaml create mode 100644 fast/stages/2-networking-e-nva-bgp/data/subnets/landing-untrusted/landing-untrusted-default-ew4.yaml create mode 100644 fast/stages/2-networking-e-nva-bgp/data/subnets/prod/prod-default-ew1.yaml create mode 100644 fast/stages/2-networking-e-nva-bgp/data/subnets/prod/prod-default-ew4.yaml create mode 100644 fast/stages/2-networking-e-nva-bgp/diagram.png create mode 100644 fast/stages/2-networking-e-nva-bgp/diagram.svg create mode 100644 fast/stages/2-networking-e-nva-bgp/dns-dev.tf create mode 100644 fast/stages/2-networking-e-nva-bgp/dns-landing.tf create mode 100644 fast/stages/2-networking-e-nva-bgp/dns-prod.tf create mode 100644 fast/stages/2-networking-e-nva-bgp/landing.tf create mode 100644 fast/stages/2-networking-e-nva-bgp/main.tf create mode 100644 fast/stages/2-networking-e-nva-bgp/monitoring.tf create mode 100644 fast/stages/2-networking-e-nva-bgp/ncc.tf create mode 100644 fast/stages/2-networking-e-nva-bgp/nva.tf create mode 100644 fast/stages/2-networking-e-nva-bgp/outputs.tf create mode 100644 fast/stages/2-networking-e-nva-bgp/regions.tf create mode 100644 fast/stages/2-networking-e-nva-bgp/spoke-dev.tf create mode 100644 fast/stages/2-networking-e-nva-bgp/spoke-prod.tf create mode 100644 fast/stages/2-networking-e-nva-bgp/test-resources.tf create mode 100644 fast/stages/2-networking-e-nva-bgp/variables.tf create mode 100644 fast/stages/2-networking-e-nva-bgp/vpn-onprem.tf create mode 100644 tests/fast/stages/s2_networking_e_nva_bgp/common.tfvars create mode 100644 tests/fast/stages/s2_networking_e_nva_bgp/stage.tfvars create mode 100644 tests/fast/stages/s2_networking_e_nva_bgp/stage.yaml create mode 100644 tests/fast/stages/s2_networking_e_nva_bgp/tftest.yaml diff --git a/fast/stages/2-networking-a-peering/README.md b/fast/stages/2-networking-a-peering/README.md index 11647b8238..9b043eb5e0 100644 --- a/fast/stages/2-networking-a-peering/README.md +++ b/fast/stages/2-networking-a-peering/README.md @@ -72,13 +72,13 @@ As mentioned initially, there are of course other ways to implement internal con This is a summary of the main options: -- [HA VPN](https://cloud.google.com/network-connectivity/docs/vpn/concepts/topologies) (implemented by [2-networking-vpn](../2-networking-b-vpn/)) +- [HA VPN](https://cloud.google.com/network-connectivity/docs/vpn/concepts/topologies) (implemented by [2-networking-b-vpn](../2-networking-b-vpn/)) - Pros: simple compatibility with GCP services that leverage peering internally, better control on routes, avoids peering groups shared quotas and limits - Cons: additional cost, marginal increase in latency, requires multiple tunnels for full bandwidth - [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (implemented here) - Pros: no additional costs, full bandwidth with no configurations, no extra latency, total environment isolation - Cons: no transitivity (e.g. to GKE masters, Cloud SQL, etc.), no selective exchange of routes, several quotas and limits shared between VPCs in a peering group -- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (implemented by [2-networking-nva](../2-networking-c-nva/)) +- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (implemented by [2-networking-c-nva](../2-networking-c-nva/) and [2-networking-e-nva-bgp](../2-networking-e-nva-bgp/)) - Pros: additional security features (e.g. IPS), potentially better integration with on-prem systems by using the same vendor - Cons: complex HA/failover setup, limited by VM bandwidth and scale, additional costs for VMs and licenses, out of band management of a critical cloud component diff --git a/fast/stages/2-networking-b-vpn/README.md b/fast/stages/2-networking-b-vpn/README.md index 835ff71cae..ed52715df3 100644 --- a/fast/stages/2-networking-b-vpn/README.md +++ b/fast/stages/2-networking-b-vpn/README.md @@ -73,10 +73,10 @@ This is a summary of the main options: - [HA VPN](https://cloud.google.com/network-connectivity/docs/vpn/concepts/topologies) (implemented here) - Pros: simple compatibility with GCP services that leverage peering internally, better control on routes, avoids peering groups shared quotas and limits - Cons: additional cost, marginal increase in latency, requires multiple tunnels for full bandwidth -- [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (implemented by [2-networking-peering](../2-networking-a-peering/)) +- [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (implemented by [2-networking-a-peering](../2-networking-a-peering/)) - Pros: no additional costs, full bandwidth with no configurations, no extra latency - Cons: no transitivity (e.g. to GKE masters, Cloud SQL, etc.), no selective exchange of routes, several quotas and limits shared between VPCs in a peering group -- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (implemented by [2-networking-nva](../2-networking-c-nva/)) +- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (implemented by [2-networking-c-nva](../2-networking-c-nva/) and [2-networking-e-nva-bgp](../2-networking-e-nva-bgp/)) - Pros: additional security features (e.g. IPS), potentially better integration with on-prem systems by using the same vendor - Cons: complex HA/failover setup, limited by VM bandwidth and scale, additional costs for VMs and licenses, out of band management of a critical cloud component diff --git a/fast/stages/2-networking-c-nva/README.md b/fast/stages/2-networking-c-nva/README.md index 8874dc4689..43bc5daf82 100644 --- a/fast/stages/2-networking-c-nva/README.md +++ b/fast/stages/2-networking-c-nva/README.md @@ -110,7 +110,7 @@ This is an options summary: - [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (used here to connect the trusted landing VPC with the spokes, also used by [02-networking-vpn](../2-networking-b-vpn/)) - Pros: no additional costs, full bandwidth with no configurations, no extra latency - Cons: no transitivity (e.g. to GKE masters, Cloud SQL, etc.), no selective exchange of routes, several quotas and limits shared between VPCs in a peering group -- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (used here to connect the trusted landing and untrusted VPCs) +- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (used here to connect the trusted landing and untrusted VPCs) and multi-NIC appliances with NCC/BGP support implemented [here](../2-networking-e-nva-bgp/) - Pros: provides additional security features (e.g. IPS), potentially better integration with on-prem systems by using the same vendor - Cons: complex HA/failover setup, limited by VM bandwidth and scale, additional costs for VMs and licenses, out of band management of a critical cloud component - [HA VPN](https://cloud.google.com/network-connectivity/docs/vpn/concepts/topologies) diff --git a/fast/stages/2-networking-d-separate-envs/README.md b/fast/stages/2-networking-d-separate-envs/README.md index 9890c865f9..596d427a9a 100644 --- a/fast/stages/2-networking-d-separate-envs/README.md +++ b/fast/stages/2-networking-d-separate-envs/README.md @@ -6,7 +6,7 @@ While no communication between environment is implemented on this design, that c - [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) - which is not recommended as it would effectively create a full line of sight between workloads belonging to different environments - [VPN HA](https://cloud.google.com/network-connectivity/docs/vpn/concepts/topologies) tunnels between environments, exchanging a subset of well-defined routes. -- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) connecting the different environments, allowing the use of NVAs to enforce networking policies. +- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) implemented in [2-networking-c-nva](../2-networking-c-nva) and [2-networking-e-nva-bgp](../2-networking-e-nva-bgp) connecting the different environments, allowing the use of NVAs to enforce networking policies. The following diagram illustrates the high-level design, and should be used as a reference for the following sections. The final number of subnets, and their IP addressing design will of course depend on customer-specific requirements, and can be easily changed via variables or external data files without having to edit the actual code. diff --git a/fast/stages/2-networking-e-nva-bgp/README.md b/fast/stages/2-networking-e-nva-bgp/README.md new file mode 100644 index 0000000000..adb68d8977 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/README.md @@ -0,0 +1,540 @@ +# Networking with Network Virtual Appliance + +This stage sets up the shared network infrastructure for the whole organization. + +It is designed for those who would like to leverage Network Virtual Appliances (NVAs) between trusted and untrusted areas of the network, for example for Intrusion Prevention System (IPS) purposes. + +We use Network Connectivity Center Router Appliance (NCC-RA) and BGP appliances (for the sake of the demo [FRRouting](https://frrouting.org/)) to avoid different limitations that static routes bring (more [here](https://medium.com/google-cloud/gcp-routing-adventures-vol-2-enterprise-multi-regional-deployments-in-google-cloud-3968e9591d59)). The goals of this design include: + +- Avoid using network tags to route traffic. +- Route traffic symmetrically to prevent breaking stateful NVAs. +- Avoid unnecessary NAT traffic at the NVAs. +- Avoid cross-regional traffic unless absolutely necessary for disaster recovery. +- Automatically send all traffic through the cross-regional NVAs if the ones in-region fail. +- Keep the trusted hub VPC unique, rather than having one per region. + +It adopts the common “hub and spoke” reference design, which is well suited for multiple scenarios, and it offers several advantages versus other designs: + +- the "trusted hub" VPC centralizes the external connectivity towards trusted network resources (e.g. on-prem, other cloud environments and the spokes), and it is ready to host cross-environment services like CI/CD, code repositories, and monitoring probes +- the "spoke" VPCs allow partitioning workloads (e.g. by environment like in this setup), while still retaining controlled access to central connectivity and services +- Shared VPCs -both in hub and spokes- split the management of the network resources into specific (host) projects, while still allowing them to be consumed from the workload (service) projects +- the design facilitates DNS centralization + +Connectivity between the hub and the spokes is established via [VPC network peerings](https://cloud.google.com/vpc/docs/vpc-peering), which offer uncapped bandwidth, lower latencies, at no additional costs and with a very low management overhead. Different ways of implementing connectivity, and related some pros and cons, are discussed below. + +The diagram shows the high-level design and it should be used as a reference throughout the following sections. + +The final number of subnets, and their IP addressing will depend on the user-specific requirements. It can be easily changed via variables or external data files, without any need to edit the code. + +

+ Networking diagram +

+ +## Table of contents + +- [Networking with Network Virtual Appliance](#networking-with-network-virtual-appliance) + - [Table of contents](#table-of-contents) + - [Design overview and choices](#design-overview-and-choices) + - [Multi-regional deployment](#multi-regional-deployment) + - [VPC design](#vpc-design) + - [External connectivity](#external-connectivity) + - [Internal connectivity](#internal-connectivity) + - [IP ranges, subnetting, routing](#ip-ranges-subnetting-routing) + - [Internet egress](#internet-egress) + - [VPC and Hierarchical Firewall](#vpc-and-hierarchical-firewall) + - [DNS](#dns) + - [Stage structure and files layout](#stage-structure-and-files-layout) + - [VPCs](#vpcs) + - [VPNs](#vpns) + - [Routing and BGP](#routing-and-bgp) + - [Firewall](#firewall) + - [DNS architecture](#dns-architecture) + - [Cloud environment](#cloud-environment) + - [Cloud to on-prem](#cloud-to-on-prem) + - [On-prem to cloud](#on-prem-to-cloud) + - [How to run this stage](#how-to-run-this-stage) + - [Provider and Terraform variables](#provider-and-terraform-variables) + - [Impersonating the automation service account](#impersonating-the-automation-service-account) + - [Variable configuration](#variable-configuration) + - [Using delayed billing association for projects](#using-delayed-billing-association-for-projects) + - [Running the stage](#running-the-stage) + - [Post-deployment activities](#post-deployment-activities) + - [Private Google Access](#private-google-access) + - [Customizations](#customizations) + - [Changing default regions](#changing-default-regions) + - [Configuring the VPNs to on prem](#configuring-the-vpns-to-on-prem) + - [Adding an environment](#adding-an-environment) + - [Files](#files) + - [Variables](#variables) + - [Outputs](#outputs) + +## Design overview and choices + +### Multi-regional deployment + +The stage deploys the the infrastructure in two regions. By default, europe-west1 and europe-west4. Regional resources include NVAs and test VMs. This provides enough redundancy to be resilient to regional failures. +In case of a regional failure, the corresponding dynamic routes are withdrawn and traffic will failover in the secondary region. + +### VPC design + +The "landing zone" is divided into two VPC networks: + +- the trusted VPC: the connectivity hub towards other trusted networks +- the untrusted VPC: the connectivity hub towards any other untrusted network + +### NCC, NVAs and BGP sessions + +The VPCs connect through two sets of sample NVA machines: one per region, each containing two instances. The appliances run [Contrainer-Optimized OS](https://cloud.google.com/container-optimized-os/docs) and a container with [FRRouting](https://frrouting.org/). + +We levarage NCC-RA to allow the NVAs to establish BGP sessions with Cloud Routers in the untrusted and in the trusted VPCs. This allows Cloud Routers to advertise routes to the NVAs, and the NVAs to announce routes to the Cloud Router, so it can program them in the VPC. + +Specifically, each NVA establishes two BGP sessions (for redundancy) with the the Cloud Router deployed in the VPC and in the subnet where the interface of that VM is attached to. + +**Cloud Routers in the untrusted VPC advertise the default route (0.0.0.0/0) to the NVAs**. The NVAs advertise the route to the Cloud Routers in the trusted VPC. These dynamic routes are then imported through VPC peerings in the spokes. + +**Cloud Routers in the trusted hub advertis to the NVAs** all the subnets of the trusted VPCs. This includes the regional subnets and the cross-regional subnets. The NVAs manipulate the route costs (MED) before advertising them to the Cloud Routers in the untrusted VPC. This is done to guarantee symmetric traffic paths (more [here](https://medium.com/google-cloud/gcp-routing-adventures-vol-2-enterprise-multi-regional-deployments-in-google-cloud-3968e9591d59)). + +NVAs establish **extra BGP sessions with both cross-regional NVAs**. In this case, the NVAs advertise the regional trusted routes only. This allows cross-spoke (environment) traffic to remain also symmetric (more [here](https://medium.com/google-cloud/gcp-routing-adventures-vol-2-enterprise-multi-regional-deployments-in-google-cloud-3968e9591d59)). We set these routes to be exchanged at a lower cost than the one set for the other routes. + +Following the majority of real-life deployments, **we assume appliances to be stateful and not able to synchronize sessions between multiple NVAs within the same regional cluster**. For this reason, within each regional cluster, NVAs announce the same routes with different MED costs (1 point of difference between the primary and the secondary). This will cause traffic to go deterministically through one applaiance at the time within each region. You can change this default behavior modifying the cost settings in the [NVAs BGP configuration file](./data/bgp-config.tftpl). + +By default, the design assumes that: + +- on-premise networks (and related resources) are considered trusted. As such, the VPNs connecting with on-premises are terminated in GCP, in the trusted VPC +- the public Internet is considered untrusted. As such [Cloud NAT](https://cloud.google.com/nat/docs/overview) has been deployed in the untrusted landing VPC only. Also, the default route is set to carry traffic from the trusted VPCs, through the NVAs, to the untrusted VPC. +- cross-spoke (environment) traffic and traffic from any untrusted network to any trusted network (and vice versa) pass through the NVAs. +- any traffic from a trusted network to an untrusted network (e.g. Internet) is natted by the NVAs. Users can configure further exclusions. + +The trusted landing VPC acts as a hub: it bridges internal resources with the outside world and it hosts the shared services consumed by the spoke VPCs, connected to the hub thorugh VPC network peerings. Spokes are used to partition the environments. By default: + +- one spoke VPC hosts the development environment resources +- one spoke VPC hosts the production environment resources + +Each virtual network is a [shared VPC](https://cloud.google.com/vpc/docs/shared-vpc): shared VPCs are managed in dedicated *host projects* and shared with other *service projects* that consume the network resources. +Shared VPCs let organization administrators delegate administrative responsibilities, such as creating and managing instances, to Service Project Admins while maintaining centralized control over network resources like subnets, routes, and firewalls. + +Users can easily extend the design to host additional environments, or adopt different logical mappings for the spokes (for example, in order to create a new spoke for each company entity). Adding spokes is trivial and it does not increase the design complexity. The steps to add more spokes are provided in the following sections. + +In multi-organization scenarios, where production and non-production resources use different Cloud Identity and GCP organizations, the hub/landing VPC is usually part of the production organization. It establishes connections with the production spokes within the same organization, and with non-production spokes in a different organization. + +### External connectivity + +External connectivity to on-prem is implemented leveraging [Cloud HA VPN](https://cloud.google.com/network-connectivity/docs/vpn/concepts/topologies) (two tunnels per region). This is what users normally deploy as a final solution, or to validate routing and to transfer data, while waiting for [interconnects](https://cloud.google.com/network-connectivity/docs/interconnect) to be provisioned. + +Connectivity to additional on-prem sites or to other cloud providers should be implemented in a similar fashion, via VPN tunnels or interconnects, in the landing VPC (either trusted or untrusted, depending by the nature of the peers), sharing the same regional routers. + +### Internal connectivity + +Internal connectivity (e.g. between the trusted landing VPC and the spokes) is realized with VPC network peerings. As mentioned, there are other ways to implement connectivity. These can be easily retrofitted with minimal code changes, although they introduce additional considerations on service interoperability, quotas and management. + +This is an options summary: + +- [VPC Peering](https://cloud.google.com/vpc/docs/vpc-peering) (used here to connect the trusted landing VPC with the spokes, also used by [02-networking-vpn](../2-networking-b-vpn/)) + - Pros: no additional costs, full bandwidth with no configurations, no extra latency + - Cons: no transitivity (e.g. to GKE masters, Cloud SQL, etc.), no selective exchange of routes, several quotas and limits shared between VPCs in a peering group +- [Multi-NIC appliances](https://cloud.google.com/architecture/best-practices-vpc-design#multi-nic) (used here to connect the trusted landing and untrusted VPCs) + - Pros: provides additional security features (e.g. IPS), potentially better integration with on-prem systems by using the same vendor + - Cons: complex HA/failover setup, limited by VM bandwidth and scale, additional costs for VMs and licenses, out of band management of a critical cloud component +- [HA VPN](https://cloud.google.com/network-connectivity/docs/vpn/concepts/topologies) + - Pros: simple compatibility with GCP services that leverage peering internally, better control on routes, avoids peering groups shared quotas and limits + - Cons: additional costs, marginal increase in latency, requires multiple tunnels for full bandwidth + +### IP ranges, subnetting, routing + +Minimizing the number of routes (and subnets) in the cloud environment is important, as it simplifies management and it 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 to carefully plan the IP space used in your cloud environment. This allows the use of larger IP CIDR blocks in routes, whenever possible. + +This stage uses a dedicated /16 block (10.128.0.0/16), which should be sized to the own needs. The subnets created in each VPC derive from this range. + +The /16 block is evenly split in eight, smaller /19 blocks, assigned to different areas of the GCP network: *landing untrusted europe-west1*, *landing untrusted europe-west4*, *landing trusted europe-west1*, *landing untrusted europe-west4*, *development europe-west1*, *development europe-west4*, *production europe-west1*, *production europe-west4*. + +The first /24 range in every area is allocated for a default subnet, which can be removed or modified as needed. + +Spoke VPCs also define and reserve three "special" CIDR ranges, derived from the respective /19, dedicated to + +- [PSA (Private Service Access)](https://cloud.google.com/vpc/docs/private-services-access): + + - The second-last /24 range is used for PSA (CloudSQL, Postrgres) + + - The third-last /24 range is used for PSA (CloudSQL, MySQL) + +- [Internal HTTPs Load Balancers (L7ILB)](https://cloud.google.com/load-balancing/docs/l7-internal): + + - The last /24 range + +This is a summary of the subnets allocated by default in this setup: + +| name | description | CIDR | +|---|---|---| +| landing-trusted-default-ew1 | Trusted landing subnet - europe-west1 | 10.128.64.0/24 | +| landing-trusted-default-ew4 | Trusted landing subnet - europe-west4 | 10.128.96.0/24 | +| landing-untrusted-default-ew1 | Untrusted landing subnet - europe-west1 | 10.128.0.0/24 | +| landing-untrusted-default-ew4 | Untrusted landing subnet - europe-west4 | 10.128.32.0/24 | +| dev-default-ew1 | Dev spoke subnet - europe-west1 | 10.128.128.0/24 | +| dev-default-ew1 (PSA MySQL) | PSA subnet for MySQL in dev spoke - europe-west1 | 10.128.157.0/24 | +| dev-default-ew1 (PSA SQL Server) | PSA subnet for Postgres in dev spoke - europe-west1 | 10.128.158.0/24 | +| dev-default-ew1 (L7 ILB) | L7 ILB subnet for dev spoke - europe-west1 | 10.128.92.0/24 | +| dev-default-ew4 | Dev spoke subnet - europe-west4 | 10.128.160.0/24 | +| dev-default-ew4 (PSA MySQL) | PSA subnet for MySQL in dev spoke - europe-west4 | 10.128.189.0/24 | +| dev-default-ew4 (PSA SQL Server) | PSA subnet for Postgres in dev spoke - europe-west4 | 10.128.190.0/24 | +| dev-default-ew4 (L7 ILB) | L7 ILB subnet for dev spoke - europe-west4 | 10.128.93.0/24 | +| prod-default-ew1 | Prod spoke subnet - europe-west1 | 10.128.192.0/24 | +| prod-default-ew1 (PSA MySQL) | PSA subnet for MySQL in prod spoke - europe-west1 | 10.128.221.0/24 | +| prod-default-ew1 (PSA SQL Server) | PSA subnet for Postgres in prod spoke - europe-west1 | 10.128.253.0/24 | +| prod-default-ew1 (L7 ILB) | L7 ILB subnet for prod spoke - europe-west1 | 10.128.60.0/24 | +| prod-default-ew4 | Prod spoke subnet - europe-west4 | 10.128.224.0/24 | +| prod-default-ew4 (PSA MySQL) | PSA subnet for MySQL in prod spoke - europe-west4 | 10.128.222.0/24 | +| prod-default-ew4 (PSA SQL Server) | PSA subnet for Postgres in prod spoke - europe-west4 | 10.128.254.0/24 | +| prod-default-ew4 (L7 ILB) | L7 ILB subnet for prod spoke - europe-west4 | 10.128.61.0/24 | + +These subnets are advertised to on-premises as a whole /16 range (10.128.0.0/16). + +Routes in GCP are either automatically created (for example, when a subnet is added to a VPC), manually created via static routes, dynamically exchanged through VPC peerings, or dynamically programmed by [Cloud Routers](https://cloud.google.com/network-connectivity/docs/router#docs) when a BGP session is established. BGP sessions can be configured to advertise VPC ranges, and/or custom ranges via custom advertisements. + +In this setup: + +- routes between multiple subnets within the same VPC are automatically exchanged by GCP +- the spokes and the trusted landing VPC exchange dynamic routes through VPC peerings +- on-premises is connected to the trusted landing VPC and it dynamically exchanges BGP routes with GCP (with the trusted VPC) using HA VPN +- the NVAs exchange dynamic routes using BGP with Cloud Routers in the untrusted VPC, Cloud Routers in the trusted VPC and cross-regional NVAs. This allows VMs in different environments and different regions to communicate. + +The Cloud Routers (connected to the VPN gateways in the trusted VPC) are configured to exclude the default advertisement of VPC ranges and they only advertise their respective aggregate ranges, via custom advertisements. This greatly simplifies the routing configuration and avoids quota or limit issues, by keeping the number of routes small, instead of making it proportional to the subnets and to the secondary ranges in the VPCs. + +### Internet egress + +In this setup, Internet egress is realized through [Cloud NAT](https://cloud.google.com/nat/docs/overview), deployed in the untrusted landing VPC. This allows instances in all other VPCs to reach the Internet, passing through the NVAs (being the public Internet considered untrusted). + +Several other scenarios are possible, with various degrees of complexity: + +- deploy Cloud NAT in every VPC +- add forwarding proxies, with optional URL filters +- send Internet traffic to on-premises, so the existing egress infrastructure can be leveraged + +Future pluggable modules will allow users to easily experiment with the above scenarios. + +### 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. Hierarchical firewall rules are applied in the networking folder for common ingress rules (egress is open by default): for example, it allows the health checks and the IAP forwarders traffic to reach the VMs. + +Rules and policies are defined in simple YAML files, described below. + +### DNS + +DNS goes hand in hand with networking, especially on GCP where Cloud DNS zones and policies are associated at the VPC level. This setup implements both DNS flows: + +- on-prem to cloud via private zones for cloud-managed domains, and an [inbound policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) used as forwarding target or via delegation (requires some extra configuration) from on-prem DNS resolvers +- cloud to on-prem via forwarding zones for the on-prem managed domains + +DNS configuration is further centralized by leveraging peering zones, so that + +- the hub/landing Cloud DNS hosts configurations for on-prem forwarding, Google API domains, and the top-level private zone/s (e.g. gcp.example.com) +- the spokes Cloud DNS host configurations for the environment-specific domains (e.g. prod.gcp.example.com), which are bound to the hub/landing leveraging [cross-project binding](https://cloud.google.com/dns/docs/zones/zones-overview#cross-project_binding); a peering zone for the `.` (root) zone is then created on each spoke, delegating all DNS resolution to hub/landing. +- Private Google Access is enabled for a selection of the [supported domains](https://cloud.google.com/vpc/docs/configure-private-google-access#domain-options), namely + - `private.googleapis.com` + - `restricted.googleapis.com` + - `gcr.io` + - `packages.cloud.google.com` + - `pkg.dev` + - `pki.goog` + +To complete the configuration, the 35.199.192.0/19 range should be routed to the VPN tunnels from on-premises, and the following names should be configured for DNS forwarding to cloud: + +- `private.googleapis.com` +- `restricted.googleapis.com` +- `gcp.example.com` (used as a placeholder) + +In GCP, a forwarding zone in the landing project is configured to forward queries to the placeholder domain `onprem.example.com` to on-premises. + +This configuration is battle-tested, and flexible enough to lend itself to simple modifications without subverting its design. + +## Stage structure and files layout + +### VPCs + +VPCs are defined in separate files, one for `landing` (trusted and untrusted), one for `prod` and one for `dev`. + +These files contain different resources: + +- **project** ([`projects`](../../../modules/project)): the "[host projects](https://cloud.google.com/vpc/docs/shared-vpc)" containing the VPCs and enabling the required APIs. +- **VPCs** ([`net-vpc`](../../../modules/net-vpc)): manage the subnets, the explicit routes for `{private,restricted}.googleapis.com` and the DNS inbound policy (for the trusted landing VPC). Subnets are created leveraging "resource factories": the configuration is separated from the module that implements it, and stored in a well-structured file. To add a new subnet, simply create a new file in the `data_folder` directory defined in the module, following the examples found in the [Fabric `net-vpc` documentation](../../../modules/net-vpc#subnet-factory). Sample subnets are shipped in [data/subnets](./data/subnets) and can be easily customized to fit users' needs. + +Subnets for [L7 ILBs](https://cloud.google.com/load-balancing/docs/l7-internal/proxy-only-subnets) are handled differently, and defined in variable `l7ilb_subnets`, while ranges for [PSA](https://cloud.google.com/vpc/docs/configure-private-services-access#allocating-range) are configured by variable `psa_ranges` - such variables are consumed by spoke VPCs. + +- **Cloud NAT** ([`net-cloudnat`](../../../modules/net-cloudnat)) (in the untrusted landing VPC only): it manages the networking infrastructure required to enable the Internet egress. + +### VPNs + +The connectivity between on-premises and GCP (the trusted landing VPC) is implemented with Cloud HA VPN ([`net-vpn`](../../../modules/net-vpn-ha)) and defined in [`vpn-onprem.tf`](./vpn-onprem.tf). The file implements a single logical connection between on-premises and the trusted landing VPC, both in `europe-west1` and `europe-west4`. The relevant parameters for its configuration are found in the variables `vpn_onprem_primary_config` and `vpn_onprem_secondary_config`. + +### Routing and BGP + +Each VPC network ([`net-vpc`](../../../modules/net-vpc)) manages a separate routing table, where you can define static routes (e.g. to private.googleapis.com) and receives dynamic routes through VPC peering and BGP sessions established with the neighbor networks (e.g. NCC routers, routers on-premises). + +NCC/Cloud Router BGP settings are defined in `ncc.tf`. +NVA BGP settings are defined in the [bpg-config.tftpl template file](./data/bgp-config.tftpl). +The variable `ncc_asn` allows to change the Autonomous System Number (ASN) assigned to the untrusted VPC Cloud Routers, to the trusted VPC Cloud Routers and to the NVAs. + +BGP sessions for trusted landing to on-premises are configured through the variable `vpn_onprem_configs`. + +### Firewall + +**VPC firewall rules** ([`net-vpc-firewall`](../../../modules/net-vpc-firewall)) are defined per-vpc on each `vpc-*.tf` file and leverage a resource factory to massively create rules. +To add a new firewall rule, create a new file or edit an existing one in the `data_folder` directory defined in the module `net-vpc-firewall`, following the examples of the "[Rules factory](../../../modules/net-vpc-firewall#rules-factory)" section of the module documentation. Sample firewall rules are shipped in [data/firewall-rules/landing-untrusted](./data/firewall-rules/landing-untrusted) and in [data/firewall-rules/landing-trusted](./data/firewall-rules/landing-trusted), and can be easily customized. + +**Hierarchical firewall policies** ([`folder`](../../../modules/folder)) are defined in `main.tf`, and managed through a policy factory implemented by the `folder` module, which applies the defined hierarchical to the `Networking` folder, which contains all the core networking infrastructure. Policies are defined in the `rules_file` file - to define a new one simply use the instructions found on "[Firewall policy factory](../../../modules/organization#firewall-policy-factory)". Sample hierarchical firewall policies are shipped in [data/hierarchical-policy-rules.yaml](./data/hierarchical-policy-rules.yaml) and can be easily customized. + +### DNS architecture + +The DNS ([`dns`](../../../modules/dns)) infrastructure is defined in [`dns-*.tf`] files. + +Cloud DNS manages onprem forwarding, the main GCP zone (in this example `gcp.example.com`) and environment-specific zones (i.e. `dev.gcp.example.com` and `prod.gcp.example.com`). + +#### Cloud environment + +The root DNS zone defined in the landing project acts as the source of truth for DNS within the Cloud environment. The resources defined in the spoke VPCs consume the landing DNS infrastructure through DNS peering (e.g. `prod-landing-root-dns-peering`). +The spokes can optionally define private zones (e.g. `prod-dns-private-zone`). Granting visibility both to the trusted and untrusted landing VPCs ensures that the whole cloud environment can query such zones. + +#### Cloud to on-prem + +Leveraging the forwarding zone defined in the landing project (e.g. `onprem-example-dns-forwarding` and `reverse-10-dns-forwarding`), the cloud environment can resolve `in-addr.arpa.` and `onprem.example.com.` using the on-premise DNS infrastructure. On-premise resolver IPs are set in the variable `dns.onprem`. + +DNS queries sent to the on-premise infrastructure come from the `35.199.192.0/19` source range. + +#### On-prem to cloud + +The [Inbound DNS Policy](https://cloud.google.com/dns/docs/server-policies-overview#dns-server-policy-in) defined in the *trusted landing VPC module* ([`landing.tf`](./landing.tf)) automatically reserves the first available IP address on each subnet (typically the third one in a CIDR) to expose the Cloud DNS service, so that it can be consumed from outside of GCP. + +## How to run this stage + +This stage is meant to be executed after the [resource management](../1-resman) stage has run, as it leverages the automation service account and bucket created there, and additional resources configured in the [bootstrap](../0-bootstrap) stage. + +It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stages for the environmental requirements. + +Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration. + +### Provider and Terraform variables + +As all other FAST stages, the [mechanism used to pass variable values and pre-built provider files from one stage to the next](../0-bootstrap/README.md#output-files-and-cross-stage-variables) is also leveraged here. + +The commands to link or copy the provider and terraform variable files can be easily derived from the `stage-links.sh` script in the FAST root folder, passing it a single argument with the local output files folder (if configured) or the GCS output bucket in the automation project (derived from stage 0 outputs). The following examples demonstrate both cases, and the resulting commands that then need to be copy/pasted and run. + +```bash +../../stage-links.sh ~/fast-config + +# copy and paste the following commands for '2-networking-a-peering' + +ln -s ~/fast-config/providers/2-networking-providers.tf ./ +ln -s ~/fast-config/tfvars/globals.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/0-bootstrap.auto.tfvars.json ./ +ln -s ~/fast-config/tfvars/1-resman.auto.tfvars.json ./ +``` + +```bash +../../stage-links.sh gs://xxx-prod-iac-core-outputs-0 + +# copy and paste the following commands for '2-networking-a-peering' + +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/providers/2-networking-providers.tf ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/globals.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-bootstrap.auto.tfvars.json ./ +gcloud alpha storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/1-resman.auto.tfvars.json ./ +``` + +### Impersonating the automation service account + +The preconfigured provider file uses impersonation to run with this stage's automation service account's credentials. The `gcp-devops` and `organization-admins` groups have the necessary IAM bindings in place to do that, so make sure the current user is a member of one of those groups. + +### Variable configuration + +Variables in this stage -- like most other FAST stages -- are broadly divided into three separate sets: + +- variables which refer to global values for the whole organization (org id, billing account id, prefix, etc.), which are pre-populated via the `globals.auto.tfvars.json` file linked or copied above +- variables which refer to resources managed by previous stage, which are prepopulated here via the `0-bootstrap.auto.tfvars.json` and `1-resman.auto.tfvars.json` files linked or copied above +- and finally variables that optionally control this stage's behaviour and customizations, and can to be set in a custom `terraform.tfvars` file + +The latter set is explained in the [Customization](#customizations) sections below, and the full list can be found in the [Variables](#variables) table at the bottom of this document. + +Note that the `outputs_location` variable is disabled by default, you need to explicitly set it in your `terraform.tfvars` file if you want output files to be generated by this stage. This is a sample `terraform.tfvars` that configures it, refer to the [bootstrap stage documentation](../0-bootstrap/README.md#output-files-and-cross-stage-variables) for more details: + +```hcl +outputs_location = "~/fast-config" +``` + +### Using delayed billing association for projects + +This configuration is possible but unsupported and only exists for development purposes, use at your own risk: + +- temporarily switch `billing_account.id` to `null` in `globals.auto.tfvars.json` +- for each project resources in the project modules used in this stage (`dev-spoke-project`, `landing-project`, `prod-spoke-project`) + - apply using `-target`, for example + `terraform apply -target 'module.landing-project.google_project.project[0]'` + - untaint the project resource after applying, for example + `terraform untaint 'module.landing-project.google_project.project[0]'` +- go through the process to associate the billing account with the two projects +- switch `billing_account.id` back to the real billing account id +- resume applying normally + +### Running the stage + +Once provider and variable values are in place and the correct user is configured, the stage can be run: + +```bash +terraform init +terraform apply +``` + +### Post-deployment activities + +- On-prem routers should be configured to advertise all relevant CIDRs to the GCP environments. To avoid hitting GCP quotas, we recomment aggregating routes as much as possible. +- On-prem routers should accept BGP sessions from their cloud peers. +- On-prem DNS servers should have forward zones for GCP-managed ones. + +#### Private Google Access + +[Private Google Access](https://cloud.google.com/vpc/docs/private-google-access) (or PGA) enables VMs and on-prem systems to consume Google APIs from within the Google network, and is already fully configured on this environment. + +For PGA to work: + +- Private Google Access should be enabled on the subnet. \ +Subnets created by the `net-vpc` module are PGA-enabled by default. + +- 199.36.153.4/30 (`restricted.googleapis.com`) and 199.36.153.8/30 (`private.googleapis.com`) should be routed from on-prem to VPC, and from there to the `default-internet-gateway`. \ +Per variable `vpn_onprem_configs` such ranges are advertised to onprem - furthermore every VPC (e.g. see `landing-vpc` in [`landing.tf`](./landing.tf)) has explicit routes set in case the `0.0.0.0/0` route is changed. + +- A private DNS zone for `googleapis.com` should be created and configured per [this article](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid#config-domain), as implemented in module `googleapis-private-zone` in [`dns-landing.tf`](./dns-landing.tf) + +## Customizations + +### Changing default regions + +Regions are defined via the `regions` variable which sets up a mapping between the `regions.primary` and `regions.secondary` logical names and actual GCP region names. If you need to change regions from the defaults: + +- change the values of the mappings in the `regions` variable to the regions you are going to use +- change the regions in the factory subnet files in the `data` folder + +### Configuring the VPNs to on prem + +This stage includes basic support for an HA VPN connecting the landing zone in the primary region to on prem. Configuration is via the `vpn_onprem_primary_config` and `vpn_onprem_secondary_config` variables, that closely mirrors the variables defined in the [`net-vpn-ha`](../../../modules/net-vpn-ha/). + +Support for the onprem VPNs is disabled by default so that no resources are created, this is an example of how to configure one variable to enable the VPN in the primary region: + +```hcl +vpn_onprem_primary_config = { + peer_external_gateways = { + default = { + redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT" + interfaces = ["8.8.8.8"] + } + } + router_config = { + asn = 65501 + custom_advertise = { + all_subnets = false + ip_ranges = { + "10.1.0.0/16" = "gcp" + "35.199.192.0/19" = "gcp-dns" + "199.36.153.4/30" = "gcp-restricted" + } + } + } + tunnels = { + "0" = { + bgp_peer = { + address = "169.254.1.1" + asn = 65500 + } + bgp_session_range = "169.254.1.2/30" + peer_external_gateway_interface = 0 + shared_secret = "foo" + vpn_gateway_interface = 0 + } + "1" = { + bgp_peer = { + address = "169.254.2.1" + asn = 64513 + } + bgp_session_range = "169.254.2.2/30" + peer_external_gateway_interface = 1 + shared_secret = "foo" + vpn_gateway_interface = 1 + } + } +} +# tftest skip +``` + +### Adding an environment + +To create a new environment (e.g. `staging`), a few changes are required: + +Create a `spoke-staging.tf` file by copying `spoke-prod.tf` file. +Adapt the new file by replacing the value "prod" with the value "staging". +Running `diff spoke-dev.tf spoke-prod.tf` can help to see how environment files differ. + +The new VPC requires a set of dedicated CIDRs, one per region, added to variable `gcp_ranges` (for example as `spoke_staging_ew1` and `spoke_staging_ew4`). +`gcp_ranges` is a map that "resolves" CIDR names to the actual addresses, and will be used later to configure routing. + +Variables managing L7 Internal Load Balancers (`l7ilb_subnets`) and Private Service Access (`psa_ranges`) should also be adapted, and subnets and firewall rules for the new spoke should be added, as described above. + +Configure the NVAs deployed updating the sample BGP [config file](./data/bgp-config.tftpl). + +DNS configurations are centralised in the `dns-*.tf` files. Spokes delegate DNS resolution to Landing through DNS peering, and optionally define a private zone (e.g. `dev.gcp.example.com`) which the landing peers to. To configure DNS for a new environment, copy one of the other environments DNS files [e.g. (dns-dev.tf)](dns-dev.tf) into a new `dns-*.tf` file suffixed with the environment name (e.g. `dns-staging.tf`), and update its content accordingly. Don't forget to add a peering zone from the landing to the newly created environment private zone. + + + + +## Files + +| name | description | modules | resources | +|---|---|---|---| +| [dns-dev.tf](./dns-dev.tf) | Development spoke DNS zones and peerings setup. | dns | | +| [dns-landing.tf](./dns-landing.tf) | Landing DNS zones and peerings setup. | dns | | +| [dns-prod.tf](./dns-prod.tf) | Production spoke DNS zones and peerings setup. | dns | | +| [landing.tf](./landing.tf) | Landing VPC and related resources. | net-cloudnat · net-vpc · net-vpc-firewall · project | | +| [main.tf](./main.tf) | Networking folder and hierarchical policy. | folder | | +| [monitoring.tf](./monitoring.tf) | Network monitoring dashboards. | | google_monitoring_dashboard | +| [ncc.tf](./ncc.tf) | None | ncc-spoke-ra | | +| [nva.tf](./nva.tf) | None | compute-vm · simple-nva | google_compute_address | +| [outputs.tf](./outputs.tf) | Module outputs. | | google_storage_bucket_object · local_file | +| [regions.tf](./regions.tf) | Compute short names for regions. | | | +| [spoke-dev.tf](./spoke-dev.tf) | Dev spoke VPC and related resources. | net-vpc · net-vpc-firewall · net-vpc-peering · project | google_project_iam_binding | +| [spoke-prod.tf](./spoke-prod.tf) | Production spoke VPC and related resources. | net-vpc · net-vpc-firewall · net-vpc-peering · project | google_project_iam_binding | +| [test-resources.tf](./test-resources.tf) | temporary instances for testing | compute-vm | | +| [variables.tf](./variables.tf) | Module variables. | | | +| [vpn-onprem.tf](./vpn-onprem.tf) | VPN between landing and onprem. | net-vpn-ha | | + +## Variables + +| name | description | type | required | default | producer | +|---|---|:---:|:---:|:---:|:---:| +| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap | +| [billing_account](variables.tf#L25) | Billing account id. If billing account is not part of the same org set `is_org_level` to false. | object({…}) | ✓ | | 0-bootstrap | +| [folder_ids](variables.tf#L75) | Folders to be used for the networking resources in folders/nnnnnnnnnnn format. If null, folder will be created. | object({…}) | ✓ | | 1-resman | +| [organization](variables.tf#L119) | Organization details. | object({…}) | ✓ | | 0-bootstrap | +| [prefix](variables.tf#L135) | Prefix used for resources that need unique names. Use 9 characters or less. | string | ✓ | | 0-bootstrap | +| [custom_roles](variables.tf#L38) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 0-bootstrap | +| [dns](variables.tf#L47) | Onprem DNS resolvers. | map(list(string)) | | {…} | | +| [factories_config](variables.tf#L55) | Configuration for network resource factories. | object({…}) | | {…} | | +| [gcp_ranges](variables.tf#L85) | GCP address ranges in name => range format. | map(string) | | {…} | | +| [ncc_asn](variables.tf#L100) | The NCC Cloud Routers ASN configuration. | map(number) | | {…} | | +| [onprem_cidr](variables.tf#L111) | Onprem addresses in name => range format. | map(string) | | {…} | | +| [outputs_location](variables.tf#L129) | Path where providers and tfvars files for the following stages are written. Leave empty to disable. | string | | null | | +| [psa_ranges](variables.tf#L146) | IP ranges used for Private Service Access (e.g. CloudSQL). Ranges is in name => range format. | object({…}) | | null | | +| [regions](variables.tf#L167) | Region definitions. | object({…}) | | {…} | | +| [service_accounts](variables.tf#L179) | Automation service accounts in name => email format. | object({…}) | | null | 1-resman | +| [vpn_onprem_primary_config](variables.tf#L193) | VPN gateway configuration for onprem interconnection in the primary region. | object({…}) | | null | | +| [vpn_onprem_secondary_config](variables.tf#L236) | VPN gateway configuration for onprem interconnection in the secondary region. | object({…}) | | null | | +| [zones](variables.tf#L279) | Zones in which NVAs are deployed. | list(string) | | ["b", "c"] | | + +## Outputs + +| name | description | sensitive | consumers | +|---|---|:---:|---| +| [host_project_ids](outputs.tf#L58) | Network project ids. | | | +| [host_project_numbers](outputs.tf#L63) | Network project numbers. | | | +| [shared_vpc_self_links](outputs.tf#L68) | Shared VPC host projects. | | | +| [tfvars](outputs.tf#L73) | Terraform variables file for the following stages. | ✓ | | +| [vpn_gateway_endpoints](outputs.tf#L79) | External IP Addresses for the GCP VPN gateways. | | | + + diff --git a/fast/stages/2-networking-e-nva-bgp/data/bgp-config.tftpl b/fast/stages/2-networking-e-nva-bgp/data/bgp-config.tftpl new file mode 100644 index 0000000000..53009c17a1 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/bgp-config.tftpl @@ -0,0 +1,76 @@ +# NVAs configuration template + +log syslog informational +no ipv6 forwarding +service integrated-vtysh-config + +interface lo + ip address ${ip_untrusted}/32 + +ip prefix-list DEFAULT seq 10 permit 0.0.0.0/0 +! +ip prefix-list PRIMARY seq 10 permit ${gcp_landing_trusted_primary} +ip prefix-list PRIMARY seq 20 permit ${gcp_dev_primary} +ip prefix-list PRIMARY seq 30 permit ${gcp_prod_primary} +! +ip prefix-list SECONDARY seq 10 permit ${gcp_landing_trusted_secondary} +ip prefix-list SECONDARY seq 20 permit ${gcp_dev_secondary} +ip prefix-list SECONDARY seq 30 permit ${gcp_prod_secondary} + +route-map TO-UNTRUSTED permit 10 + match ip address prefix-list PRIMARY + set metric ${cost_primary} +! +route-map TO-UNTRUSTED permit 20 + match ip address prefix-list SECONDARY + set metric ${cost_secondary} +! +route-map TO-TRUSTED permit 10 + match ip address prefix-list DEFAULT + set metric 100 +! +route-map TO-NVA permit 10 + match ip address prefix-list ${announce-to-nva} + set metric 50 + +router bgp ${asn_nva} + bgp router-id ${ip_untrusted} + bgp bestpath as-path ignore + bgp disable-ebgp-connected-route-check + bgp timers 20 60 +! + no bgp ebgp-requires-policy + no bgp network import-check +! + neighbor ${ip_neighbor_untrusted_0} remote-as ${asn_untrusted} + neighbor ${ip_neighbor_untrusted_1} remote-as ${asn_untrusted} +! + neighbor ${ip_neighbor_trusted_0} remote-as ${asn_trusted} + neighbor ${ip_neighbor_trusted_0} update-source ${ip_trusted} + neighbor ${ip_neighbor_trusted_1} remote-as ${asn_trusted} + neighbor ${ip_neighbor_trusted_1} update-source ${ip_trusted} +! + neighbor ${ip_neighbor_cross_region_nva_0} remote-as ${asn_nva_cross_region} + neighbor ${ip_neighbor_cross_region_nva_0} ebgp-multihop 2 + neighbor ${ip_neighbor_cross_region_nva_1} remote-as ${asn_nva_cross_region} + neighbor ${ip_neighbor_cross_region_nva_1} ebgp-multihop 2 +! + address-family ipv4 unicast + neighbor ${ip_neighbor_untrusted_0} route-map TO-UNTRUSTED out + neighbor ${ip_neighbor_untrusted_0} soft-reconfiguration inbound +! + neighbor ${ip_neighbor_untrusted_1} route-map TO-UNTRUSTED out + neighbor ${ip_neighbor_untrusted_1} soft-reconfiguration inbound +! + neighbor ${ip_neighbor_trusted_0} route-map TO-TRUSTED out + neighbor ${ip_neighbor_trusted_0} soft-reconfiguration inbound +! + neighbor ${ip_neighbor_trusted_1} route-map TO-TRUSTED out + neighbor ${ip_neighbor_trusted_1} soft-reconfiguration inbound +! + neighbor ${ip_neighbor_cross_region_nva_0} route-map TO-NVA out + neighbor ${ip_neighbor_cross_region_nva_0} soft-reconfiguration inbound +! + neighbor ${ip_neighbor_cross_region_nva_1} route-map TO-NVA out + neighbor ${ip_neighbor_cross_region_nva_1} soft-reconfiguration inbound + exit-address-family diff --git a/fast/stages/2-networking-e-nva-bgp/data/cidrs.yaml b/fast/stages/2-networking-e-nva-bgp/data/cidrs.yaml new file mode 100644 index 0000000000..93d7bb0b12 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/cidrs.yaml @@ -0,0 +1,27 @@ +# skip boilerplate check + +healthchecks: + - 35.191.0.0/16 + - 130.211.0.0/22 + - 209.85.152.0/22 + - 209.85.204.0/22 + +ncc_cloud_routers_trusted: + - 10.128.64.201/32 + - 10.128.64.202/32 + - 10.128.96.201/32 + - 10.128.96.202/32 + +ncc_cloud_routers_untrusted: + - 10.128.0.201/32 + - 10.128.0.202/32 + - 10.128.32.201/32 + - 10.128.32.202/32 + +rfc1918: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + +onprem_probes: + - 10.255.255.254/32 diff --git a/fast/stages/2-networking-e-nva-bgp/data/dashboards/firewall_insights.json b/fast/stages/2-networking-e-nva-bgp/data/dashboards/firewall_insights.json new file mode 100644 index 0000000000..4c0ebe2878 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/dashboards/firewall_insights.json @@ -0,0 +1,68 @@ +{ + "displayName": "Firewall Insights Monitoring", + "gridLayout": { + "columns": "2", + "widgets": [ + { + "title": "Subnet Firewall Hit Counts", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"firewallinsights.googleapis.com/subnet/firewall_hit_count\" resource.type=\"gce_subnetwork\"", + "secondaryAggregation": {} + }, + "unitOverride": "1" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "VM Firewall Hit Counts", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"firewallinsights.googleapis.com/vm/firewall_hit_count\" resource.type=\"gce_instance\"", + "secondaryAggregation": {} + }, + "unitOverride": "1" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + } + ] + } +} diff --git a/fast/stages/2-networking-e-nva-bgp/data/dashboards/vpn.json b/fast/stages/2-networking-e-nva-bgp/data/dashboards/vpn.json new file mode 100644 index 0000000000..1aec3e45b2 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/dashboards/vpn.json @@ -0,0 +1,248 @@ +{ + "displayName": "VPN Monitoring", + "gridLayout": { + "columns": "2", + "widgets": [ + { + "title": "Number of connections", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_MEAN" + }, + "filter": "metric.type=\"vpn.googleapis.com/gateway/connections\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "1" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "Tunnel established", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_MEAN" + }, + "filter": "metric.type=\"vpn.googleapis.com/tunnel_established\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "1" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "Cloud VPN Gateway - Received bytes", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"vpn.googleapis.com/network/received_bytes_count\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "By" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "Cloud VPN Gateway - Sent bytes", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"vpn.googleapis.com/network/sent_bytes_count\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "By" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "Cloud VPN Gateway - Received packets", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"vpn.googleapis.com/network/received_packets_count\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "{packets}" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "Cloud VPN Gateway - Sent packets", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"vpn.googleapis.com/network/sent_packets_count\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "{packets}" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "Incoming packets dropped", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"vpn.googleapis.com/network/dropped_received_packets_count\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "1" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + }, + { + "title": "Outgoing packets dropped", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"vpn.googleapis.com/network/dropped_sent_packets_count\" resource.type=\"vpn_gateway\"", + "secondaryAggregation": {} + }, + "unitOverride": "1" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "label": "y1Axis", + "scale": "LINEAR" + } + } + } + ] + } +} diff --git a/fast/stages/2-networking-e-nva-bgp/data/firewall-rules/dev/rules.yaml b/fast/stages/2-networking-e-nva-bgp/data/firewall-rules/dev/rules.yaml new file mode 100644 index 0000000000..cab42edc94 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/firewall-rules/dev/rules.yaml @@ -0,0 +1,21 @@ +# skip boilerplate check + +ingress: + ingress-allow-composer-nodes: + description: "Allow traffic to Composer nodes." + sources: + - composer-worker + targets: + - composer-worker + rules: + - protocol: tcp + ports: [80, 443, 3306, 3307] + ingress-allow-dataflow-load: + description: "Allow traffic to Dataflow nodes." + sources: + - dataflow + targets: + - dataflow + rules: + - protocol: tcp + ports: [12345, 12346] diff --git a/fast/stages/2-networking-e-nva-bgp/data/firewall-rules/landing-trusted/rules.yaml b/fast/stages/2-networking-e-nva-bgp/data/firewall-rules/landing-trusted/rules.yaml new file mode 100644 index 0000000000..6e00603bd5 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/firewall-rules/landing-trusted/rules.yaml @@ -0,0 +1,31 @@ +# skip boilerplate check + +ingress: + allow-hc-nva-ssh-trusted: + description: "Allow traffic from Google healthchecks to NVA appliances" + source_ranges: + - healthchecks + rules: + - protocol: tcp + ports: + - 22 + allow-onprem-probes-trusted-example: + description: "Allow traffic from onprem probes" + source_ranges: + - onprem_probes + rules: + - protocol: tcp + ports: + - 12345 + # This is not really needed, but it's good to have it + # in place if the more generic hierarchical firewall policies + # get deleted + allow-ncc-nva-bgp-trusted: + description: "Allow BGP traffic from NCC Cloud Routers to NVAs" + source_ranges: + - ncc_cloud_routers_trusted + targets: ["nva"] + rules: + - protocol: tcp + ports: + - 179 diff --git a/fast/stages/2-networking-e-nva-bgp/data/firewall-rules/landing-untrusted/rules.yaml b/fast/stages/2-networking-e-nva-bgp/data/firewall-rules/landing-untrusted/rules.yaml new file mode 100644 index 0000000000..c6077013cf --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/firewall-rules/landing-untrusted/rules.yaml @@ -0,0 +1,31 @@ +# skip boilerplate check + +ingress: + allow-hc-nva-ssh-untrusted: + description: "Allow traffic from Google healthchecks to NVA appliances" + source_ranges: + - healthchecks + rules: + - protocol: tcp + ports: + - 22 + # these are not really needed, but it's good to have them + # in place if the more generic hierarchical firewall policies + # get deleted + allow-ncc-nva-bgp-untrusted: + description: "Allow BGP traffic from NCC Cloud Routers to NVAs" + source_ranges: + - ncc_cloud_routers_untrusted + targets: ["nva"] + rules: + - protocol: tcp + ports: + - 179 + allow-nva-nva-bgp-untrusted: + description: "Allow BGP traffic from cross-regional NVAs" + sources: ["nva"] + targets: ["nva"] + rules: + - protocol: tcp + ports: + - 179 diff --git a/fast/stages/2-networking-e-nva-bgp/data/hierarchical-policy-rules.yaml b/fast/stages/2-networking-e-nva-bgp/data/hierarchical-policy-rules.yaml new file mode 100644 index 0000000000..0172a3091e --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/hierarchical-policy-rules.yaml @@ -0,0 +1,49 @@ +# skip boilerplate check + +allow-admins: + description: Access from the admin subnet to all subnets + direction: INGRESS + action: allow + priority: 1000 + ranges: + - $rfc1918 + ports: + all: [] + target_resources: null + enable_logging: false + +allow-healthchecks: + description: Enable HTTP and HTTPS healthchecks + direction: INGRESS + action: allow + priority: 1001 + ranges: + - $healthchecks + ports: + tcp: ["80", "443"] + target_resources: null + enable_logging: false + +allow-ssh-from-iap: + description: Enable SSH from IAP + direction: INGRESS + action: allow + priority: 1002 + ranges: + - 35.235.240.0/20 + ports: + tcp: ["22"] + target_resources: null + enable_logging: false + +allow-icmp: + description: Enable ICMP + direction: INGRESS + action: allow + priority: 1003 + ranges: + - 0.0.0.0/0 + ports: + icmp: [] + target_resources: null + enable_logging: false diff --git a/fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-dataplatform-ew1.yaml b/fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-dataplatform-ew1.yaml new file mode 100644 index 0000000000..2c682405c5 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-dataplatform-ew1.yaml @@ -0,0 +1,8 @@ +# skip boilerplate check + +region: europe-west1 +description: Default subnet for dev Data Platform +ip_cidr_range: 10.128.48.0/24 +secondary_ip_ranges: + pods: 100.128.48.0/20 + services: 100.255.48.0/24 diff --git a/fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-default-ew1.yaml b/fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-default-ew1.yaml new file mode 100644 index 0000000000..d6e4fcdb5a --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-default-ew1.yaml @@ -0,0 +1,5 @@ +# skip boilerplate check + +region: europe-west1 +ip_cidr_range: 10.128.128.0/24 +description: Default europe-west1 subnet for dev diff --git a/fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-default-ew4.yaml b/fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-default-ew4.yaml new file mode 100644 index 0000000000..2a070fb5b6 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/subnets/dev/dev-default-ew4.yaml @@ -0,0 +1,5 @@ +# skip boilerplate check + +region: europe-west4 +ip_cidr_range: 10.128.160.0/24 +description: Default europe-west4 subnet for dev diff --git a/fast/stages/2-networking-e-nva-bgp/data/subnets/landing-trusted/landing-trusted-default-ew1.yaml b/fast/stages/2-networking-e-nva-bgp/data/subnets/landing-trusted/landing-trusted-default-ew1.yaml new file mode 100644 index 0000000000..1a83620cf8 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/subnets/landing-trusted/landing-trusted-default-ew1.yaml @@ -0,0 +1,5 @@ +# skip boilerplate check + +region: europe-west1 +ip_cidr_range: 10.128.64.0/24 +description: Default europe-west1 subnet for landing trusted diff --git a/fast/stages/2-networking-e-nva-bgp/data/subnets/landing-trusted/landing-trusted-default-ew4.yaml b/fast/stages/2-networking-e-nva-bgp/data/subnets/landing-trusted/landing-trusted-default-ew4.yaml new file mode 100644 index 0000000000..b302fef6a7 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/subnets/landing-trusted/landing-trusted-default-ew4.yaml @@ -0,0 +1,5 @@ +# skip boilerplate check + +region: europe-west4 +ip_cidr_range: 10.128.96.0/24 +description: Default europe-west4 subnet for landing trusted diff --git a/fast/stages/2-networking-e-nva-bgp/data/subnets/landing-untrusted/landing-untrusted-default-ew1.yaml b/fast/stages/2-networking-e-nva-bgp/data/subnets/landing-untrusted/landing-untrusted-default-ew1.yaml new file mode 100644 index 0000000000..fae0f2eb4b --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/subnets/landing-untrusted/landing-untrusted-default-ew1.yaml @@ -0,0 +1,5 @@ +# skip boilerplate check + +region: europe-west1 +ip_cidr_range: 10.128.0.0/24 +description: Default europe-west1 subnet for landing untrusted diff --git a/fast/stages/2-networking-e-nva-bgp/data/subnets/landing-untrusted/landing-untrusted-default-ew4.yaml b/fast/stages/2-networking-e-nva-bgp/data/subnets/landing-untrusted/landing-untrusted-default-ew4.yaml new file mode 100644 index 0000000000..5588d55e00 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/subnets/landing-untrusted/landing-untrusted-default-ew4.yaml @@ -0,0 +1,5 @@ +# skip boilerplate check + +region: europe-west4 +ip_cidr_range: 10.128.32.0/24 +description: Default europe-west4 subnet for landing untrusted diff --git a/fast/stages/2-networking-e-nva-bgp/data/subnets/prod/prod-default-ew1.yaml b/fast/stages/2-networking-e-nva-bgp/data/subnets/prod/prod-default-ew1.yaml new file mode 100644 index 0000000000..241558403d --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/subnets/prod/prod-default-ew1.yaml @@ -0,0 +1,5 @@ +# skip boilerplate check + +region: europe-west1 +ip_cidr_range: 10.128.192.0/24 +description: Default europe-west1 subnet for prod diff --git a/fast/stages/2-networking-e-nva-bgp/data/subnets/prod/prod-default-ew4.yaml b/fast/stages/2-networking-e-nva-bgp/data/subnets/prod/prod-default-ew4.yaml new file mode 100644 index 0000000000..26260e42c4 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/data/subnets/prod/prod-default-ew4.yaml @@ -0,0 +1,5 @@ +# skip boilerplate check + +region: europe-west4 +ip_cidr_range: 10.128.224.0/24 +description: Default europe-west4 subnet for prod diff --git a/fast/stages/2-networking-e-nva-bgp/diagram.png b/fast/stages/2-networking-e-nva-bgp/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..53c4daca2e5ea7452b18bccd6811d93c01362f82 GIT binary patch literal 81573 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 z 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 RQXvFfs)`R4-rq9~{4W#Y=1Kqn literal 0 HcmV?d00001 diff --git a/fast/stages/2-networking-e-nva-bgp/diagram.svg b/fast/stages/2-networking-e-nva-bgp/diagram.svg new file mode 100644 index 0000000000..7fad1244b2 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/diagram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fast/stages/2-networking-e-nva-bgp/dns-dev.tf b/fast/stages/2-networking-e-nva-bgp/dns-dev.tf new file mode 100644 index 0000000000..4eb472a159 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/dns-dev.tf @@ -0,0 +1,53 @@ +/** + * Copyright 2022 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 Development spoke DNS zones and peerings setup. + +# GCP-specific environment zone + +module "dev-dns-private-zone" { + source = "../../../modules/dns" + project_id = module.dev-spoke-project.project_id + type = "private" + name = "dev-gcp-example-com" + domain = "dev.gcp.example.com." + client_networks = [module.landing-trusted-vpc.self_link, module.landing-untrusted-vpc.self_link] + recordsets = { + "A localhost" = { records = ["127.0.0.1"] } + } +} + +# root zone peering to landing to centralize configuration; remove if unneeded + +module "dev-landing-root-dns-peering" { + source = "../../../modules/dns" + project_id = module.dev-spoke-project.project_id + type = "peering" + name = "dev-root-dns-peering" + domain = "." + client_networks = [module.dev-spoke-vpc.self_link] + peer_network = module.landing-trusted-vpc.self_link +} + +module "dev-reverse-10-dns-peering" { + source = "../../../modules/dns" + project_id = module.dev-spoke-project.project_id + type = "peering" + name = "dev-reverse-10-dns-peering" + domain = "10.in-addr.arpa." + client_networks = [module.dev-spoke-vpc.self_link] + peer_network = module.landing-trusted-vpc.self_link +} diff --git a/fast/stages/2-networking-e-nva-bgp/dns-landing.tf b/fast/stages/2-networking-e-nva-bgp/dns-landing.tf new file mode 100644 index 0000000000..40090279f9 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/dns-landing.tf @@ -0,0 +1,155 @@ +/** + * Copyright 2022 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 Landing DNS zones and peerings setup. + +# forwarding to on-prem DNS resolvers + +module "onprem-example-dns-forwarding" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "forwarding" + name = "example-com" + domain = "onprem.example.com." + client_networks = [ + module.landing-untrusted-vpc.self_link, + module.landing-trusted-vpc.self_link + ] + forwarders = { for ip in var.dns.onprem : ip => null } +} + +module "reverse-10-dns-forwarding" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "forwarding" + name = "root-reverse-10" + domain = "10.in-addr.arpa." + client_networks = [ + module.landing-untrusted-vpc.self_link, + module.landing-trusted-vpc.self_link + ] + forwarders = { for ip in var.dns.onprem : ip => null } +} + +module "gcp-example-dns-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "gcp-example-com" + domain = "gcp.example.com." + client_networks = [ + module.landing-untrusted-vpc.self_link, + module.landing-trusted-vpc.self_link + ] + recordsets = { + "A localhost" = { records = ["127.0.0.1"] } + } +} + +# Google APIs + +module "googleapis-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "googleapis-com" + domain = "googleapis.com." + client_networks = [ + module.landing-untrusted-vpc.self_link, + module.landing-trusted-vpc.self_link + ] + recordsets = { + "A private" = { records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "A restricted" = { records = [ + "199.36.153.4", "199.36.153.5", "199.36.153.6", "199.36.153.7" + ] } + "CNAME *" = { records = ["private.googleapis.com."] } + } +} + +module "gcrio-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "gcr-io" + domain = "gcr.io." + client_networks = [ + module.landing-untrusted-vpc.self_link, + module.landing-trusted-vpc.self_link + ] + recordsets = { + "A gcr.io." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "packages-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "packages-cloud" + domain = "packages.cloud.google.com." + client_networks = [ + module.landing-untrusted-vpc.self_link, + module.landing-trusted-vpc.self_link + ] + recordsets = { + "A packages.cloud.google.com." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "pkgdev-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "pkg-dev" + domain = "pkg.dev." + client_networks = [ + module.landing-untrusted-vpc.self_link, + module.landing-trusted-vpc.self_link + ] + recordsets = { + "A pkg.dev." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} + +module "pkigoog-private-zone" { + source = "../../../modules/dns" + project_id = module.landing-project.project_id + type = "private" + name = "pki-goog" + domain = "pki.goog." + client_networks = [ + module.landing-untrusted-vpc.self_link, + module.landing-trusted-vpc.self_link + ] + recordsets = { + "A pki.goog." = { ttl = 300, records = [ + "199.36.153.8", "199.36.153.9", "199.36.153.10", "199.36.153.11" + ] } + "CNAME *" = { ttl = 300, records = ["private.googleapis.com."] } + } +} diff --git a/fast/stages/2-networking-e-nva-bgp/dns-prod.tf b/fast/stages/2-networking-e-nva-bgp/dns-prod.tf new file mode 100644 index 0000000000..b54609dfdb --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/dns-prod.tf @@ -0,0 +1,53 @@ +/** + * Copyright 2022 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 Production spoke DNS zones and peerings setup. + +# GCP-specific environment zone + +module "prod-dns-private-zone" { + source = "../../../modules/dns" + project_id = module.prod-spoke-project.project_id + type = "private" + name = "prod-gcp-example-com" + domain = "prod.gcp.example.com." + client_networks = [module.landing-trusted-vpc.self_link, module.landing-untrusted-vpc.self_link] + recordsets = { + "A localhost" = { records = ["127.0.0.1"] } + } +} + +# root zone peering to landing to centralize configuration; remove if unneeded + +module "prod-landing-root-dns-peering" { + source = "../../../modules/dns" + project_id = module.prod-spoke-project.project_id + type = "peering" + name = "prod-root-dns-peering" + domain = "." + client_networks = [module.prod-spoke-vpc.self_link] + peer_network = module.landing-trusted-vpc.self_link +} + +module "prod-reverse-10-dns-peering" { + source = "../../../modules/dns" + project_id = module.prod-spoke-project.project_id + type = "peering" + name = "prod-reverse-10-dns-peering" + domain = "10.in-addr.arpa." + client_networks = [module.prod-spoke-vpc.self_link] + peer_network = module.landing-trusted-vpc.self_link +} diff --git a/fast/stages/2-networking-e-nva-bgp/landing.tf b/fast/stages/2-networking-e-nva-bgp/landing.tf new file mode 100644 index 0000000000..8f0b195927 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/landing.tf @@ -0,0 +1,145 @@ +/** + * Copyright 2022 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 Landing VPC and related resources. + +module "landing-project" { + source = "../../../modules/project" + billing_account = var.billing_account.id + name = "prod-net-landing-0" + parent = var.folder_ids.networking-prod + prefix = var.prefix + services = [ + "compute.googleapis.com", + "dns.googleapis.com", + "iap.googleapis.com", + "networkconnectivity.googleapis.com", + "networkmanagement.googleapis.com", + "stackdriver.googleapis.com" + ] + shared_vpc_host_config = { + enabled = true + } + iam = { + "roles/dns.admin" = compact([ + try(local.service_accounts.project-factory-prod, null) + ]) + (local.custom_roles.service_project_network_admin) = compact([ + try(local.service_accounts.project-factory-prod, null) + ]) + } +} + +# Untrusted VPC + +module "landing-untrusted-vpc" { + source = "../../../modules/net-vpc" + project_id = module.landing-project.project_id + name = "prod-untrusted-landing-0" + mtu = 1500 + dns_policy = { + inbound = false + logging = false + } + data_folder = "${var.factories_config.data_dir}/subnets/landing-untrusted" +} + +module "landing-untrusted-firewall" { + source = "../../../modules/net-vpc-firewall" + project_id = module.landing-project.project_id + network = module.landing-untrusted-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/landing-untrusted" + } +} + +# NAT + +moved { + from = module.landing-nat-ew1 + to = module.landing-nat-primary +} + +module "landing-nat-primary" { + source = "../../../modules/net-cloudnat" + project_id = module.landing-project.project_id + region = var.regions.primary + name = local.region_shortnames[var.regions.primary] + router_create = true + router_name = "prod-nat-${local.region_shortnames[var.regions.primary]}" + router_network = module.landing-untrusted-vpc.name + router_asn = 4200001024 +} + +moved { + from = module.landing-nat-ew4 + to = module.landing-nat-secondary +} + +module "landing-nat-secondary" { + source = "../../../modules/net-cloudnat" + project_id = module.landing-project.project_id + region = var.regions.secondary + name = local.region_shortnames[var.regions.secondary] + router_create = true + router_name = "prod-nat-${local.region_shortnames[var.regions.secondary]}" + router_network = module.landing-untrusted-vpc.name + router_asn = 4200001024 +} + +# Trusted VPC + +module "landing-trusted-vpc" { + source = "../../../modules/net-vpc" + project_id = module.landing-project.project_id + name = "prod-trusted-landing-0" + delete_default_routes_on_create = true + mtu = 1500 + data_folder = "${var.factories_config.data_dir}/subnets/landing-trusted" + dns_policy = { + inbound = true + } + # Set explicit routes for googleapis in case the default route is deleted + routes = { + private-googleapis = { + dest_range = "199.36.153.8/30" + next_hop_type = "gateway" + next_hop = "default-internet-gateway" + } + restricted-googleapis = { + dest_range = "199.36.153.4/30" + next_hop_type = "gateway" + next_hop = "default-internet-gateway" + } + } +} + +module "landing-trusted-firewall" { + source = "../../../modules/net-vpc-firewall" + project_id = module.landing-project.project_id + network = module.landing-trusted-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/landing-trusted" + } +} diff --git a/fast/stages/2-networking-e-nva-bgp/main.tf b/fast/stages/2-networking-e-nva-bgp/main.tf new file mode 100644 index 0000000000..0a56396ecd --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/main.tf @@ -0,0 +1,57 @@ +/** + * Copyright 2022 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 Networking folder and hierarchical policy. + +locals { + custom_roles = coalesce(var.custom_roles, {}) + # combine all regions from variables and subnets + regions = distinct(concat( + values(var.regions), + values(module.dev-spoke-vpc.subnet_regions), + values(module.landing-trusted-vpc.subnet_regions), + values(module.landing-untrusted-vpc.subnet_regions), + values(module.prod-spoke-vpc.subnet_regions), + )) + service_accounts = { + for k, v in coalesce(var.service_accounts, {}) : + k => "serviceAccount:${v}" if v != null + } + stage3_sas_delegated_grants = [ + "roles/composer.sharedVpcAgent", + "roles/compute.networkUser", + "roles/compute.networkViewer", + "roles/container.hostServiceAgentUser", + "roles/multiclusterservicediscovery.serviceAgent", + "roles/vpcaccess.user", + ] +} + +module "folder" { + source = "../../../modules/folder" + parent = "organizations/${var.organization.id}" + name = "Networking" + folder_create = var.folder_ids.networking == null + id = var.folder_ids.networking + firewall_policy_factory = { + cidr_file = "${var.factories_config.data_dir}/cidrs.yaml" + policy_name = var.factories_config.firewall_policy_name + rules_file = "${var.factories_config.data_dir}/hierarchical-policy-rules.yaml" + } + firewall_policy_association = { + factory-policy = var.factories_config.firewall_policy_name + } +} diff --git a/fast/stages/2-networking-e-nva-bgp/monitoring.tf b/fast/stages/2-networking-e-nva-bgp/monitoring.tf new file mode 100644 index 0000000000..be3a47faac --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/monitoring.tf @@ -0,0 +1,32 @@ +/** + * Copyright 2022 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 Network monitoring dashboards. + +locals { + dashboard_path = "${var.factories_config.data_dir}/dashboards" + dashboard_files = fileset(local.dashboard_path, "*.json") + dashboards = { + for filename in local.dashboard_files : + filename => "${local.dashboard_path}/${filename}" + } +} + +resource "google_monitoring_dashboard" "dashboard" { + for_each = local.dashboards + project = module.landing-project.project_id + dashboard_json = file(each.value) +} diff --git a/fast/stages/2-networking-e-nva-bgp/ncc.tf b/fast/stages/2-networking-e-nva-bgp/ncc.tf new file mode 100644 index 0000000000..0df8fd416e --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/ncc.tf @@ -0,0 +1,108 @@ +/** + * Copyright 2023 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 "spokes-trusted" { + for_each = var.regions + source = "../../../modules/ncc-spoke-ra" + name = "prod-spoke-trusted-${local.region_shortnames[each.value]}" + project_id = module.landing-project.project_id + region = each.value + + hub = { + create = true, + name = "prod-hub-trusted-${each.value}" + } + + router_appliances = [ + for key, config in local.nva_configs : + { + internal_ip = module.nva[key].internal_ips[1] + vm_self_link = module.nva[key].self_link + } if config.region == each.value + ] + + router_config = { + asn = var.ncc_asn.trusted + ip_interface0 = cidrhost(module.landing-trusted-vpc.subnet_ips["${each.value}/landing-trusted-default-${local.region_shortnames[each.value]}"], 201) + ip_interface1 = cidrhost(module.landing-trusted-vpc.subnet_ips["${each.value}/landing-trusted-default-${local.region_shortnames[each.value]}"], 202) + peer_asn = ( + each.key == "primary" + ? var.ncc_asn.nva_primary + : var.ncc_asn.nva_secondary + ) + ruotes_priority = 100 + + custom_advertise = { + all_subnets = false + ip_ranges = { + "${var.gcp_ranges.gcp_landing_trusted_primary}" = "GCP landing trusted primary." + "${var.gcp_ranges.gcp_landing_trusted_secondary}" = "GCP landing trusted secondary." + "${var.gcp_ranges.gcp_dev_primary}" = "GCP dev primary.", + "${var.gcp_ranges.gcp_dev_secondary}" = "GCP dev secondary.", + "${var.gcp_ranges.gcp_prod_primary}" = "GCP prod primary.", + "${var.gcp_ranges.gcp_prod_secondary}" = "GCP prod secondary.", + } + } + } + + vpc_config = { + network_name = module.landing-trusted-vpc.self_link + subnet_self_link = module.landing-trusted-vpc.subnet_self_links["${each.value}/landing-trusted-default-${local.region_shortnames[each.value]}"] + } +} + +module "spokes-untrusted" { + for_each = var.regions + source = "../../../modules/ncc-spoke-ra" + name = "prod-spoke-untrusted-${local.region_shortnames[each.value]}" + project_id = module.landing-project.project_id + region = each.value + + hub = { + create = true, + name = "prod-hub-untrusted-${each.value}" + } + + router_appliances = [ + for key, config in local.nva_configs : + { + internal_ip = module.nva[key].internal_ips[0] + vm_self_link = module.nva[key].self_link + } if config.region == each.value + ] + + router_config = { + asn = var.ncc_asn.untrusted + ip_interface0 = cidrhost(module.landing-untrusted-vpc.subnet_ips["${each.value}/landing-untrusted-default-${local.region_shortnames[each.value]}"], 201) + ip_interface1 = cidrhost(module.landing-untrusted-vpc.subnet_ips["${each.value}/landing-untrusted-default-${local.region_shortnames[each.value]}"], 202) + peer_asn = ( + each.key == "primary" + ? var.ncc_asn.nva_primary + : var.ncc_asn.nva_secondary + ) + ruotes_priority = 100 + + custom_advertise = { + all_subnets = false + ip_ranges = { "0.0.0.0/0" = "Deafult route." } + } + } + + vpc_config = { + network_name = module.landing-untrusted-vpc.self_link + subnet_self_link = module.landing-untrusted-vpc.subnet_self_links["${each.value}/landing-untrusted-default-${local.region_shortnames[each.value]}"] + } +} diff --git a/fast/stages/2-networking-e-nva-bgp/nva.tf b/fast/stages/2-networking-e-nva-bgp/nva.tf new file mode 100644 index 0000000000..baf6018a53 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/nva.tf @@ -0,0 +1,187 @@ +/** + * Copyright 2023 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 { + _nva_zones = ["b", "c"] + + # The configurations used to create the NVA VMs. + # + # Rendered as following: + # nva_configs = { + # primary-b = {...} + # primary-c = {...} + # secondary-b = {...} + # secondary-c = {...} + # } + nva_configs = { + for v in setproduct(keys(var.regions), local._nva_zones) : + join("-", v) => { + # Each NVA announces its trusted regional subnets + announce-to-nva = upper(v.0) + # NVAs in each region have their own ASN + # and peer with cross-regional NVAs. + asn_nva = ( + v.0 == "primary" + ? var.ncc_asn.nva_primary + : var.ncc_asn.nva_secondary + ) + asn_nva_cross_region = ( + v.0 == "primary" + ? var.ncc_asn.nva_secondary + : var.ncc_asn.nva_primary + ) + asn_trusted = var.ncc_asn.trusted + asn_untrusted = var.ncc_asn.untrusted + # To guarantee traffic to remain symmetric, + # NVAs need to advertise cross-region routes with a higher cost (10100) + cost_primary = v.0 == "primary" ? "100" : "10100" + cost_secondary = v.0 == "primary" ? "10100" : "100" + gcp_dev_primary = var.gcp_ranges.gcp_dev_primary + gcp_dev_secondary = var.gcp_ranges.gcp_dev_secondary + gcp_landing_trusted_primary = var.gcp_ranges.gcp_landing_trusted_primary + gcp_landing_trusted_secondary = var.gcp_ranges.gcp_landing_trusted_secondary + gcp_landing_untrusted_primary = var.gcp_ranges.gcp_landing_untrusted_primary + gcp_landing_untrusted_secondary = var.gcp_ranges.gcp_landing_untrusted_secondary + gcp_prod_primary = var.gcp_ranges.gcp_prod_primary + gcp_prod_secondary = var.gcp_ranges.gcp_prod_secondary + # The IPs of cross-region NVA VMs in the untrusted VPC (x.y.w.z) + ip_neighbor_cross_region_nva_0 = cidrhost(module.landing-untrusted-vpc.subnet_ips["${local._regions_cross[v.0]}/landing-untrusted-default-${local.region_shortnames[local._regions_cross[v.0]]}"], 101) + ip_neighbor_cross_region_nva_1 = cidrhost(module.landing-untrusted-vpc.subnet_ips["${local._regions_cross[v.0]}/landing-untrusted-default-${local.region_shortnames[local._regions_cross[v.0]]}"], 102) + # The Cloud router IPs (x.y.w.z) in the untrusted + # and in the trusted VPCs, where the NVA connects to + ip_neighbor_trusted_0 = cidrhost(module.landing-trusted-vpc.subnet_ips["${var.regions[v.0]}/landing-trusted-default-${local.region_shortnames[var.regions[v.0]]}"], 201) + ip_neighbor_trusted_1 = cidrhost(module.landing-trusted-vpc.subnet_ips["${var.regions[v.0]}/landing-trusted-default-${local.region_shortnames[var.regions[v.0]]}"], 202) + ip_neighbor_untrusted_0 = cidrhost(module.landing-untrusted-vpc.subnet_ips["${var.regions[v.0]}/landing-untrusted-default-${local.region_shortnames[var.regions[v.0]]}"], 201) + ip_neighbor_untrusted_1 = cidrhost(module.landing-untrusted-vpc.subnet_ips["${var.regions[v.0]}/landing-untrusted-default-${local.region_shortnames[var.regions[v.0]]}"], 202) + # The IPs to assign to the NVA NICs + # in the trusted and in the untrusted VPCs. + ip_trusted = cidrhost(module.landing-trusted-vpc.subnet_ips["${var.regions[v.0]}/landing-trusted-default-${local.region_shortnames[var.regions[v.0]]}"], 101 + index(var.zones, v.1)) + ip_untrusted = cidrhost(module.landing-untrusted-vpc.subnet_ips["${var.regions[v.0]}/landing-untrusted-default-${local.region_shortnames[var.regions[v.0]]}"], 101 + index(var.zones, v.1)) + # Either primary or secondary + name = v.0 + # The name of the region where the NVA lives. + # For example, europe-west1 or europe-west4 + region = var.regions[v.0] + # the short name for the region. For example, ew1 or ew4 + shortname = local.region_shortnames[var.regions[v.0]] + # The zone where the NVA lives. For example, b or c + zone = v.1 + } + } + + # The routing_config should be aligned to the NVA NICs. + # For example: + # local.routing_config[0] configures eth0; + # local.routing_config[0] configures eth1. + routing_config = [ + { + enable_masquerading = true + name = "untrusted" + routes = [ + var.gcp_ranges.gcp_landing_untrusted_primary, + var.gcp_ranges.gcp_landing_untrusted_secondary + ] + }, + { + name = "trusted" + routes = [ + var.gcp_ranges.gcp_landing_trusted_primary, + var.gcp_ranges.gcp_landing_trusted_secondary + ] + } + ] +} + +module "nva-bgp-cloud-config" { + for_each = local.nva_configs + source = "../../../modules/cloud-config-container/simple-nva" + enable_health_checks = true + network_interfaces = local.routing_config + frr_config = { + config_file = templatefile("data/bgp-config.tftpl", each.value) + daemons_enabled = ["bgpd"] + } +} + +resource "google_compute_address" "nva_static_ip_trusted" { + for_each = local.nva_configs + name = "nva-ip-trusted-${each.value.shortname}-${each.value.zone}" + project = module.landing-project.project_id + subnetwork = module.landing-trusted-vpc.subnet_self_links["${each.value.region}/landing-trusted-default-${each.value.shortname}"] + address_type = "INTERNAL" + address = each.value.ip_trusted + region = each.value.region +} + +resource "google_compute_address" "nva_static_ip_untrusted" { + for_each = local.nva_configs + name = "nva-ip-untrusted-${each.value.shortname}-${each.value.zone}" + project = module.landing-project.project_id + subnetwork = module.landing-untrusted-vpc.subnet_self_links["${each.value.region}/landing-untrusted-default-${each.value.shortname}"] + address_type = "INTERNAL" + address = each.value.ip_untrusted + region = each.value.region +} + +module "nva" { + for_each = local.nva_configs + source = "../../../modules/compute-vm" + project_id = module.landing-project.project_id + name = "nva-${each.value.shortname}-${each.value.zone}" + instance_type = "e2-standard-2" + can_ip_forward = true + zone = "${each.value.region}-${each.value.zone}" + tags = ["nva"] + + network_interfaces = [ + { + network = module.landing-untrusted-vpc.self_link + subnetwork = module.landing-untrusted-vpc.subnet_self_links["${each.value.region}/landing-untrusted-default-${each.value.shortname}"] + nat = false + addresses = { + external = null + internal = google_compute_address.nva_static_ip_untrusted[each.key].address + } + }, + { + network = module.landing-trusted-vpc.self_link + subnetwork = module.landing-trusted-vpc.subnet_self_links["${each.value.region}/landing-trusted-default-${each.value.shortname}"] + nat = false + addresses = { + external = null + internal = google_compute_address.nva_static_ip_trusted[each.key].address + } + } + ] + + boot_disk = { + initialize_params = { + image = "projects/cos-cloud/global/images/family/cos-stable" + size = 10 + type = "pd-balanced" + } + } + + options = { + allow_stopping_for_update = true + deletion_protection = false + termination_action = "STOP" + } + + metadata = { + user-data = module.nva-bgp-cloud-config[each.key].cloud_config + } +} diff --git a/fast/stages/2-networking-e-nva-bgp/outputs.tf b/fast/stages/2-networking-e-nva-bgp/outputs.tf new file mode 100644 index 0000000000..eb53a63f4f --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/outputs.tf @@ -0,0 +1,91 @@ +/** + * Copyright 2022 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 { + host_project_ids = { + dev-spoke-0 = module.dev-spoke-project.project_id + prod-landing = module.landing-project.project_id + prod-spoke-0 = module.prod-spoke-project.project_id + } + host_project_numbers = { + dev-spoke-0 = module.dev-spoke-project.number + prod-landing = module.landing-project.number + prod-spoke-0 = module.prod-spoke-project.number + } + tfvars = { + host_project_ids = local.host_project_ids + host_project_numbers = local.host_project_numbers + vpc_self_links = local.vpc_self_links + } + vpc_self_links = { + prod-landing-trusted = module.landing-trusted-vpc.self_link + prod-landing-untrusted = module.landing-untrusted-vpc.self_link + dev-spoke-0 = module.dev-spoke-vpc.self_link + prod-spoke-0 = module.prod-spoke-vpc.self_link + } +} + +# 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" { + bucket = var.automation.outputs_bucket + name = "tfvars/2-networking.auto.tfvars.json" + content = jsonencode(local.tfvars) +} + +# outputs + +output "host_project_ids" { + description = "Network project ids." + value = local.host_project_ids +} + +output "host_project_numbers" { + description = "Network project numbers." + value = local.host_project_numbers +} + +output "shared_vpc_self_links" { + description = "Shared VPC host projects." + value = local.vpc_self_links +} + +output "tfvars" { + description = "Terraform variables file for the following stages." + sensitive = true + value = local.tfvars +} + +output "vpn_gateway_endpoints" { + description = "External IP Addresses for the GCP VPN gateways." + value = { + onprem-primary = var.vpn_onprem_primary_config == null ? {} : { + for v in module.landing-to-onprem-primary-vpn.0.gateway.vpn_interfaces : + v.id => v.ip_address + } + onprem-secondary = var.vpn_onprem_secondary_config == null ? {} : { + for v in module.landing-to-onprem-secondary-vpn.0.gateway.vpn_interfaces : + v.id => v.ip_address + } + } +} diff --git a/fast/stages/2-networking-e-nva-bgp/regions.tf b/fast/stages/2-networking-e-nva-bgp/regions.tf new file mode 100644 index 0000000000..ee6b0d6928 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/regions.tf @@ -0,0 +1,46 @@ +/** + * Copyright 2023 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 Compute short names for regions. + +locals { + # only map when the first character would not work + _region_cardinal = { + southeast = "se" + } + _regions_cross = { + primary = var.regions["secondary"] + secondary = var.regions["primary"] + } + # only map when the first character would not work + _region_geo = { + australia = "o" + } + # split in [geo, cardinal, number] tokens + _region_tokens = { + for v in local.regions : v => regexall("(?:[a-z]+)|(?:[0-9]+)", v) + } + region_shortnames = { + for k, v in local._region_tokens : k => join("", [ + # first token via geo alias map or first character + lookup(local._region_geo, v.0, substr(v.0, 0, 1)), + # first token via cardinal alias map or first character + lookup(local._region_cardinal, v.1, substr(v.1, 0, 1)), + # region number as is + v.2 + ]) + } +} diff --git a/fast/stages/2-networking-e-nva-bgp/spoke-dev.tf b/fast/stages/2-networking-e-nva-bgp/spoke-dev.tf new file mode 100644 index 0000000000..967a2746ff --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/spoke-dev.tf @@ -0,0 +1,112 @@ +/** + * Copyright 2022 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 Dev spoke VPC and related resources. + +module "dev-spoke-project" { + source = "../../../modules/project" + billing_account = var.billing_account.id + name = "dev-net-spoke-0" + parent = var.folder_ids.networking-dev + prefix = var.prefix + services = [ + "compute.googleapis.com", + "dns.googleapis.com", + "iap.googleapis.com", + "networkmanagement.googleapis.com", + "servicenetworking.googleapis.com", + "stackdriver.googleapis.com", + "vpcaccess.googleapis.com" + ] + shared_vpc_host_config = { + enabled = true + } + metric_scopes = [module.landing-project.project_id] + iam = { + "roles/dns.admin" = compact([ + try(local.service_accounts.gke-dev, null), + try(local.service_accounts.project-factory-dev, null), + try(local.service_accounts.project-factory-prod, null), + ]) + } +} + +module "dev-spoke-vpc" { + source = "../../../modules/net-vpc" + project_id = module.dev-spoke-project.project_id + name = "dev-spoke-0" + mtu = 1500 + data_folder = "${var.factories_config.data_dir}/subnets/dev" + delete_default_routes_on_create = true + psa_config = try(var.psa_ranges.dev, null) + # Set explicit routes for googleapis; send everything else to NVAs + routes = { + private-googleapis = { + dest_range = "199.36.153.8/30" + priority = 999 + next_hop_type = "gateway" + next_hop = "default-internet-gateway" + } + restricted-googleapis = { + dest_range = "199.36.153.4/30" + priority = 999 + next_hop_type = "gateway" + next_hop = "default-internet-gateway" + } + } +} + +module "dev-spoke-firewall" { + source = "../../../modules/net-vpc-firewall" + project_id = module.dev-spoke-project.project_id + network = module.dev-spoke-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/dev" + } +} + +module "peering-dev" { + source = "../../../modules/net-vpc-peering" + prefix = "dev-peering-0" + local_network = module.dev-spoke-vpc.self_link + peer_network = module.landing-trusted-vpc.self_link + export_local_custom_routes = true + export_peer_custom_routes = true +} + +# Create delegated grants for stage3 service accounts +resource "google_project_iam_binding" "dev_spoke_project_iam_delegated" { + project = module.dev-spoke-project.project_id + role = "roles/resourcemanager.projectIamAdmin" + members = compact([ + try(local.service_accounts.data-platform-dev, null), + try(local.service_accounts.project-factory-dev, null), + try(local.service_accounts.project-factory-prod, null), + try(local.service_accounts.gke-dev, null), + ]) + condition { + title = "dev_stage3_sa_delegated_grants" + description = "Development host project delegated grants." + expression = format( + "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])", + join(",", formatlist("'%s'", local.stage3_sas_delegated_grants)) + ) + } +} diff --git a/fast/stages/2-networking-e-nva-bgp/spoke-prod.tf b/fast/stages/2-networking-e-nva-bgp/spoke-prod.tf new file mode 100644 index 0000000000..9978608145 --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/spoke-prod.tf @@ -0,0 +1,110 @@ +/** + * Copyright 2022 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 Production spoke VPC and related resources. + +module "prod-spoke-project" { + source = "../../../modules/project" + billing_account = var.billing_account.id + name = "prod-net-spoke-0" + parent = var.folder_ids.networking-prod + prefix = var.prefix + services = [ + "compute.googleapis.com", + "dns.googleapis.com", + "iap.googleapis.com", + "networkmanagement.googleapis.com", + "servicenetworking.googleapis.com", + "stackdriver.googleapis.com", + "vpcaccess.googleapis.com" + ] + shared_vpc_host_config = { + enabled = true + } + metric_scopes = [module.landing-project.project_id] + iam = { + "roles/dns.admin" = compact([ + try(local.service_accounts.gke-prod, null), + try(local.service_accounts.project-factory-prod, null), + ]) + } +} + +module "prod-spoke-vpc" { + source = "../../../modules/net-vpc" + project_id = module.prod-spoke-project.project_id + name = "prod-spoke-0" + mtu = 1500 + data_folder = "${var.factories_config.data_dir}/subnets/prod" + delete_default_routes_on_create = true + psa_config = try(var.psa_ranges.prod, null) + # Set explicit routes for googleapis; send everything else to NVAs + routes = { + private-googleapis = { + dest_range = "199.36.153.8/30" + priority = 999 + next_hop_type = "gateway" + next_hop = "default-internet-gateway" + } + restricted-googleapis = { + dest_range = "199.36.153.4/30" + priority = 999 + next_hop_type = "gateway" + next_hop = "default-internet-gateway" + } + } +} + +module "prod-spoke-firewall" { + source = "../../../modules/net-vpc-firewall" + project_id = module.prod-spoke-project.project_id + network = module.prod-spoke-vpc.name + default_rules_config = { + disabled = true + } + factories_config = { + cidr_tpl_file = "${var.factories_config.data_dir}/cidrs.yaml" + rules_folder = "${var.factories_config.data_dir}/firewall-rules/prod" + } +} + +module "peering-prod" { + source = "../../../modules/net-vpc-peering" + prefix = "prod-peering-0" + local_network = module.prod-spoke-vpc.self_link + peer_network = module.landing-trusted-vpc.self_link + export_local_custom_routes = true + export_peer_custom_routes = true +} + +# Create delegated grants for stage3 service accounts +resource "google_project_iam_binding" "prod_spoke_project_iam_delegated" { + project = module.prod-spoke-project.project_id + role = "roles/resourcemanager.projectIamAdmin" + members = compact([ + try(local.service_accounts.data-platform-prod, null), + try(local.service_accounts.project-factory-prod, null), + try(local.service_accounts.gke-prod, null), + ]) + condition { + title = "prod_stage3_sa_delegated_grants" + description = "Production host project delegated grants." + expression = format( + "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])", + join(",", formatlist("'%s'", local.stage3_sas_delegated_grants)) + ) + } +} diff --git a/fast/stages/2-networking-e-nva-bgp/test-resources.tf b/fast/stages/2-networking-e-nva-bgp/test-resources.tf new file mode 100644 index 0000000000..b2816e4fdb --- /dev/null +++ b/fast/stages/2-networking-e-nva-bgp/test-resources.tf @@ -0,0 +1,245 @@ +/** + * Copyright 2023 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 temporary instances for testing + +# # Untrusted (Landing) + +# module "test-vm-landing-untrusted-primary-0" { +# source = "../../../modules/compute-vm" +# project_id = module.landing-project.project_id +# zone = "${var.regions.primary}-b" +# name = "test-vm-lnd-unt-primary-0" +# network_interfaces = [{ +# network = module.landing-untrusted-vpc.self_link +# subnetwork = module.landing-untrusted-vpc.subnet_self_links["${var.regions.primary}/landing-untrusted-default-${local.region_shortnames[var.regions.primary]}"] +# }] +# tags = ["primary", "ssh"] +# boot_disk = { +# initialize_params = { +# image = "projects/debian-cloud/global/images/family/debian-11" +# } +# } +# options = { +# spot = true +# termination_action = "STOP" +# } +# metadata = { +# startup-script = < { external = v } + } + tunnels = try(var.vpn_onprem_primary_config.tunnels, {}) +} + +module "landing-to-onprem-secondary-vpn" { + count = var.vpn_onprem_secondary_config == null ? 0 : 1 + source = "../../../modules/net-vpn-ha" + project_id = module.landing-project.project_id + network = module.landing-trusted-vpc.self_link + region = var.regions.secondary + name = "vpn-to-onprem-${local.region_shortnames[var.regions.secondary]}" + router_config = try(var.vpn_onprem_secondary_config.router_config, {}) + peer_gateways = { + for k, v in local.onprem_peer_gateways.secondary : k => { external = v } + } + tunnels = try(var.vpn_onprem_secondary_config.tunnels, {}) +} diff --git a/fast/stages/3-project-factory/dev/README.md b/fast/stages/3-project-factory/dev/README.md index 62d31f3219..e2a75c2fa3 100644 --- a/fast/stages/3-project-factory/dev/README.md +++ b/fast/stages/3-project-factory/dev/README.md @@ -28,7 +28,7 @@ The project factory takes care of the following activities: ## How to run this stage -This stage is meant to be executed after "foundational stages" (i.e., stages [`00-bootstrap`](../../0-bootstrap), [`01-resman`](../../1-resman), 02-networking (either [VPN](../../2-networking-b-vpn) or [NVA](../../2-networking-c-nva)) and [`02-security`](../../2-security)) have been run. +This stage is meant to be executed after "foundational stages" (i.e., stages [`00-bootstrap`](../../0-bootstrap), [`01-resman`](../../1-resman), 02-networking (either [VPN](../../2-networking-b-vpn), [NVA](../../2-networking-c-nva), [NVA with BGP support](../../2-networking-e-nva-bgp)) and [`02-security`](../../2-security)) have been run. It's of course possible to run this stage in isolation, by making sure the architectural prerequisites are satisfied (e.g., networking), and that the Service Account running the stage is granted the roles/permissions below: diff --git a/fast/stages/COMPANION.md b/fast/stages/COMPANION.md index 96506d0083..9914ec9ac4 100644 --- a/fast/stages/COMPANION.md +++ b/fast/stages/COMPANION.md @@ -176,9 +176,10 @@ In this stage, we will deploy one of the 3 available Hub&Spoke networking topolo 1. VPC Peering 2. HA VPN 3. Multi-NIC appliances (NVA) +4. Multi-NIC appliances (NVA) with NCC / BGP support ```bash -# move to the 02-networking-XXX directory (where XXX should be one of vpn|peering|nva) +# move to the 02-networking-XXX directory (where XXX should be one of a-peering|b-vpn|c-nva|d-separate-envs|e-nva-bgp) cd $FAST_PWD/2-networking-XXX # setup providers and variables from previous stages diff --git a/fast/stages/README.md b/fast/stages/README.md index acb14be6c4..f90198ea7c 100644 --- a/fast/stages/README.md +++ b/fast/stages/README.md @@ -37,8 +37,8 @@ Implemented via separate stages that configure separate FAST-enabled hierarchie - [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.\ Exports: KMS key ids -- Networking ([Peering](2-networking-a-peering/README.md)/[VPN](2-networking-b-vpn/README.md)/[NVA](2-networking-c-nva/README.md)/[Separate environments](2-networking-d-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](2-networking-a-peering/README.md), [spokes connected via VPN](2-networking-b-vpn/README.md), [and spokes connected via appliances](2-networking-c-nva/README.md), and [separated network environments](2-networking-d-separate-envs/README.md).\ +- Networking ([Peering](2-networking-a-peering/README.md)/[VPN](2-networking-b-vpn/README.md)/[NVA](2-networking-c-nva/README.md)/[NVA with BGP support](2-networking-e-nva-bgp/README.md)/[Separate environments](2-networking-d-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](2-networking-a-peering/README.md), [spokes connected via VPN](2-networking-b-vpn/README.md), [spokes connected via appliances](2-networking-c-nva/README.md), [spokes connected via appliances leveraging NCC and BGP](2-networking-e-nva-bgp/README.md) and [separated network environments](2-networking-d-separate-envs/README.md).\ Exports: host project ids and numbers, vpc self links ## Environment-level resources (3) diff --git a/tests/fast/stages/s2_networking_e_nva_bgp/common.tfvars b/tests/fast/stages/s2_networking_e_nva_bgp/common.tfvars new file mode 100644 index 0000000000..a4b1cdd88d --- /dev/null +++ b/tests/fast/stages/s2_networking_e_nva_bgp/common.tfvars @@ -0,0 +1,106 @@ +automation = { + outputs_bucket = "test" +} +billing_account = { + id = "000000-111111-222222" +} +custom_roles = { + service_project_network_admin = "organizations/123456789012/roles/foo" +} +folder_ids = { + networking = null + networking-dev = null + networking-prod = null +} +service_accounts = { + data-platform-dev = "string" + data-platform-prod = "string" + gke-dev = "string" + gke-prod = "string" + project-factory-dev = "string" + project-factory-prod = "string" +} +organization = { + domain = "fast.example.com" + id = 123456789012 + customer_id = "C00000000" +} +prefix = "fast2" +vpn_onprem_primary_config = { + peer_external_gateways = { + default = { + redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT" + interfaces = ["8.8.8.8"] + } + } + router_config = { + asn = 65501 + custom_advertise = { + all_subnets = false + ip_ranges = { + "10.1.0.0/16" = "gcp" + "35.199.192.0/19" = "gcp-dns" + "199.36.153.4/30" = "gcp-restricted" + } + } + } + tunnels = { + "0" = { + bgp_peer = { + address = "169.254.1.1" + asn = 65500 + } + bgp_session_range = "169.254.1.2/30" + shared_secret = "foo" + vpn_gateway_interface = 0 + } + "1" = { + bgp_peer = { + address = "169.254.2.1" + asn = 64513 + } + bgp_session_range = "169.254.2.2/30" + shared_secret = "foo" + vpn_gateway_interface = 1 + } + } +} +vpn_onprem_secondary_config = { + peer_external_gateways = { + default = { + redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT" + interfaces = ["8.8.4.4"] + } + } + router_config = { + asn = 65501 + custom_advertise = { + all_subnets = false + ip_ranges = { + "10.1.0.0/16" = "gcp" + "35.199.192.0/19" = "gcp-dns" + "199.36.153.4/30" = "gcp-restricted" + } + } + } + tunnels = { + "0" = { + bgp_peer = { + address = "169.254.1.1" + asn = 65500 + } + bgp_session_range = "169.254.3.2/30" + shared_secret = "foo" + vpn_gateway_interface = 0 + } + "1" = { + bgp_peer = { + address = "169.254.2.1" + asn = 64513 + } + bgp_session_range = "169.254.4.2/30" + shared_secret = "foo" + vpn_gateway_interface = 1 + } + } +} diff --git a/tests/fast/stages/s2_networking_e_nva_bgp/stage.tfvars b/tests/fast/stages/s2_networking_e_nva_bgp/stage.tfvars new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/fast/stages/s2_networking_e_nva_bgp/stage.yaml b/tests/fast/stages/s2_networking_e_nva_bgp/stage.yaml new file mode 100644 index 0000000000..660e718fb6 --- /dev/null +++ b/tests/fast/stages/s2_networking_e_nva_bgp/stage.yaml @@ -0,0 +1,17 @@ +# Copyright 2023 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. + +counts: + modules: 39 + resources: 181 diff --git a/tests/fast/stages/s2_networking_e_nva_bgp/tftest.yaml b/tests/fast/stages/s2_networking_e_nva_bgp/tftest.yaml new file mode 100644 index 0000000000..b722ebee9e --- /dev/null +++ b/tests/fast/stages/s2_networking_e_nva_bgp/tftest.yaml @@ -0,0 +1,20 @@ +# Copyright 2023 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-e-nva-bgp +common_tfvars: + - common.tfvars + +tests: + stage: