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.