Home CloudAWS AWS, Azure: Site-to-site (S2S) VPN tunnel between AWS and Azure

AWS, Azure: Site-to-site (S2S) VPN tunnel between AWS and Azure

by Kliment Andreev
520 views

In this post I’ll create a site-to-site (S2S) tunnel between AWS and Azure. It’s an HA tunnel so it’s regionally resilient. I’ll use Terraform for the deployment using a set of modules (download here). The Terraform files will be separated in two different folders. One for Azure and one for AWS.

The architectural diagram looks like this.

Table of Contents

Azure

For Azure, we will provision a public subnet (10.1.0.0/24) and a Gateway subnet (10.1.1.0/24). Then, we’ll provision one VNET Gateway with active-active BGP tunnel. The Azure setup requires that we provision 4 local network gateways and S2S connections, but the output will return only 2 public IPs. These are the files.
provider.tf

terraform {
  required_version = "~>1.9.5"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~>3.110.0"
    }
  }
}

provider "azurerm" {
  features {
    resource_group {
      prevent_deletion_if_contains_resources = false
    }
  }
}

azure.tf

# Create a resource group
module "rg" {
  source = "../../modules/azure/rg-0.0.4"

  az_rg_name     = var.rg_ResourceGroupName
  az_rg_location = var.rgn_RegionLocation
}

# Create a VNET
module "vnet" {
  source = "../../modules/azure/vnet-4.0.0"

  vnet_name           = var.vnet_Name
  resource_group_name = var.rg_ResourceGroupName
  vnet_location       = var.rgn_RegionLocation
  use_for_each        = false
  address_space       = [var.vnet_CIDR]
  subnet_prefixes     = [var.sub_PublicCIDR, var.sub_GatewayCIDR]
  subnet_names        = [var.sub_PublicName, var.sub_GatewayName]

  depends_on = [module.rg]
}

# Create a virtual network gateway
module "vnetgw" {
  source = "../../modules/azure/vnetgw-0.5.1"

  location                  = var.rgn_RegionLocation
  name                      = var.vgw_Name
  subnet_address_prefix     = var.sub_GatewayCIDR
  sku                       = var.vgw_SKU
  type                      = var.vgw_Type
  virtual_network_id        = module.vnet.vnet_id
  vpn_generation            = var.vgw_Generation
  vpn_type                  = var.vpn_Type
  subnet_creation_enabled   = false
  vpn_active_active_enabled = true
  vpn_bgp_enabled           = true
  vpn_bgp_settings = {
    asn = var.asn_Azure
  }
  # Create virtual network gateway configurations
  ip_configurations = {
    "ip_config_01" = {
      name            = var.gwc_BGPName1
      apipa_addresses = var.ip_APIPAAddresses1
      public_ip = {
        allocation_method = var.ip_allocation_method
      }
    },
    "ip_config_02" = {
      name            = var.gwc_BGPName2
      apipa_addresses = var.ip_APIPAAddresses2
      public_ip = {
        allocation_method = var.ip_allocation_method
      }
    }
  }
  # Create local network gateways and connections
  local_network_gateways = {
    # AWS Tunnel 1 to Azure Instance 0
    "lngw_01" = {
      name            = var.lng_AWST1A0
      gateway_address = var.ip_AWST1A0
      bgp_settings = {
        asn                 = var.asn_AWS
        bgp_peering_address = var.ip_peering_AWST1A0
      }
      connection = {
        name       = var.conn_AWST1A0
        type       = var.conn_Type
        enable_bgp = true
        shared_key = var.key_SharedKey
        custom_bgp_addresses = {
          primary   = var.ip_primary_AWST1A0
          secondary = var.ip_secondary_AWST1A0
        }
      }
    },
    # AWS Tunnel 2 to Azure Instance 0
    "lngw_02" = {
      name            = var.lng_AWST2A0
      gateway_address = var.ip_AWST2A0
      bgp_settings = {
        asn                 = var.asn_AWS
        bgp_peering_address = var.ip_peering_AWST2A0
      }
      connection = {
        name       = var.conn_AWST2A0
        type       = var.conn_Type
        enable_bgp = true
        shared_key = var.key_SharedKey
        custom_bgp_addresses = {
          primary = var.ip_primary_AWST2A0
        secondary = var.ip_secondary_AWST2A0 }
      }
    },
    # AWS Tunnel 1 to Azure Instance 1
    "lngw_03" = {
      name            = var.lng_AWST1A1
      gateway_address = var.ip_AWST1A1
      bgp_settings = {
        asn                 = var.asn_AWS
        bgp_peering_address = var.ip_peering_AWST1A1
      }
      connection = {
        name       = var.conn_AWST1A1
        type       = var.conn_Type
        enable_bgp = true
        shared_key = var.key_SharedKey
        custom_bgp_addresses = {
          primary = var.ip_primary_AWST1A1
        secondary = var.ip_secondary_AWST1A1 }
      }
    },
    # AWS Tunnel 2 to Azure Instance 1
    "lngw_04" = {
      name            = var.lng_AWST2A1
      gateway_address = var.ip_AWST2A1
      bgp_settings = {
        asn                 = var.asn_AWS
        bgp_peering_address = var.ip_peering_AWST2A1
      }
      connection = {
        name       = var.conn_AWST2A1
        type       = var.conn_Type
        enable_bgp = true
        shared_key = var.key_SharedKey
        custom_bgp_addresses = {
          primary = var.ip_primary_AWST2A1
        secondary = var.ip_secondary_AWST2A1 }
      }
    }
  }

  depends_on = [module.vnet]
}

variables.tf

# Resource group
variable "rg_ResourceGroupName" {
  type        = string
  default     = "rgVPN"
  description = "Resource group name"
}

variable "rgn_RegionLocation" {
  type        = string
  default     = "eastus2"
  description = "Azure region"
}

# VNET
variable "vnet_Name" {
  type        = string
  default     = "vnetVPN"
  description = "VNET name"
}

variable "vnet_CIDR" {
  type        = string
  default     = "10.1.0.0/16"
  description = "VNET CIDR"
}

variable "sub_PublicCIDR" {
  type        = string
  default     = "10.1.0.0/24"
  description = "CIDR of the public subnet"
}

variable "sub_GatewayCIDR" {
  type        = string
  default     = "10.1.1.0/24"
  description = "CIDR of the Gateway subnet"
}

variable "sub_PublicName" {
  type        = string
  default     = "subPublic"
  description = "Name of the public subnet"
}

variable "sub_GatewayName" {
  type        = string
  default     = "GatewaySubnet"
  description = "Name of the Gateway subnet - DO NOT CHANGE. It has to stay as GatewaySubnet"
}

# Virtual network gateway
variable "vgw_Name" {
  type        = string
  default     = "vgwVPN"
  description = "Name of the virtual gateway"
}

variable "vgw_SKU" {
  type        = string
  default     = "VpnGw2AZ"
  description = "Virtual Gateway SKU"
}

variable "vgw_Type" {
  type        = string
  default     = "Vpn"
  description = "Virtual Gateway Type (VPN or ExpressRoute)"
}

variable "vgw_Generation" {
  type        = string
  default     = "Generation2"
  description = "Virtual Gateway Generation"
}

variable "vpn_Type" {
  type        = string
  default     = "RouteBased"
  description = "VPN type - RouteBased or PolicyBased"
}

variable "asn_Azure" {
  type        = string
  default     = "65000"
  description = "Azure ASN"
}

variable "asn_AWS" {
  type        = string
  default     = "64512"
  description = "AWS ASN"
}

# Virtual network gateway config 1
variable "gwc_BGPName1" {
  type        = string
  default     = "vnetGatewayConfig01"
  description = "Name of the virtual network gateway configuration for the first tunnel"
}

variable "ip_APIPAAddresses1" {
  type        = list(string)
  default     = ["169.254.21.2", "169.254.22.2"]
  description = "APIPA address for the first tunnel"
}

# Virtual network gateway config 2
variable "gwc_BGPName2" {
  type        = string
  default     = "vnetGatewayConfig02"
  description = "Name of the virtual network gateway configuration for the second tunnel"
}

variable "ip_APIPAAddresses2" {
  type        = list(string)
  default     = ["169.254.21.6", "169.254.22.6"]
  description = "APIPA address for the second tunnel"
}

variable "ip_allocation_method" {
  type        = string
  default     = "Static"
  description = "Static or Dynamic public IP"
}

# Local network gateway common settings
variable "conn_Type" {
  type        = string
  default     = "IPsec"
  description = "Connection type: IPsec or Vnet2Vnet"
}

variable "key_SharedKey" {
  type        = string
  default     = "KlimentAndreev1970_"
  description = "Make sure it's the same as 'key_SharedKey' variable in variables.tf under the AWS folder"
  # Don't go crazy with complexity. AWS side can support only 
  # between 8 and 64 characters in length and cannot start with zero (0). 
  # Allowed characters are alphanumeric characters, periods (.), and underscores (_).
}

# Local network gateway AWS Tunnel 1 to Azure Instance 0
# lngw_01
variable "lng_AWST1A0" {
  type        = string
  default     = "lng_AWSTunnel1ToAzureInstance0"
  description = "From AWS Tunnel 1 to Azure Instance 0"
}

###
### REPLACE FROM THE OUTPUT OF AWS IN STEP 2
###
variable "ip_AWST1A0" {
  type        = string
  default     = "13.58.80.164"
  description = "Enter the output of ip_AWST1IP1 from the AWS output.tf script"
}

variable "ip_peering_AWST1A0" {
  type        = string
  default     = "169.254.21.1"
  description = "BGP Peering IP for AWS Tunnel1 to Azure Instance 0"
}

variable "ip_primary_AWST1A0" {
  type        = string
  default     = "169.254.21.2"
  description = "Primary Custom BGP Address for AWS Tunnel1 to Azure Instance 0"
}

variable "ip_secondary_AWST1A0" {
  type        = string
  default     = "169.254.21.6"
  description = "Secondary Custom BGP Address for AWS Tunnel1 to Azure Instance 0"
}

variable "conn_AWST1A0" {
  type        = string
  default     = "connAWSTunnel1toAzureInstance0"
  description = "Connection name for AWS Tunnel1 to Azure Instance 0"
}

# Local network gateway AWS Tunnel 2 to Azure Instance 0
# lngw_02
variable "lng_AWST2A0" {
  type        = string
  default     = "lng_AWSTunnel2ToAzureInstance0"
  description = "From AWS Tunnel 2 to Azure Instance 0"
}

###
### REPLACE FROM THE OUTPUT OF AWS IN STEP 2
###
variable "ip_AWST2A0" {
  type        = string
  default     = "18.119.84.239"
  description = "Enter the output of ip_AWST2A0 from the AWS output.tf script"
}

variable "ip_peering_AWST2A0" {
  type        = string
  default     = "169.254.22.1"
  description = "BGP Peering IP for AWS Tunnel2 to Azure Instance 0"
}

variable "ip_primary_AWST2A0" {
  type        = string
  default     = "169.254.22.2"
  description = "Primary Custom BGP Address for AWS Tunnel2 to Azure Instance 0"
}

variable "ip_secondary_AWST2A0" {
  type        = string
  default     = "169.254.21.6"
  description = "Secondary Custom BGP Address for AWS Tunnel1 to Azure Instance 1"
}

variable "conn_AWST2A0" {
  type        = string
  default     = "connAWSTunnel2toAzureInstance0"
  description = "Connection name for AWS Tunnel2 to Azure Instance 0"
}

# Local network gateway AWS Tunnel 1 to Azure Instance 1
# lngw_03
variable "lng_AWST1A1" {
  type        = string
  default     = "lng_AWSTunnel1ToAzureInstance1"
  description = "From AWS Tunnel 1 to Azure Instance 1"
}

###
### REPLACE FROM THE OUTPUT OF AWS IN STEP 2
###
variable "ip_AWST1A1" {
  type        = string
  default     = "18.119.2.80"
  description = "Enter the output of ip_AWST1A1 from the AWS output.tf script"
}

variable "ip_peering_AWST1A1" {
  type        = string
  default     = "169.254.21.5"
  description = "BGP Peering IP for AWS Tunnel1 to Azure Instance 1"
}

variable "ip_primary_AWST1A1" {
  type        = string
  default     = "169.254.21.2"
  description = "Primary Custom BGP Address for AWS Tunnel1 to Azure Instance 1"
}

variable "ip_secondary_AWST1A1" {
  type        = string
  default     = "169.254.21.6"
  description = "Secondary Custom BGP Address for AWS Tunnel1 to Azure Instance 1"
}

variable "conn_AWST1A1" {
  type        = string
  default     = "connAWSTunnel1toAzureInstance1"
  description = "Connection name for AWS Tunnel1 to Azure Instance 1"
}

# Local network gateway AWS Tunnel 2 to Azure Instance 1
# lngw_04
variable "lng_AWST2A1" {
  type        = string
  default     = "lng_AWSTunnel2ToAzureInstance1"
  description = "From AWS Tunnel 2 to Azure Instance 1"
}

###
### REPLACE FROM THE OUTPUT OF AWS IN STEP 2
###
variable "ip_AWST2A1" {
  type        = string
  default     = "18.220.73.188"
  description = "Enter the output of ip_AWST2A1 from the AWS ouput.tf script"
}

variable "ip_peering_AWST2A1" {
  type        = string
  default     = "169.254.22.5"
  description = "BGP Peering IP for AWS Tunnel2 to Azure Instance 1"
}

variable "ip_primary_AWST2A1" {
  type        = string
  default     = "169.254.21.2"
  description = "Primary Custom BGP Address for AWS Tunnel2 to Azure Instance 1"
}

variable "ip_secondary_AWST2A1" {
  type        = string
  default     = "169.254.22.6"
  description = "Secondary Custom BGP Address for AWS Tunnel2 to Azure Instance 1"
}

variable "conn_AWST2A1" {
  type        = string
  default     = "connAWSTunnel2toAzureInstance1"
  description = "Connection name for AWS Tunnel2 to Azure Instance 1"
}

outputs.tf

output "vgw_publicip_1" {
  value       = module.vnetgw.public_ip_addresses.ip_config_01.ip_address
  description = "Use this output to populate the same variable in AWS variables.tf file."
}

output "vgw_publicip_2" {
  value       = module.vnetgw.public_ip_addresses.ip_config_02.ip_address
  description = "Use this output to populate the same variable in AWS variables.tf file."
}

Once you run the initial terraform apply it will take about 35-40 mins and you’ll get two IPs as output. You’ll need these momentarily in the file variables.tf under 02.AWS folder.

vgw_publicip_1 = "48.211.240.129"
vgw_publicip_2 = "48.211.240.186"

AWS

In AWS we’ll provision a private and a public subnet as well as two VPN tunnels. This is the Terraform code for that.
provider.tf

terraform {
  required_version = "~>1.9.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~>5.57.0"
    }
  }
}

provider "aws" {
  region = var.rgn_RegionLocation
}

aws.tf

# Create VPC
module "vpc" {

  source = "../../modules/aws/vpc-5.5.1"

  name = var.vpc_Name
  cidr = var.vpc_CIDR

  azs             = var.vpc_AZs
  private_subnets = var.sub_pvtCIDR
  public_subnets  = var.sub_pubCIDR

  enable_nat_gateway = true
  enable_vpn_gateway = true
  amazon_side_asn    = var.asn_AWS
}

# Change the route to enable propagation to public route table
# resource "aws_vpn_gateway_route_propagation" "rtPropg" {
#   vpn_gateway_id = module.vpc.vgw_id
#   route_table_id = module.vpc.public_route_table_ids[0]
# }

# Change the route to enable propagation to private route table
resource "aws_vpn_gateway_route_propagation" "rtPvtPropg" {
  vpn_gateway_id = module.vpc.vgw_id
  route_table_id = module.vpc.private_route_table_ids[0]
}

# Add the NAT gateway as a route to Internet to the private route table
# resource "aws_route" "r" {
#   route_table_id         = module.vpc.private_route_table_ids[0]
#   destination_cidr_block = "0.0.0.0/0"
#   nat_gateway_id         = module.vpc.natgw_ids[0]
# }

# Create 2 customer gateways
module "cgw" {
  source = "../../modules/aws/cgw-2.0.1/"

  name = var.cgw_Name

  customer_gateways = {
    IP1 = {
      bgp_asn    = var.asn_Azure
      ip_address = var.vgw_publicip_1
    },
    IP2 = {
      bgp_asn    = var.asn_Azure
      ip_address = var.vgw_publicip_2
    }
  }
}

module "vpngw1" {
  source = "../../modules/aws/vpngw-3.7.2/"

  customer_gateway_id               = module.cgw.ids[0]
  vpc_id                            = module.vpc.vpc_id
  vpn_gateway_id                    = module.vpc.vgw_id
  vpn_connection_static_routes_only = false
  local_ipv4_network_cidr           = "0.0.0.0/0"

  tunnel1_inside_cidr   = var.ip_AWST1A0
  tunnel2_inside_cidr   = var.ip_AWST2A0
  tunnel1_preshared_key = var.key_SharedKey
  tunnel2_preshared_key = var.key_SharedKey

  tags = {
    Name = var.vpn_AWSA0Name
  }

  depends_on = [module.cgw]

}

module "vpngw2" {
  source = "../../modules/aws/vpngw-3.7.2/"

  customer_gateway_id               = module.cgw.ids[1]
  vpc_id                            = module.vpc.vpc_id
  vpn_gateway_id                    = module.vpc.vgw_id
  vpn_connection_static_routes_only = false
  local_ipv4_network_cidr           = "0.0.0.0/0"

  tunnel1_inside_cidr   = var.ip_AWST1A1
  tunnel2_inside_cidr   = var.ip_AWST2A1
  tunnel1_preshared_key = var.key_SharedKey
  tunnel2_preshared_key = var.key_SharedKey

  tags = {
    Name = var.vpn_AWSA1Name
  }

  depends_on = [module.cgw]

}

variables.tf – replace the IPs in line 50 and 59 from the previous Azure output

# VPC
variable "rgn_RegionLocation" {
  type        = string
  default     = "us-east-2"
  description = "AWS region"
}

variable "vpc_Name" {
  type        = string
  default     = "vpcVPN"
  description = "Name of the VPC in AWS"
}

variable "vpc_CIDR" {
  type        = string
  default     = "10.2.0.0/16"
  description = "CIDR of the VPC in AWS"
}

variable "vpc_AZs" {
  type        = list(string)
  default     = ["us-east-2a"]
  description = "Location of available zones"
}

variable "sub_pubCIDR" {
  type        = list(string)
  default     = ["10.2.0.0/24"]
  description = "Public subnet CIDR"
}

variable "sub_pvtCIDR" {
  type        = list(string)
  default     = ["10.2.1.0/24"]
  description = "Private subnet CIDR"
}

# Customer gateways
variable "cgw_Name" {
  type        = string
  default     = "cgwVPN"
  description = "Name of the Customer Gateway"
}

####
#### REPLACE WITH OUTPUT FROM AZURE
####
variable "vgw_publicip_1" {
  type        = string
  default     = "20.10.220.96"
  description = "Use the output from previous Azure script. First public IP of the Virtual Network Gateway in Azure"
}

####
#### REPLACE WITH OUTPUT FROM AZURE
####
variable "vgw_publicip_2" {
  type        = string
  default     = "4.152.124.252"
  description = "Use the output from previous Azure script. Second public IP of the Virtual Network Gateway in Azure"
}

variable "asn_AWS" {
  type        = string
  default     = "64512"
  description = "AWS ASN"
}

variable "asn_Azure" {
  type        = string
  default     = "65000"
  description = "Azure ASN"
}

# VPN site to site 1
variable "key_SharedKey" {
  type        = string
  default     = "KlimentAndreev1970_"
  description = "Make sure it's the same as vartiables.tf in  Azure folder"
  # Don't go crazy with complexity. AWS side can support only 
  # between 8 and 64 characters in length and cannot start with zero (0). 
  # Allowed characters are alphanumeric characters, periods (.), and underscores (_).

}

variable "vpn_AWSA0Name" {
  type        = string
  default     = "vpnToAzureInstance0"
  description = "AWS Tunnel to Azure Instance 0 name"
}

variable "ip_AWST1A0" {
  type    = string
  default = "169.254.21.0/30"
}

variable "ip_AWST2A0" {
  type    = string
  default = "169.254.22.0/30"
}

# VPN site to site 2
variable "vpn_AWSA1Name" {
  type        = string
  default     = "vpnToAzureInstance1"
  description = "AWS Tunnel to Azure Instance 1 name"
}

variable "ip_AWST1A1" {
  type    = string
  default = "169.254.21.4/30"
}

variable "ip_AWST2A1" {
  type    = string
  default = "169.254.22.4/30"
}

outputs.tf

output "ip_AWST1A0" {
  value = module.vpngw1.vpn_connection_tunnel1_address
}

output "ip_AWST2A0" {
  value = module.vpngw1.vpn_connection_tunnel2_address
}

output "ip_AWST1A1" {
  value = module.vpngw2.vpn_connection_tunnel1_address
}

output "ip_AWST2A1" {
  value = module.vpngw2.vpn_connection_tunnel2_address
}

Once you run terraform apply you’ll get 4 IPs as an output.

ip_AWST1A0 = "3.137.10.4"
ip_AWST1A1 = "3.20.47.74"
ip_AWST2A0 = "18.220.233.198"
ip_AWST2A1 = "18.190.37.23"

You have to go back and replace these 4 IPs in variables.tf in the previous Azure folder. So, you run terraform apply in 01.Azure folder first, then 02.AWS second and then again terraform apply in 01.Azure.

Azure

Edit variables.tf file and add the 4 IPs. Make sure that the variables match, do not just assign IPs randomly. Replace the IPs in the lines 155, 196, 237 and 278. The second time you run terraform apply it will take about a minute or two.

Verification

Now, go to AWS and go to VPC | VPN | Site-to-Site VPN connections. You’ll see two tunnels there. For each one select the tunnel and then click on the Tunnel details tab. Both should be up for each tunnel.

In Azure, go to Virtual Network Gateways, click on the vgw, then Monitoring and BGP peers.
You should see something like this.

Related Articles

Leave a Comment

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More