CloudOpsGuide
terraform

Terraform AWS Modules: Production AWS in 20 Lines of HCL

Intermediate
10 minutes
October 2026
CloudOpsGuide Team

Terraform AWS Modules: Production AWS in 20 Lines of HCL

Writing raw Terraform for AWS works — until you need a VPC with public and private subnets across three availability zones, NAT gateways, route tables, and flow logs. That's easily 500 lines of HCL and a week of testing. The terraform-aws-modules organization on GitHub exists because everyone kept writing that same 500 lines. It's a community collection of 60+ battle-tested modules covering VPC, EKS, RDS, IAM, Lambda, ECS, S3, and more — each maintained, versioned, and documented. This tutorial shows how to use them properly, from the registry to a working production-shaped network.

Table of Contents

Why use community modules

A module is a reusable bundle of resources. The terraform-aws-modules collection encodes years of hard-won defaults: sane tagging, the right number of route tables, NAT gateway placement, security group rules that aren't 0.0.0.0/0 everywhere. Instead of learning every AWS edge case yourself, you learn the module's input variables.

The headline modules:

  • terraform-aws-modules/vpc/aws — VPCs, subnets, NAT gateways, NACLs, flow logs
  • terraform-aws-modules/eks/aws — EKS clusters and node groups
  • terraform-aws-modules/rds/aws — managed databases
  • terraform-aws-modules/iam/aws — roles, policies, OIDC providers
  • terraform-aws-modules/s3-bucket/aws — buckets with encryption and versioning
  • terraform-aws-modules/lambda/aws — Lambda including build/package/deploy plumbing

Your first module: a real VPC

Modules from the Terraform Registry use a namespaced source string — namespace/name/provider. Here's a production-shaped VPC in twenty lines:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"   # always pin a version

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

  azs             = ["eu-west-1a", "eu-west-1b", "eu-west-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"
  }
}

That one block creates the VPC, six subnets across three AZs, an internet gateway, three NAT gateways, route tables, and all the associations — resources that would take hundreds of lines by hand. Then:

terraform init    # downloads the module from the registry
terraform plan    # shows every resource it will create
terraform apply

Understanding NAT gateway modes

NAT gateways are where AWS bills get painful (~$32/month each plus data), so the module gives you three modes — pick deliberately:

ModeFlagsWhen
One per subnet (default)enable_nat_gateway=trueMaximum redundancy, most expensive
Single NATsingle_nat_gateway=trueDev/staging — one AZ outage kills egress, but cheap
One per AZone_nat_gateway_per_az=trueProduction sweet spot — AZ-level redundancy

If single_nat_gateway and one_nat_gateway_per_az are both true, single wins. Also note: by default the module allocates fresh Elastic IPs for NAT gateways — if you want stable egress IPs across VPC rebuilds (for IP allowlists), allocate EIPs yourself and pass them via reuse_nat_ips = true and external_nat_ip_ids.

Private vs "intra" subnets

A subtle but important distinction in the VPC module:

  • private_subnets get a route to the internet via NAT — for things that need outbound calls (app servers pulling images, calling APIs).
  • intra_subnets get no NAT route at all — for things that must stay RFC1918-internal: Lambda functions hitting VPC endpoints, internal databases, anything that should be unreachable-by-design rather than unreachable-by-firewall-rule.

Using intra_subnets for Lambda is a real cost pattern: Lambda scales ENIs with traffic, and intra subnets keep that traffic internal while letting you skip NAT data-processing charges.

Composing modules: VPC + EKS

Modules chain through outputs. The VPC module exports subnet IDs; EKS consumes them:

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"

  cluster_name    = "production"
  cluster_version = "1.31"

  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets

  eks_managed_node_groups = {
    general = {
      instance_types = ["t3.large"]
      min_size       = 2
      max_size       = 6
      desired_size   = 3
    }
  }
}

module.vpc.private_subnets is a module output — every module documents its outputs on its registry page. This is the "output contract" that makes composition work.

Practices that matter

  • Pin versions. version = "~> 5.0" accepts patches, blocks breaking majors. Unpinned modules are the #1 cause of "plan suddenly wants to recreate everything."
  • Read variables.tf first. These modules have hundreds of inputs — the README tables and examples/ directory in each repo are your reference.
  • Check create flags. Most modules accept create = false to make themselves conditional — useful for optional environments.
  • Tag once, in the module. Pass a tags map; modules propagate it to every resource. Your future cost-allocation self will thank you.
  • Watch for deprecations. Example: VPC flow logs inside the root module are deprecated in v6 — the standalone flow-log submodule is the replacement. Registry changelogs flag these.

When NOT to use them

Community modules trade control for speed. Skip them when you need non-standard networking, when your org requires all infrastructure code in-house, or when the module's abstraction fights you more than the raw resources would. The escape hatch: fork the module, or use it as reference documentation for writing your own — which connects directly to our Terraform modules tutorial.

Further reading: the terraform-aws-modules org and the Terraform Registry namespace. Pair this with Terraform State Explained — you'll want a remote backend before applying any of this for real.

Related Articles


Last Updated: October 2026 Author: CloudOpsGuide Team Difficulty: Intermediate Estimated Reading Time: 10 minutes