How to Design a 3-Tier AWS VPC with NAT Gateways Using Terraform


What Is a 3-Tier VPC Architecture?
Ever wondered how to spin up a robust AWS network setup that follows best practices and scales with ease? That’s where Terraform and the power of modular infrastructure as code (IaC) come in. In this guide, we’re going to build a
3-Tier AWS VPC with NAT Gateways using Terraform
, starting from scratch and moving toward a reusable, standardized architecture.What is a NAT Gateway?
A NAT Gateway (Network Address Translation Gateway) is an AWS-managed service that allows resources in a private subnet (like EC2 instances, RDS databases, etc.) to access the internet (for software updates, downloading packages, etc.) without exposing them to incoming internet traffic.
In simpler terms:
It’s like a bouncer at a private club. Your instances can go out to the internet, but nothing from the internet gets back in unless you specifically allow it.
🔒 Why Do You Need NAT Gateways?
When designing a 3-tier architecture (public, private, and database subnets), private and database subnets shouldn't be directly exposed to the internet for security reasons.
But what if those instances need to:
Download software updates?
Connect to AWS APIs?
Install OS patches?
You can't attach a public IP to them. This is where a NAT Gateway comes in.
🧱 NAT Gateway Use Cases in AWS
Private EC2 instances accessing the internet.
Lambda functions in private subnets making external API calls.
Patching OS/DB updates in isolated environments.
⚙️ Why Use NAT Gateways in Terraform?
Terraform allows you to provision NAT Gateways automatically as part of your infrastructure code. Here's why it's super useful:
✅ 1. Automation
With Terraform, you don’t have to manually create:
Elastic IPs
NAT Gateways
Route table entries
You declare it once, and it’s done every time you apply.
✅ 2. Scalability
In larger setups, you might want multiple NAT Gateways — one per AZ. Terraform makes scaling this painless with modules and variables.
✅ 3. Consistency
Your environments (dev, staging, prod) get the exact same configuration — no “it worked on my AWS account” issues.
🔧 Quick Terraform Example of NAT Gateway
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "2.78.0"
# Enable NAT Gateway
enable_nat_gateway = true
single_nat_gateway = true
# Subnet Definitions
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
# Other Configs...
}
🚨 NAT Gateway vs. NAT Instance: What’s the Difference?
Feature | NAT Gateway | NAT Instance |
Managed by AWS | ✅ Yes | ❌ No |
High Availability | ✅ Yes (in single AZ or multi-AZ) | ❌ Manual setup required |
Performance | ✅ Scales automatically | ❌ Limited by instance type |
Maintenance | ✅ Minimal | ❌ You patch/manage it |
Cost | 💰 Higher | 🪙 Lower |
What Are Terraform Modules?
Think of Terraform modules as Lego blocks. Instead of writing the same code over and over, you group resources into reusable units — modules — making your infrastructure clean and scalable.
Benefits of Using Modules
Reusability: Define once, use everywhere.
Simplified management: Break complex deployments into manageable chunks.
Consistency: Reduce human errors and configuration drift
🧱 Step-2: Choosing the Right Terraform Registry Module
What Is the Terraform Public Registry?
A massive library of ready-to-use modules. The one we’re using is terraform-aws-modules/vpc/aws
.
Why HashiCorp Verified Modules Matter
Modules with the verified tag are tested, maintained, and trusted.
Evaluate Module Quality
Downloads: High download counts indicate community trust.
Release history: Frequent updates = actively maintained.
Feature support: Ensure it supports NAT Gateways, Subnets, IGW, etc.
Step 2: Creating the VPC Module Configuration
Files to create:
versions.tf
: Specifies provider and Terraform version.variables.tf
: Hardcoded input values.vpc.tf
: Contains themodule "vpc"
block.
Sample Module Block (Hardcoded)
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "2.78.0"
name = "vpc-dev"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
database_subnets = ["10.0.151.0/24", "10.0.152.0/24"]
create_database_subnet_group = true
create_database_subnet_route_table = true
enable_nat_gateway = true
single_nat_gateway = true
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Owner = "kalyan"
Environment = "dev"
}
}
🧪 Step-03: Running Terraform Commands
Initialize the Directory
terraform init
Check the .terraform
folder for downloaded modules.
Validate Configuration
terraform validate
Preview Changes
terraform plan
Apply Infrastructure
terraform apply -auto-approve
Verify
VPC
Public/Private/DB Subnets
Internet Gateway
NAT Gateway
Routes
Destroy When Done
terraform destroy -auto-approve
🔐 Step-04: Version Constraints in Terraform
Why Lock Module Versions?
Prevent unintentional breakages when module authors update their code.
version = "2.78.0"
Use specific versions for third-party modules; for internal ones, version ranges can be considered if releases are stable.
⚙️ Step-5: Standardized Module – v2-vpc-module-standardized
Switch from hardcoded to parameterized setup using variables and locals.
🧮 Step-06: Using Local Values
Local Values File: locals.tf
locals {
owners = var.business_divsion
environment = var.environment
name = "${var.business_divsion}-${var.environment}"
common_tags = {
owners = local.owners
environment = local.environment
}
}
📥 Step-07: Modularizing Input Variables
Split out variables into variables.tf
, keeping things clean. Here’s a sample:
variable "vpc_name" {
description = "VPC Name"
type = string
default = "myvpc"
}
Repeat for other subnet ranges, NAT configs, and AZs.
🔧 Step-08: Cleaned-Up vpc-module.tf
With all variables and locals in place, your module block becomes elegant and readable:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "2.78.0"
name = "${local.name}-${var.vpc_name}"
cidr = var.vpc_cidr_block
...
tags = local.common_tags
}
📤 Step-09: Output Configuration
Define these in outputs.tf
:
output "vpc_id" {
value = module.vpc.vpc_id
}
Other outputs include:
public_subnets
private_subnets
nat_public_ips
azs
📁 Step-10 & Step-11: Using .tfvars
Files
These files help manage values externally from your code.
terraform.tfvars
aws_region = "us-east-1"
environment = "dev"
business_divsion = "HR"
vpc.auto
.tfvars
vpc_name = "myvpc"
vpc_cidr_block = "10.0.0.0/16"
...
🚀 Step-12: Deploying the Standardized VPC
Repeat your Terraform commands:
terraform init
terraform validate
terraform plan
terraform apply -auto-approve
Post-Apply Checklist
✅ VPC created
✅ All subnets in place
✅ IGW and NAT configured
✅ No public route to private/DB subnets
🧹 Step-13: Clean Up
Destroy everything once you’re done:
terraform destroy -auto-approve
rm -rf .terraform* terraform.tfstate*
✅ Conclusion
Building a 3-Tier AWS VPC with NAT Gateways using Terraform doesn’t have to be complex. By leveraging modules, variables, and Terraform best practices, you can design infrastructure that’s reusable, scalable, and secure. Whether you're building a dev/test environment or a production-grade network, modular design gives you full control with minimal effort.
❓ FAQs
1. What is the advantage of using a single NAT Gateway?
It helps reduce costs in dev environments. For high availability, use multiple NATs.
2. Can I extend this setup to include private ECS clusters or RDS?
Absolutely. You just need to define additional subnet types and update your routing accordingly.
3. What happens if I don’t define version constraints?
You might face unexpected errors when module authors release breaking changes.
4. Is it necessary to use both terraform.tfvars
and .auto.tfvars
?
No, but using both helps separate general configs from resource-specific configs.
5. How do I test if private subnets can reach the internet via NAT?
Spin up a test EC2 in the private subnet and curl a public URL. No response means NAT is misconfigured.
Subscribe to my newsletter
Read articles from Pratiksha kadam directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
