Terraform AWS Modules: Production AWS in 20 Lines of HCL
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
- Your first module: a real VPC
- Understanding NAT gateway modes
- Private vs "intra" subnets
- Composing modules: VPC + EKS
- Practices that matter
- When NOT to use them
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:
| Mode | Flags | When |
|---|---|---|
| One per subnet (default) | enable_nat_gateway=true | Maximum redundancy, most expensive |
| Single NAT | single_nat_gateway=true | Dev/staging — one AZ outage kills egress, but cheap |
| One per AZ | one_nat_gateway_per_az=true | Production 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
- Terraform 101: Managing Cloud Infrastructure as Code
- Terraform Modules: Building Reusable Infrastructure
Last Updated: October 2026 Author: CloudOpsGuide Team Difficulty: Intermediate Estimated Reading Time: 10 minutes