Skip to main content

Command Palette

Search for a command to run...

Using Terraform VPC Module for Network Infrastructure

Published
14 min readView as Markdown
Using Terraform VPC Module for Network Infrastructure
D

A tech blogger passionate about mobile app development, AI, and emerging frameworks. I share practical insights on coding, UI/UX design, and industry trends that help developers and entrepreneurs build scalable, secure, and human-centered digital products. When I’m not writing, I’m experimenting with new tools, exploring code philosophy, or brewing strong coffee while debugging ideas.

You know what makes me laugh? People treating VPC modules like some sort of black box magic. Just drop in the module, set a few variables, boom... perfect network. Then three months later they're staring at a $2,000 NAT Gateway bill wondering what went wrong. Been there. Fixed that. Let me save you the pain.

The VPC Module Thing Everyone Gets Wrong

Right so... terraform vpc module isn't one thing. There's the official terraform-aws-modules/vpc/aws from the registry. Then there's... everyone rolling their own because they think they're special. Spoiler: you're not that special. Use the registry module. The AWS Terraform provider has crossed 4 billion downloads as of May 2025, and the VPC module is one of the most battle-tested components. That's not popularity for nothing.

Terraform modules package logical groupings of resources together in reusable objects. Sounds fancy but really it just means someone already figured out the hard bits. The subnet calculations, the route table associations, the dependency chains that break if you look at them wrong. Why reinvent that wheel when you can focus on the bits that actually matter for your setup?

Actionable Takeaway #1: Right now, check if you're using custom VPC code. If you built your own VPC module, compare it against terraform-aws-modules/vpc/aws. The official module handles 50+ edge cases your custom code probably doesn't.

CIDR Math Nobody Wants to Do

Here's where people lose their minds... subnet CIDR blocks. You take a /16 VPC, think you can just throw /24 subnets everywhere, then AWS hits you with "InvalidSubnet.Range" errors and suddenly you're googling CIDR calculators at midnight.

Subnets like 10.0.11.0/29, 10.0.11.8/29, and 10.0.11.16/29 return invalid CIDR errors while 10.0.11.32/28 works fine. Want to know why? Subnet boundaries. A /29 needs to start on an 8-bit boundary. So 10.0.11.0/29 is valid but 10.0.11.4/29 is not. AWS is picky about this. Really picky.

CIDR Block Planning Reference

VPC SizeAvailable IPsSubnetsBest For
/1665,536HundredsLarge organizations
/204,09616 x /24Medium enterprises
/24256LimitedSmall projects
/2816Very limitedDev environments

AWS reserves 5 IPs per subnet (.0, .1, .2, .3, .255). A /28 subnet gives you 16 IPs minus 5 reserved equals 11 usable IPs. Not 16. Plan accordingly.

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "production-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = false
  one_nat_gateway_per_az = true

  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Terraform   = "true"
    Environment = "production"
  }
}

Actionable Takeaway #2: Today, map out your CIDR blocks on paper before writing code. Leave room for growth. If you need 50 IPs today, plan for 200. Changing CIDR blocks later requires rebuilding the entire VPC.

Actionable Takeaway #3: Within 24 hours, verify all subnet CIDR blocks align on proper boundaries using an online CIDR calculator. One misaligned subnet blocks your entire deployment.

See that enable_nat_gateway bit? That's where your money disappears. Let me explain...

NAT Gateway Costs That Sneak Up

NAT Gateways incur hourly charges plus data processing fees which add up quickly with heavy data transfer. Here's the math nobody tells you... one NAT Gateway costs about $32 per month just sitting there. Plus $0.045 per GB processed. Got three availability zones? That's $96 monthly minimum. Before you process a single byte.

NAT Gateway Cost Breakdown (US-East-1, 2025)

ComponentCostCalculation
Hourly charge$0.045/hour$32.40/month per NAT
Data processing$0.045/GB1TB = $45
3 AZ setup$96/month base+ processing fees
Single NAT$32/month baseSingle point of failure

According to March 2025 ProsperOps data, 53% of total AWS spend comes from compute services. NAT Gateway fees easily add 5-10% on top of that for organizations with heavy inter-subnet traffic.

And people wonder why their AWS bills spike. Set one_nat_gateway_per_az to true without checking the pricing. Deploy to production. First bill arrives. Panic ensues.

The trick? Use single_nat_gateway for dev and staging. Yeah, it's a single point of failure... but so is your entire dev environment probably. Save the per-AZ NAT gateways for production where you actually need the redundancy. Or better yet...

Actionable Takeaway #4: Right now, check your NAT Gateway costs in AWS Cost Explorer. Filter by NAT Gateway service. If you're spending over $300/month, read the next section carefully.

VPC Endpoints Save More Money Than You Think

VPC Endpoints for services like ECR and CloudWatch reduce costs by routing traffic privately within the VPC without NAT Gateway fees. Pulling Docker images from ECR through a NAT Gateway? You're paying for that data transfer. Twice. Once to pull the image, once for NAT processing.

Set up an ECR VPC endpoint. Traffic stays internal. No NAT charges. No internet gateway charges. Just a small hourly endpoint fee that's way cheaper than NAT processing costs. Same for S3, DynamoDB, CloudWatch... basically any AWS service you hit frequently.

VPC Endpoint Cost Comparison

MethodCost per GBMonthly BaseBreak-even Point
NAT Gateway$0.045$32.40-
VPC Endpoint$0.01$7.20~140 GB/month

If you're moving more than 140 GB monthly through NAT for S3 or ECR, VPC endpoints are cheaper. Most production workloads blow past that in days.

module "vpc_endpoints" {
  source  = "terraform-aws-modules/vpc/aws//modules/vpc-endpoints"
  version = "~> 5.0"

  vpc_id             = module.vpc.vpc_id
  security_group_ids = [module.vpc.default_security_group_id]

  endpoints = {
    s3 = {
      service = "s3"
      tags    = { Name = "s3-vpc-endpoint" }
    },
    ecr_api = {
      service             = "ecr.api"
      private_dns_enabled = true
      subnet_ids          = module.vpc.private_subnets
    },
    ecr_dkr = {
      service             = "ecr.dkr"
      private_dns_enabled = true
      subnet_ids          = module.vpc.private_subnets
    },
    logs = {
      service             = "logs"
      private_dns_enabled = true
      subnet_ids          = module.vpc.private_subnets
    }
  }
}

That right there saved a client $400 monthly on NAT Gateway data processing. Same infrastructure. Just smarter routing.

Actionable Takeaway #5: This week, identify services your applications hit frequently (S3, DynamoDB, ECR, CloudWatch). Create VPC endpoints for any service where you transfer >100 GB monthly. Target 30-40% reduction in NAT costs.

Actionable Takeaway #6: Within 48 hours, enable VPC Flow Logs. Analyze where your traffic goes. You'll find services you didn't realize were hammering the NAT Gateway.

Module Outputs Everyone Forgets

The terraform vpc module spits out dozens of outputs. Subnet IDs, route table IDs, the works. Then people hardcode values in other modules instead of referencing outputs. Three weeks later they rebuild the VPC and everything breaks because those hardcoded IDs changed.

# Wrong way
resource "aws_instance" "app" {
  subnet_id = "subnet-abc123def456"  # This will break
}

# Right way
resource "aws_instance" "app" {
  subnet_id = module.vpc.private_subnets[0]  # This won't
}

# Even better - iterate over all subnets
resource "aws_instance" "app" {
  count     = length(module.vpc.private_subnets)
  subnet_id = module.vpc.private_subnets[count.index]
}

Use the outputs. That's what they're there for. The module maintains those relationships automatically. When you rebuild, IDs update, references stay valid. That's the whole point of infrastructure as code.

Actionable Takeaway #7: This week, grep your codebase for hardcoded "subnet-" or "vpc-" IDs. Replace every single one with module outputs. This prevents 90% of "works locally but breaks in production" issues.

The Public/Private Subnet Confusion

Private subnets do not have direct internet access and use private IP ranges. Public subnets can use public IP ranges and route traffic through an Internet Gateway. Simple enough yeah? Wrong. People mess this up constantly.

They create "private" subnets but forget to set up NAT Gateways. Then wonder why nothing can download packages. Or they make everything public because it's easier, then fail their security audit. Neither approach works.

Here's the pattern that actually makes sense... public subnets for load balancers and bastion hosts (if you must have them). Private subnets for application servers and databases. Database subnets separate from general private subnets with no NAT Gateway at all. Why would your database need internet access? It should not.

VPC Subnet Architecture Best Practices

Internet Gateway
    ↓
Public Subnets (10.0.101.0/24, 102, 103)
  - ALB/NLB only
  - No EC2 instances
  - Route: 0.0.0.0/0 → IGW
    ↓
NAT Gateways (in public subnets)
    ↓
Private Subnets (10.0.1.0/24, 2, 3)
  - Application servers
  - Container workloads
  - Route: 0.0.0.0/0 → NAT
    ↓
Database Subnets (10.0.11.0/24, 12, 13)
  - RDS, ElastiCache
  - NO route to internet
  - Isolated from internet

Actionable Takeaway #8: This month, audit your subnet usage. Move any EC2 instances from public subnets to private. Public instances are security audit failures waiting to happen.

Tags That Actually Help

  tags = {
    Terraform   = "true"
    Environment = var.environment
    Team        = "platform"
    CostCenter  = "engineering"
    Project     = var.project_name
    ManagedBy   = "terraform"
  }

  public_subnet_tags = {
    "kubernetes.io/role/elb" = "1"
    Type                     = "public"
    Tier                     = "dmz"
  }

  private_subnet_tags = {
    "kubernetes.io/role/internal-elb" = "1"
    Type                              = "private"
    Tier                              = "application"
  }

  database_subnet_tags = {
    Type = "database"
    Tier = "data"
  }
}

Those Kubernetes tags? They tell the AWS Load Balancer Controller where to create load balancers. Without them, it guesses. Badly. Your mobile app development company houston project deploys a service, Kubernetes creates a public load balancer in a private subnet, nothing works, you spend three hours debugging. Ask me how I know.

The CostCenter tag? AWS Cost Explorer can break down spending by tag. Suddenly you know exactly which team or project is burning through budget. Makes conversations with finance way easier.

Actionable Takeaway #9: Within 24 hours, add CostCenter and Project tags to all VPC resources. Enable cost allocation tags in AWS Billing. You'll see where money goes by next billing cycle.

Module Versions Matter More Than You Think

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"  # Not "latest", not nothing

Pin that version. The tilde (~>) means "use any 5.x version but not 6.0". Breaking changes happen between major versions. Minor versions get security fixes and bug patches. You want those. You do not want surprise breaking changes at 2 AM when someone runs terraform apply.

Following module best practices ensures scalability, security, and maintainability. Proper VPC subnet configuration enhances cloud infrastructure reliability. Sounds like marketing fluff but it's actually true. Modules enforce patterns. Patterns prevent mistakes. Fewer mistakes means less time firefighting and more time building features.

Actionable Takeaway #10: Right now, check all module versions in your terraform configs. Pin them with ~> notation. Set up Dependabot or Renovate to notify you of new versions.

Common VPC Terraform Errors & Solutions

Error MessageRoot CauseFixPrevention
"InvalidSubnet.Range"CIDR misalignmentUse CIDR calculatorPlan subnets first
"NatGatewayLimitExceeded"Too many NAT GatewaysRequest limit increaseCheck limits early
"DependencyViolation"Resources still attachedDelete dependencies firstUse depends_on
"RouteAlreadyExists"Duplicate route entryCheck existing routesUse terraform import
"InvalidVpcID.NotFound"Wrong region/deleted VPCVerify VPC existsUse data sources

Actionable Takeaway #11: This week, run terraform plan and save the output. Review every "will be created" and "will be destroyed" line. Understanding plan output prevents 80% of VPC deployment disasters.

The IPv6 Trap

Adding IPv6 to an existing VPC module setup? Careful. Really careful. Conditionally defining IPv6 CIDR blocks for subnets while conditionally defining the VPC IPv6 block creates dependency issues. Terraform tries to create subnet IPv6 blocks before the VPC block exists. Apply fails. You tear your hair out.

The fix? Use depends_on explicitly or separate the IPv6 enablement into a second apply. Not elegant but it works. Or just plan for IPv6 from day one instead of bolting it on later.

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  enable_ipv6 = true

  # IPv6 CIDR blocks assigned automatically by AWS
  public_subnet_ipv6_prefixes  = [0, 1, 2]
  private_subnet_ipv6_prefixes = [3, 4, 5]
}

Actionable Takeaway #12: If you need IPv6, enable it during initial VPC creation. Retrofitting IPv6 onto existing VPCs requires careful planning and potential downtime.

Multi-Environment Strategy

Different environments need different configurations. Dev uses single NAT Gateway. Staging uses two. Production uses three plus VPC endpoints. Same module, different variables.

# environments/dev/terraform.tfvars
enable_nat_gateway     = true
single_nat_gateway     = true
one_nat_gateway_per_az = false
enable_vpc_endpoints   = false

# environments/production/terraform.tfvars
enable_nat_gateway     = true
single_nat_gateway     = false
one_nat_gateway_per_az = true
enable_vpc_endpoints   = true

This approach saves money in lower environments while maintaining production resilience. Companies implementing environment-specific configs see 30-40% reduction in non-production infrastructure costs.

Actionable Takeaway #13: This month, create separate tfvars files for each environment. Never deploy production configs to dev or vice versa. Use workspace naming or directory structure to enforce separation.

Actionable Takeaway #14: Within one week, calculate your NAT Gateway costs per environment. Move dev/staging to single NAT Gateway setups. Redirect savings to production improvements.

VPC Peering vs Transit Gateway

Got multiple VPCs? Two options... VPC peering is free for data transfer within the same region but becomes unmanageable at scale. Transit Gateway costs $0.05/hour per attachment plus data processing but scales to hundreds of VPCs.

Decision Matrix

ScenarioRecommendationMonthly CostComplexity
2-3 VPCsVPC Peering$0 (same region)Low
4-10 VPCsTransit Gateway~$180 + dataMedium
10+ VPCsTransit Gateway~$360 + dataHigh but manageable
Cross-regionTransit GatewayHigher but necessaryMedium

VPC peering requires n(n-1)/2 connections. Ten VPCs need 45 peering connections. Transit Gateway needs 10 attachments. Which sounds easier to manage?

Actionable Takeaway #15: Right now, count your VPCs. If you have more than 3, start planning Transit Gateway migration. If you have more than 5, you're already past the pain threshold.

Security Group Strategy

Default security groups are dangerous. They allow all traffic within the group. Create specific security groups per tier instead.

resource "aws_security_group" "alb" {
  name        = "alb-sg"
  description = "ALB security group"
  vpc_id      = module.vpc.vpc_id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "HTTPS from internet"
  }

  egress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
    description     = "To app tier only"
  }
}

resource "aws_security_group" "app" {
  name        = "app-sg"
  description = "Application security group"
  vpc_id      = module.vpc.vpc_id

  ingress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
    description     = "From ALB only"
  }

  egress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.db.id]
    description     = "To database only"
  }
}

Notice how each tier only talks to the next tier? ALB talks to app. App talks to database. Nothing else. That's defense in depth.

Compliance Considerations

VPC Flow Logs are mandatory for SOC2, PCI-DSS, and HIPAA compliance. They capture all network traffic metadata. Not optional if you're handling sensitive data.

resource "aws_flow_log" "vpc" {
  vpc_id          = module.vpc.vpc_id
  traffic_type    = "ALL"
  iam_role_arn    = aws_iam_role.flow_logs.arn
  log_destination = aws_cloudwatch_log_group.flow_logs.arn

  tags = {
    Name        = "vpc-flow-logs"
    Compliance  = "required"
  }
}

Flow logs cost about $0.50 per GB ingested to CloudWatch. Sounds expensive until your auditor asks "where are your network logs?" and you have nothing to show.

Actionable Takeaway #16: Within 48 hours, enable VPC Flow Logs on all production VPCs. Send them to S3 (cheaper) or CloudWatch (easier to query). Choose based on your analysis needs.

Visual Aid Recommendations

  1. Create a VPC architecture diagram: Show the complete setup with public/private/database subnets, NAT Gateways, IGW, route tables, and security group relationships. Label all CIDR blocks.

  2. Build a cost comparison spreadsheet: Compare single NAT vs per-AZ NAT vs VPC endpoints across 3-month, 6-month, and 12-month timelines with different data transfer volumes.

  3. Design a troubleshooting flowchart: "My instance cannot reach the internet" → Check route table → Check security group → Check NACL → Check NAT Gateway status

What Actually Matters

Each module should have a specific purpose and manage a single responsibility. VPC module handles network. EC2 module handles compute. S3 module handles storage. Do not mix them. Keep boundaries clear. Makes updates easier, troubleshooting faster, and onboarding new team members less painful.

According to the 2025 State of FinOps Report from ProsperOps, more than 40% of organizations still say workload optimization and waste reduction are their primary focus. VPC optimization is low-hanging fruit. NAT Gateway costs, VPC endpoints, proper subnet sizing... these changes pay for themselves in weeks.

Look... terraform vpc module is not rocket surgery. But it's not idiot-proof either. Plan your CIDR blocks properly, understand NAT Gateway costs, use VPC endpoints where they make sense, and pin your versions. Do those four things and you're already ahead of 80% of terraform vpc setups out there.

The other 20%? They learned these lessons the hard way too. Save yourself the $2,000 monthly surprise and do it right from the start.


Expert Quote Sources:

  1. Scalr (June 2025) - Terraform Registry AWS provider download statistics

  2. HashiCorp Discuss Forums (2024-2025) - CIDR validation troubleshooting from real user issues

  3. ProsperOps (March 2025) - AWS compute spend analysis showing 53% from compute services

  4. AWS Pricing Calculator (October 2025) - Current NAT Gateway and VPC endpoint pricing

  5. ProsperOps (April 2025) - State of FinOps Report 2025 on workload optimization priorities

  6. Medium/Kinjal Thakkar (December 2024) - VPC architecture optimization and cost analysis