Terraform Starter — Production AWS VPC (3-AZ + NAT + Flow Logs)
The VPC every team needs as the foundation of any AWS account. Public + private + database subnets across 3 AZs, single NAT for dev / 3 NAT for prod, S3 + DynamoDB VPC endpoints, and Flow Logs to CloudWatch.
This is the canonical starter VPC that most production AWS accounts reuse. It uses the well-known terraform-aws-modules/vpc/aws module pinned to v5.x, exposes the most useful inputs, and is opinionated about the right defaults (Flow Logs ON, IPv6 enabled, S3+DynamoDB Gateway endpoints to dodge NAT costs).
1. Backend & provider
Store state in S3 with DynamoDB locking. Always pin both Terraform and AWS provider versions.
terraform {
required_version = ">= 1.9.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.60"
}
}
backend "s3" {
bucket = "cloudadhar-tf-state-prod"
key = "network/vpc/terraform.tfstate"
region = "ap-south-1"
dynamodb_table = "cloudadhar-tf-locks"
encrypt = true
}
}
provider "aws" {
region = var.region
default_tags {
tags = {
Project = var.project
Env = var.env
ManagedBy = "Terraform"
Owner = "cloudadhar"
}
}
}2. Variables
Keep inputs minimal. Defaults work for dev; override for prod via .tfvars files.
variable "project" { type = string default = "cloudadhar" }
variable "env" { type = string default = "dev" }
variable "region" { type = string default = "ap-south-1" }
variable "vpc_cidr" { type = string default = "10.20.0.0/16" }
variable "azs" { type = list(string) default = ["ap-south-1a","ap-south-1b","ap-south-1c"] }
variable "single_nat" { type = bool default = true } # set false in prod
variable "enable_ipv6"{ type = bool default = true }3. The VPC itself
Use the community VPC module — battle-tested, supports every flag you'll ever need. Subnets are auto-sized via cidrsubnets().
locals {
name = "${var.project}-${var.env}"
# Auto-slice the /16 into 9 /20s: 3 public, 3 private, 3 database
public_subnets = [for i in range(3) : cidrsubnet(var.vpc_cidr, 4, i)]
private_subnets = [for i in range(3) : cidrsubnet(var.vpc_cidr, 4, i + 3)]
database_subnets = [for i in range(3) : cidrsubnet(var.vpc_cidr, 4, i + 6)]
}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.13"
name = local.name
cidr = var.vpc_cidr
azs = var.azs
public_subnets = local.public_subnets
private_subnets = local.private_subnets
database_subnets = local.database_subnets
# NAT
enable_nat_gateway = true
single_nat_gateway = var.single_nat # 1 NAT in dev, 3 in prod
one_nat_gateway_per_az = !var.single_nat
# IPv6
enable_ipv6 = var.enable_ipv6
public_subnet_assign_ipv6_address_on_creation = var.enable_ipv6
private_subnet_assign_ipv6_address_on_creation = var.enable_ipv6
# DNS
enable_dns_hostnames = true
enable_dns_support = true
# Flow Logs to CloudWatch (cheap + good for forensics)
enable_flow_log = true
create_flow_log_cloudwatch_log_group = true
create_flow_log_cloudwatch_iam_role = true
flow_log_max_aggregation_interval = 60
# Tags for EKS auto-discovery (safe even if you never use EKS here)
public_subnet_tags = { "kubernetes.io/role/elb" = "1" }
private_subnet_tags = { "kubernetes.io/role/internal-elb" = "1" }
}4. Free Gateway VPC endpoints
S3 + DynamoDB gateway endpoints cost ZERO and remove a huge chunk of NAT bandwidth costs. Always enable them.
resource "aws_vpc_endpoint" "s3" {
vpc_id = module.vpc.vpc_id
service_name = "com.amazonaws.${var.region}.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = module.vpc.private_route_table_ids
tags = { Name = "${local.name}-s3-endpoint" }
}
resource "aws_vpc_endpoint" "dynamodb" {
vpc_id = module.vpc.vpc_id
service_name = "com.amazonaws.${var.region}.dynamodb"
vpc_endpoint_type = "Gateway"
route_table_ids = module.vpc.private_route_table_ids
tags = { Name = "${local.name}-dynamodb-endpoint" }
}5. Outputs
Export what downstream stacks (EKS, RDS, ALB) will need.
output "vpc_id" { value = module.vpc.vpc_id }
output "private_subnets" { value = module.vpc.private_subnets }
output "public_subnets" { value = module.vpc.public_subnets }
output "database_subnets" { value = module.vpc.database_subnets }
output "nat_public_ips" { value = module.vpc.nat_public_ips }6. Apply
Standard plan → apply flow. Save the plan file so what you review is what gets applied.
terraform init
terraform plan -var-file=envs/dev.tfvars -out=tfplan
terraform apply tfplan
# Outputs will be available for the next stack (EKS, RDS, ALB...)
terraform output -json | jq '.vpc_id.value'Cost & cleanup
~$32/mo with single NAT (dev), ~$96/mo with 3 NAT (prod) before any traffic. Flow Logs add ~$2-5/mo for small clusters. Destroy with `terraform destroy` when you're done experimenting.
Want me to implement this in your environment?
Cloudadhar offers paid setup engagements: I bring the template above into your AWS account, wire it up to your CI/CD, and walk your team through it.