IaC AWS

Terraform Module: AWS Site-to-Site VPN — production-grade IPsec tunnels with BGP failover in one call

Quick take — Reusable Terraform module for AWS Site-to-Site VPN: provision aws_vpn_connection with dynamic BGP routing, dual-tunnel options, IKEv2, and CloudWatch tunnel logging without hand-wiring every IPsec knob. New here? Jump to the Quickstart below to deploy it in minutes; read on for how it works and when to reach for it.

Quickstart (copy-paste)

Minimal, runnable configuration — drop this in a .tf file and fill in the "..." placeholders (each required input is commented):

provider "aws" {
  region = "us-east-1"
}

module "vpn" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-vpn?ref=v1.0.0"

  name = "..."  # Base name used for tagging the VPN connection and custo…
}

Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.

What this module is

AWS Site-to-Site VPN establishes encrypted IPsec tunnels between your on-premises (or another cloud’s) network and AWS. A single aws_vpn_connection always provisions two tunnels terminating on AWS endpoints in different Availability Zones, giving you redundancy out of the box. You attach it to either a Virtual Private Gateway (aws_vpn_gateway, single-VPC) or a Transit Gateway (aws_ec2_transit_gateway, hub-and-spoke across many VPCs), and you choose between static routing or dynamic BGP routing.

The problem is that doing this correctly involves a sprawl of fiddly, security-sensitive arguments: per-tunnel pre-shared keys, inside CIDR allocation from the RFC 6996 169.254.0.0/16 range, IKE versions, Phase 1/Phase 2 DH groups and cipher suites, rekey margins, DPD (dead peer detection) timeouts, and tunnel-level CloudWatch logging. Hand-writing this for every spoke is error-prone and drifts between teams. This module wraps aws_vpn_connection (plus an optional aws_customer_gateway and the route plumbing) so a consumer supplies a handful of typed, validated inputs and gets two hardened tunnels with sane IKEv2 defaults, optional BGP, and observability wired in.

When to use it

Module structure

terraform-module-aws-vpn/
├── versions.tf      # provider + Terraform version pins
├── main.tf          # customer gateway + vpn connection + routes
├── variables.tf     # typed, validated inputs
└── outputs.tf       # connection id, tunnel addresses, BGP info

versions.tf

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

main.tf

locals {
  # Tunnel options are only emitted when the consumer provides values,
  # so AWS-assigned PSKs / inside CIDRs are left untouched when null.
  enable_tunnel1_logging = var.tunnel_cloudwatch_log_group_arn != null
  enable_tunnel2_logging = var.tunnel_cloudwatch_log_group_arn != null
}

# Optionally create the Customer Gateway representing the on-prem device.
# Set create_customer_gateway = false to reuse an existing one by id.
resource "aws_customer_gateway" "this" {
  count = var.create_customer_gateway ? 1 : 0

  bgp_asn     = var.customer_gateway_bgp_asn
  ip_address  = var.customer_gateway_ip_address
  type        = "ipsec.1"
  device_name = var.customer_gateway_device_name

  tags = merge(var.tags, {
    Name = "${var.name}-cgw"
  })
}

locals {
  customer_gateway_id = var.create_customer_gateway ? aws_customer_gateway.this[0].id : var.existing_customer_gateway_id
}

resource "aws_vpn_connection" "this" {
  customer_gateway_id = local.customer_gateway_id
  type                = "ipsec.1"

  # Attach to exactly one of: a Virtual Private Gateway or a Transit Gateway.
  vpn_gateway_id     = var.vpn_gateway_id
  transit_gateway_id = var.transit_gateway_id

  # static_routes_only = true  -> static routing (you manage routes)
  # static_routes_only = false -> dynamic BGP routing
  static_routes_only = var.static_routes_only

  # Optional outside IP version / local & remote selector CIDRs (IPv4 default).
  tunnel_inside_ip_version       = var.tunnel_inside_ip_version
  local_ipv4_network_cidr        = var.local_ipv4_network_cidr
  remote_ipv4_network_cidr       = var.remote_ipv4_network_cidr
  enable_acceleration            = var.enable_acceleration
  outside_ip_address_type        = var.outside_ip_address_type

  # ---- Tunnel 1 ----
  tunnel1_preshared_key                = var.tunnel1_preshared_key
  tunnel1_inside_cidr                  = var.tunnel1_inside_cidr
  tunnel1_ike_versions                 = var.ike_versions
  tunnel1_phase1_dh_group_numbers      = var.phase1_dh_group_numbers
  tunnel1_phase2_dh_group_numbers      = var.phase2_dh_group_numbers
  tunnel1_phase1_encryption_algorithms = var.phase1_encryption_algorithms
  tunnel1_phase2_encryption_algorithms = var.phase2_encryption_algorithms
  tunnel1_phase1_integrity_algorithms  = var.phase1_integrity_algorithms
  tunnel1_phase2_integrity_algorithms  = var.phase2_integrity_algorithms
  tunnel1_startup_action               = var.tunnel_startup_action
  tunnel1_dpd_timeout_action           = var.dpd_timeout_action

  dynamic "tunnel1_log_options" {
    for_each = local.enable_tunnel1_logging ? [1] : []
    content {
      cloudwatch_log_options {
        log_enabled       = true
        log_group_arn     = var.tunnel_cloudwatch_log_group_arn
        log_output_format = var.tunnel_log_output_format
      }
    }
  }

  # ---- Tunnel 2 ----
  tunnel2_preshared_key                = var.tunnel2_preshared_key
  tunnel2_inside_cidr                  = var.tunnel2_inside_cidr
  tunnel2_ike_versions                 = var.ike_versions
  tunnel2_phase1_dh_group_numbers      = var.phase1_dh_group_numbers
  tunnel2_phase2_dh_group_numbers      = var.phase2_dh_group_numbers
  tunnel2_phase1_encryption_algorithms = var.phase1_encryption_algorithms
  tunnel2_phase2_encryption_algorithms = var.phase2_encryption_algorithms
  tunnel2_phase1_integrity_algorithms  = var.phase1_integrity_algorithms
  tunnel2_phase2_integrity_algorithms  = var.phase2_integrity_algorithms
  tunnel2_startup_action               = var.tunnel_startup_action
  tunnel2_dpd_timeout_action           = var.dpd_timeout_action

  dynamic "tunnel2_log_options" {
    for_each = local.enable_tunnel2_logging ? [1] : []
    content {
      cloudwatch_log_options {
        log_enabled       = true
        log_group_arn     = var.tunnel_cloudwatch_log_group_arn
        log_output_format = var.tunnel_log_output_format
      }
    }
  }

  tags = merge(var.tags, {
    Name = var.name
  })
}

# Static route(s) — only valid with a VGW and static_routes_only = true.
resource "aws_vpn_connection_route" "this" {
  for_each = var.static_routes_only && var.vpn_gateway_id != null ? toset(var.static_route_cidrs) : toset([])

  destination_cidr_block = each.value
  vpn_connection_id      = aws_vpn_connection.this.id
}

# Propagate BGP-learned routes into a route table (VGW + dynamic routing).
resource "aws_vpn_gateway_route_propagation" "this" {
  for_each = !var.static_routes_only && var.vpn_gateway_id != null ? toset(var.propagation_route_table_ids) : toset([])

  vpn_gateway_id = var.vpn_gateway_id
  route_table_id = each.value
}

variables.tf

variable "name" {
  description = "Base name used for tagging the VPN connection and customer gateway."
  type        = string

  validation {
    condition     = length(var.name) > 0 && length(var.name) <= 64
    error_message = "name must be between 1 and 64 characters."
  }
}

variable "tags" {
  description = "Tags applied to all resources created by this module."
  type        = map(string)
  default     = {}
}

# ---- Customer Gateway ----
variable "create_customer_gateway" {
  description = "Whether to create a Customer Gateway. If false, set existing_customer_gateway_id."
  type        = bool
  default     = true
}

variable "existing_customer_gateway_id" {
  description = "ID of an existing Customer Gateway to reuse when create_customer_gateway = false."
  type        = string
  default     = null
}

variable "customer_gateway_ip_address" {
  description = "Public IP address of the on-premises VPN device (the customer gateway)."
  type        = string
  default     = null

  validation {
    condition     = var.customer_gateway_ip_address == null || can(regex("^(\\d{1,3}\\.){3}\\d{1,3}$", var.customer_gateway_ip_address))
    error_message = "customer_gateway_ip_address must be a valid IPv4 address."
  }
}

variable "customer_gateway_bgp_asn" {
  description = "BGP ASN of the customer gateway. Use 65000 (or any private ASN) when routing is static."
  type        = number
  default     = 65000

  validation {
    condition     = var.customer_gateway_bgp_asn >= 1 && var.customer_gateway_bgp_asn <= 4294967294
    error_message = "customer_gateway_bgp_asn must be a valid ASN (1-4294967294)."
  }
}

variable "customer_gateway_device_name" {
  description = "Optional friendly name for the on-prem device (e.g. 'mum-edge-fw-01')."
  type        = string
  default     = null
}

# ---- Gateway attachment (choose exactly one) ----
variable "vpn_gateway_id" {
  description = "Virtual Private Gateway ID to attach to. Mutually exclusive with transit_gateway_id."
  type        = string
  default     = null
}

variable "transit_gateway_id" {
  description = "Transit Gateway ID to attach to. Mutually exclusive with vpn_gateway_id."
  type        = string
  default     = null
}

# ---- Routing ----
variable "static_routes_only" {
  description = "true = static routing (manage routes yourself); false = dynamic BGP routing."
  type        = bool
  default     = false
}

variable "static_route_cidrs" {
  description = "On-prem CIDRs to add as static routes (only used with a VGW + static_routes_only)."
  type        = list(string)
  default     = []
}

variable "propagation_route_table_ids" {
  description = "Route table IDs to receive BGP route propagation (VGW + dynamic routing)."
  type        = list(string)
  default     = []
}

# ---- Connection-level options ----
variable "tunnel_inside_ip_version" {
  description = "Inside IP version for the tunnels: ipv4 or ipv6."
  type        = string
  default     = "ipv4"

  validation {
    condition     = contains(["ipv4", "ipv6"], var.tunnel_inside_ip_version)
    error_message = "tunnel_inside_ip_version must be 'ipv4' or 'ipv6'."
  }
}

variable "outside_ip_address_type" {
  description = "Outside IP address type: PublicIpv4 or PrivateIpv4 (PrivateIpv4 requires a TGW + DX)."
  type        = string
  default     = "PublicIpv4"

  validation {
    condition     = contains(["PublicIpv4", "PrivateIpv4"], var.outside_ip_address_type)
    error_message = "outside_ip_address_type must be 'PublicIpv4' or 'PrivateIpv4'."
  }
}

variable "enable_acceleration" {
  description = "Enable AWS Global Accelerator for the tunnels (Transit Gateway connections only)."
  type        = bool
  default     = false
}

variable "local_ipv4_network_cidr" {
  description = "IPv4 CIDR on the customer gateway (on-prem) side allowed over the tunnel."
  type        = string
  default     = "0.0.0.0/0"
}

variable "remote_ipv4_network_cidr" {
  description = "IPv4 CIDR on the AWS side allowed over the tunnel."
  type        = string
  default     = "0.0.0.0/0"
}

# ---- Per-tunnel secrets / inside CIDRs (null => AWS auto-assigns) ----
variable "tunnel1_preshared_key" {
  description = "Pre-shared key for tunnel 1. Leave null to let AWS generate one (recommended)."
  type        = string
  default     = null
  sensitive   = true
}

variable "tunnel2_preshared_key" {
  description = "Pre-shared key for tunnel 2. Leave null to let AWS generate one (recommended)."
  type        = string
  default     = null
  sensitive   = true
}

variable "tunnel1_inside_cidr" {
  description = "Inside /30 CIDR for tunnel 1 from 169.254.0.0/16 (null => AWS-assigned)."
  type        = string
  default     = null
}

variable "tunnel2_inside_cidr" {
  description = "Inside /30 CIDR for tunnel 2 from 169.254.0.0/16 (null => AWS-assigned)."
  type        = string
  default     = null
}

# ---- IPsec/IKE hardening (apply to both tunnels) ----
variable "ike_versions" {
  description = "Permitted IKE versions. Default to IKEv2 only for security."
  type        = list(string)
  default     = ["ikev2"]

  validation {
    condition     = alltrue([for v in var.ike_versions : contains(["ikev1", "ikev2"], v)])
    error_message = "ike_versions entries must be 'ikev1' or 'ikev2'."
  }
}

variable "phase1_dh_group_numbers" {
  description = "Permitted Diffie-Hellman group numbers for Phase 1."
  type        = list(number)
  default     = [14, 15, 16, 19, 20, 21]
}

variable "phase2_dh_group_numbers" {
  description = "Permitted Diffie-Hellman group numbers for Phase 2."
  type        = list(number)
  default     = [14, 15, 16, 19, 20, 21]
}

variable "phase1_encryption_algorithms" {
  description = "Permitted Phase 1 encryption algorithms (AES-256 / GCM only by default)."
  type        = list(string)
  default     = ["AES256", "AES256-GCM-16"]

  validation {
    condition     = !contains(var.phase1_encryption_algorithms, "AES128") && !contains(var.phase1_encryption_algorithms, "AES128-GCM-16")
    error_message = "AES128 ciphers are disallowed; use AES256 variants."
  }
}

variable "phase2_encryption_algorithms" {
  description = "Permitted Phase 2 encryption algorithms (AES-256 / GCM only by default)."
  type        = list(string)
  default     = ["AES256", "AES256-GCM-16"]

  validation {
    condition     = !contains(var.phase2_encryption_algorithms, "AES128") && !contains(var.phase2_encryption_algorithms, "AES128-GCM-16")
    error_message = "AES128 ciphers are disallowed; use AES256 variants."
  }
}

variable "phase1_integrity_algorithms" {
  description = "Permitted Phase 1 integrity algorithms (SHA-2 only by default)."
  type        = list(string)
  default     = ["SHA2-256", "SHA2-384", "SHA2-512"]

  validation {
    condition     = !contains(var.phase1_integrity_algorithms, "SHA1")
    error_message = "SHA1 integrity is disallowed; use SHA2-256 or stronger."
  }
}

variable "phase2_integrity_algorithms" {
  description = "Permitted Phase 2 integrity algorithms (SHA-2 only by default)."
  type        = list(string)
  default     = ["SHA2-256", "SHA2-384", "SHA2-512"]

  validation {
    condition     = !contains(var.phase2_integrity_algorithms, "SHA1")
    error_message = "SHA1 integrity is disallowed; use SHA2-256 or stronger."
  }
}

variable "tunnel_startup_action" {
  description = "Tunnel startup behaviour: 'add' (passive) or 'start' (AWS initiates IKE)."
  type        = string
  default     = "add"

  validation {
    condition     = contains(["add", "start"], var.tunnel_startup_action)
    error_message = "tunnel_startup_action must be 'add' or 'start'."
  }
}

variable "dpd_timeout_action" {
  description = "Action on DPD timeout: 'clear', 'none', or 'restart'."
  type        = string
  default     = "restart"

  validation {
    condition     = contains(["clear", "none", "restart"], var.dpd_timeout_action)
    error_message = "dpd_timeout_action must be 'clear', 'none', or 'restart'."
  }
}

# ---- Tunnel logging ----
variable "tunnel_cloudwatch_log_group_arn" {
  description = "CloudWatch Logs group ARN for tunnel activity logs. Null disables tunnel logging."
  type        = string
  default     = null
}

variable "tunnel_log_output_format" {
  description = "Tunnel log output format: 'json' or 'text'."
  type        = string
  default     = "json"

  validation {
    condition     = contains(["json", "text"], var.tunnel_log_output_format)
    error_message = "tunnel_log_output_format must be 'json' or 'text'."
  }
}

outputs.tf

output "vpn_connection_id" {
  description = "ID of the Site-to-Site VPN connection."
  value       = aws_vpn_connection.this.id
}

output "vpn_connection_arn" {
  description = "ARN of the Site-to-Site VPN connection."
  value       = aws_vpn_connection.this.arn
}

output "customer_gateway_id" {
  description = "ID of the customer gateway in use (created or existing)."
  value       = local.customer_gateway_id
}

output "tunnel1_address" {
  description = "Public IP address of the AWS endpoint for tunnel 1."
  value       = aws_vpn_connection.this.tunnel1_address
}

output "tunnel2_address" {
  description = "Public IP address of the AWS endpoint for tunnel 2."
  value       = aws_vpn_connection.this.tunnel2_address
}

output "tunnel1_cgw_inside_address" {
  description = "Inside (customer gateway side) IP address for tunnel 1, used for BGP peering."
  value       = aws_vpn_connection.this.tunnel1_cgw_inside_address
}

output "tunnel2_cgw_inside_address" {
  description = "Inside (customer gateway side) IP address for tunnel 2, used for BGP peering."
  value       = aws_vpn_connection.this.tunnel2_cgw_inside_address
}

output "tunnel1_bgp_asn" {
  description = "BGP ASN on the AWS side of tunnel 1 (relevant for dynamic routing)."
  value       = aws_vpn_connection.this.tunnel1_bgp_asn
}

output "tunnel1_preshared_key" {
  description = "Pre-shared key for tunnel 1 (sensitive)."
  value       = aws_vpn_connection.this.tunnel1_preshared_key
  sensitive   = true
}

output "tunnel2_preshared_key" {
  description = "Pre-shared key for tunnel 2 (sensitive)."
  value       = aws_vpn_connection.this.tunnel2_preshared_key
  sensitive   = true
}

How to use it

A common production pattern is a Transit Gateway hub with one VPN connection per branch site, using dynamic BGP routing and tunnel logging.

resource "aws_cloudwatch_log_group" "vpn" {
  name              = "/aws/vpn/mumbai-branch"
  retention_in_days = 90
}

module "site_to_site_vpn" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-vpn?ref=v1.0.0"

  name = "mumbai-branch"

  # Create the customer gateway from the branch firewall's public IP + ASN.
  create_customer_gateway      = true
  customer_gateway_ip_address  = "203.0.113.40"
  customer_gateway_bgp_asn     = 65010
  customer_gateway_device_name = "mum-edge-fw-01"

  # Attach to the Transit Gateway hub with dynamic BGP routing.
  transit_gateway_id = aws_ec2_transit_gateway.hub.id
  static_routes_only = false

  # Restrict the selectors instead of allowing 0.0.0.0/0 both ways.
  local_ipv4_network_cidr  = "10.50.0.0/16" # branch LAN
  remote_ipv4_network_cidr = "10.0.0.0/8"   # AWS aggregate

  # Pin inside tunnel CIDRs so on-prem BGP config is deterministic.
  tunnel1_inside_cidr = "169.254.10.0/30"
  tunnel2_inside_cidr = "169.254.10.4/30"

  # Tunnel activity logging to CloudWatch.
  tunnel_cloudwatch_log_group_arn = aws_cloudwatch_log_group.vpn.arn

  tags = {
    Environment = "prod"
    Owner       = "network-team"
    Site        = "mumbai-branch"
  }
}

# Downstream reference: alarm on tunnel-down using the connection id output.
resource "aws_cloudwatch_metric_alarm" "tunnel_down" {
  alarm_name          = "vpn-mumbai-branch-tunnel-down"
  namespace           = "AWS/VPN"
  metric_name         = "TunnelState"
  statistic           = "Maximum"
  comparison_operator = "LessThanThreshold"
  threshold           = 1
  period              = 60
  evaluation_periods  = 3
  treat_missing_data  = "breaching"

  dimensions = {
    VpnId = module.site_to_site_vpn.vpn_connection_id
  }
}

With Terragrunt

Terragrunt keeps this module DRY across environments — define the backend and provider once in a root config, then a thin terragrunt.hcl per environment supplies only the inputs that differ.

1. Root configlive/terragrunt.hcl (inherited by every module):

remote_state {
  backend = "s3"
  generate = { path = "backend.tf", if_exists = "overwrite" }
  config = {
    # ...s3 state bucket/container + key per path...
  }
}

2. Module configlive/prod/vpn/terragrunt.hcl:

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-vpn?ref=v1.0.0"
}

inputs = {
  name = "..."
}

3. Deploy one environment, or roll out all modules together:

cd live/prod/vpn && terragrunt apply        # this module
terragrunt run-all apply                      # every module under live/prod

Why Terragrunt here: the backend and provider live in one place instead of being copy-pasted into every module; inputs is overridden per environment (dev / stage / prod) without forking the module; and run-all orchestrates dependencies across modules. Reach for it once you have more than one environment or more than a handful of modules — for a single stack, the plain Quickstart above is enough.

Inputs

Name Type Default Required Description
name string Yes Base name used for tagging the VPN connection and customer gateway.
tags map(string) {} No Tags applied to all resources created by this module.
create_customer_gateway bool true No Whether to create a Customer Gateway; if false, set existing_customer_gateway_id.
existing_customer_gateway_id string null No ID of an existing Customer Gateway to reuse.
customer_gateway_ip_address string null Conditional Public IP of the on-prem VPN device (required when creating a CGW).
customer_gateway_bgp_asn number 65000 No BGP ASN of the customer gateway.
customer_gateway_device_name string null No Friendly name for the on-prem device.
vpn_gateway_id string null Conditional Virtual Private Gateway ID (mutually exclusive with transit_gateway_id).
transit_gateway_id string null Conditional Transit Gateway ID (mutually exclusive with vpn_gateway_id).
static_routes_only bool false No true = static routing; false = dynamic BGP routing.
static_route_cidrs list(string) [] No On-prem CIDRs added as static routes (VGW + static routing).
propagation_route_table_ids list(string) [] No Route table IDs to receive BGP route propagation (VGW + dynamic).
tunnel_inside_ip_version string “ipv4” No Inside IP version: ipv4 or ipv6.
outside_ip_address_type string “PublicIpv4” No Outside IP type: PublicIpv4 or PrivateIpv4.
enable_acceleration bool false No Enable Global Accelerator (Transit Gateway connections only).
local_ipv4_network_cidr string “0.0.0.0/0” No IPv4 CIDR on the on-prem side allowed over the tunnel.
remote_ipv4_network_cidr string “0.0.0.0/0” No IPv4 CIDR on the AWS side allowed over the tunnel.
tunnel1_preshared_key string (sensitive) null No PSK for tunnel 1; null lets AWS generate one.
tunnel2_preshared_key string (sensitive) null No PSK for tunnel 2; null lets AWS generate one.
tunnel1_inside_cidr string null No Inside /30 CIDR for tunnel 1 from 169.254.0.0/16.
tunnel2_inside_cidr string null No Inside /30 CIDR for tunnel 2 from 169.254.0.0/16.
ike_versions list(string) [“ikev2”] No Permitted IKE versions.
phase1_dh_group_numbers list(number) [14,15,16,19,20,21] No Permitted Phase 1 DH group numbers.
phase2_dh_group_numbers list(number) [14,15,16,19,20,21] No Permitted Phase 2 DH group numbers.
phase1_encryption_algorithms list(string) [“AES256”,“AES256-GCM-16”] No Permitted Phase 1 encryption algorithms.
phase2_encryption_algorithms list(string) [“AES256”,“AES256-GCM-16”] No Permitted Phase 2 encryption algorithms.
phase1_integrity_algorithms list(string) [“SHA2-256”,“SHA2-384”,“SHA2-512”] No Permitted Phase 1 integrity algorithms.
phase2_integrity_algorithms list(string) [“SHA2-256”,“SHA2-384”,“SHA2-512”] No Permitted Phase 2 integrity algorithms.
tunnel_startup_action string “add” No Tunnel startup behaviour: add (passive) or start (AWS initiates).
dpd_timeout_action string “restart” No Action on DPD timeout: clear, none, or restart.
tunnel_cloudwatch_log_group_arn string null No CloudWatch Logs group ARN for tunnel logs; null disables logging.
tunnel_log_output_format string “json” No Tunnel log output format: json or text.

Outputs

Name Description
vpn_connection_id ID of the Site-to-Site VPN connection.
vpn_connection_arn ARN of the Site-to-Site VPN connection.
customer_gateway_id ID of the customer gateway in use (created or existing).
tunnel1_address Public IP address of the AWS endpoint for tunnel 1.
tunnel2_address Public IP address of the AWS endpoint for tunnel 2.
tunnel1_cgw_inside_address Inside (CGW side) IP for tunnel 1, used for BGP peering.
tunnel2_cgw_inside_address Inside (CGW side) IP for tunnel 2, used for BGP peering.
tunnel1_bgp_asn BGP ASN on the AWS side of tunnel 1.
tunnel1_preshared_key Pre-shared key for tunnel 1 (sensitive).
tunnel2_preshared_key Pre-shared key for tunnel 2 (sensitive).

Enterprise scenario

A retail company runs 120 stores, each with a branch firewall, and consolidates connectivity into a single Transit Gateway hub in ap-south-1. Their networking team loops this module in a for_each over a map of store records, passing each firewall’s public IP and a unique private BGP ASN, with static_routes_only = false so new store subnets propagate automatically without Terraform changes. Tunnel logs stream to a per-store CloudWatch Logs group for the SOC, and a CloudWatch alarm built from vpn_connection_id pages the on-call engineer if either tunnel on a store stays down for three minutes — turning what used to be 120 hand-built console connections into one reviewed, version-pinned module call.

Best practices

TerraformAWSSite-to-Site VPNModuleIaC
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading