A
Arun's Blog
← Back to all posts

AWS Site-to-Site VPN via Terraform

VPNTerraformNetworking
TL;DR

Deploy an AWS Site-to-Site VPN connection using Terraform in minutes. This guide provides complete IaC code to create a Customer Gateway, Virtual Private Gateway, VPN connection with static routing, and a test EC2 instance - all automated for quick hybrid connectivity.

Introduction

Connecting your AWS environment can be accomplished in multiple ways. One can use Direct Connect, which can be expensive and have some lead times associated with it. Others may choose to use the Marketplace and deploy a vendor-specific solution to connect to the same vendor objects at the data center. To spin up a quick and easy connection, nothing beats the Site-to-Site VPN service AWS offers. With that, I've attempted to demonstrate how easily we can set up a Site-to-Site VPN connection in AWS with Terraform.

Prerequisites

As this will be a Infrastructure as Code demonstration, you should have:

  1. An Understanding of Terraform
  2. An AWS Account with the correct privileges to administer a VPC, EC2, and Site to Site VPN Connections and related objects.

Logical Diagram of Final Output

Terraform

The below code will use the default VPC with a pre-determined access key for the IPSec tunnels. We will assume the private on-premise subnet will have a CIDR of 192.168.0.0/16 and the public IP address of the router/firewall on-premise will be 20.163.133.4. We will also create a test EC2 instance in the private subnet we can use to test connectivity from on-premise to AWS.

Important Security Note

Never hardcode pre-shared keys in production Terraform code. Use AWS Secrets Manager or Terraform variables with sensitive flag to manage credentials securely. The example below uses a hardcoded key for demonstration purposes only.

AWS

#use the aws provider and find the default profile in the aws credentials file
provider "aws" {
  region = "us-east-1"
  profile                 = "default"
}

#find the latest AWS Linux AMI
data "aws_ssm_parameter" "linux" {
  name = "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
}

#we will use the default vpc (172.31.0.0/16)
data "aws_vpc" "default" {
    default = true
}

#defining a subnet in the default vpc to place my Linux EC2
data "aws_subnet" "default" {
    vpc_id = data.aws_vpc.default.id
    availability_zone = "us-east-1a"
    default_for_az = true
}

#create a security group to allow all inbound from local AWS CIDR and on-premise CIDR of 192.168.0.0/16; also include all outbound traffic to anywhere
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"]
  }
}

#Linux creation in the default VPC and subnet defined earlier, tied to security group created earlier, and outputting the private IP address of the EC2 after creation
resource "aws_instance" "awslinux01" {
    ami = "${data.aws_ssm_parameter.linux.value}"
    instance_type = "t3.medium"
    tags = {
        Name = "awslinux"
    }
    subnet_id = data.aws_subnet.default.id
    vpc_security_group_ids = [aws_security_group.allowIn.id]

}

output "AWSLinuxPrivateIP" {
    value = aws_instance.awslinux01.private_ip
}

#This IP address will be the public IP of the router/firewall on-premise
resource "aws_customer_gateway" "cgw" {
  bgp_asn    = 65000
  ip_address = "20.163.133.4"
  type       = "ipsec.1"

  tags = {
    Name = "On-Premise Customer Gateway"
  }
}

#Virtual Private Gateway creation and attachment to AWS VPC; Route propagation enabled
resource "aws_vpn_gateway" "vpngw" {
  vpc_id = data.aws_vpc.default.id

  tags = {
    Name = "AWS VGW"
  }
}

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
}

#Creation of site to site VPN in AWS using the AWS Virtual Private Gateway, the Customer Gateway of the on-premise router/firewall, and a predefined pre-shared key for the tunnel
resource "aws_vpn_connection" "vpn" {
  vpn_gateway_id      = aws_vpn_gateway.vpngw.id
  customer_gateway_id = aws_customer_gateway.cgw.id
  type                = "ipsec.1"
  static_routes_only  = true
  tunnel1_preshared_key = "abc123xyz987"
}

#create static route to the on-premise network on the AWS VPN side
resource "aws_vpn_connection_route" "onpremNetwork" {
  destination_cidr_block = "192.168.0.0/16"
  vpn_connection_id      = aws_vpn_connection.vpn.id
}

#output of Tunnel 1 IP address
output "AWStunnel1IP" {
  value = aws_vpn_connection.vpn.tunnel1_address
}

#output of Tunnel 2 IP address
output "AWStunnel2IP" {
  value = aws_vpn_connection.vpn.tunnel2_address
}
Pro Tip

Use route propagation (as shown in the code) to automatically update route tables when VPN routes change. This eliminates manual route management and reduces configuration errors.

Cleaning Up

Once you are done with the above and/or no longer need the resources created, you should either manually delete all resources or via Terraform run terraform destroy (optionally with the -auto-approve to bypass conformational messages).

Troubleshooting

Issue Possible Cause Solution
VPN tunnel shows "Down" status Pre-shared key mismatch or firewall blocking IPSec Verify the pre-shared key matches on both ends. Ensure UDP 500 and 4500 are allowed, and protocol ESP (50) is permitted.
Cannot ping EC2 from on-premise Security group or route table misconfiguration Verify the security group allows ICMP from 192.168.0.0/16. Check that route propagation is enabled and routes are populated.
Terraform apply fails on VPN connection Customer Gateway IP already in use Each Customer Gateway public IP can only be used once per region. Delete existing CGW or use a different public IP.
Routes not appearing in route table Route propagation not enabled Ensure aws_vpn_gateway_route_propagation resource is applied and the VPN connection is in "available" state.
Connection timeout on specific ports Security group too restrictive Review security group ingress rules. Consider allowing specific ports rather than all traffic for production environments.