Terraform for Cloud Networking: Complete Infrastructure Guide
Infrastructure as Code transforms network management from click-ops to repeatable, version-controlled, and reviewable configurations. This guide covers Terraform patterns for cloud networking across AWS, Azure, and GCP.
Why Terraform for Networking?
Network infrastructure benefits enormously from IaC:
- Repeatability: Deploy identical environments for dev/staging/prod
- Version control: Track changes, review PRs, rollback if needed
- Documentation: Code IS the documentation
- Drift detection: Catch manual changes that deviate from desired state
- Multi-cloud: Manage AWS, Azure, GCP with consistent tooling
Terraform Networking Fundamentals
Resource Dependencies
Network resources have complex dependencies. Terraform handles this, but understanding helps:
# Dependency chain
VPC → Subnets → Route Tables → Routes
VPC → Internet Gateway → NAT Gateway (public subnet)
VPC → Security Groups → References to other SGs
Subnets → Load Balancers → Target Groups
State Management
Networking state requires special attention:
- Remote state: Always use S3/GCS/Azure Blob with locking
- State isolation: Separate networking state from application state
- Cross-stack references: Use
terraform_remote_statedata sources
AWS Networking with Terraform
VPC Module Structure
# modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
}
}
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.environment}-public-${var.availability_zones[count.index]}"
Type = "public"
}
}
resource "aws_subnet" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.environment}-private-${var.availability_zones[count.index]}"
Type = "private"
}
}
Internet and NAT Gateways
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
resource "aws_eip" "nat" {
count = var.single_nat ? 1 : length(var.availability_zones)
domain = "vpc"
}
resource "aws_nat_gateway" "main" {
count = var.single_nat ? 1 : length(var.availability_zones)
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
depends_on = [aws_internet_gateway.main]
}
Route Tables
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
}
resource "aws_route_table" "private" {
count = var.single_nat ? 1 : length(var.availability_zones)
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[var.single_nat ? 0 : count.index].id
}
}
Security Groups Best Practices
Modular Security Groups
# Security group with dynamic rules
resource "aws_security_group" "web" {
name_prefix = "${var.environment}-web-"
vpc_id = aws_vpc.main.id
dynamic "ingress" {
for_each = var.web_ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
security_groups = ingress.value.security_groups
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
lifecycle {
create_before_destroy = true
}
}
# Reference other security groups
resource "aws_security_group" "app" {
name_prefix = "${var.environment}-app-"
vpc_id = aws_vpc.main.id
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
}
Load Balancer Configuration
resource "aws_lb" "main" {
name = "${var.environment}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = aws_subnet.public[*].id
enable_deletion_protection = var.environment == "production"
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = var.certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
resource "aws_lb_target_group" "app" {
name = "${var.environment}-app-tg"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.main.id
target_type = "ip"
health_check {
path = "/health"
healthy_threshold = 2
unhealthy_threshold = 3
interval = 30
}
}
Multi-Cloud Patterns
Provider Configuration
# providers.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
provider "aws" {
region = var.aws_region
}
provider "google" {
project = var.gcp_project
region = var.gcp_region
}
provider "azurerm" {
features {}
subscription_id = var.azure_subscription_id
}
Abstraction Layer
Create abstractions for multi-cloud consistency:
# modules/network/main.tf
# Uses count or for_each to create provider-specific resources
locals {
is_aws = var.provider == "aws"
is_gcp = var.provider == "gcp"
is_azure = var.provider == "azure"
}
module "aws_vpc" {
source = "./aws"
count = local.is_aws ? 1 : 0
# ...
}
module "gcp_vpc" {
source = "./gcp"
count = local.is_gcp ? 1 : 0
# ...
}
Terraform Patterns for Networking
CIDR Calculation
# Use cidrsubnet for consistent subnet allocation
locals {
vpc_cidr = "10.0.0.0/16"
# Public: 10.0.0.0/24, 10.0.1.0/24, 10.0.2.0/24
public_subnets = [for i in range(3) : cidrsubnet(local.vpc_cidr, 8, i)]
# Private: 10.0.10.0/24, 10.0.11.0/24, 10.0.12.0/24
private_subnets = [for i in range(3) : cidrsubnet(local.vpc_cidr, 8, i + 10)]
# Database: 10.0.20.0/24, 10.0.21.0/24, 10.0.22.0/24
database_subnets = [for i in range(3) : cidrsubnet(local.vpc_cidr, 8, i + 20)]
}
Environment-Specific Configuration
# Use locals to define environment differences
locals {
env_config = {
development = {
single_nat = true
instance_type = "t3.small"
multi_az = false
}
staging = {
single_nat = true
instance_type = "t3.medium"
multi_az = true
}
production = {
single_nat = false
instance_type = "t3.large"
multi_az = true
}
}
config = local.env_config[var.environment]
}
Common Pitfalls
- Security group cycles: SG A references B, B references A—use
aws_security_group_ruleseparately - Destroying in wrong order: Load balancers must be destroyed before target groups
- NAT Gateway EIP: Remember EIPs cost money when not attached
- State drift: Console changes create drift—run
terraform planregularly
Testing Network Infrastructure
# Use Terratest for infrastructure testing
func TestVpcCreation(t *testing.T) {
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../examples/vpc",
})
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcId)
}
Key Takeaways
- Use modules for reusable network components
- Store state remotely with locking enabled
- Use
cidrsubnet()for consistent IP allocation - Create abstractions for multi-cloud deployments
- Test infrastructure with Terratest or similar
Need Help with Network Infrastructure as Code?
We implement Terraform-based networking solutions. Contact us for a consultation.