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

Pratiksha kadamPratiksha kadam
6 min read

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?

FeatureNAT GatewayNAT 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:

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.

0
Subscribe to my newsletter

Read articles from Pratiksha kadam directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Pratiksha kadam
Pratiksha kadam