diff --git a/modules/ncc-spoke-ra/README.md b/modules/ncc-spoke-ra/README.md new file mode 100644 index 00000000..58f5cd91 --- /dev/null +++ b/modules/ncc-spoke-ra/README.md @@ -0,0 +1,152 @@ +# NCC Spoke RA Module + +This module allows management of NCC Spokes backed by Router Appliances. Network virtual appliances used as router appliances allow to connect an external network to Google Cloud by using a SD-WAN router or another appliance with BGP capabilities (_site-to-cloud_ connectivity). It is also possible to enable site-to-site data transfer, although this feature is not available in all regions, particularly not in EMEA. + +The module manages a hub (optionally), a spoke, and the corresponding Cloud Router and BGP sessions to the router appliance(s). + +## Examples + +### Simple hub & spoke + +```hcl +module "spoke-ra" { + source = "./fabric/modules/ncc-spoke-ra" + hub = { create = true, name = "ncc-hub" } + name = "spoke-ra" + project_id = "my-project" + region = "europe-west1" + router_appliances = [ + { + internal_ip = "10.0.0.3" + vm_self_link = "projects/my-project/zones/europe-west1-b/instances/router-app" + } + ] + router_config = { + asn = 65000 + ip_interface1 = "10.0.0.14" + ip_interface2 = "10.0.0.15" + peer_asn = 65001 + } + vpc_config = { + network_name = "my-vpc" + subnet_self_link = var.subnet.self_link + } +} +# tftest modules=1 resources=7 +``` + +### Two spokes + +```hcl +module "spoke-ra-a" { + source = "./fabric/modules/ncc-spoke-ra" + hub = { name = "ncc-hub" } + name = "spoke-ra-a" + project_id = "my-project" + region = "europe-west1" + router_appliances = [ + { + internal_ip = "10.0.0.3" + vm_self_link = "projects/my-project/zones/europe-west1-b/instances/router-app-a" + } + ] + router_config = { + asn = 65000 + ip_interface1 = "10.0.0.14" + ip_interface2 = "10.0.0.15" + peer_asn = 65001 + } + vpc_config = { + network_name = "my-vpc1" + subnet_self_link = "projects/my-project/regions/europe-west1/subnetworks/subnet" + } +} + +module "spoke-ra-b" { + source = "./fabric/modules/ncc-spoke-ra" + hub = { name = "ncc-hub" } + name = "spoke-ra-b" + project_id = "my-project" + region = "europe-west3" + router_appliances = [ + { + internal_ip = "10.1.0.5" + vm_self_link = "projects/my-project/zones/europe-west3-b/instances/router-app-b" + } + ] + router_config = { + asn = 65000 + ip_interface1 = "10.0.0.14" + ip_interface2 = "10.0.0.15" + peer_asn = 65002 + } + vpc_config = { + network_name = "my-vpc2" + subnet_self_link = "projects/my-project/regions/europe-west3/subnetworks/subnet" + } +} +# tftest modules=2 resources=12 +``` + +### Spoke with load-balanced router appliances + +```hcl +module "spoke-ra" { + source = "./fabric/modules/ncc-spoke-ra" + hub = { name = "ncc-hub" } + name = "spoke-ra" + project_id = "my-project" + region = "europe-west1" + router_appliances = [ + { + internal_ip = "10.0.0.3" + vm_self_link = "projects/my-project/zones/europe-west1-b/instances/router-app-a" + }, + { + internal_ip = "10.0.0.4" + vm_self_link = "projects/my-project/zones/europe-west1-c/instances/router-app-b" + } + ] + router_config = { + asn = 65000 + custom_advertise = { + all_subnets = true + ip_ranges = { + "10.10.0.0/24" = "peered-vpc" + } + } + ip_interface1 = "10.0.0.14" + ip_interface2 = "10.0.0.15" + peer_asn = 65001 + } + vpc_config = { + network_name = "my-vpc" + subnet_self_link = var.subnet.self_link + } +} +# tftest modules=1 resources=8 +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [hub](variables.tf#L23) | The name of the NCC hub to create or use. | object({…}) | ✓ | | +| [name](variables.tf#L32) | The name of the NCC spoke. | string | ✓ | | +| [project_id](variables.tf#L37) | The ID of the project where the NCC hub & spokes will be created. | string | ✓ | | +| [region](variables.tf#L42) | Region where the spoke is located. | string | ✓ | | +| [router_appliances](variables.tf#L47) | List of router appliances this spoke is associated with. | list(object({…})) | ✓ | | +| [router_config](variables.tf#L55) | Configuration of the Cloud Router. | object({…}) | ✓ | | +| [vpc_config](variables.tf#L70) | Network and subnetwork for the CR interfaces. | object({…}) | ✓ | | +| [data_transfer](variables.tf#L17) | Site-to-site data transfer feature, available only in some regions. | bool | | false | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [hub](outputs.tf#L17) | NCC hub resource (only if auto-created). | | +| [router](outputs.tf#L22) | Cloud Router resource. | | +| [spoke-ra](outputs.tf#L27) | NCC spoke resource. | | + + diff --git a/modules/ncc-spoke-ra/main.tf b/modules/ncc-spoke-ra/main.tf new file mode 100644 index 00000000..ab009dab --- /dev/null +++ b/modules/ncc-spoke-ra/main.tf @@ -0,0 +1,123 @@ +/** + * 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 { + spoke_vms = [ + for ras in var.router_appliances : { + ip = ras.internal_ip + vm = ras.vm_self_link + vm_name = element( + split("/", ras.vm_self_link), length(split("/", ras.vm_self_link)) - 1 + ) + } + ] +} + +resource "google_network_connectivity_hub" "hub" { + count = var.hub.create ? 1 : 0 + project = var.project_id + name = var.hub.name + description = var.hub.description +} + +resource "google_network_connectivity_spoke" "spoke-ra" { + project = var.project_id + hub = try(google_network_connectivity_hub.hub[0].name, var.hub.name) + location = var.region + name = var.name + linked_router_appliance_instances { + dynamic "instances" { + for_each = var.router_appliances + content { + ip_address = instances.value["internal_ip"] + virtual_machine = instances.value["vm_self_link"] + } + } + site_to_site_data_transfer = var.data_transfer + } +} + +resource "google_compute_router" "cr" { + project = var.project_id + name = "${var.name}-cr" + network = var.vpc_config.network_name + region = var.region + bgp { + advertise_mode = ( + var.router_config.custom_advertise != null ? "CUSTOM" : "DEFAULT" + ) + advertised_groups = ( + try(var.router_config.custom_advertise.all_subnets, false) + ? ["ALL_SUBNETS"] : [] + ) + dynamic "advertised_ip_ranges" { + for_each = try(var.router_config.custom_advertise.ip_ranges, {}) + content { + description = advertised_ip_ranges.value + range = advertised_ip_ranges.key + } + } + asn = var.router_config.asn + keepalive_interval = try(var.router_config.keepalive, null) + } +} + +resource "google_compute_router_interface" "intf1" { + project = var.project_id + name = "intf1" + router = google_compute_router.cr.name + region = var.region + subnetwork = var.vpc_config.subnet_self_link + private_ip_address = var.router_config.ip_interface1 +} + +resource "google_compute_router_interface" "intf2" { + project = var.project_id + name = "intf2" + router = google_compute_router.cr.name + region = var.region + subnetwork = var.vpc_config.subnet_self_link + private_ip_address = var.router_config.ip_interface2 + redundant_interface = google_compute_router_interface.intf1.name +} + +resource "google_compute_router_peer" "peer1" { + for_each = { + for idx, entry in local.spoke_vms : idx => entry + } + project = var.project_id + name = "peer1-${each.value.vm_name}" + router = google_compute_router.cr.name + region = var.region + interface = google_compute_router_interface.intf1.name + peer_asn = var.router_config.peer_asn + peer_ip_address = each.value.ip + router_appliance_instance = each.value.vm +} + +resource "google_compute_router_peer" "peer2" { + for_each = { + for idx, entry in local.spoke_vms : idx => entry + } + project = var.project_id + name = "peer2-${each.value.vm_name}" + router = google_compute_router.cr.name + region = var.region + interface = google_compute_router_interface.intf2.name + peer_asn = var.router_config.peer_asn + peer_ip_address = each.value.ip + router_appliance_instance = each.value.vm +} diff --git a/modules/ncc-spoke-ra/outputs.tf b/modules/ncc-spoke-ra/outputs.tf new file mode 100644 index 00000000..d1bfdb36 --- /dev/null +++ b/modules/ncc-spoke-ra/outputs.tf @@ -0,0 +1,30 @@ +/** + * 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. + */ + +output "hub" { + description = "NCC hub resource (only if auto-created)." + value = one(google_network_connectivity_hub.hub[*]) +} + +output "router" { + description = "Cloud Router resource." + value = google_compute_router.cr +} + +output "spoke-ra" { + description = "NCC spoke resource." + value = google_network_connectivity_spoke.spoke-ra +} diff --git a/modules/ncc-spoke-ra/variables.tf b/modules/ncc-spoke-ra/variables.tf new file mode 100644 index 00000000..1e3a6df3 --- /dev/null +++ b/modules/ncc-spoke-ra/variables.tf @@ -0,0 +1,76 @@ +/** + * 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. + */ + +variable "data_transfer" { + description = "Site-to-site data transfer feature, available only in some regions." + type = bool + default = false +} + +variable "hub" { + description = "The name of the NCC hub to create or use." + type = object({ + create = optional(bool, false) + description = optional(string) + name = string + }) +} + +variable "name" { + description = "The name of the NCC spoke." + type = string +} + +variable "project_id" { + description = "The ID of the project where the NCC hub & spokes will be created." + type = string +} + +variable "region" { + description = "Region where the spoke is located." + type = string +} + +variable "router_appliances" { + description = "List of router appliances this spoke is associated with." + type = list(object({ + internal_ip = string + vm_self_link = string + })) +} + +variable "router_config" { + description = "Configuration of the Cloud Router." + type = object({ + asn = number + custom_advertise = optional(object({ + all_subnets = bool + ip_ranges = map(string) + })) + ip_interface1 = string + ip_interface2 = string + keepalive = optional(number) + peer_asn = number + }) +} + +variable "vpc_config" { + description = "Network and subnetwork for the CR interfaces." + type = object({ + network_name = string + subnet_self_link = string + }) +}