A
Arun's Blog
← Back to all posts

AWS Site-to-Site VPN with Azure

Multi-CloudVPNTerraformNetworking
TL;DR

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.

AWS to Azure VPN Architecture

Prerequisites

As this is an Infrastructure as Code demonstration, you should have:

  1. Understanding of Terraform
  2. An AWS Account with privileges to administer VPC, EC2, and Site-to-Site VPN
  3. An Azure Subscription with privileges to administer Resource Groups, VNets, and VPN Connections
Important

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

AWS Azure VPN Logical Diagram

Terraform Configuration

Note

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
}
Pro Tip

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

  1. Run Azure Terraform first to create VNG and get its public IP
  2. Update AWS Terraform with Azure VNG public IP in Customer Gateway
  3. Run AWS Terraform to create VPN connection
  4. Note the AWS tunnel1 IP from output
  5. 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 refresh to 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.