diff --git a/.gitignore b/.gitignore index 0733276d..677250f0 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,4 @@ terraform.tfvars.json # password, private keys, and other secrets. But allow example files. *.tfvars *.tfvars.json -!*/example.tfvars +!**/example.tfvars diff --git a/modules/vpn/Makefile b/modules/vpn/Makefile new file mode 100644 index 00000000..f9cee6eb --- /dev/null +++ b/modules/vpn/Makefile @@ -0,0 +1,2 @@ +validate: + @../../makefile.sh validate \ No newline at end of file diff --git a/modules/vpn/README.md b/modules/vpn/README.md new file mode 100644 index 00000000..4a2b07af --- /dev/null +++ b/modules/vpn/README.md @@ -0,0 +1,250 @@ +# VPN + +This module makes it easy to deploy either GCP-to-GCP or GCP-to-On-prem VPN using [Cloud HA VPN](https://cloud.google.com/vpn/docs/concepts/overview#ha-vpn) including HA VPN Gateway itself. VPN includes one or more VPN instances (connections). + +Each created VPN instance is represented by 1..4 VPN tunnels that taget remote VPN gateway(s) located in a single remote location. Remote VPN gateway(s) might have singe IP address (`redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT"`) or 2 IP addresses (`redundancy_type = "TWO_IPS_REDUNDANCY"`). + +## Example + +```hcl +data "google_compute_network" "test" { + name = "" + project = "" +} + +module "vpn" { + source = "../../../modules/vpn" + + project = "" + region = "us-central1" + + vpn_gateway_name = "my-test-gateway" + router_name = "my-test-router" + network = data.google_compute_network.test.self_link + + vpn_config = { + router_asn = 65000 + local_network = "vpc-vpn" + + router_advertise_config = { + ip_ranges = { + "10.10.0.0/16" : "GCP range 1" + } + mode = "CUSTOM" + groups = null + } + + instances = { + vpn-to-onprem1 = { + name = "vpn-to-onprem1", + peer_external_gateway = { + redundancy_type = "SINGLE_IP_INTERNALLY_REDUNDANT" + interfaces = [{ + id = 0 + ip_address = "1.1.1.1" + }] + }, + tunnels = { + remote0 = { + bgp_peer = { + address = "169.254.1.2" + asn = 65001 + } + bgp_peer_options = null + bgp_session_range = "169.254.1.1/30" + ike_version = 2 + vpn_gateway_interface = 0 + peer_external_gateway_interface = 0 + shared_secret = "secret" + } + remote1 = { + bgp_peer = { + address = "169.254.1.6" + asn = 65001 + } + bgp_peer_options = null + bgp_session_range = "169.254.1.5/30" + ike_version = 2 + vpn_gateway_interface = 1 + peer_external_gateway_interface = null + shared_secret = "secret" + } + } + } + vpn-to-onprem2 = { + name = "vpn-to-onprem2", + peer_external_gateway = { + redundancy_type = "TWO_IPS_REDUNDANCY" + interfaces = [{ + id = 0 + ip_address = "3.3.3.3" + }, { + id = 1 + ip_address = "4.4.4.4" + }] + }, + tunnels = { + remote0 = { + bgp_peer = { + address = "169.254.2.2" + asn = 65002 + } + bgp_peer_options = null + bgp_session_range = "169.254.2.1/30" + ike_version = 2 + vpn_gateway_interface = 0 + peer_external_gateway_interface = 0 + shared_secret = "secret" + } + remote1 = { + bgp_peer = { + address = "169.254.2.6" + asn = 65002 + } + bgp_peer_options = null + bgp_session_range = "169.254.2.5/30" + ike_version = 2 + vpn_gateway_interface = 1 + peer_external_gateway_interface = 1 + shared_secret = "secret" + } + } + } + vpn-to-gcp = { + name = "vpn-to-gcp", + + peer_gcp_gateway = "https://www.googleapis.com/compute/v1/projects//regions//vpnGateways/" + + tunnels = { + remote0 = { + bgp_peer = { + address = "169.254.3.2" + asn = 65003 + } + bgp_peer_options = null + bgp_session_range = "169.254.3.1/30" + ike_version = 2 + vpn_gateway_interface = 0 + peer_external_gateway_interface = null + shared_secret = "secret" + } + remote1 = { + bgp_peer = { + address = "169.254.3.6" + asn = 65003 + } + bgp_peer_options = null + bgp_session_range = "169.254.3.5/30" + ike_version = 2 + vpn_gateway_interface = 1 + peer_external_gateway_interface = 1 + shared_secret = "secret" + } + } + } + } + } +} +``` + +## Reference + +### Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.2, < 2.0 | +| [google](#requirement\_google) | >= 4.58 | + +### Providers + +| Name | Version | +|------|---------| +| [google](#provider\_google) | >= 4.58 | +| [google-beta](#provider\_google-beta) | n/a | +| [random](#provider\_random) | n/a | + +### Modules + +No modules. + +### Resources + +| Name | Type | +|------|------| +| [google-beta_google_compute_vpn_tunnel.tunnels](https://registry.terraform.io/providers/hashicorp/google-beta/latest/docs/resources/google_compute_vpn_tunnel) | resource | +| [google_compute_external_vpn_gateway.external_gateway](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_external_vpn_gateway) | resource | +| [google_compute_ha_vpn_gateway.ha_gateway](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_ha_vpn_gateway) | resource | +| [google_compute_router.router](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_router) | resource | +| [google_compute_router_interface.router_interface](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_router_interface) | resource | +| [google_compute_router_peer.bgp_peer](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_router_peer) | resource | +| [random_id.secret](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | + +### Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [labels](#input\_labels) | Labels for VPN components | `map(string)` | `{}` | no | +| [network](#input\_network) | VPC network ID that should be used for deployment | `string` | n/a | yes | +| [project](#input\_project) | n/a | `string` | `null` | no | +| [region](#input\_region) | Region to deploy VPN gateway in | `string` | n/a | yes | +| [router\_name](#input\_router\_name) | Cloud router name. The router is created by the module | `string` | `null` | no | +| [vpn\_config](#input\_vpn\_config) | VPN configuration from GCP to on-prem or from GCP to GCP.
If you'd like secrets to be randomly generated set `shared_secret` to empty string ("").

Example:
vpn_config = {
router_asn = 65000
local_network = "vpc-vpn"

router_advertise_config = {
ip_ranges = {
"10.10.0.0/16" : "GCP range 1"
}
mode = "CUSTOM"
groups = null
}

instances = {
vpn-to-onprem = {
name = "vpn-to-onprem",
peer_external_gateway = {
redundancy_type = "TWO_IPS_REDUNDANCY"
interfaces = [{
id = 0
ip_address = "1.1.1.1"
}, {
id = 1
ip_address = "2.2.2.2"
}]
},
tunnels = {
remote0 = {
bgp_peer = {
address = "169.254.1.2"
asn = 65001
}
bgp_peer_options = null
bgp_session_range = "169.254.1.1/30"
ike_version = 2
vpn_gateway_interface = 0
peer_external_gateway_interface = 0
shared_secret = "secret"
}
remote1 = {
bgp_peer = {
address = "169.254.1.6"
asn = 65001
}
bgp_peer_options = null
bgp_session_range = "169.254.1.5/30"
ike_version = 2
vpn_gateway_interface = 1
peer_external_gateway_interface = 1
shared_secret = "secret"
}
}
}
}
}
| `any` | n/a | yes | +| [vpn\_gateway\_name](#input\_vpn\_gateway\_name) | VPN gateway name. Gateway created by the module | `string` | n/a | yes | + +### Outputs + +| Name | Description | +|------|-------------| +| [random\_secret](#output\_random\_secret) | HA VPN IPsec tunnels secret that has been randomly generated | +| [vpn\_gw\_local\_address\_1](#output\_vpn\_gw\_local\_address\_1) | HA VPN gateway IP address 1 | +| [vpn\_gw\_local\_address\_2](#output\_vpn\_gw\_local\_address\_2) | HA VPN gateway IP address 2 | +| [vpn\_gw\_name](#output\_vpn\_gw\_name) | HA VPN gateway name | +| [vpn\_gw\_self\_link](#output\_vpn\_gw\_self\_link) | HA VPN gateway self\_link | + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.2, < 2.0 | +| [google](#requirement\_google) | == 4.58 | + +## Providers + +| Name | Version | +|------|---------| +| [google](#provider\_google) | == 4.58 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [vpn\_ha](#module\_vpn\_ha) | terraform-google-modules/vpn/google | 3.0.1 | + +## Resources + +| Name | Type | +|------|------| +| [google_compute_ha_vpn_gateway.ha_gateway](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_ha_vpn_gateway) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [project](#input\_project) | n/a | `string` | `null` | no | +| [region](#input\_region) | Region to deploy VPN gateway in | `string` | n/a | yes | +| [vpc\_network\_id](#input\_vpc\_network\_id) | VPC network ID that should be used for deployment | `string` | n/a | yes | +| [vpn](#input\_vpn) | VPN configuration from GCP to on-prem or from GCP to GCP.
If you'd like secrets to be randomly generated set `shared_secret` to empty string ("").

Example:
vpn = {
router_asn = 65000
local_network = "vpc-vpn"

router_advertise_config = {
ip_ranges = {
"10.10.0.0/16" : "GCP range 1"
}
mode = "CUSTOM"
groups = null
}

instances = {
vpn-to-onprem = {
name = "vpn-to-onprem",
peer_external_gateway = {
redundancy_type = "TWO_IPS_REDUNDANCY"
interfaces = [{
id = 0
ip_address = "1.1.1.1"
}, {
id = 1
ip_address = "2.2.2.2"
}]
},
tunnels = {
remote0 = {
bgp_peer = {
address = "169.254.1.2"
asn = 65001
}
bgp_peer_options = null
bgp_session_range = "169.254.1.1/30"
ike_version = 2
vpn_gateway_interface = 0
peer_external_gateway_interface = 0
shared_secret = "secret"
}
remote1 = {
bgp_peer = {
address = "169.254.1.6"
asn = 65001
}
bgp_peer_options = null
bgp_session_range = "169.254.1.5/30"
ike_version = 2
vpn_gateway_interface = 1
peer_external_gateway_interface = 1
shared_secret = "secret"
}
}
}
}
| `any` | n/a | yes | +| [vpn\_gateway\_name](#input\_vpn\_gateway\_name) | VPN gateway name | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [local\_ipsec\_gw2\_address\_2](#output\_local\_ipsec\_gw2\_address\_2) | HA VPN gateway IP address 2 | +| [local\_ipsec\_gw\_address\_1](#output\_local\_ipsec\_gw\_address\_1) | HA VPN gateway IP address 1 | +| [random\_secrets\_map](#output\_random\_secrets\_map) | HA VPN IPsec tunnels secrets that were randomly generated | +| [vpn\_gateway\_name](#output\_vpn\_gateway\_name) | HA VPN gateway name | +| [vpn\_gateway\_self\_link](#output\_vpn\_gateway\_self\_link) | HA VPN gateway self\_link | + \ No newline at end of file diff --git a/modules/vpn/main.tf b/modules/vpn/main.tf new file mode 100644 index 00000000..5262d685 --- /dev/null +++ b/modules/vpn/main.tf @@ -0,0 +1,159 @@ +locals { + secret = random_id.secret.b64_url + + tunnels_tmp = flatten([ + for vpn_instance_name, vpn_instance_config in var.vpn_config.instances : [ + for tunnel_name, tunnel_config in vpn_instance_config.tunnels : { + tunnel_name = "${vpn_instance_name}-${tunnel_name}" + tunnel_config = merge( + tunnel_config, + { + vpn_instance_name = vpn_instance_name + peer_external_gateway = try(vpn_instance_config.peer_external_gateway, null) + peer_gcp_gateway = try(vpn_instance_config.peer_gcp_gateway, null) + } + ) + } + ] + ]) + + tunnels = { + for k, v in local.tunnels_tmp : v.tunnel_name => v.tunnel_config + } +} + +resource "google_compute_ha_vpn_gateway" "ha_gateway" { + name = var.vpn_gateway_name + project = var.project + region = var.region + network = var.network +} + +resource "google_compute_router" "router" { + name = coalesce(var.router_name, "${var.vpn_gateway_name}-rtr") + project = var.project + region = var.region + network = var.network + bgp { + advertise_mode = ( + var.vpn_config.router_advertise_config == null + ? null + : var.vpn_config.router_advertise_config.mode + ) + advertised_groups = ( + var.vpn_config.router_advertise_config == null ? null : ( + var.vpn_config.router_advertise_config.mode != "CUSTOM" + ? null + : var.vpn_config.router_advertise_config.groups + ) + ) + dynamic "advertised_ip_ranges" { + for_each = ( + var.vpn_config.router_advertise_config == null ? {} : ( + var.vpn_config.router_advertise_config.mode != "CUSTOM" + ? {} + : var.vpn_config.router_advertise_config.ip_ranges + ) + ) + iterator = range + content { + range = range.key + description = range.value + } + } + asn = var.vpn_config.router_asn + keepalive_interval = try(var.vpn_config.keepalive_interval, 20) + } +} + +# Represents a VPN gateway managed outside of GCP +resource "google_compute_external_vpn_gateway" "external_gateway" { + for_each = { for k, v in var.vpn_config.instances : k => v if try(v.peer_external_gateway, null) != null } + + name = try(each.value.peer_external_gateway.name, null) != null ? each.value.peer_external_gateway.name : "${each.value.name}-external-gw" + project = var.project + redundancy_type = each.value.peer_external_gateway.redundancy_type + description = try(each.value.external_vpn_gateway_description, null) + labels = var.labels + dynamic "interface" { + for_each = each.value.peer_external_gateway.interfaces + content { + id = interface.value.id + ip_address = interface.value.ip_address + } + } +} + +resource "google_compute_router_peer" "bgp_peer" { + for_each = local.tunnels + region = var.region + project = var.project + name = try(each.value.bgp_session_name, null) != null ? each.value.bgp_session_name : "${var.vpn_gateway_name}-${each.key}" + router = google_compute_router.router.name + peer_ip_address = each.value.bgp_peer.address + peer_asn = each.value.bgp_peer.asn + ip_address = each.value.bgp_peer_options == null ? null : each.value.bgp_peer_options.ip_address + advertised_route_priority = ( + each.value.bgp_peer_options == null ? try(each.value.route_priority, 1000) : ( + each.value.bgp_peer_options.route_priority == null + ? each.value.route_priority + : each.value.bgp_peer_options.route_priority + ) + ) + advertise_mode = ( + each.value.bgp_peer_options == null ? null : each.value.bgp_peer_options.advertise_mode + ) + advertised_groups = ( + each.value.bgp_peer_options == null ? null : ( + each.value.bgp_peer_options.advertise_mode != "CUSTOM" + ? null + : each.value.bgp_peer_options.advertise_groups + ) + ) + dynamic "advertised_ip_ranges" { + for_each = ( + each.value.bgp_peer_options == null ? {} : ( + each.value.bgp_peer_options.advertise_mode != "CUSTOM" + ? {} + : each.value.bgp_peer_options.advertise_ip_ranges + ) + ) + iterator = range + content { + range = range.key + description = range.value + } + } + interface = google_compute_router_interface.router_interface[each.key].name +} + +resource "google_compute_router_interface" "router_interface" { + for_each = local.tunnels + project = var.project + region = var.region + name = try(each.value.bgp_session_name, null) != null ? each.value.bgp_session_name : each.key + router = google_compute_router.router.name + ip_range = each.value.bgp_session_range == "" ? null : each.value.bgp_session_range + vpn_tunnel = google_compute_vpn_tunnel.tunnels[each.key].name +} + +resource "google_compute_vpn_tunnel" "tunnels" { + provider = google-beta + for_each = local.tunnels + project = var.project + region = var.region + name = "${var.vpn_gateway_name}-${each.key}" + router = google_compute_router.router.name + peer_external_gateway = try(google_compute_external_vpn_gateway.external_gateway[each.value.vpn_instance_name].self_link, null) + peer_external_gateway_interface = each.value.peer_external_gateway_interface + peer_gcp_gateway = each.value.peer_gcp_gateway + vpn_gateway_interface = each.value.vpn_gateway_interface + ike_version = each.value.ike_version + shared_secret = each.value.shared_secret == "" ? local.secret : each.value.shared_secret + vpn_gateway = google_compute_ha_vpn_gateway.ha_gateway.self_link + labels = var.labels +} + +resource "random_id" "secret" { + byte_length = 16 +} diff --git a/modules/vpn/outputs.tf b/modules/vpn/outputs.tf new file mode 100644 index 00000000..518aa136 --- /dev/null +++ b/modules/vpn/outputs.tf @@ -0,0 +1,25 @@ +output "vpn_gw_name" { + value = google_compute_ha_vpn_gateway.ha_gateway.name + description = "HA VPN gateway name" +} + +output "vpn_gw_self_link" { + value = google_compute_ha_vpn_gateway.ha_gateway.self_link + description = "HA VPN gateway self_link" +} + +output "vpn_gw_local_address_1" { + value = google_compute_ha_vpn_gateway.ha_gateway.vpn_interfaces[0].ip_address + description = "HA VPN gateway IP address 1" +} + +output "vpn_gw_local_address_2" { + value = google_compute_ha_vpn_gateway.ha_gateway.vpn_interfaces[1].ip_address + description = "HA VPN gateway IP address 2" +} + +output "random_secret" { + value = local.secret + sensitive = true + description = "HA VPN IPsec tunnels secret that has been randomly generated" +} \ No newline at end of file diff --git a/modules/vpn/variables.tf b/modules/vpn/variables.tf new file mode 100644 index 00000000..1475da45 --- /dev/null +++ b/modules/vpn/variables.tf @@ -0,0 +1,98 @@ +variable "project" { + default = null + type = string +} + +variable "region" { + description = "Region to deploy VPN gateway in" + type = string +} + +variable "vpn_gateway_name" { + description = "VPN gateway name. Gateway created by the module" + type = string +} + +variable "router_name" { + description = "Cloud router name. The router is created by the module" + type = string + default = null +} + +variable "network" { + description = "VPC network ID that should be used for deployment" + type = string +} + +variable "labels" { + description = "Labels for VPN components" + type = map(string) + default = {} +} + +variable "vpn_config" { + type = any + description = <<-EOF + VPN configuration from GCP to on-prem or from GCP to GCP. + If you'd like secrets to be randomly generated set `shared_secret` to empty string (""). + + Example: + + ``` + vpn_config = { + router_asn = 65000 + local_network = "vpc-vpn" + + router_advertise_config = { + ip_ranges = { + "10.10.0.0/16" : "GCP range 1" + } + mode = "CUSTOM" + groups = null + } + + instances = { + vpn-to-onprem = { + name = "vpn-to-onprem", + peer_external_gateway = { + redundancy_type = "TWO_IPS_REDUNDANCY" + interfaces = [{ + id = 0 + ip_address = "1.1.1.1" + }, { + id = 1 + ip_address = "2.2.2.2" + }] + }, + tunnels = { + remote0 = { + bgp_peer = { + address = "169.254.1.2" + asn = 65001 + } + bgp_peer_options = null + bgp_session_range = "169.254.1.1/30" + ike_version = 2 + vpn_gateway_interface = 0 + peer_external_gateway_interface = 0 + shared_secret = "secret" + } + remote1 = { + bgp_peer = { + address = "169.254.1.6" + asn = 65001 + } + bgp_peer_options = null + bgp_session_range = "169.254.1.5/30" + ike_version = 2 + vpn_gateway_interface = 1 + peer_external_gateway_interface = 1 + shared_secret = "secret" + } + } + } + } + } + ``` + EOF +} \ No newline at end of file diff --git a/modules/vpn/versions.tf b/modules/vpn/versions.tf new file mode 100644 index 00000000..fcbc0254 --- /dev/null +++ b/modules/vpn/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_version = ">= 1.2, < 2.0" + required_providers { + google = { + version = ">= 4.58" + } + } +}