AWS Site-to-Site VPN with Azure
Connect AWS and Azure using Site-to-Site VPN with Terraform. Create a Virtual Private Gateway in AWS and a Virtual Network Gateway in Azure, then establish an IPsec tunnel between them. Use non-overlapping CIDRs (e.g., AWS 172.31.0.0/16, Azure 192.168.0.0/16) and configure route propagation for seamless connectivity.
Introduction
Silos cannot function in the modern world. Interoperability is crucial for successful businesses and as much as one may want, a truly homogenous environment is hard to come by. In our everyday demands, all of us experience the urgent necessity to quickly establish communication across various technological environments from various companies.
With that, I've attempted to demonstrate how easily we can set up a Site-to-Site VPN connection between AWS and Azure, and even further ease the need of a rinse-and-repeat option with Terraform.

Prerequisites
As this is an Infrastructure as Code demonstration, you should have:
- Understanding of Terraform
- An AWS Account with privileges to administer VPC, EC2, and Site-to-Site VPN
- An Azure Subscription with privileges to administer Resource Groups, VNets, and VPN Connections
Ensure your AWS and Azure network CIDRs do not overlap. In this example, AWS uses 172.31.0.0/16 and Azure uses 192.168.0.0/16. Overlapping CIDRs will cause routing conflicts.
Final Architecture

Terraform Configuration
Because each vendor's Terraform file has dependencies on the other, you must run one first and use its output as input for the other. The Azure VNG public IP is needed for AWS Customer Gateway, and the AWS tunnel IP is needed for Azure Local Network Gateway.
AWS Configuration
# Provider configuration
provider "aws" {
region = "us-east-1"
profile = "default"
}
# Find latest Amazon Linux AMI
data "aws_ssm_parameter" "linux" {
name = "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
}
# Use default VPC (172.31.0.0/16)
data "aws_vpc" "default" {
default = true
}
data "aws_subnet" "default" {
vpc_id = data.aws_vpc.default.id
availability_zone = "us-east-1a"
default_for_az = true
}
# Security group allowing AWS and Azure traffic
resource "aws_security_group" "allowIn" {
name = "allow_inbound"
vpc_id = data.aws_vpc.default.id
ingress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [data.aws_vpc.default.cidr_block, "192.168.0.0/16"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Test EC2 instance
resource "aws_instance" "awslinux01" {
ami = data.aws_ssm_parameter.linux.value
instance_type = "t3.medium"
subnet_id = data.aws_subnet.default.id
vpc_security_group_ids = [aws_security_group.allowIn.id]
tags = { Name = "awslinux" }
}
# Customer Gateway (Azure VNG public IP)
resource "aws_customer_gateway" "azureVNG" {
bgp_asn = 65000
ip_address = "20.163.133.4" # Replace with Azure VNG public IP
type = "ipsec.1"
tags = { Name = "AWS-Virtual Network Gateway" }
}
# Virtual Private Gateway
resource "aws_vpn_gateway" "vpngw" {
vpc_id = data.aws_vpc.default.id
tags = { Name = "vpngw" }
}
resource "aws_vpn_gateway_attachment" "vpngw_attachment" {
vpc_id = data.aws_vpc.default.id
vpn_gateway_id = aws_vpn_gateway.vpngw.id
}
resource "aws_vpn_gateway_route_propagation" "routepropagation" {
vpn_gateway_id = aws_vpn_gateway.vpngw.id
route_table_id = data.aws_vpc.default.main_route_table_id
}
# VPN Connection
resource "aws_vpn_connection" "vpn" {
vpn_gateway_id = aws_vpn_gateway.vpngw.id
customer_gateway_id = aws_customer_gateway.azureVNG.id
type = "ipsec.1"
static_routes_only = true
tunnel1_preshared_key = "abc123xyz987" # Use a secure key!
}
# Static route to Azure
resource "aws_vpn_connection_route" "AzureNetwork" {
destination_cidr_block = "192.168.0.0/16"
vpn_connection_id = aws_vpn_connection.vpn.id
}
output "AWStunnel1IP" {
value = aws_vpn_connection.vpn.tunnel1_address
}
output "AWSLinuxPrivateIP" {
value = aws_instance.awslinux01.private_ip
}
Use a strong, randomly generated pre-shared key in production. Store it securely in AWS Secrets Manager or Azure Key Vault. Never commit secrets to version control.
Azure Configuration
# Provider configuration
provider "azurerm" {
features {
resource_group {
prevent_deletion_if_contains_resources = false
}
}
subscription_id = "12a3b45c-6d78-9012-e3f4-5648g90hi123" # Your subscription
}
# Resource Group
resource "azurerm_resource_group" "myResourceGroup" {
name = "myrg"
location = "East US"
}
# Virtual Network (non-overlapping with AWS)
resource "azurerm_virtual_network" "vnet" {
name = "vnet1"
location = azurerm_resource_group.myResourceGroup.location
resource_group_name = azurerm_resource_group.myResourceGroup.name
address_space = ["192.168.0.0/16"]
tags = { environment = "Testing" }
}
# Private subnet for VMs
resource "azurerm_subnet" "privateSubnet" {
name = "defaultsubnet"
resource_group_name = azurerm_resource_group.myResourceGroup.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["192.168.1.0/24"]
}
# Gateway subnet (required for VNG)
resource "azurerm_subnet" "gatewaysubnet" {
name = "GatewaySubnet" # Must be named exactly this
resource_group_name = azurerm_resource_group.myResourceGroup.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["192.168.10.0/27"]
}
# Public IP for VNG
resource "azurerm_public_ip" "ipVNG" {
name = "vngPubIP"
location = azurerm_resource_group.myResourceGroup.location
resource_group_name = azurerm_resource_group.myResourceGroup.name
allocation_method = "Dynamic"
}
# Virtual Network Gateway
resource "azurerm_virtual_network_gateway" "vng" {
name = "vng"
location = azurerm_resource_group.myResourceGroup.location
resource_group_name = azurerm_resource_group.myResourceGroup.name
type = "Vpn"
vpn_type = "RouteBased"
active_active = false
enable_bgp = false
sku = "VpnGw1"
ip_configuration {
name = "vnetGatewayConfig"
public_ip_address_id = azurerm_public_ip.ipVNG.id
private_ip_address_allocation = "Dynamic"
subnet_id = azurerm_subnet.gatewaysubnet.id
}
}
# Network Security Group
resource "azurerm_network_security_group" "nsg" {
name = "myNetworkSecurityGroup"
location = azurerm_resource_group.myResourceGroup.location
resource_group_name = azurerm_resource_group.myResourceGroup.name
security_rule {
name = "fromAWS"
priority = 1001
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "172.31.0.0/16" # AWS CIDR
destination_address_prefix = "192.168.0.0/16"
}
}
# NIC for test VM
resource "azurerm_network_interface" "nicAzureVM" {
name = "nic01"
location = azurerm_resource_group.myResourceGroup.location
resource_group_name = azurerm_resource_group.myResourceGroup.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.privateSubnet.id
private_ip_address_allocation = "Dynamic"
}
}
resource "azurerm_network_interface_security_group_association" "nicAssoc" {
network_interface_id = azurerm_network_interface.nicAzureVM.id
network_security_group_id = azurerm_network_security_group.nsg.id
}
# Test Windows VM
resource "azurerm_windows_virtual_machine" "azureVM" {
name = "VM01"
resource_group_name = azurerm_resource_group.myResourceGroup.name
location = azurerm_resource_group.myResourceGroup.location
size = "Standard_F2"
admin_username = "adminuser"
admin_password = "someP@ssw0rd" # Use a secure password!
network_interface_ids = [azurerm_network_interface.nicAzureVM.id]
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = "2019-Datacenter"
version = "latest"
}
}
output "toEnterInAWScgw" {
value = azurerm_public_ip.ipVNG.ip_address
}
output "AzureWindowsPrivateIP" {
value = azurerm_windows_virtual_machine.azureVM.private_ip_addresses
}
Deployment Order
- Run Azure Terraform first to create VNG and get its public IP
- Update AWS Terraform with Azure VNG public IP in Customer Gateway
- Run AWS Terraform to create VPN connection
- Note the AWS tunnel1 IP from output
- Create Azure Local Network Gateway and Connection (add to Azure Terraform)
Cleanup
When resources are no longer needed, run terraform destroy (optionally with -auto-approve) to remove all created resources and avoid ongoing charges.
Troubleshooting
- VPN tunnel shows DOWN - Verify pre-shared keys match exactly on both sides. Check that the Customer Gateway IP matches the Azure VNG public IP.
- Azure VNG public IP shows empty - Dynamic allocation means the IP is assigned after VNG creation. Apply Terraform, then run
terraform refreshto see the IP. - Can't ping between AWS and Azure VMs - Check security groups on both sides allow ICMP. Verify route tables have correct routes to the peer network.
- VNG creation takes too long - Azure VNG creation typically takes 30-45 minutes. This is normal behavior.
- Routing issues - Ensure AWS route propagation is enabled. On Azure, verify the VNG is RouteBased type.
- Overlapping CIDRs error - Change one side's CIDR range. VPN cannot route between overlapping address spaces.