diff --git a/.github/workflows/terraform-docs.yml b/.github/workflows/terraform-docs.yml new file mode 100644 index 0000000..54e0ad2 --- /dev/null +++ b/.github/workflows/terraform-docs.yml @@ -0,0 +1,17 @@ +name: Generate terraform docs +on: + - pull_request + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - name: Render terraform docs and push changes back to PR + uses: terraform-docs/gh-actions@main + with: + working-dir: . + git-push: "true" \ No newline at end of file diff --git a/.github/workflows/terraform-fmt.yml b/.github/workflows/terraform-fmt.yml new file mode 100644 index 0000000..051e79f --- /dev/null +++ b/.github/workflows/terraform-fmt.yml @@ -0,0 +1,17 @@ +name: Generate terraform docs +on: + - pull_request + +jobs: + format: + runs-on: ubuntu-latest + name: Terraform Format Check + steps: + - name: Setup terraform + uses: hashicorp/setup-terraform@v2 + with: + terraform_version: ${{ inputs.terraform_version }} + + - name: Terraform fmt check + run: terraform fmt -check -diff -recursive + id: fmt \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5224284 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +.idea \ No newline at end of file diff --git a/.terraform-docs.yml b/.terraform-docs.yml new file mode 100644 index 0000000..616f685 --- /dev/null +++ b/.terraform-docs.yml @@ -0,0 +1,12 @@ +formatter: "markdown" +output: + file: "README.md" + mode: inject + +sections: + hide: + - resources + - modules + +header-from: docs/.header.md +footer-from: docs/.footer.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..abc1a1c --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ + +# HA VPN between GCP and OCI + +This repository contains a drop-in Terraform module that sets up a HA VPN between Azure and Oracle Cloud Infrastructure (OCI). + +## Features: + +- On GCP side a HA VPN is set up with two or four tunnels. (See [documentation](https://cloud.google.com/network-connectivity/docs/vpn/how-to/creating-ha-vpn)) +- On OCI side two Site-to-Site VPN connections are set up with two connections. +- Both sides are configured to establish BGP sessions between each other, so two platforms automatically learn the routes from each other. +- Proper routes are propagated from the GCP side to enable Private Google Access from OCI. + +## Installation + +On OCI side: A compartment, a DRG +On GCP side: A project, a VPC network + +## Requirements + +| Name | Version | +|------|---------| +| [google](#requirement\_google) | >= 3.30.0, < 5.0 | +| [oci](#requirement\_oci) | ~> 5.0 | + +## Providers + +| Name | Version | +|------|---------| +| [google](#provider\_google) | >= 3.30.0, < 5.0 | +| [oci](#provider\_oci) | ~> 5.0 | +| [random](#provider\_random) | n/a | + +## Resources + +| Name | Type | +|------|------| +| [google_compute_network.vpc](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/compute_network) | data source | +| [google_project.gcp_project](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/project) | data source | +| [oci_core_ipsec_connection_tunnels.gcp](https://registry.terraform.io/providers/hashicorp/oci/latest/docs/data-sources/core_ipsec_connection_tunnels) | data source | +| [oci_identity_compartment.compartment](https://registry.terraform.io/providers/hashicorp/oci/latest/docs/data-sources/identity_compartment) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [four\_tunnels\_redundancy](#input\_four\_tunnels\_redundancy) | Whether to deploy four tunnels or not. When set to `false`, only two tunnels are established. | `bool` | `false` | no | +| [gcp\_asn](#input\_gcp\_asn) | Specifies the ASN of GCP side of the BGP session | `number` | `65516` | no | +| [gcp\_network\_name](#input\_gcp\_network\_name) | Specifies the name of the VPC the VPN will be located in | `string` | n/a | yes | +| [gcp\_project\_id](#input\_gcp\_project\_id) | Specifies the project ID of Google project the VPN will be located in | `string` | n/a | yes | +| [gcp\_vpn\_region](#input\_gcp\_vpn\_region) | Specifies the GCP region the VPN will be located in | `string` | n/a | yes | +| [oci\_compartment\_id](#input\_oci\_compartment\_id) | OCID of the compartment where the VPN will be created | `string` | n/a | yes | +| [oci\_drg\_id](#input\_oci\_drg\_id) | OCID of the DRG (Dynamic Routing Gateway) where the VPN will be connected to | `string` | n/a | yes | +| [shared\_secret](#input\_shared\_secret) | Shared secret for the VPN connection. When left empty, a random secret is created and shared between GCP and OCI. | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [shared\_secret](#output\_shared\_secret) | Shared Secret that was used to establish the VPN connection | + +## License + +[Apache License 2.0](LICENSE) + \ No newline at end of file diff --git a/docs/.footer.md b/docs/.footer.md new file mode 100644 index 0000000..d31958c --- /dev/null +++ b/docs/.footer.md @@ -0,0 +1,3 @@ +## License + +[Apache License 2.0](LICENSE) \ No newline at end of file diff --git a/docs/.header.md b/docs/.header.md new file mode 100644 index 0000000..b48c44e --- /dev/null +++ b/docs/.header.md @@ -0,0 +1,15 @@ +# HA VPN between GCP and OCI + +This repository contains a drop-in Terraform module that sets up a HA VPN between Azure and Oracle Cloud Infrastructure (OCI). + +## Features: + +- On GCP side a HA VPN is set up with two or four tunnels. (See [documentation](https://cloud.google.com/network-connectivity/docs/vpn/how-to/creating-ha-vpn)) +- On OCI side two Site-to-Site VPN connections are set up with two connections. +- Both sides are configured to establish BGP sessions between each other, so two platforms automatically learn the routes from each other. +- Proper routes are propagated from the GCP side to enable Private Google Access from OCI. + +## Installation + +On OCI side: A compartment, a DRG +On GCP side: A project, a VPC network \ No newline at end of file diff --git a/gcp.tf b/gcp.tf new file mode 100644 index 0000000..0d57a87 --- /dev/null +++ b/gcp.tf @@ -0,0 +1,136 @@ +data "google_project" "gcp_project" { + project_id = var.gcp_project_id +} + +data "google_compute_network" "vpc" { + project = data.google_project.gcp_project.project_id + name = var.gcp_network_name +} + + +resource "google_compute_ha_vpn_gateway" "ha_gateway" { + name = "to-oci" + project = data.google_project.gcp_project.project_id + region = var.gcp_vpn_region + network = data.google_compute_network.vpc.name +} + +resource "google_compute_external_vpn_gateway" "external_gateway" { + count = 2 + name = "oci-${count.index + 1}" + project = data.google_project.gcp_project.project_id + redundancy_type = "TWO_IPS_REDUNDANCY" + + interface { + id = "0" + ip_address = data.oci_core_ipsec_connection_tunnels.gcp[count.index].ip_sec_connection_tunnels[0].vpn_ip + } + + interface { + id = "1" + ip_address = data.oci_core_ipsec_connection_tunnels.gcp[count.index].ip_sec_connection_tunnels[1].vpn_ip + } +} + + +resource "google_compute_router" "router" { + + name = "vpn-to-oci" + project = data.google_project.gcp_project.project_id + region = var.gcp_vpn_region + network = data.google_compute_network.vpc.name + bgp { + advertise_mode = "CUSTOM" + advertised_groups = ["ALL_SUBNETS"] + advertised_ip_ranges { + range = "199.36.153.8/30" + description = "private.googleapis.com" + } + advertised_ip_ranges { + range = "199.36.153.4/30" + description = "restricted.googleapis.com" + } + asn = var.gcp_asn + } +} + + +resource "google_compute_vpn_tunnel" "tunnels_primary" { + depends_on = [ + oci_core_ipsec_connection_tunnel_management.primary, + ] + count = 2 + project = data.google_project.gcp_project.project_id + region = var.gcp_vpn_region + name = "oci-primary-${count.index + 1}" + router = google_compute_router.router.id + peer_external_gateway = google_compute_external_vpn_gateway.external_gateway[count.index].id + peer_external_gateway_interface = 0 + vpn_gateway_interface = count.index + ike_version = 2 + shared_secret = local.shared_secret + vpn_gateway = google_compute_ha_vpn_gateway.ha_gateway.self_link +} + + +resource "google_compute_vpn_tunnel" "tunnels_secondary" { + depends_on = [ + oci_core_ipsec_connection_tunnel_management.secondary, + ] + count = var.four_tunnels_redundancy ? 2 : 0 + project = data.google_project.gcp_project.project_id + region = var.gcp_vpn_region + name = "oci-secondary-${count.index + 1}" + router = google_compute_router.router.id + peer_external_gateway = google_compute_external_vpn_gateway.external_gateway[count.index].id + peer_external_gateway_interface = 1 + vpn_gateway_interface = count.index + ike_version = 2 + shared_secret = local.shared_secret + vpn_gateway = google_compute_ha_vpn_gateway.ha_gateway.self_link +} + +resource "google_compute_router_interface" "router_interface_primary" { + count = 2 + project = data.google_project.gcp_project.project_id + region = var.gcp_vpn_region + name = "oci-primary-${count.index + 1}" + router = google_compute_router.router.name + vpn_tunnel = google_compute_vpn_tunnel.tunnels_primary[count.index].name +} + +resource "google_compute_router_interface" "router_interface_secondary" { + count = var.four_tunnels_redundancy ? 2 : 0 + project = data.google_project.gcp_project.project_id + region = var.gcp_vpn_region + name = "oci-secondary-${count.index + 1}" + router = google_compute_router.router.name + vpn_tunnel = google_compute_vpn_tunnel.tunnels_secondary[count.index].name +} + + + +resource "google_compute_router_peer" "bgp_peer_primary" { + count = 2 + project = data.google_project.gcp_project.project_id + region = var.gcp_vpn_region + name = "oci-primary-${count.index + 1}" + router = google_compute_router.router.name + ip_address = "169.254.2${count.index + 1}.1" #oci_core_ipsec_connection_tunnel_management.primary[count.index].bgp_session_info[0].customer_interface_ip + peer_ip_address = "169.254.2${count.index + 1}.2" #oci_core_ipsec_connection_tunnel_management.primary[count.index].bgp_session_info[0].oracle_interface_ip + peer_asn = oci_core_ipsec_connection_tunnel_management.primary[count.index].bgp_session_info[0].oracle_bgp_asn + interface = google_compute_router_interface.router_interface_primary[count.index].name +} + + +resource "google_compute_router_peer" "bgp_peer_secondary" { + count = var.four_tunnels_redundancy ? 2 : 0 + project = data.google_project.gcp_project.project_id + region = var.gcp_vpn_region + name = "oci-secondary-${count.index + 1}" + router = google_compute_router.router.name + ip_address = "169.254.3${count.index + 1}.1" #oci_core_ipsec_connection_tunnel_management.primary[count.index].bgp_session_info[0].customer_interface_ip + peer_ip_address = "169.254.3${count.index + 1}.2" #oci_core_ipsec_connection_tunnel_management.primary[count.index].bgp_session_info[0].oracle_interface_ip + peer_asn = oci_core_ipsec_connection_tunnel_management.secondary[count.index].bgp_session_info[0].oracle_bgp_asn + interface = google_compute_router_interface.router_interface_secondary[count.index].name +} \ No newline at end of file diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..c05c6d5 --- /dev/null +++ b/main.tf @@ -0,0 +1,28 @@ +terraform { + required_providers { + oci = { + version = "~> 5.0" + } + google = { + source = "hashicorp/google" + version = ">= 3.30.0, < 5.0" + } + } +} + +resource "random_password" "shared_secret" { + count = var.shared_secret == null ? 1 : 0 + length = 16 + lower = true + special = false +} + +locals { + shared_secret = var.shared_secret == null ? random_password.shared_secret[0].result : var.shared_secret +} + +output "shared_secret" { + sensitive = true + value = local.shared_secret + description = "Shared Secret that was used to establish the VPN connection" +} \ No newline at end of file diff --git a/oci.tf b/oci.tf new file mode 100644 index 0000000..ad53440 --- /dev/null +++ b/oci.tf @@ -0,0 +1,64 @@ +data "oci_identity_compartment" "compartment" { + id = var.oci_compartment_id +} + +resource "oci_core_cpe" "gcp" { + count = 2 + compartment_id = data.oci_identity_compartment.compartment.id + ip_address = google_compute_ha_vpn_gateway.ha_gateway.vpn_interfaces[count.index].ip_address + display_name = "gcp-${count.index + 1}" +} + +resource "oci_core_ipsec" "gcp" { + count = 2 + compartment_id = data.oci_identity_compartment.compartment.id + cpe_id = oci_core_cpe.gcp[count.index].id + drg_id = var.oci_drg_id + display_name = "to-gcp-${count.index + 1}" + static_routes = [ + "1.1.1.1/32" #https://github.com/oracle/terraform-provider-oci/issues/1509#issuecomment-1126299971 + ] + +} + +data "oci_core_ipsec_connection_tunnels" "gcp" { + count = 2 + ipsec_id = oci_core_ipsec.gcp[count.index].id +} + +resource "oci_core_ipsec_connection_tunnel_management" "primary" { + count = 2 + #Required + ipsec_id = oci_core_ipsec.gcp[count.index].id + tunnel_id = data.oci_core_ipsec_connection_tunnels.gcp[count.index].ip_sec_connection_tunnels[0].id + routing = "BGP" + #Optional + display_name = "to-gcp-${count.index + 1}-primary" + bgp_session_info { + #Optional + customer_bgp_asn = var.gcp_asn + customer_interface_ip = "169.254.2${count.index + 1}.1/30" + oracle_interface_ip = "169.254.2${count.index + 1}.2/30" + } + + shared_secret = local.shared_secret + ike_version = "V2" +} + +resource "oci_core_ipsec_connection_tunnel_management" "secondary" { + count = 2 + #Required + ipsec_id = oci_core_ipsec.gcp[count.index].id + tunnel_id = data.oci_core_ipsec_connection_tunnels.gcp[count.index].ip_sec_connection_tunnels[1].id + routing = "BGP" + #Optional + display_name = "to-gcp-${count.index + 1}-primary" + bgp_session_info { + #Optional + customer_bgp_asn = var.gcp_asn + customer_interface_ip = "169.254.3${count.index + 1}.1/30" + oracle_interface_ip = "169.254.3${count.index + 1}.2/30" + } + shared_secret = local.shared_secret + ike_version = "V2" +} \ No newline at end of file diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..d921406 --- /dev/null +++ b/variables.tf @@ -0,0 +1,46 @@ +# Common variables + +variable "shared_secret" { + type = string + default = null + description = "Shared secret for the VPN connection. When left empty, a random secret is created and shared between GCP and OCI." +} + +# GCP variables +variable "gcp_project_id" { + type = string + description = "Specifies the project ID of Google project the VPN will be located in" +} + +variable "gcp_network_name" { + type = string + description = "Specifies the name of the VPC the VPN will be located in" +} + +variable "gcp_vpn_region" { + type = string + description = "Specifies the GCP region the VPN will be located in" +} + +variable "gcp_asn" { + type = number + description = "Specifies the ASN of GCP side of the BGP session" + default = 65516 +} + +# OCI +variable "oci_compartment_id" { + type = string + description = "OCID of the compartment where the VPN will be created" +} + +variable "oci_drg_id" { + type = string + description = "OCID of the DRG (Dynamic Routing Gateway) where the VPN will be connected to" +} + +variable "four_tunnels_redundancy" { + type = bool + default = false + description = "Whether to deploy four tunnels or not. When set to `false`, only two tunnels are established." +} \ No newline at end of file