qasim@wiki:~$

Terraform — Layer 3 Architecture as Code

BuildsThe WAF + ALB + Auto Scaling + MariaDB architecture in this section
StructureRoot config + three modules (vpc, app, database)
SkillsTerraform modules, VPC design, security-group tiering, WAF, RDS, S3
This is the reference architecture expressed as reusable Terraform — a root configuration wiring together three modules: vpc (network foundation), app (ALB, WAF, Auto Scaling), and database (MariaDB RDS + S3). Every module takes variables in and returns outputs; nothing is hardcoded across module boundaries.
Layer 3 architecture with WAF, ELB and Auto Scaling
The architecture this Terraform builds.

Project structure

terraform-aws-architecture/
├── modules/
│   ├── vpc/          # VPC, subnets (public/app/db), IGW, NAT, DB subnet group
│   ├── app/          # Security groups, ALB, WAFv2, target group, listeners, ASG
│   └── database/     # DB security group, MariaDB RDS, S3 bucket
├── main.tf           # wires the modules together
├── variables.tf
├── outputs.tf
└── terraform.tfvars
What I fixed & hardened from the first draft. The original was structurally sound; these are the corrections and improvements made before it was production-ready:
  • Syntax bug: variable "x" { type = string, default = "y" } is invalid HCL — the comma between type and default fails terraform validate. Split onto separate lines (affected instance_type and db_password).
  • Missing HTTPS:443 listener: the diagram shows an HTTPS:443 listener but the draft only created HTTP:80. Added a 443 listener (conditional on an ACM certificate ARN).
  • Encryption everywhere: added EBS encryption on the launch template, storage_encrypted on RDS, and SSE-KMS + versioning + public-access-block on the S3 bucket.
  • IMDSv2 enforced (http_tokens = "required") on the launch template — a real, free hardening control.
  • Secret removed from tfvars: the draft hardcoded db_password in terraform.tfvars. Removed — pass it via TF_VAR_db_password or Secrets Manager, never commit it.
  • ELB health-check type and a target-tracking scaling policy (keep CPU at 60%) added to the ASG so it actually scales.

The security-group tiering (the heart of the design)

Each tier's security group references the previous tier's SG by ID, not by CIDR. That's what makes the "Firewall 80+443 / 443 / 3306" boundaries in the diagram real: the app tier accepts traffic only from the ALB, and the database accepts 3306 only from the app tier. The database is never reachable from the internet.

# --- Security Groups (Firewalls) ---
# 1. External Firewall (Ports 80 + 443) — the only public-facing tier
resource "aws_security_group" "alb_sg" {
  name        = "${var.environment}-alb-fw-80-443"
  description = "Firewall 80+443: Allows public HTTP/HTTPS traffic to ALB"
  vpc_id      = var.vpc_id
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "${var.environment}-alb-fw-80-443" }
}

# 2. App Firewall (Port 8080) — accepts traffic from the ALB SG only
resource "aws_security_group" "app_sg" {
  name        = "${var.environment}-app-fw-443"
  description = "Firewall 443: Allows traffic from ALB only"
  vpc_id      = var.vpc_id
  ingress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.alb_sg.id]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "${var.environment}-app-fw-443" }
}

Root configuration — wiring the modules

terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

module "vpc" {
  source              = "./modules/vpc"
  environment         = var.environment
  vpc_cidr            = var.vpc_cidr
  public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
  app_subnet_cidrs    = ["10.0.10.0/24", "10.0.11.0/24"]
  db_subnet_cidrs     = ["10.0.20.0/24", "10.0.21.0/24"]
  availability_zones  = ["${var.aws_region}a", "${var.aws_region}b"]
}

module "app" {
  source            = "./modules/app"
  environment       = var.environment
  vpc_id            = module.vpc.vpc_id
  public_subnet_ids = module.vpc.public_subnet_ids
  app_subnet_ids    = module.vpc.app_subnet_ids
  ami_id            = var.ami_id
  instance_type     = "t3.micro"
  certificate_arn   = var.certificate_arn
}

module "database" {
  source                = "./modules/database"
  environment           = var.environment
  vpc_id                = module.vpc.vpc_id
  db_subnet_group_name  = module.vpc.db_subnet_group_name
  app_security_group_id = module.app.app_security_group_id
  db_password           = var.db_password
}

Database module — MariaDB + S3, hardened

# DB Security Group (Firewall 3306) — accepts 3306 from the app tier only
resource "aws_security_group" "db_sg" {
  name        = "${var.environment}-db-fw-3306"
  description = "Firewall 3306: Allows MySQL/MariaDB access from Application Tier"
  vpc_id      = var.vpc_id
  ingress {
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [var.app_security_group_id]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "${var.environment}-db-fw-3306" }
}

# S3 Bucket for Database Exports/Backups (encrypted, private, versioned)
resource "aws_s3_bucket" "db_storage" {
  bucket        = "${var.environment}-mariadb-storage-s3-bucket"
  force_destroy = true
  tags          = { Name = "${var.environment}-mariadb-storage" }
}

resource "aws_s3_bucket_public_access_block" "db_storage" {
  bucket                  = aws_s3_bucket.db_storage.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "db_storage" {
  bucket = aws_s3_bucket.db_storage.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
  }
}

resource "aws_s3_bucket_versioning" "db_storage" {
  bucket = aws_s3_bucket.db_storage.id
  versioning_configuration {
    status = "Enabled"
  }
}

# MariaDB RDS Instance — encrypted at rest, private, not publicly accessible
resource "aws_db_instance" "mariadb" {
  identifier             = "${var.environment}-mariadb-db"
  engine                 = "mariadb"
  engine_version         = "10.11"
  instance_class         = "db.t3.micro"
  allocated_storage      = 20
  storage_encrypted      = true
  db_name                = "layer3db"
  username               = var.db_username
  password               = var.db_password
  db_subnet_group_name   = var.db_subnet_group_name
  vpc_security_group_ids = [aws_security_group.db_sg.id]
  publicly_accessible    = false
  skip_final_snapshot    = true
  tags                   = { Name = "${var.environment}-mariadb" }
}
Full source: all twelve files (root + three modules) are available as a downloadable archive alongside this portfolio. Every file passes HCL validation and the module inputs/outputs cross-check cleanly.

How to run

# 1. Provide the DB password out-of-band (never in tfvars)
export TF_VAR_db_password='your-strong-password'

# 2. Initialise, review, apply
terraform init
terraform plan       # read the diff before applying
terraform apply

# 3. (optional) supply an ACM cert to enable the HTTPS:443 listener
terraform apply -var="certificate_arn=arn:aws:acm:...:certificate/xxxx"
Production notes. For real HA, run one NAT gateway per AZ (the sample uses one for cost). Prefer a pre-baked golden AMI over user_data package installs so scale-out is fast. And put the RDS credentials in Secrets Manager with rotation rather than a plain variable.