Blog DevOps

VPN site-to-site entre AWS et GCP avec routage BGP | Theodo Cloud

Rédigé par Damien Jablonski | 26 sept. 2024 12:05:39

Qu’est-ce qu’un VPN avec routage BGP ?

Le VPN, que vous connaissez sûrement, permet de relier deux réseaux distants de manière chiffrée. Lors de cette configuration, en plus de définir vos adresses IP privées ou votre ASN (Autonomous System Number), il vous sera demandé de spécifier les réseaux internes que vous souhaitez connecter entre eux.

Vous pouvez configurer manuellement ces routes en spécifiant les sous-réseaux à interconnecter, mais cela peut devenir long et sujet à erreurs si vous avez beaucoup de réseaux à gérer. Alternativement, vous pouvez automatiser cette configuration en utilisant le protocole de routage dynamique avec BGP.

Ce protocole permet à vos routeurs d'échanger automatiquement les informations de routage et d'adapter les chemins empruntés en fonction des changements dans votre réseau ou ceux de vos partenaires.

Notre Architecture

Lors de ce déploiement, nous aurons 4 liens. GCP nous fournit 2 interfaces comprenant 2 IPs publiques. Coté AWS, nous allons créer 2 Customer Gateway qui vont chacune nous fournir 2 IPs publiques.

Implémentation avec Terraform

Pour déployer notre VPN, nous utilisons Terraform en nous nous concentrons uniquement sur la création du VPN. Ni l’initialisation du projet sur Terraform ni la création du VPC côté AWS et GCP ne sont abordés. De plus, si vous souhaitez déployer plusieurs VPN, nous suggérons l’utilisation de Terragrunt afin de réduire la répétition du code et d’améliorer sa lisibilité.

Commençons par créer un module qui contient les fichiers suivants

├── modules
│   └── aws-to-gcp-vpn
│       ├── aws.tf
│       ├── gcp.tf
│       ├── providers.tf
│       └── variables.tf

Contenu du fichier variables.tf et providers.tf

variable "project_id" {
  type = string
  description = "Le projet GCP sur lequel nous voulons déployer le VPN"
}

variable "gcp_vpn_gwy_region" {
  type = string
  default = "europe-west1"
  description = "La region sur GPC sur laquelle nous voulons déployer le VPN"
}

variable "gcp_router_asn" {
  type = string
  default = "65000"
  description = "L'identifiant ASN qu'on va utiliser pour le routage BGP côté GCP"
}

variable "aws_router_asn" {
  type = string
  default = "65001"
  description = "L'identifiant ASN qu'on va utiliser pour le routage BGP côté AWS"
}

variable "aws_vpc_id" {
  type = string
  default = "AWS VPC id"
  description = "L'ID VPC auquel on veut se connecter côté AWS"
}

variable "gcp_network" {
  type        = string
  description = "Le nom du VPC auquel on veut se connecter côté GCP"
}

variable "aws_vpc_cidr" {
  type = string
  description = "Le CIDR du VPC côté AWS, sous la forme x.x.x.x/x"
}

variable "prefix" {
  type        = string
  description = "Un préfix pour le naming des ressources"
}

variable "tunnel_preshared_key" {
  type = string
  description = "Notre preshared_key utilisée pour la création du tunnel"
  default = "thisismysecret"
}

variable "num_tunnels" {
  type = number
  validation {
    condition     = var.num_tunnels % 2 == 0
    error_message = "number of tunnels needs to be in multiples of 2."
  }
  validation {
    condition     = var.num_tunnels >= 4
    error_message = "min 4 tunnels required for high availability."
  }
  description = < < EOF  
Nombre total de tunnels VPN. Cela doit être un multiple de 2..
  EOF
}

variable "route_table_id" {
  type = list(string)
  description = "Nos tables de routage coté AWS."
}

variable "destination_cidr_block" {
  type = list(string)
  description = "Le CIDR de notre VPC GCP pour creer les routes"
}

variable "gcp_cidr" {
  type = string
  description = "Le CIDR de notre VPC GCP pour la création du VPN"
}
provider "google" {
  project  = var.project_id
}

provider "aws" {
  region = "AWS_REGION"
  profile = "AWS_PROFILE"
}

Sur le fichier providers.tf, nous spécifions les providers GCP et AWS afin de pouvoir interagir avec nos 2 clouds.

Contenu du fichier aws.tf

locals {
  default_num_ha_vpn_interfaces = 2
}

resource "aws_customer_gateway" "gwy" {
  count = local.default_num_ha_vpn_interfaces

  device_name = "${var.prefix}-gwy-${count.index}"
  bgp_asn     = var.gcp_router_asn
  type        = "ipsec.1"
  ip_address  = google_compute_ha_vpn_gateway.gwy.vpn_interfaces[count.index]["ip_address"]
}

resource "aws_vpn_connection" "vpn_conn" {
  count = var.num_tunnels / 2

  customer_gateway_id   = aws_customer_gateway.gwy[count.index % 2].id
  type                  = "ipsec.1"
  vpn_gateway_id = aws_vpn_gateway.this.id
  tunnel1_preshared_key = var.tunnel_preshared_key
  tunnel2_preshared_key = var.tunnel_preshared_key

  tags = {
    Name = "${var.prefix}-vpn-connn"
  }
}

resource "aws_vpn_gateway" "this" {
  vpc_id = var.aws_vpc_id
  amazon_side_asn = var.aws_router_asn
}

module "vpn_gateway" {
  count = local.default_num_ha_vpn_interfaces
  source  = "terraform-aws-modules/vpn-gateway/aws"
  version = "~> 3.0"
  vpn_gateway_id      = aws_vpn_gateway.this.id
  customer_gateway_id = aws_customer_gateway.gwy[count.index].id

  vpc_id                       = var.aws_vpc_id

  create_vpn_connection = false

  local_ipv4_network_cidr      = var.aws_vpc_cidr
  remote_ipv4_network_cidr     = var.gcp_cidr
}

resource "aws_vpn_gateway_route_propagation" "private_subnets_vpn_routing" {
  count = length(var.route_table_id)

  vpn_gateway_id = aws_vpn_gateway.this.id
  route_table_id = element(var.route_table_id, count.index)
}

Ce fichier va créer tous les services sur AWS nécessaire au bon fonctionnement du VPN.

Nous avons tout d’abord le service aws_vpn_gateway qui crée une passerelle VPN (VPN Gateway) sur AWS dans notre VPC.

Vient ensuite le service aws_customer_gateway qui va créer une passerelle cliente (Customer Gateway) pour la connexion VPN entre AWS et GCP. Coté GCP, nous avons 2 interfaces, donc nous créons 2 Customer Gateway.

Afin de nous simplifier la vie, nous utilisons le module vpn_gateway qui va se charger de configurer la passerelle VPN.

Pour finir sur la configuration du VPN coté AWS, nous créons via la ressource aws_vpn_connection qui va créer la connexion avec le VPN GCP. Par simplicité, nous avons mis la preshared_key en clair dans cet exemple. Nous vous conseillons fortement l’utilisation d’un secret manager.

En dernier, nous allons activer la propagation des routes afin que nos réseaux sachent comment communiquer avec GCP. Nous faisons cela avec la ressource aws_vpn_gateway_route_propagation. Grâce au protocole BGP, tous nos sous réseaux GCP seront automatiquement renseignés dans notre table de routage AWS.

Contenu du fichier gcp.tf

locals {
  four_interface_ext_gwys = [for i in range(floor(var.num_tunnels / 4)) :
    { key : i, redundancy_type = "FOUR_IPS_REDUNDANCY" }
  ]
  two_interface_ext_gwys = [for i in range(ceil(var.num_tunnels / 4) - length(local.four_interface_ext_gwys)) :
    {
      key : i + length(local.four_interface_ext_gwys),
      redundancy_type = "TWO_IPS_REDUNDANCY"
    } if var.num_tunnels % 4 != 0
  ]
  num_ext_gwys = concat(local.four_interface_ext_gwys, local.two_interface_ext_gwys)
  aws_vpn_conn_addresses = {
    for k, v in chunklist([
      for k, v in flatten([
        for k, v in aws_vpn_connection.vpn_conn :
        [v.tunnel1_address, v.tunnel2_address]
      ]) : v
    ], 4) :
    k => v
  }
  tunnels = chunklist(flatten([
    for i in range(length(local.num_ext_gwys)) : [
      for k, v in setproduct(range(2), chunklist(range(4), 2)) :
      {
        ext_gwy : i,
        peer_gwy_interface : k,
        vpn_gwy_interface : v[0] % 2
      }
    ]
  ]), var.num_tunnels)[0]
  bgp_sessions = {
    for k, v in flatten([
      for k, v in aws_vpn_connection.vpn_conn :
      [
        {
          ip_address : v.tunnel1_cgw_inside_address,
          peer_ip_address : v.tunnel1_vgw_inside_address
        },
        {
          ip_address : v.tunnel2_cgw_inside_address,
          peer_ip_address : v.tunnel2_vgw_inside_address
        }
      ]
    ]) : k => v
  }
}

resource "google_compute_ha_vpn_gateway" "gwy" {
  name    = "${var.prefix}-ha-vpn-gwy"
  network = var.gcp_network
  region  = var.gcp_vpn_gwy_region
}

resource "google_compute_external_vpn_gateway" "ext_gwy" {
  for_each = { for k, v in local.num_ext_gwys : k => v }

  name            = "${var.prefix}-ext-vpn-gwy-${each.key}"
  redundancy_type = each.value["redundancy_type"]
  dynamic "interface" {
    for_each = local.aws_vpn_conn_addresses[each.key]
    content {
      id         = interface.key
      ip_address = interface.value
    }
  }
}

resource "google_compute_router" "router" {
  name    = "vpn-router"
  network = var.gcp_network
  region  = var.gcp_vpn_gwy_region
  bgp {
    asn            = var.gcp_router_asn
    advertise_mode = "CUSTOM"
    advertised_groups = [
      "ALL_SUBNETS"
    ]
  }
}

resource "google_compute_vpn_tunnel" "tunnel" {
  for_each = { for k, v in local.tunnels : k => v }

  name                            = "${var.prefix}-tunnel-${each.key}"
  shared_secret                   = var.tunnel_preshared_key
  peer_external_gateway           = google_compute_external_vpn_gateway.ext_gwy[each.value["ext_gwy"]].name
  peer_external_gateway_interface = each.value["peer_gwy_interface"]
  region                          = var.gcp_vpn_gwy_region
  router                          = google_compute_router.router.name
  ike_version                     = "2"
  vpn_gateway                     = google_compute_ha_vpn_gateway.gwy.id
  vpn_gateway_interface           = each.value["vpn_gwy_interface"]
}

resource "google_compute_router_interface" "interface" {
  for_each = local.bgp_sessions

  name       = "${var.prefix}-interface-${each.key}"
  router     = google_compute_router.router.name
  region     = var.gcp_vpn_gwy_region
  ip_range   = "${each.value["ip_address"]}/30"
  vpn_tunnel = google_compute_vpn_tunnel.tunnel[each.key].name
}

resource "google_compute_router_peer" "peer" {
  for_each = local.bgp_sessions

  name            = "${var.prefix}-peer-${each.key}"
  interface       = "${var.prefix}-interface-${each.key}"
  peer_asn        = var.aws_router_asn
  ip_address      = each.value["ip_address"]
  peer_ip_address = each.value["peer_ip_address"]
  router          = google_compute_router.router.name
  region          = var.gcp_vpn_gwy_region
}

Coté GCP, nous avons une configuration similaire.

Le bloc locals, sans rentrer dans les détails, va créer entre autres des listes ou alors transformer des valeurs afin de les réutiliser dans nos ressources GCP. Cela va permettre l’utilisation d’une boucle “for_each” afin d’éviter une répétition du code due au fait que nous devons connecter nos 2 interfaces GCP à 4 IPs publiques AWS.

La ressource google_compute_ha_vpn_gateway crée une passerelle VPN HA (High Availability).

La ressource google_compute_router crée un routeur GCP pour gérer le trafic BGP associé aux tunnels VPN.

Ensuite, nous avons la ressource google_compute_external_vpn_gateway qui crée des passerelles VPN externes pour GCP avec différentes configurations de redondance.

Avec la ressource google_compute_vpn_tunnel, nous créons des tunnels VPN HA en utilisant les passerelles VPN externes (peer_external_gateway) et les interfaces associées.

La ressource google_compute_router_interface permet la création des interfaces sur le routeur GCP pour chaque session BGP.

Pour finir, nous avons la ressource google_compute_router_peer qui crée des pairs BGP sur le routeur GCP, permettant la communication entre les sous-réseaux GCP et AWS.

Déploiement du code Terraform

Maintenant que nous avons notre code Terraform, nous allons le déployer.

À la racine de votre projet, créez un dossier Terraform. Dans celui-ci créez un fichier main.tf

├── modules
│   └── aws-to-gcp-vpn
│       ├── aws.tf
│       ├── gcp.tf
│       ├── providers.tf
│       └── variables.tf
└── terraform
    └── main.tf

Votre main.tf devra contenir les différentes variables selon les ressources que vous avez sur GCP et AWS, voici les miennes.

module "aws_to_gcp_vpn" {
  source = "../modules/aws-to-gcp-vpn"
  project_id = "theodo-cloud-lab"
  gcp_router_asn = "65000"
  aws_router_asn = "65001"
  aws_vpc_id = "vpc-0ab08ef3d971e565d"
  gcp_network = "damienj"
  aws_vpc_cidr = "10.0.0.0/16"
  prefix = "test"
  num_tunnels = 4
  route_table_id = ["rtb-0a2ee2e1a5f8ccc63"]
  destination_cidr_block = [ "192.168.0.0/24" ]
  gcp_cidr = "192.168.0.0/24"
}

Après apply, voilà ce que vous devriez avoir sur AWS et GCP :

Nos 4 tunnels sont UP coté AWS

Notre route pour communiquer avec notre réseau GCP renseigné automatiquement grâce au protocole BGP

Nos 4 tunnels sont UP sur GCP

Vous pouvez tester que votre connexion fonctionne en créant une machine sur GCP et sur AWS et essayer de les ping entre elles via leur adresse IP.

Conclusion

Nous venons de voir comment facilement et rapidement déployer un VPN entre AWS et GCP avec du routage BGP. Vous pouvez aller plus loin dans la configuration et créer une pre shared key aléatoire avec la ressource random. Vous pouvez également réduire la plage d’IP GCP à laquelle AWS peut se connecter.