- https://techcommunity.microsoft.com/blog/startupsatmicrosoftblog/how-to-easily-set-up-a-vpn-between-azure-and-aws-using-managed-services-updated-/4278966
AWS¶
AWS portal¶
todo
AWS command line¶
VPC¶
vpc_id=$( aws ec2 create-vpc --cidr-block 10.0.0.0/16 --query 'Vpc.VpcId' --output text )
aws ec2 create-tags --resource "${vpc_id}" --tags Key=Name,Value=vpc-AzureAWSVPN
Subnet¶
subnet_id=$( aws ec2 create-subnet --vpc-id "${vpc_id}" --cidr-block 10.0.1.0/24 --availability-zone ca-central-1 --query 'Subnet.SubnetId' --output text )
aws ec2 create-tags --resource "${subnet_id}" --tags Key=Name,Value=subnet-AzureAWSVPN
Virtual private gateway (VGW)¶
vgw_id=$( aws ec2 create-vpn-gateway --type ipsec.1 --amazon-side-asn 64512 )
aws ec2 create-tags --resource "${vgw_id}" --tags Key=Name,Value=vgw-AzureAWSVPN
Attach vgw to vpc¶
aws ec2 attach-vpn-gatway --vpn-gateway-id $vgw_id --vpc-id $vpc_id
Customer gateway (CGW)¶
cgw_id=$( aws ec2 create-customer-gateway --type ipsec.1 --public-ip [ azure_gw_ip ] --bgp-asn 65000)
aws ec2 create-tags --resource "${cgw_id}" --tags Key=Name,Value=cgw-AzureAWSVPN
Site-to-site VPN connection¶
vpn_id=$( aws ec2 create-vpn-connection --type ipsec.1 --customer-gateway-id ="${cgw_id}" --vpn-gateway-id "${vgw_id}" --options StaticRoutesOnly=true
aws ec2 create-tags --resource "${vpn_id}" --tags Key=Name,Value=vpn-AzureAWSVPN
Enable route propagation¶
todo
Add routes for the vpn¶
todo
Create an Internet Gateway¶
todo
Attach the igw to the vpc¶
todo
Create a route to the igw¶
todo
Create a security group¶
To allow SSH & ICMP
todo
Create a VM in the VPC¶
todo
Azure¶
- Azure VNet peering overview
- Create VNet peering (Portal)
- Create VNet peering (CLI)
- Create VNet peering (PowerShell)
Azure portal¶
Create the VPN gateway¶
- Hybrid connectivity => VPN gateways => Create
- Project details:
- Subscription: auto fills
- Instance details:
Name: aws
Region: Canada Central
Gateway type: VPN
SKU: VpnGw2AZ
Generation: Generation2
Virtual network => Create virtual network
Name: vnet-aws
Resource group => Create new
Name: rg-aws-vpn-gw
Address space: leave as-is
Subnets:
default: leave as-is
Add a new one:
Name: aws
Address range: 10.0.200.0/24 => Ok - Public IP address:
Public IP address: Create new
Name: pubip-aws-vpn-gw
Enable active-active mode: Disabled
Configure BGP: Disabled - Review + create => Create
Create the vnet¶
- Project details:
- Subscription: auto fills
- Resource group => Create new => Name: rg-aws-vpn-gw
-
Instance details:
- Virtual network name: vnet-aws-vpn-gw
- Region: Canada Central (ca-central-1)
-
Leave everything else as-is => Review + create => Create
Create the Local network gateway¶
Create the VPN connection¶
Azure command line¶
Resource group¶
rg_name="RG-AzureAWSVPN"
vnet_name="VNet-AzureAWSVPN"
ip_name="IP-AzureAWSVPN"
gw_name="GW-AzureAWSVPN"
az group create --name $rg_name --location canadacentral
VNet¶
az network vnet create --name $vnet_name --resource-group $rg_name --address-prefix 172.16.0.0/16 --subnet-name Subnet-AzureAWSVPN --subnet-prefix 172.16.1.0/24
Gateway subnet¶
az network vnet subnet create --resource-group $rg_name --vnet-name $vnet_name --name GatewaySubnet --address-prefixes 172.16.254.0/27
Public IP¶
az network public-ip create --name $ip_name --resource-group $rg_name --allocation-method Static
VPN Gateway¶
az network vnet-gateway create --name $gw_name --resource-group $rg_name --public-ip-address $ip_name --vnet $vnet_name --gateway-type Vpn --vpn-type RouteBased --sku VpnGw1
this may take awhile, start the AWS stuff while the wheels are turning here
Create the local network gateway¶
todo
Terraform to do it all¶
GitLab Terraform repository incoming in the near future
But for now:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.110"
}
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
required_version = ">= 1.6.0"
}
# -------------------------------
# Providers
# -------------------------------
provider "azurerm" {
features {}
subscription_id = var.azure_subscription_id
}
provider "aws" {
region = var.aws_region
}
# -------------------------------
# Azure side
# -------------------------------
# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group
resource "azurerm_resource_group" "azure" {
name = "RG-AzureAWSVPN"
location = var.azure_location
}
# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network
resource "azurerm_virtual_network" "azure" {
name = "VNet-AzureAWSVPN"
address_space = ["172.16.0.0/16"]
location = azurerm_resource_group.azure.location
resource_group_name = azurerm_resource_group.azure.name
}
# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet
resource "azurerm_subnet" "internal" {
name = "Subnet-AzureAWSVPN"
resource_group_name = azurerm_resource_group.azure.name
virtual_network_name = azurerm_virtual_network.azure.name
address_prefixes = ["172.16.1.0/24"]
}
# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet
resource "azurerm_subnet" "gateway" {
name = "GatewaySubnet"
resource_group_name = azurerm_resource_group.azure.name
virtual_network_name = azurerm_virtual_network.azure.name
address_prefixes = ["172.16.254.0/27"]
}
# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/public_ip
resource "azurerm_public_ip" "vpn" {
name = "IP-AzureAWSVPN"
location = azurerm_resource_group.azure.location
resource_group_name = azurerm_resource_group.azure.name
allocation_method = "Static"
sku = "Standard"
}
# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network_gateway
resource "azurerm_virtual_network_gateway" "vpn" {
name = "GW-AzureAWSVPN"
location = azurerm_resource_group.azure.location
resource_group_name = azurerm_resource_group.azure.name
type = "Vpn"
vpn_type = "RouteBased"
active_active = false
enable_bgp = false
sku = "VpnGw2"
ip_configuration {
name = "gw-ipconfig"
public_ip_address_id = azurerm_public_ip.vpn.id
subnet_id = azurerm_subnet.gateway.id
}
}
# AWS will create its gateway IP; Azure needs that for the LNG
# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/local_network_gateway
resource "azurerm_local_network_gateway" "aws" {
name = "LNG-AzureAWSVPN"
location = azurerm_resource_group.azure.location
resource_group_name = azurerm_resource_group.azure.name
#gateway_address = aws_vpn_gateway.aws_vgw.id != "" ? aws_vpn_gateway.aws_vgw.tags["PublicIp"] : null
# We'll override this below properly
#gateway_address = aws_vpn_connection.aws.connection_static_ip
gateway_address = aws_vpn_connection.aws.tunnel1_address
#address_space = ["10.0.1.0/24"]
address_space = ["10.0.0.0/16"]
}
# VPN connection (Azure → AWS)
# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network_gateway_connection
resource "azurerm_virtual_network_gateway_connection" "vpn" {
name = "CON-AzureAWSVPN"
location = azurerm_resource_group.azure.location
resource_group_name = azurerm_resource_group.azure.name
type = "IPsec"
virtual_network_gateway_id = azurerm_virtual_network_gateway.vpn.id
local_network_gateway_id = azurerm_local_network_gateway.aws.id
shared_key = var.shared_key
}
# Security group allowing SSH + ICMP
resource "azurerm_network_security_group" "nsg" {
name = "NSG-AzureAWSVPN"
location = azurerm_resource_group.azure.location
resource_group_name = azurerm_resource_group.azure.name
security_rule {
name = "AllowSSHFromAnywhere"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
}
security_rule {
name = "AllowICMPInternal"
priority = 200
direction = "Inbound"
access = "Allow"
protocol = "Icmp"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "172.16.1.0/24"
destination_address_prefix = "172.16.1.0/24"
}
}
# -------------------------------
# AWS side
# -------------------------------
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc
resource "aws_vpc" "aws" {
cidr_block = "10.0.0.0/16"
tags = { Name = "VPC-AzureAWSVPN" }
}
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet
resource "aws_subnet" "aws" {
vpc_id = aws_vpc.aws.id
cidr_block = "10.0.1.0/24"
availability_zone = "${var.aws_region}a"
tags = { Name = "Subnet-AzureAWSVPN" }
}
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpn_gateway
resource "aws_vpn_gateway" "aws_vgw" {
vpc_id = aws_vpc.aws.id
amazon_side_asn = 64512
tags = { Name = "VGW-AzureAWSVPN" }
}
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpn_gateway_attachment
resource "aws_vpn_gateway_attachment" "attach" {
vpc_id = aws_vpc.aws.id
vpn_gateway_id = aws_vpn_gateway.aws_vgw.id
}
# Customer gateway (uses Azure VPN gateway public IP)
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/customer_gateway
resource "aws_customer_gateway" "azure_cgw" {
bgp_asn = 65000
ip_address = azurerm_public_ip.vpn.ip_address
type = "ipsec.1"
tags = { Name = "CGW-AzureAWSVPN" }
}
# VPN connection (AWS → Azure)
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpn_connection
resource "aws_vpn_connection" "aws" {
customer_gateway_id = aws_customer_gateway.azure_cgw.id
#customer_gateway_id = aws_customer_gateway.aws_cgw.id
vpn_gateway_id = aws_vpn_gateway.aws_vgw.id
type = "ipsec.1"
tunnel1_preshared_key = var.shared_key
tunnel2_preshared_key = var.shared_key
static_routes_only = true
# Deprecated apparently
#Provider Version Inline routes block aws_vpn_connection_route resource
#≤ v4.0 ❌ Not supported ✅ Required (only method)
#4.1 – 4.63 ✅ Supported ✅ Still supported (both worked)
#≥ v5.0 (current) ❌ Removed (read-only) ✅ Only supported method
#routes = [
# {
# destination_cidr_block = "172.16.1.0/24"
# source = "static"
# state = "active"
# }
#]
tags = { Name = "AWS-VPN-To-Azure" }
}
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpn_connection_route
# Use this instead of the inline routes parameter for vpn_connection apparently
resource "aws_vpn_connection_route" "azure_subnet" {
vpn_connection_id = aws_vpn_connection.aws.id
destination_cidr_block = "172.16.1.0/24"
}
# Enable route propagation
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpn_gateway_route_propagation
resource "aws_vpn_gateway_route_propagation" "prop" {
route_table_id = aws_vpc.aws.main_route_table_id
vpn_gateway_id = aws_vpn_gateway.aws_vgw.id
}
# -------------------------------
# Example Azure VM (depends implicitly on network resources)
# -------------------------------
resource "azurerm_public_ip" "vm_ip" {
name = "vm-public-ip"
location = azurerm_resource_group.azure.location
resource_group_name = azurerm_resource_group.azure.name
allocation_method = "Static"
sku = "Standard"
}
resource "azurerm_network_interface" "vmnic" {
name = "nic-azureaws"
location = azurerm_resource_group.azure.location
resource_group_name = azurerm_resource_group.azure.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.internal.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.vm_ip.id
}
}
resource "azurerm_linux_virtual_machine" "vm" {
name = "VM-AzureAWSVPN"
resource_group_name = azurerm_resource_group.azure.name
location = azurerm_resource_group.azure.location
size = "Standard_B1s"
admin_username = "azureuser"
network_interface_ids = [azurerm_network_interface.vmnic.id]
admin_ssh_key {
username = "azureuser"
public_key = file("~/.ssh/id_rsa.pub")
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
}
# Associate NSG with subnet
resource "azurerm_subnet_network_security_group_association" "assoc" {
subnet_id = azurerm_subnet.internal.id
network_security_group_id = azurerm_network_security_group.nsg.id
}
# ---------------------------------------
# AWS Internet Gateway + Route Table
# ---------------------------------------
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.aws.id
tags = { Name = "IGW-AzureAWSVPN" }
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.aws.id
tags = { Name = "RT-Public-AzureAWSVPN" }
}
# Default route through Internet Gateway
resource "aws_route" "default_internet_access" {
route_table_id = aws_route_table.public.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
# Associate route table with subnet
resource "aws_route_table_association" "public_assoc" {
subnet_id = aws_subnet.aws.id
route_table_id = aws_route_table.public.id
}
# Enable VPN route propagation as well
resource "aws_vpn_gateway_route_propagation" "prop_public" {
route_table_id = aws_route_table.public.id
vpn_gateway_id = aws_vpn_gateway.aws_vgw.id
}
# ---------------------------------------
# AWS Security Group for VM
# ---------------------------------------
resource "aws_security_group" "vm_sg" {
name = "SG-AzureAWSVPN"
description = "Allow SSH from anywhere, ICMP from Azure internal subnet"
vpc_id = aws_vpc.aws.id
ingress {
description = "Allow SSH from anywhere"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "Allow ICMP from Azure subnet"
from_port = -1
to_port = -1
protocol = "icmp"
cidr_blocks = ["172.16.1.0/24"]
}
egress {
description = "Allow all outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "SG-AzureAWSVPN" }
}
# ---------------------------------------
# AWS EC2 Instance
# ---------------------------------------
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance
resource "aws_instance" "vm" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t2.micro"
subnet_id = aws_subnet.aws.id
vpc_security_group_ids = [aws_security_group.vm_sg.id]
associate_public_ip_address = true
key_name = var.aws_key_pair
tags = { Name = "VM-AzureAWSVPN" }
}
# Fetch latest Amazon Linux 2 AMI automatically
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
variable "aws_region" {
type = string
default = "ca-central-1"
}
variable "azure_subscription_id" {
type = string
default = "[ subscription_id ]"
}
variable "azure_location" {
type = string
default = "canadacentral"
}
variable "aws_key_pair" {
type = string
default = "[ keypair_name ]"
}
variable "shared_key" {
type = string
sensitive = true
default = "[ shared_key ]"
}
# ---------------------------------------
# Output: Azure & AWS VPN Connection Info
# ---------------------------------------
output "azure_vpn_gateway_public_ip" {
description = "Public IP address of the Azure VPN Gateway (connect here from AWS)"
value = azurerm_public_ip.vpn.ip_address
}
output "aws_customer_gateway_public_ip" {
description = "Public IP address of the AWS Customer Gateway (connect here from Azure)"
value = aws_vpn_connection.aws.tunnel1_address
}
output "aws_vpn_tunnel1_external_ip" {
description = "AWS tunnel 1 external IP (connects to Azure VPN Gateway)"
value = aws_vpn_connection.aws.tunnel1_address
}
output "aws_vpn_tunnel2_external_ip" {
description = "AWS tunnel 2 external IP (secondary tunnel)"
value = aws_vpn_connection.aws.tunnel2_address
}
output "aws_vpn_tunnel1_inside_cidr" {
description = "AWS tunnel 1 inside CIDR"
value = aws_vpn_connection.aws.tunnel1_inside_cidr
}
output "aws_vpn_tunnel2_inside_cidr" {
description = "AWS tunnel 2 inside CIDR"
value = aws_vpn_connection.aws.tunnel2_inside_cidr
}
output "aws_vpn_connection_shared_key_tunnel1" {
description = "Preshared key for tunnel 1"
value = aws_vpn_connection.aws.tunnel1_preshared_key
sensitive = true
}
output "aws_vpn_connection_shared_key_tunnel2" {
description = "Preshared key for tunnel 2"
value = aws_vpn_connection.aws.tunnel2_preshared_key
sensitive = true
}
output "aws_vpn_static_routes" {
description = "Static routes configured on the AWS VPN Connection"
value = aws_vpn_connection.aws.routes
}
output "azure_to_aws_static_routes" {
description = "Azure local network gateway address space(s)"
value = azurerm_local_network_gateway.aws.address_space
}
# ---------------------------------------
# Optional: Render a simple configuration template for each side
# ---------------------------------------
output "azure_vpn_device_config_template" {
description = "Configuration summary for Azure side (for manual validation)"
value = <<EOT
Azure VPN Gateway: ${azurerm_virtual_network_gateway.vpn.name}
Public IP: ${azurerm_public_ip.vpn.ip_address}
Connects to AWS VPN Gateway:
- AWS Tunnel 1 IP: ${aws_vpn_connection.aws.tunnel1_address}
- AWS Tunnel 2 IP: ${aws_vpn_connection.aws.tunnel2_address}
Shared Key Tunnel 1: ${aws_vpn_connection.aws.tunnel1_preshared_key}
Shared Key Tunnel 2: ${aws_vpn_connection.aws.tunnel2_preshared_key}
Address Spaces:
Azure VNet: 172.16.1.0/24
AWS Subnet: 10.0.1.0/24
EOT
sensitive = true
}
output "aws_vpn_device_config_template" {
description = "Configuration summary for AWS side (for manual validation)"
value = <<EOT
AWS VPN Connection: ${aws_vpn_connection.aws.id}
Tunnels:
- Tunnel 1: ${aws_vpn_connection.aws.tunnel1_address}
- Tunnel 2: ${aws_vpn_connection.aws.tunnel2_address}
Tunnel Inside CIDRs:
- ${aws_vpn_connection.aws.tunnel1_inside_cidr}
- ${aws_vpn_connection.aws.tunnel2_inside_cidr}
Connects to Azure Gateway:
- IP: ${azurerm_public_ip.vpn.ip_address}
Static Route(s): ${join(", ", [for r in aws_vpn_connection.aws.routes : r.destination_cidr_block])}
Shared Key Tunnel 1: ${aws_vpn_connection.aws.tunnel1_preshared_key}
Shared Key Tunnel 2: ${aws_vpn_connection.aws.tunnel2_preshared_key}
EOT
sensitive = true
}
Bicep to do the Azure stuff¶
CloudFormation to do the AWS stuff¶
todo