Batch 01 · Aarambh — AWS + Agentic AI starts 28 June 2026Batch 01 · Aarambh — AWS + Agentic AI starts 28 June 2026Batch 01 · Aarambh — AWS + Agentic AI starts 28 June 2026Batch 01 · Aarambh — AWS + Agentic AI starts 28 June 2026Batch 01 · Aarambh — AWS + Agentic AI starts 28 June 2026Batch 01 · Aarambh — AWS + Agentic AI starts 28 June 2026Batch 01 · Aarambh — AWS + Agentic AI starts 28 June 2026Batch 01 · Aarambh — AWS + Agentic AI starts 28 June 2026
All templates
Terraform · Template

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.

AWSTerraform 1.9+VPCNAT GatewayFlow Logs
Updated 2026-05-21 8 min View on GitHub

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.

versions.tfhcl
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.

variables.tfhcl
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().

main.tfhcl
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.

endpoints.tfhcl
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.

outputs.tfhcl
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.

shellbash
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.