Hybrid Connectivity: Site-to-Site VPN with BGP Routing
Scenario
Your organisation has an on-premises data center in London with a Cisco ASA firewall. Internal teams need access to cloud resources in AWS using private IP addresses, and the cloud ECS workloads need to call on-prem APIs. You need a Site-to-Site VPN connection between your on-prem network and the AWS VPC, with BGP dynamic routing so that route changes on either side propagate automatically — no static route maintenance.
- Create a aws_vpn_gateway (Virtual Private Gateway) and attach it to the VPC.
- Create a aws_customer_gateway with your on-prem firewall's public IP and BGP ASN.
- Create an aws_vpn_connection with dynamic routing (BGP). Configure tunnel options: IKE version 2, DH group 14, AES-256 encryption.
- Enable aws_vpn_gateway_route_propagation on the VPC's private route tables so BGP-learned on-prem routes are automatically added.
- Output the VPN connection ID, both tunnel outside IP addresses (to configure on the Cisco ASA), and both tunnel pre-shared keys.
Additional Context
AWS creates two VPN tunnels per connection for redundancy — each terminates on a different AWS endpoint in different AZs. During normal operation, traffic uses one tunnel; if it fails, it fails over to the second. Both tunnels should be kept operational on your on-prem device. Configure BGP on both tunnels for automatic failover.
Route Propagation: With dynamic routing enabled, the Virtual Private Gateway advertises on-prem BGP routes into the VPC route table automatically (no manual aws_route resources needed). You still need aws_vpn_gateway_route_propagation to authorise propagation to each specific route table. Static routing (routing = "static") requires manually specifying static_routes_only = true and adding each on-prem CIDR as an aws_vpn_connection_route.
variables.tf
variable "aws_region" { type = string; default = "eu-west-1" }
variable "vpc_id" { type = string }
variable "private_route_table_ids" { type = list(string); description = "Route tables to advertise BGP routes into." }
variable "on_prem_public_ip" { type = string; description = "Public IP of the on-premises firewall." }
variable "on_prem_bgp_asn" { type = number; default = 65000; description = "On-prem customer BGP ASN (private range: 64512-65534)." }
variable "environment" { type = string; default = "prod" }main.tf
provider "aws" { region = var.aws_region }
locals { tags = { Environment = var.environment, ManagedBy = "terraform" } }
# Virtual Private Gateway — the AWS-side VPN concentrator attached to the VPC.
resource "aws_vpn_gateway" "main" {
vpc_id = var.vpc_id
amazon_side_asn = 64512 # AWS BGP ASN for this VGW (private range)
tags = merge(local.tags, { Name = "vgw-${var.environment}" })
}
# Customer Gateway — represents your on-premises device.
resource "aws_customer_gateway" "onprem" {
bgp_asn = var.on_prem_bgp_asn # your on-prem router's BGP ASN
ip_address = var.on_prem_public_ip # public IP of your on-prem firewall
type = "ipsec.1"
tags = merge(local.tags, { Name = "cgw-onprem-${var.environment}" })
}
# Site-to-Site VPN Connection with dynamic BGP routing.
resource "aws_vpn_connection" "onprem" {
vpn_gateway_id = aws_vpn_gateway.main.id
customer_gateway_id = aws_customer_gateway.onprem.id
type = "ipsec.1"
# Dynamic routing — BGP advertises routes automatically from both sides.
static_routes_only = false
# Tunnel 1 configuration — hardened cryptographic settings.
tunnel1_ike_versions = ["ikev2"]
tunnel1_phase1_encryption_algorithms = ["AES256"]
tunnel1_phase2_encryption_algorithms = ["AES256"]
tunnel1_phase1_integrity_algorithms = ["SHA2-256"]
tunnel1_phase2_integrity_algorithms = ["SHA2-256"]
tunnel1_phase1_dh_group_numbers = [14]
tunnel1_phase2_dh_group_numbers = [14]
tunnel1_startup_action = "start" # initiate from AWS side
# Tunnel 2 — same config for redundancy.
tunnel2_ike_versions = ["ikev2"]
tunnel2_phase1_encryption_algorithms = ["AES256"]
tunnel2_phase2_encryption_algorithms = ["AES256"]
tunnel2_phase1_integrity_algorithms = ["SHA2-256"]
tunnel2_phase2_integrity_algorithms = ["SHA2-256"]
tunnel2_phase1_dh_group_numbers = [14]
tunnel2_phase2_dh_group_numbers = [14]
tunnel2_startup_action = "start"
tags = merge(local.tags, { Name = "vpn-to-onprem-${var.environment}" })
}
# Route Propagation — BGP-learned on-prem routes automatically appear in these route tables.
resource "aws_vpn_gateway_route_propagation" "private" {
count = length(var.private_route_table_ids)
vpn_gateway_id = aws_vpn_gateway.main.id
route_table_id = var.private_route_table_ids[count.index]
depends_on = [aws_vpn_connection.onprem]
}
# CloudWatch alarm for tunnel state monitoring.
resource "aws_cloudwatch_metric_alarm" "tunnel1_down" {
alarm_name = "vpn-tunnel1-down-${var.environment}"
comparison_operator = "LessThanThreshold"
evaluation_periods = 3
metric_name = "TunnelState"
namespace = "AWS/VPN"
period = 60
statistic = "Maximum"
threshold = 1
alarm_description = "VPN Tunnel 1 is down"
dimensions = { VpnId = aws_vpn_connection.onprem.id; TunnelIpAddress = aws_vpn_connection.onprem.tunnel1_address }
tags = local.tags
}outputs.tf
output "vpn_connection_id" { value = aws_vpn_connection.onprem.id }
output "tunnel1_outside_ip" { value = aws_vpn_connection.onprem.tunnel1_address }
output "tunnel2_outside_ip" { value = aws_vpn_connection.onprem.tunnel2_address }
output "tunnel1_psk" { value = aws_vpn_connection.onprem.tunnel1_preshared_key; sensitive = true }
output "tunnel2_psk" { value = aws_vpn_connection.onprem.tunnel2_preshared_key; sensitive = true }
output "tunnel1_cgw_inside_ip" { value = aws_vpn_connection.onprem.tunnel1_cgw_inside_address }
output "tunnel1_vgw_inside_ip" { value = aws_vpn_connection.onprem.tunnel1_vgw_inside_address }Workflow Commands
terraform init
terraform apply \
-var="vpc_id=vpc-xxxx" \
-var="on_prem_public_ip=203.0.113.1" \
-var="on_prem_bgp_asn=65000" \
-var='private_route_table_ids=["rtb-aaa","rtb-bbb"]'
# Get tunnel configuration details for your on-prem firewall (use sensitive outputs)
terraform output tunnel1_outside_ip # Configure as IKE peer on Cisco ASA
terraform output -raw tunnel1_psk # Pre-shared key for IKE authentication
terraform output tunnel1_cgw_inside_ip # BGP peer IP on your side (/30 subnet)
terraform output tunnel1_vgw_inside_ip # BGP peer IP on AWS side
# Download full VPN device configuration from AWS
aws ec2 describe-vpn-connections \
--vpn-connection-ids $(terraform output -raw vpn_connection_id) \
--query 'VpnConnections[0].CustomerGatewayConfiguration' \
--output text > vpn-config.xml
# Monitor tunnel status
aws ec2 describe-vpn-connections \
--vpn-connection-ids $(terraform output -raw vpn_connection_id) \
--query 'VpnConnections[0].VgwTelemetry[*].{Status:Status,OutsideIp:OutsideIpAddress}'
terraform destroy -var="vpc_id=..." -var="on_prem_public_ip=..." -var='private_route_table_ids=["..."]'Apply complete! Resources: 5 added, 0 changed, 0 destroyed. Outputs: tunnel1_cgw_inside_ip = "169.254.10.2" (your on-prem BGP peer address) tunnel1_outside_ip = "52.48.xxx.xxx" (configure as IKE peer on Cisco ASA) tunnel1_psk = <sensitive> tunnel1_vgw_inside_ip = "169.254.10.1" (AWS BGP peer address) tunnel2_outside_ip = "52.49.yyy.yyy" tunnel2_psk = <sensitive> vpn_connection_id = "vpn-0xxxxxxxxxxxxxxx"

