LAB48_AWS_Hybrid Connectivity: Site-to-Site VPN with BGP Routing

Hybrid Connectivity: Site-to-Site VPN with BGP Routing

☁ AWS 🔥 Advanced Site-to-Site VPN Virtual Private Gateway Customer Gateway BGP Hybrid Cloud

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.

Your Objectives
  • 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=["..."]'
✓ Expected Output After Apply
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"