Hybrid Cloud Connectivity with AWS Transit Gateway: Enterprise Architecture Guide
Hybrid cloud architectures require robust, scalable networking solutions that can connect on-premises data centers with cloud resources while maintaining security, performance, and manageability. AWS Transit Gateway provides a centralized hub for managing network connectivity across multiple VPCs, regions, and hybrid environments. This comprehensive guide explores enterprise-grade hybrid cloud connectivity patterns using Transit Gateway.
Understanding AWS Transit Gateway Architecture
Core Concepts
AWS Transit Gateway acts as a cloud router that connects VPCs and on-premises networks through a central hub. It simplifies network architecture by eliminating the need for complex peering relationships and reducing the number of connections required.
Key Components
Transit Gateway Architecture:
┌─────────────────┐
│ Transit GW │
│ (Regional) │
└────────┬────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌───▼────┐
│ VPC 1 │ │ VPC 2 │ │ VPN │
│ App │ │ Data │ │Connection│
└─────────┘ └─────────┘ └───┬────┘
│
┌──────▼──────┐
│ On-Premises│
│ Data Center│
└─────────────┘
Transit Gateway Benefits
Centralized Management:
- Single point of network control
- Simplified routing policies
- Consistent security policies
- Reduced operational complexity
Scalability:
- Up to 5,000 VPC attachments per gateway
- 50 Gbps bandwidth per VPC attachment
- Support for thousands of routes
- Multi-region peering
Cost Optimization:
- Reduced NAT Gateway costs
- Consolidated VPN connections
- Efficient bandwidth utilization
- Pay-per-use pricing model
Deploying Transit Gateway Infrastructure
Terraform Configuration
Transit Gateway Base Setup
# variables.tf
variable "environment" {
description = "Environment name"
type = string
default = "production"
}
variable "aws_region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "enable_dns_support" {
description = "Enable DNS support for Transit Gateway"
type = bool
default = true
}
variable "enable_vpn_ecmp_support" {
description = "Enable ECMP for VPN connections"
type = bool
default = true
}
variable "default_route_table_association" {
description = "Enable default route table association"
type = bool
default = false
}
variable "default_route_table_propagation" {
description = "Enable default route table propagation"
type = bool
default = false
}
variable "on_premises_cidrs" {
description = "On-premises network CIDRs"
type = list(string)
default = ["10.0.0.0/8", "172.16.0.0/12"]
}
# transit-gateway.tf
resource "aws_ec2_transit_gateway" "main" {
description = "Main Transit Gateway for ${var.environment}"
amazon_side_asn = 64512
default_route_table_association = var.default_route_table_association ? "enable" : "disable"
default_route_table_propagation = var.default_route_table_propagation ? "enable" : "disable"
dns_support = var.enable_dns_support ? "enable" : "disable"
vpn_ecmp_support = var.enable_vpn_ecmp_support ? "enable" : "disable"
multicast_support = "disable"
auto_accept_shared_attachments = "disable"
tags = {
Name = "tgw-${var.environment}"
Environment = var.environment
ManagedBy = "Terraform"
Purpose = "Hybrid-Cloud-Connectivity"
}
}
# Create custom route tables for segmentation
resource "aws_ec2_transit_gateway_route_table" "production" {
transit_gateway_id = aws_ec2_transit_gateway.main.id
tags = {
Name = "tgw-rt-production"
Environment = "production"
Tier = "production"
}
}
resource "aws_ec2_transit_gateway_route_table" "development" {
transit_gateway_id = aws_ec2_transit_gateway.main.id
tags = {
Name = "tgw-rt-development"
Environment = "development"
Tier = "development"
}
}
resource "aws_ec2_transit_gateway_route_table" "shared_services" {
transit_gateway_id = aws_ec2_transit_gateway.main.id
tags = {
Name = "tgw-rt-shared-services"
Environment = var.environment
Tier = "shared-services"
}
}
resource "aws_ec2_transit_gateway_route_table" "on_premises" {
transit_gateway_id = aws_ec2_transit_gateway.main.id
tags = {
Name = "tgw-rt-on-premises"
Environment = var.environment
Tier = "on-premises"
}
}
# outputs.tf
output "transit_gateway_id" {
description = "Transit Gateway ID"
value = aws_ec2_transit_gateway.main.id
}
output "transit_gateway_arn" {
description = "Transit Gateway ARN"
value = aws_ec2_transit_gateway.main.arn
}
output "transit_gateway_route_table_ids" {
description = "Transit Gateway route table IDs"
value = {
production = aws_ec2_transit_gateway_route_table.production.id
development = aws_ec2_transit_gateway_route_table.development.id
shared_services = aws_ec2_transit_gateway_route_table.shared_services.id
on_premises = aws_ec2_transit_gateway_route_table.on_premises.id
}
}
VPC Attachments Configuration
Production VPC Attachment
# vpc-attachments.tf
data "aws_vpc" "production" {
filter {
name = "tag:Environment"
values = ["production"]
}
}
data "aws_subnets" "production_private" {
filter {
name = "vpc-id"
values = [data.aws_vpc.production.id]
}
filter {
name = "tag:Tier"
values = ["private"]
}
}
resource "aws_ec2_transit_gateway_vpc_attachment" "production" {
subnet_ids = data.aws_subnets.production_private.ids
transit_gateway_id = aws_ec2_transit_gateway.main.id
vpc_id = data.aws_vpc.production.id
dns_support = "enable"
ipv6_support = "disable"
appliance_mode_support = "disable"
transit_gateway_default_route_table_association = false
transit_gateway_default_route_table_propagation = false
tags = {
Name = "tgw-attach-production-vpc"
Environment = "production"
VPC = data.aws_vpc.production.id
}
}
# Associate with production route table
resource "aws_ec2_transit_gateway_route_table_association" "production" {
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.production.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.production.id
}
# Propagate production routes to on-premises route table
resource "aws_ec2_transit_gateway_route_table_propagation" "production_to_on_premises" {
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.production.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.on_premises.id
}
# Propagate production routes to shared services route table
resource "aws_ec2_transit_gateway_route_table_propagation" "production_to_shared" {
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.production.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.shared_services.id
}
# Update VPC route tables to route through Transit Gateway
data "aws_route_tables" "production_private" {
vpc_id = data.aws_vpc.production.id
filter {
name = "tag:Tier"
values = ["private"]
}
}
resource "aws_route" "production_to_on_premises" {
count = length(data.aws_route_tables.production_private.ids)
route_table_id = data.aws_route_tables.production_private.ids[count.index]
destination_cidr_block = var.on_premises_cidrs[0]
transit_gateway_id = aws_ec2_transit_gateway.main.id
depends_on = [aws_ec2_transit_gateway_vpc_attachment.production]
}
resource "aws_route" "production_to_shared_services" {
count = length(data.aws_route_tables.production_private.ids)
route_table_id = data.aws_route_tables.production_private.ids[count.index]
destination_cidr_block = "100.64.0.0/16" # Shared services CIDR
transit_gateway_id = aws_ec2_transit_gateway.main.id
depends_on = [aws_ec2_transit_gateway_vpc_attachment.production]
}
VPN Connection Configuration
Site-to-Site VPN Setup
Customer Gateway Configuration
# customer-gateway.tf
variable "customer_gateway_ip" {
description = "On-premises VPN endpoint public IP"
type = string
}
variable "customer_gateway_bgp_asn" {
description = "BGP ASN for customer gateway"
type = number
default = 65000
}
resource "aws_customer_gateway" "main" {
bgp_asn = var.customer_gateway_bgp_asn
ip_address = var.customer_gateway_ip
type = "ipsec.1"
tags = {
Name = "cgw-${var.environment}-main"
Environment = var.environment
Location = "on-premises-dc1"
}
}
# VPN connection with ECMP support (two tunnels)
resource "aws_vpn_connection" "main" {
customer_gateway_id = aws_customer_gateway.main.id
transit_gateway_id = aws_ec2_transit_gateway.main.id
type = aws_customer_gateway.main.type
static_routes_only = false # Use BGP for dynamic routing
tunnel1_inside_cidr = "169.254.10.0/30"
tunnel1_preshared_key = random_password.tunnel1_psk.result
tunnel2_inside_cidr = "169.254.10.4/30"
tunnel2_preshared_key = random_password.tunnel2_psk.result
tunnel1_dpd_timeout_action = "restart"
tunnel2_dpd_timeout_action = "restart"
tunnel1_ike_versions = ["ikev2"]
tunnel2_ike_versions = ["ikev2"]
tunnel1_phase1_dh_group_numbers = [14, 15, 16, 17, 18]
tunnel1_phase1_encryption_algorithms = ["AES256", "AES128"]
tunnel1_phase1_integrity_algorithms = ["SHA2-256", "SHA2-384", "SHA2-512"]
tunnel1_phase1_lifetime_seconds = 28800
tunnel2_phase1_dh_group_numbers = [14, 15, 16, 17, 18]
tunnel2_phase1_encryption_algorithms = ["AES256", "AES128"]
tunnel2_phase1_integrity_algorithms = ["SHA2-256", "SHA2-384", "SHA2-512"]
tunnel2_phase1_lifetime_seconds = 28800
tunnel1_phase2_dh_group_numbers = [14, 15, 16, 17, 18]
tunnel1_phase2_encryption_algorithms = ["AES256", "AES128"]
tunnel1_phase2_integrity_algorithms = ["SHA2-256", "SHA2-384", "SHA2-512"]
tunnel1_phase2_lifetime_seconds = 3600
tunnel2_phase2_dh_group_numbers = [14, 15, 16, 17, 18]
tunnel2_phase2_encryption_algorithms = ["AES256", "AES128"]
tunnel2_phase2_integrity_algorithms = ["SHA2-256", "SHA2-384", "SHA2-512"]
tunnel2_phase2_lifetime_seconds = 3600
tags = {
Name = "vpn-${var.environment}-main"
Environment = var.environment
}
}
resource "random_password" "tunnel1_psk" {
length = 32
special = true
}
resource "random_password" "tunnel2_psk" {
length = 32
special = true
}
# Store VPN configuration in Secrets Manager
resource "aws_secretsmanager_secret" "vpn_config" {
name = "${var.environment}/vpn/main-connection"
description = "VPN connection configuration for ${var.environment}"
tags = {
Environment = var.environment
Purpose = "VPN-Configuration"
}
}
resource "aws_secretsmanager_secret_version" "vpn_config" {
secret_id = aws_secretsmanager_secret.vpn_config.id
secret_string = jsonencode({
tunnel1 = {
outside_ip_address = aws_vpn_connection.main.tunnel1_address
inside_cidr = aws_vpn_connection.main.tunnel1_inside_cidr
preshared_key = random_password.tunnel1_psk.result
bgp_asn = aws_vpn_connection.main.tunnel1_bgp_asn
bgp_holdtime = aws_vpn_connection.main.tunnel1_bgp_holdtime
}
tunnel2 = {
outside_ip_address = aws_vpn_connection.main.tunnel2_address
inside_cidr = aws_vpn_connection.main.tunnel2_inside_cidr
preshared_key = random_password.tunnel2_psk.result
bgp_asn = aws_vpn_connection.main.tunnel2_bgp_asn
bgp_holdtime = aws_vpn_connection.main.tunnel2_bgp_holdtime
}
customer_gateway_ip = var.customer_gateway_ip
})
}
# Attach VPN to Transit Gateway
resource "aws_ec2_transit_gateway_route_table_association" "vpn" {
transit_gateway_attachment_id = aws_vpn_connection.main.transit_gateway_attachment_id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.on_premises.id
}
# Propagate VPN routes to production and shared services
resource "aws_ec2_transit_gateway_route_table_propagation" "vpn_to_production" {
transit_gateway_attachment_id = aws_vpn_connection.main.transit_gateway_attachment_id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.production.id
}
resource "aws_ec2_transit_gateway_route_table_propagation" "vpn_to_shared" {
transit_gateway_attachment_id = aws_vpn_connection.main.transit_gateway_attachment_id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.shared_services.id
}
On-Premises VPN Configuration (StrongSwan)
#!/bin/bash
# configure-strongswan.sh - Configure StrongSwan for AWS VPN
set -euo pipefail
# Retrieve VPN configuration from AWS Secrets Manager
VPN_CONFIG=$(aws secretsmanager get-secret-value \
--secret-id production/vpn/main-connection \
--query SecretString \
--output text)
TUNNEL1_OUTSIDE_IP=$(echo "$VPN_CONFIG" | jq -r '.tunnel1.outside_ip_address')
TUNNEL1_INSIDE_CIDR=$(echo "$VPN_CONFIG" | jq -r '.tunnel1.inside_cidr')
TUNNEL1_PSK=$(echo "$VPN_CONFIG" | jq -r '.tunnel1.preshared_key')
TUNNEL1_BGP_ASN=$(echo "$VPN_CONFIG" | jq -r '.tunnel1.bgp_asn')
TUNNEL2_OUTSIDE_IP=$(echo "$VPN_CONFIG" | jq -r '.tunnel2.outside_ip_address')
TUNNEL2_INSIDE_CIDR=$(echo "$VPN_CONFIG" | jq -r '.tunnel2.inside_cidr')
TUNNEL2_PSK=$(echo "$VPN_CONFIG" | jq -r '.tunnel2.preshared_key')
TUNNEL2_BGP_ASN=$(echo "$VPN_CONFIG" | jq -r '.tunnel2.bgp_asn')
LOCAL_IP=$(curl -s http://169.254.169.254/latest/meta-data/public-ipv4)
LOCAL_BGP_ASN="65000"
# Configure StrongSwan
cat > /etc/ipsec.conf <<EOF
config setup
charondebug="ike 2, knl 2, cfg 2, net 2, esp 2, dmn 2, mgr 2"
strictcrlpolicy=no
uniqueids=no
# Tunnel 1
conn aws-tunnel1
auto=start
left=%defaultroute
leftid=$LOCAL_IP
right=$TUNNEL1_OUTSIDE_IP
type=tunnel
ikelifetime=8h
keylife=1h
rekeymargin=3m
keyingtries=%forever
keyexchange=ikev2
ike=aes256-sha256-modp2048,aes128-sha256-modp2048!
esp=aes256-sha256-modp2048,aes128-sha256-modp2048!
leftsubnet=0.0.0.0/0
rightsubnet=0.0.0.0/0
dpddelay=10s
dpdtimeout=30s
dpdaction=restart
mark=%unique
# Tunnel 2
conn aws-tunnel2
auto=start
left=%defaultroute
leftid=$LOCAL_IP
right=$TUNNEL2_OUTSIDE_IP
type=tunnel
ikelifetime=8h
keylife=1h
rekeymargin=3m
keyingtries=%forever
keyexchange=ikev2
ike=aes256-sha256-modp2048,aes128-sha256-modp2048!
esp=aes256-sha256-modp2048,aes128-sha256-modp2048!
leftsubnet=0.0.0.0/0
rightsubnet=0.0.0.0/0
dpddelay=10s
dpdtimeout=30s
dpdaction=restart
mark=%unique
EOF
# Configure PSKs
cat > /etc/ipsec.secrets <<EOF
$LOCAL_IP $TUNNEL1_OUTSIDE_IP : PSK "$TUNNEL1_PSK"
$LOCAL_IP $TUNNEL2_OUTSIDE_IP : PSK "$TUNNEL2_PSK"
EOF
chmod 600 /etc/ipsec.secrets
# Configure BGP with FRRouting
TUNNEL1_LOCAL_IP=$(echo "$TUNNEL1_INSIDE_CIDR" | cut -d'/' -f1 | awk -F. '{print $1"."$2"."$3"."$4+1}')
TUNNEL1_PEER_IP=$(echo "$TUNNEL1_INSIDE_CIDR" | cut -d'/' -f1 | awk -F. '{print $1"."$2"."$3"."$4+2}')
TUNNEL2_LOCAL_IP=$(echo "$TUNNEL2_INSIDE_CIDR" | cut -d'/' -f1 | awk -F. '{print $1"."$2"."$3"."$4+1}')
TUNNEL2_PEER_IP=$(echo "$TUNNEL2_INSIDE_CIDR" | cut -d'/' -f1 | awk -F. '{print $1"."$2"."$3"."$4+2}')
cat > /etc/frr/frr.conf <<EOF
frr version 8.0
frr defaults traditional
hostname vpn-gateway
log syslog informational
service integrated-vtysh-config
# Configure BGP
router bgp $LOCAL_BGP_ASN
bgp router-id $LOCAL_IP
bgp log-neighbor-changes
no bgp default ipv4-unicast
# Tunnel 1 neighbor
neighbor $TUNNEL1_PEER_IP remote-as $TUNNEL1_BGP_ASN
neighbor $TUNNEL1_PEER_IP timers 10 30 30
# Tunnel 2 neighbor
neighbor $TUNNEL2_PEER_IP remote-as $TUNNEL2_BGP_ASN
neighbor $TUNNEL2_PEER_IP timers 10 30 30
address-family ipv4 unicast
network 10.0.0.0/8
network 172.16.0.0/12
neighbor $TUNNEL1_PEER_IP activate
neighbor $TUNNEL1_PEER_IP soft-reconfiguration inbound
neighbor $TUNNEL1_PEER_IP route-map AWS-IN in
neighbor $TUNNEL1_PEER_IP route-map AWS-OUT out
neighbor $TUNNEL2_PEER_IP activate
neighbor $TUNNEL2_PEER_IP soft-reconfiguration inbound
neighbor $TUNNEL2_PEER_IP route-map AWS-IN in
neighbor $TUNNEL2_PEER_IP route-map AWS-OUT out
exit-address-family
# Route maps for traffic engineering
route-map AWS-IN permit 10
set local-preference 100
route-map AWS-OUT permit 10
match ip address prefix-list ADVERTISE-TO-AWS
# Prefix lists
ip prefix-list ADVERTISE-TO-AWS seq 10 permit 10.0.0.0/8
ip prefix-list ADVERTISE-TO-AWS seq 20 permit 172.16.0.0/12
line vty
EOF
# Restart services
systemctl restart strongswan-starter
systemctl restart frr
echo "VPN configuration completed successfully"
AWS Direct Connect Integration
Direct Connect Gateway Configuration
# direct-connect.tf
variable "direct_connect_gateway_asn" {
description = "BGP ASN for Direct Connect Gateway"
type = number
default = 64513
}
variable "allowed_prefixes" {
description = "Allowed prefixes for Direct Connect"
type = list(string)
default = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
}
# Direct Connect Gateway
resource "aws_dx_gateway" "main" {
name = "dxgw-${var.environment}"
amazon_side_asn = var.direct_connect_gateway_asn
}
# Associate Direct Connect Gateway with Transit Gateway
resource "aws_dx_gateway_association" "transit_gateway" {
dx_gateway_id = aws_dx_gateway.main.id
associated_gateway_id = aws_ec2_transit_gateway.main.id
allowed_prefixes = var.allowed_prefixes
}
# Virtual Interface (typically created by AWS or your carrier)
# This is a placeholder - actual VIF creation depends on your Direct Connect setup
resource "aws_dx_private_virtual_interface" "main" {
connection_id = var.direct_connect_connection_id # Provided by your setup
name = "vif-${var.environment}-main"
vlan = 1000
address_family = "ipv4"
bgp_asn = var.customer_gateway_bgp_asn
# BGP configuration
amazon_address = "169.254.255.1/30"
customer_address = "169.254.255.2/30"
bgp_auth_key = random_password.dx_bgp_key.result
dx_gateway_id = aws_dx_gateway.main.id
tags = {
Name = "vif-${var.environment}-main"
Environment = var.environment
}
}
resource "random_password" "dx_bgp_key" {
length = 32
special = true
}
# Monitor Direct Connect connection status
resource "aws_cloudwatch_metric_alarm" "dx_connection_state" {
alarm_name = "dx-connection-state-${var.environment}"
comparison_operator = "LessThanThreshold"
evaluation_periods = 2
metric_name = "ConnectionState"
namespace = "AWS/DX"
period = 60
statistic = "Minimum"
threshold = 1
alarm_description = "Direct Connect connection is down"
treat_missing_data = "breaching"
dimensions = {
ConnectionId = var.direct_connect_connection_id
}
alarm_actions = [aws_sns_topic.network_alerts.arn]
}
resource "aws_cloudwatch_metric_alarm" "dx_connection_bps_egress" {
alarm_name = "dx-high-egress-${var.environment}"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "ConnectionBpsEgress"
namespace = "AWS/DX"
period = 300
statistic = "Average"
threshold = 8000000000 # 8 Gbps for 10G connection
alarm_description = "Direct Connect egress bandwidth high"
dimensions = {
ConnectionId = var.direct_connect_connection_id
}
alarm_actions = [aws_sns_topic.network_alerts.arn]
}
Advanced Routing Patterns
Route Segmentation and Isolation
# advanced-routing.tf
# Static routes for specific use cases
resource "aws_ec2_transit_gateway_route" "blackhole_suspicious_network" {
destination_cidr_block = "192.0.2.0/24" # Example: block suspicious network
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.production.id
blackhole = true
}
# Route to inspection VPC for security scanning
resource "aws_ec2_transit_gateway_route" "internet_to_inspection" {
destination_cidr_block = "0.0.0.0/0"
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.inspection.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.production.id
}
# Development environment isolation
resource "aws_ec2_transit_gateway_route" "dev_to_shared_services" {
destination_cidr_block = "100.64.0.0/16"
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.shared_services.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.development.id
}
# Block development from accessing production
# (No routes from dev route table to production VPC)
# Shared services access to all environments
resource "aws_ec2_transit_gateway_route_table_propagation" "shared_to_prod" {
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.production.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.shared_services.id
}
resource "aws_ec2_transit_gateway_route_table_propagation" "shared_to_dev" {
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.development.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.shared_services.id
}
Traffic Inspection Architecture
# inspection-vpc.tf
resource "aws_vpc" "inspection" {
cidr_block = "100.65.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "vpc-inspection-${var.environment}"
Environment = var.environment
Purpose = "Network-Inspection"
}
}
# Deploy AWS Network Firewall
resource "aws_networkfirewall_firewall" "main" {
name = "nfw-${var.environment}"
firewall_policy_arn = aws_networkfirewall_firewall_policy.main.arn
vpc_id = aws_vpc.inspection.id
dynamic "subnet_mapping" {
for_each = aws_subnet.inspection_firewall
content {
subnet_id = subnet_mapping.value.id
}
}
tags = {
Name = "nfw-${var.environment}"
Environment = var.environment
}
}
resource "aws_networkfirewall_firewall_policy" "main" {
name = "nfw-policy-${var.environment}"
firewall_policy {
stateless_default_actions = ["aws:forward_to_sfe"]
stateless_fragment_default_actions = ["aws:forward_to_sfe"]
stateful_rule_group_reference {
resource_arn = aws_networkfirewall_rule_group.block_malicious.arn
}
stateful_rule_group_reference {
resource_arn = aws_networkfirewall_rule_group.allow_internal.arn
}
}
tags = {
Name = "nfw-policy-${var.environment}"
Environment = var.environment
}
}
resource "aws_networkfirewall_rule_group" "block_malicious" {
capacity = 100
name = "block-malicious-${var.environment}"
type = "STATEFUL"
rule_group {
rules_source {
rules_source_list {
generated_rules_type = "DENYLIST"
target_types = ["HTTP_HOST", "TLS_SNI"]
targets = [
".malware-domain.com",
".phishing-site.net"
]
}
}
}
tags = {
Name = "nfw-rg-block-malicious"
Environment = var.environment
}
}
resource "aws_networkfirewall_rule_group" "allow_internal" {
capacity = 100
name = "allow-internal-${var.environment}"
type = "STATEFUL"
rule_group {
rules_source {
stateful_rule {
action = "PASS"
header {
destination = "10.0.0.0/8"
destination_port = "ANY"
direction = "ANY"
protocol = "IP"
source = "10.0.0.0/8"
source_port = "ANY"
}
rule_option {
keyword = "sid:1"
}
}
}
}
tags = {
Name = "nfw-rg-allow-internal"
Environment = var.environment
}
}
Monitoring and Observability
CloudWatch Dashboards
# monitoring.tf
resource "aws_cloudwatch_dashboard" "transit_gateway" {
dashboard_name = "transit-gateway-${var.environment}"
dashboard_body = jsonencode({
widgets = [
{
type = "metric"
properties = {
metrics = [
["AWS/TransitGateway", "BytesIn", { stat = "Sum", label = "Bytes In" }],
[".", "BytesOut", { stat = "Sum", label = "Bytes Out" }]
]
period = 300
stat = "Sum"
region = var.aws_region
title = "Transit Gateway Traffic"
yAxis = {
left = {
label = "Bytes"
}
}
}
},
{
type = "metric"
properties = {
metrics = [
["AWS/TransitGateway", "PacketDropCountBlackhole", { stat = "Sum" }],
[".", "PacketDropCountNoRoute", { stat = "Sum" }]
]
period = 300
stat = "Sum"
region = var.aws_region
title = "Dropped Packets"
}
},
{
type = "metric"
properties = {
metrics = [
["AWS/VPN", "TunnelState", {
dimensions = {
VpnId = aws_vpn_connection.main.id
}
stat = "Minimum"
}]
]
period = 60
stat = "Minimum"
region = var.aws_region
title = "VPN Tunnel Status"
}
}
]
})
}
# SNS topic for alerts
resource "aws_sns_topic" "network_alerts" {
name = "network-alerts-${var.environment}"
tags = {
Environment = var.environment
Purpose = "Network-Monitoring"
}
}
resource "aws_sns_topic_subscription" "network_alerts_email" {
topic_arn = aws_sns_topic.network_alerts.arn
protocol = "email"
endpoint = "network-ops@example.com"
}
# VPN tunnel alarms
resource "aws_cloudwatch_metric_alarm" "vpn_tunnel1_down" {
alarm_name = "vpn-tunnel1-down-${var.environment}"
comparison_operator = "LessThanThreshold"
evaluation_periods = 2
metric_name = "TunnelState"
namespace = "AWS/VPN"
period = 60
statistic = "Minimum"
threshold = 1
alarm_description = "VPN Tunnel 1 is down"
treat_missing_data = "breaching"
dimensions = {
VpnId = aws_vpn_connection.main.id
}
alarm_actions = [aws_sns_topic.network_alerts.arn]
}
resource "aws_cloudwatch_metric_alarm" "vpn_tunnel2_down" {
alarm_name = "vpn-tunnel2-down-${var.environment}"
comparison_operator = "LessThanThreshold"
evaluation_periods = 2
metric_name = "TunnelState"
namespace = "AWS/VPN"
period = 60
statistic = "Minimum"
threshold = 1
alarm_description = "VPN Tunnel 2 is down"
treat_missing_data = "breaching"
dimensions = {
VpnId = aws_vpn_connection.main.id
}
alarm_actions = [aws_sns_topic.network_alerts.arn]
}
# Transit Gateway attachment alarms
resource "aws_cloudwatch_metric_alarm" "tgw_packet_drop" {
alarm_name = "tgw-packet-drop-high-${var.environment}"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "PacketDropCountNoRoute"
namespace = "AWS/TransitGateway"
period = 300
statistic = "Sum"
threshold = 1000
alarm_description = "High packet drop rate on Transit Gateway"
dimensions = {
TransitGateway = aws_ec2_transit_gateway.main.id
}
alarm_actions = [aws_sns_topic.network_alerts.arn]
}
VPC Flow Logs Analysis
# flow-logs.tf
resource "aws_flow_log" "transit_gateway" {
log_destination = aws_cloudwatch_log_group.flow_logs.arn
log_destination_type = "cloud-watch-logs"
traffic_type = "ALL"
iam_role_arn = aws_iam_role.flow_logs.arn
transit_gateway_id = aws_ec2_transit_gateway.main.id
tags = {
Name = "flow-logs-tgw-${var.environment}"
Environment = var.environment
}
}
resource "aws_cloudwatch_log_group" "flow_logs" {
name = "/aws/transitgateway/${var.environment}/flowlogs"
retention_in_days = 30
tags = {
Environment = var.environment
}
}
resource "aws_iam_role" "flow_logs" {
name = "transit-gateway-flow-logs-${var.environment}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "vpc-flow-logs.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy" "flow_logs" {
name = "transit-gateway-flow-logs-policy"
role = aws_iam_role.flow_logs.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams"
]
Effect = "Allow"
Resource = "*"
}
]
})
}
# CloudWatch Insights queries for flow logs
resource "aws_cloudwatch_query_definition" "top_talkers" {
name = "TransitGateway-TopTalkers-${var.environment}"
log_group_names = [
aws_cloudwatch_log_group.flow_logs.name
]
query_string = <<-QUERY
fields @timestamp, srcAddr, dstAddr, bytes
| filter action = "ACCEPT"
| stats sum(bytes) as totalBytes by srcAddr, dstAddr
| sort totalBytes desc
| limit 20
QUERY
}
resource "aws_cloudwatch_query_definition" "rejected_connections" {
name = "TransitGateway-RejectedConnections-${var.environment}"
log_group_names = [
aws_cloudwatch_log_group.flow_logs.name
]
query_string = <<-QUERY
fields @timestamp, srcAddr, dstAddr, srcPort, dstPort, protocol
| filter action = "REJECT"
| stats count() as rejectedCount by srcAddr, dstAddr, dstPort
| sort rejectedCount desc
| limit 50
QUERY
}
Multi-Region Transit Gateway Peering
Cross-Region Peering Configuration
# multi-region.tf
variable "peer_region" {
description = "Peer region for Transit Gateway peering"
type = string
default = "us-west-2"
}
# Create Transit Gateway in peer region
provider "aws" {
alias = "peer"
region = var.peer_region
}
resource "aws_ec2_transit_gateway" "peer" {
provider = aws.peer
description = "Transit Gateway for ${var.environment} in ${var.peer_region}"
amazon_side_asn = 64514
default_route_table_association = "disable"
default_route_table_propagation = "disable"
dns_support = "enable"
vpn_ecmp_support = "enable"
tags = {
Name = "tgw-${var.environment}-${var.peer_region}"
Environment = var.environment
Region = var.peer_region
}
}
# Create peering attachment
resource "aws_ec2_transit_gateway_peering_attachment" "main_to_peer" {
peer_region = var.peer_region
peer_transit_gateway_id = aws_ec2_transit_gateway.peer.id
transit_gateway_id = aws_ec2_transit_gateway.main.id
tags = {
Name = "tgw-peer-${var.aws_region}-to-${var.peer_region}"
Environment = var.environment
}
}
# Accept peering attachment in peer region
resource "aws_ec2_transit_gateway_peering_attachment_accepter" "peer" {
provider = aws.peer
transit_gateway_attachment_id = aws_ec2_transit_gateway_peering_attachment.main_to_peer.id
tags = {
Name = "tgw-peer-${var.peer_region}-from-${var.aws_region}"
Environment = var.environment
}
}
# Add routes for cross-region traffic
resource "aws_ec2_transit_gateway_route" "main_to_peer_cidrs" {
for_each = toset(["10.1.0.0/16", "10.2.0.0/16"]) # Peer region VPC CIDRs
destination_cidr_block = each.value
transit_gateway_attachment_id = aws_ec2_transit_gateway_peering_attachment.main_to_peer.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.production.id
}
resource "aws_ec2_transit_gateway_route" "peer_to_main_cidrs" {
provider = aws.peer
for_each = toset(["10.10.0.0/16", "10.20.0.0/16"]) # Main region VPC CIDRs
destination_cidr_block = each.value
transit_gateway_attachment_id = aws_ec2_transit_gateway_peering_attachment.main_to_peer.id
transit_gateway_route_table_id = aws_ec2_transit_gateway_route_table.production.id
}
Troubleshooting and Operations
Diagnostic Scripts
#!/usr/bin/env python3
"""
transit_gateway_diagnostics.py - Comprehensive Transit Gateway diagnostics
"""
import boto3
import json
from datetime import datetime, timedelta
from typing import Dict, List, Any
class TransitGatewayDiagnostics:
def __init__(self, region: str):
self.ec2 = boto3.client('ec2', region_name=region)
self.cloudwatch = boto3.client('cloudwatch', region_name=region)
self.region = region
def get_transit_gateway_details(self, tgw_id: str) -> Dict[str, Any]:
"""Get Transit Gateway configuration details"""
response = self.ec2.describe_transit_gateways(
TransitGatewayIds=[tgw_id]
)
return response['TransitGateways'][0]
def get_attachments(self, tgw_id: str) -> List[Dict[str, Any]]:
"""Get all Transit Gateway attachments"""
attachments = []
paginator = self.ec2.get_paginator('describe_transit_gateway_attachments')
for page in paginator.paginate(
Filters=[{'Name': 'transit-gateway-id', 'Values': [tgw_id]}]
):
attachments.extend(page['TransitGatewayAttachments'])
return attachments
def get_route_tables(self, tgw_id: str) -> List[Dict[str, Any]]:
"""Get all Transit Gateway route tables"""
route_tables = []
paginator = self.ec2.get_paginator('describe_transit_gateway_route_tables')
for page in paginator.paginate(
Filters=[{'Name': 'transit-gateway-id', 'Values': [tgw_id]}]
):
route_tables.extend(page['TransitGatewayRouteTables'])
return route_tables
def get_routes(self, route_table_id: str) -> List[Dict[str, Any]]:
"""Get routes from a Transit Gateway route table"""
response = self.ec2.search_transit_gateway_routes(
TransitGatewayRouteTableId=route_table_id,
Filters=[{'Name': 'state', 'Values': ['active', 'blackhole']}]
)
return response['Routes']
def check_vpn_health(self, tgw_id: str) -> List[Dict[str, Any]]:
"""Check VPN connection health"""
vpn_status = []
# Get VPN attachments
attachments = [a for a in self.get_attachments(tgw_id)
if a['ResourceType'] == 'vpn']
for attachment in attachments:
vpn_id = attachment['ResourceId']
# Get VPN connection details
vpn_response = self.ec2.describe_vpn_connections(
VpnConnectionIds=[vpn_id]
)
if vpn_response['VpnConnections']:
vpn = vpn_response['VpnConnections'][0]
status = {
'vpn_id': vpn_id,
'state': vpn['State'],
'tunnels': []
}
for i, tunnel in enumerate(vpn['VgwTelemetry'], 1):
tunnel_info = {
'tunnel_number': i,
'status': tunnel['Status'],
'outside_ip': tunnel.get('OutsideIpAddress'),
'last_status_change': tunnel.get('LastStatusChange'),
'status_message': tunnel.get('StatusMessage')
}
status['tunnels'].append(tunnel_info)
# Get CloudWatch metrics for tunnel
metrics = self.get_vpn_tunnel_metrics(vpn_id, i)
tunnel_info['metrics'] = metrics
vpn_status.append(status)
return vpn_status
def get_vpn_tunnel_metrics(self, vpn_id: str, tunnel: int) -> Dict[str, Any]:
"""Get CloudWatch metrics for VPN tunnel"""
end_time = datetime.utcnow()
start_time = end_time - timedelta(hours=1)
metrics = {}
# Tunnel data in/out
for metric_name in ['TunnelDataIn', 'TunnelDataOut']:
response = self.cloudwatch.get_metric_statistics(
Namespace='AWS/VPN',
MetricName=metric_name,
Dimensions=[
{'Name': 'VpnId', 'Value': vpn_id},
{'Name': 'TunnelIpAddress', 'Value': str(tunnel)}
],
StartTime=start_time,
EndTime=end_time,
Period=300,
Statistics=['Sum', 'Average']
)
if response['Datapoints']:
metrics[metric_name.lower()] = {
'average': sum(d['Average'] for d in response['Datapoints']) / len(response['Datapoints']),
'total': sum(d['Sum'] for d in response['Datapoints'])
}
return metrics
def check_attachment_bandwidth(self, tgw_id: str) -> List[Dict[str, Any]]:
"""Check bandwidth utilization for attachments"""
attachments = self.get_attachments(tgw_id)
bandwidth_stats = []
end_time = datetime.utcnow()
start_time = end_time - timedelta(hours=1)
for attachment in attachments:
attachment_id = attachment['TransitGatewayAttachmentId']
stats = {
'attachment_id': attachment_id,
'resource_type': attachment['ResourceType'],
'resource_id': attachment['ResourceId'],
'state': attachment['State']
}
# Get bytes in/out metrics
for metric_name in ['BytesIn', 'BytesOut']:
response = self.cloudwatch.get_metric_statistics(
Namespace='AWS/TransitGateway',
MetricName=metric_name,
Dimensions=[
{'Name': 'TransitGateway', 'Value': tgw_id},
{'Name': 'TransitGatewayAttachment', 'Value': attachment_id}
],
StartTime=start_time,
EndTime=end_time,
Period=300,
Statistics=['Average', 'Maximum', 'Sum']
)
if response['Datapoints']:
datapoints = response['Datapoints']
stats[metric_name.lower()] = {
'average_bps': sum(d['Average'] for d in datapoints) / len(datapoints) * 8,
'max_bps': max(d['Maximum'] for d in datapoints) * 8,
'total_bytes': sum(d['Sum'] for d in datapoints)
}
bandwidth_stats.append(stats)
return bandwidth_stats
def verify_routing(self, tgw_id: str) -> Dict[str, Any]:
"""Verify routing configuration"""
route_tables = self.get_route_tables(tgw_id)
routing_info = {
'route_tables': [],
'issues': []
}
for rt in route_tables:
rt_id = rt['TransitGatewayRouteTableId']
rt_info = {
'route_table_id': rt_id,
'state': rt['State'],
'routes': [],
'associations': [],
'propagations': []
}
# Get routes
routes = self.get_routes(rt_id)
for route in routes:
route_info = {
'destination': route['DestinationCidrBlock'],
'type': route['Type'],
'state': route['State']
}
if 'TransitGatewayAttachments' in route:
route_info['attachment'] = route['TransitGatewayAttachments'][0].get('TransitGatewayAttachmentId')
rt_info['routes'].append(route_info)
# Check for blackhole routes
if route['State'] == 'blackhole':
routing_info['issues'].append({
'type': 'blackhole_route',
'route_table': rt_id,
'destination': route['DestinationCidrBlock']
})
# Get associations
assoc_response = self.ec2.get_transit_gateway_route_table_associations(
TransitGatewayRouteTableId=rt_id
)
rt_info['associations'] = [
{
'attachment_id': a['TransitGatewayAttachmentId'],
'resource_type': a['ResourceType'],
'resource_id': a['ResourceId'],
'state': a['State']
}
for a in assoc_response['Associations']
]
# Get propagations
prop_response = self.ec2.get_transit_gateway_route_table_propagations(
TransitGatewayRouteTableId=rt_id
)
rt_info['propagations'] = [
{
'attachment_id': p['TransitGatewayAttachmentId'],
'resource_type': p['ResourceType'],
'resource_id': p['ResourceId'],
'state': p['State']
}
for p in prop_response['TransitGatewayRouteTablePropagations']
]
routing_info['route_tables'].append(rt_info)
return routing_info
def generate_report(self, tgw_id: str) -> str:
"""Generate comprehensive diagnostic report"""
print(f"Generating diagnostic report for Transit Gateway: {tgw_id}")
report = {
'timestamp': datetime.utcnow().isoformat(),
'region': self.region,
'transit_gateway_id': tgw_id,
'details': self.get_transit_gateway_details(tgw_id),
'attachments': self.get_attachments(tgw_id),
'vpn_health': self.check_vpn_health(tgw_id),
'bandwidth_utilization': self.check_attachment_bandwidth(tgw_id),
'routing': self.verify_routing(tgw_id)
}
return json.dumps(report, indent=2, default=str)
def main():
import argparse
parser = argparse.ArgumentParser(
description='Transit Gateway Diagnostics Tool'
)
parser.add_argument(
'--tgw-id',
required=True,
help='Transit Gateway ID'
)
parser.add_argument(
'--region',
default='us-east-1',
help='AWS region (default: us-east-1)'
)
parser.add_argument(
'--output',
help='Output file for report (default: stdout)'
)
args = parser.parse_args()
diagnostics = TransitGatewayDiagnostics(args.region)
report = diagnostics.generate_report(args.tgw_id)
if args.output:
with open(args.output, 'w') as f:
f.write(report)
print(f"Report saved to {args.output}")
else:
print(report)
if __name__ == '__main__':
main()
Best Practices and Recommendations
Security Considerations
Network Segmentation:
- Use separate route tables for different security zones
- Implement strict routing policies between environments
- Block unnecessary cross-environment traffic
Encryption:
- Always use IPSec for VPN connections
- Enable MACsec for Direct Connect when available
- Use TLS 1.2+ for all management connections
Access Control:
- Implement strict IAM policies for Transit Gateway management
- Use AWS Organizations SCPs for additional guardrails
- Enable CloudTrail logging for all API calls
Traffic Inspection:
- Deploy AWS Network Firewall for deep packet inspection
- Use VPC Flow Logs for traffic analysis
- Implement automated threat detection
High Availability Design
Redundancy:
- Deploy multiple VPN tunnels with ECMP
- Use Direct Connect with VPN backup
- Implement multi-AZ attachments
Monitoring:
- Configure CloudWatch alarms for all critical metrics
- Set up automated runbooks for common issues
- Implement synthetic monitoring for connectivity tests
Disaster Recovery:
- Document failover procedures
- Test DR scenarios regularly
- Maintain configuration backups
Cost Optimization
Attachment Management:
- Consolidate VPC attachments where possible
- Remove unused attachments promptly
- Monitor data transfer costs
Bandwidth Optimization:
- Use VPC peering for high-bandwidth same-region traffic
- Optimize application traffic patterns
- Implement caching strategies
Resource Tagging:
- Tag all resources for cost allocation
- Use AWS Cost Explorer for analysis
- Implement automated cost reporting
Conclusion
AWS Transit Gateway provides a robust, scalable solution for hybrid cloud connectivity. By following the patterns and best practices outlined in this guide, you can build an enterprise-grade network architecture that supports complex multi-VPC, multi-region, and hybrid scenarios while maintaining security, performance, and operational efficiency.
Key takeaways:
- Transit Gateway simplifies network architecture through centralized routing
- Proper route table segmentation enables security isolation
- VPN and Direct Connect integration provides reliable hybrid connectivity
- Comprehensive monitoring ensures operational visibility
- Following best practices reduces costs and improves reliability
The examples provided demonstrate production-ready configurations that can be adapted to your specific requirements, ensuring a secure and scalable hybrid cloud infrastructure.