Three layer terraform architecture
Source: Dev.to
Prerequisites
- Terraform ≥ 1.0
- AWS credentials with permissions to create S3 buckets, DynamoDB tables, KMS keys, VPCs, subnets, IGWs, and route tables.
Folder Structure
Create the following directory layout from the project root:
terraform-3layer-aws/
├── composition/
│ ├── remote-backend/
│ │ └── us-east-2/
│ │ └── prod/
│ └── vpc/
│ └── us-east-2/
│ └── prod/
├── infra/
│ ├── remote-backend/
│ └── vpc/
└── resource-modules/
├── storage/
│ └── s3-backend/
├── dynamodb/
│ └── backend/
├── kms/
│ └── backend/
└── vpc/
└── basic/
Resource Modules (Lowest Layer)
2.1 S3 Backend Bucket Module
Path: resource-modules/storage/s3-backend/main.tf
resource "aws_s3_bucket" "this" {
bucket = var.bucket_name
force_destroy = var.force_destroy
tags = var.tags
}
resource "aws_s3_bucket_versioning" "this" {
bucket = aws_s3_bucket.this.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
bucket = aws_s3_bucket.this.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = var.kms_key_arn
}
}
}
resource "aws_s3_bucket_public_access_block" "this" {
bucket = aws_s3_bucket.this.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
variable "bucket_name" { type = string }
variable "force_destroy" { type = bool }
variable "kms_key_arn" { type = string }
variable "tags" { type = map(string) }
output "bucket_name" { value = aws_s3_bucket.this.id }
output "bucket_arn" { value = aws_s3_bucket.this.arn }
2.2 DynamoDB Backend Table Module
Path: resource-modules/dynamodb/backend/main.tf
resource "aws_dynamodb_table" "this" {
name = var.table_name
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
tags = var.tags
}
variable "table_name" { type = string }
variable "tags" { type = map(string) }
output "table_name" { value = aws_dynamodb_table.this.name }
output "table_arn" { value = aws_dynamodb_table.this.arn }
2.3 KMS Key Module
Path: resource-modules/kms/backend/main.tf
resource "aws_kms_key" "this" {
description = var.description
deletion_window_in_days = 10
enable_key_rotation = true
tags = var.tags
}
variable "description" { type = string }
variable "tags" { type = map(string) }
output "key_id" { value = aws_kms_key.this.id }
output "key_arn" { value = aws_kms_key.this.arn }
2.4 Basic VPC Module
Path: resource-modules/vpc/basic/main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
tags = merge(var.tags, { Name = "${var.project}-${var.environment}-vpc" })
}
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = var.tags
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnet_cidr
map_public_ip_on_launch = true
availability_zone = var.az
tags = var.tags
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.this.id
cidr_block = var.private_subnet_cidr
availability_zone = var.az
tags = var.tags
}
resource "aws_subnet" "database" {
vpc_id = aws_vpc.this.id
cidr_block = var.database_subnet_cidr
availability_zone = var.az
tags = var.tags
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
tags = var.tags
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
variable "project" { type = string }
variable "environment" { type = string }
variable "cidr_block" { type = string }
variable "az" { type = string }
variable "public_subnet_cidr" { type = string }
variable "private_subnet_cidr" { type = string }
variable "database_subnet_cidr" { type = string }
variable "tags" { type = map(string) }
output "vpc_id" { value = aws_vpc.this.id }
output "public_subnet_id" { value = aws_subnet.public.id }
output "private_subnet_id" { value = aws_subnet.private.id }
output "database_subnet_id" { value = aws_subnet.database.id }
Infra Layer – Remote Backend Facade
This layer bundles the three backend‑related resource modules and exposes a single, simple interface.
Path: infra/remote-backend/variables.tf
variable "project" { type = string }
variable "environment"{ type = string }
variable "region" { type = string }
resource "random_integer" "suffix" {
min = 1000
max = 9999
}
module "kms_backend" {
source = "../../resource-modules/kms/backend"
description = "${var.project}-${var.environment} backend key"
tags = {
Project = var.project
Environment = var.environment
}
}
module "s3_backend" {
source = "../../resource-modules/storage/s3-backend"
bucket_name = "${var.project}-${var.environment}-${random_integer.suffix.result}"
force_destroy = true
kms_key_arn = module.kms_backend.key_arn
tags = {
Project = var.project
Environment = var.environment
}
}
module "dynamodb_backend" {
source = "../../resource-modules/dynamodb/backend"
table_name = "${var.project}-${var.environment}-lock"
tags = {
Project = var.project
Environment = var.environment
}
}
output "backend_bucket_name" { value = module.s3_backend.bucket_name }
output "backend_dynamodb_table_name" { value = module.dynamodb_backend.table_name }
output "backend_kms_key_arn" { value = module.kms_backend.key_arn }
Composition – Remote Backend (us-east-2 / prod)
Entry point for creating the remote backend in a specific region/environment.
Path: composition/remote-backend/us-east-2/prod/main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# IMPORTANT: keep backend local for the first run.
# After the bucket & table exist, uncomment the block below.
# backend "s3" {
# bucket = ""
# key = "terraform.tfstate"
# region = "us-east-2"
# dynamodb_table = ""
# encrypt = true
# }
}
provider "aws" {
region = var.region
}
module "remote_backend" {
source = "../../../infra/remote-backend"
project = var.project
environment = var.environment
region = var.region
}
output "backend_bucket_name" { value = module.remote_backend.backend_bucket_name }
output "backend_dynamodb_table_name" { value = module.remote_backend.backend_dynamodb_table_name }
output "backend_kms_key_arn" { value = module.remote_backend.backend_kms_key_arn }
variable "project" { type = string }
variable "environment"{ type = string }
variable "region" { type = string }
# Example values
# project = "my-demo"
# environment= "prod"
# region = "us-east-2"
Run the Remote Backend Stack
cd composition/remote-backend/us-east-2/prod
terraform init
terraform apply
After the bucket and DynamoDB table are created, you can configure other stacks to use the remote backend:
backend "s3" {
bucket = ""
key = "path/to/terraform.tfstate"
region = "us-east-2"
dynamodb_table = ""
encrypt = true
}
Infra – VPC Facade
Wraps the basic VPC resource module.
Path: infra/vpc/locals.tf
variable "project" { type = string }
variable "cidr_block" { type = string }
variable "public_subnet_cidr" { type = string }
variable "private_subnet_cidr" { type = string }
variable "database_subnet_cidr" { type = string }
variable "az" { type = string }
variable "tags" { type = map(string) }
module "vpc_basic" {
source = "../../resource-modules/vpc/basic"
project = var.project
environment = var.environment
cidr_block = var.cidr_block
az = var.az
public_subnet_cidr = var.public_subnet_cidr
private_subnet_cidr = var.private_subnet_cidr
database_subnet_cidr = var.database_subnet_cidr
tags = var.tags
}
output "vpc_id" { value = module.vpc_basic.vpc_id }
output "public_subnet_id" { value = module.vpc_basic.public_subnet_id }
output "private_subnet_id" { value = module.vpc_basic.private_subnet_id }
output "database_subnet_id" { value = module.vpc_basic.database_subnet_id }
Composition – VPC (us-east-2 / prod)
Creates the VPC using the infra façade.
Path: composition/vpc/us-east-2/prod/main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# OPTIONAL: after the remote backend exists, configure it here.
# backend "s3" { ... }
}
provider "aws" {
region = var.region
}
module "vpc" {
source = "../../../infra/vpc"
project = var.project
environment = var.environment
region = var.region
cidr_block = var.cidr_block
az = var.az
public_subnet_cidr = var.public_subnet_cidr
private_subnet_cidr = var.private_subnet_cidr
database_subnet_cidr= var.database_subnet_cidr
tags = {
Project = var.project
Environment = var.environment
}
}
output "vpc_id" { value = module.vpc.vpc_id }
output "public_subnet_id" { value = module.vpc.public_subnet_id }
output "private_subnet_id" { value = module.vpc.private_subnet_id }
output "database_subnet_id" { value = module.vpc.database_subnet_id }
variable "project" { type = string }
variable "environment" { type = string }
variable "region" { type = string }
variable "cidr_block" { type = string }
variable "az" { type = string }
variable "public_subnet_cidr" { type = string }
variable "private_subnet_cidr" { type = string }
variable "database_subnet_cidr" { type = string }
# Example values
# project = "my-demo"
# environment = "prod"
# region = "us-east-2"
# cidr_block = "10.0.0.0/16"
# az = "us-east-2a"
# public_subnet_cidr = "10.0.1.0/24"
# private_subnet_cidr = "10.0.2.0/24"
# database_subnet_cidr = "10.0.3.0/24"
Run the VPC Stack
cd composition/vpc/us-east-2/prod
terraform init
terraform apply
How This Matches the 3‑Layer Architecture
| Layer | Purpose | Example Modules |
|---|---|---|
| Resource modules | Raw AWS resources, no environment logic | s3-backend, dynamodb-backend, kms-backend, vpc-basic |
| Infra modules (facades) | Bundle related resources into a reusable unit | infra/remote-backend (S3 + DynamoDB + KMS), infra/vpc (VPC + subnets) |
| Composition layer | Environment‑specific entry points (region + env) | composition/remote-backend/us-east-2/prod, composition/vpc/us-east-2/prod |
This mirrors the lecture’s “resource → infra → composition” pattern, but with concise, readable code suitable for teaching and real‑world use.