Skip to main content

Command Palette

Search for a command to run...

2-Tier Architecture Setup on AWS Using Terraform

Published
5 min read

Architecture Diagram

Custom Modules

Using custom modules for this project, check out the previous blogs to know more about it.

Repo link

EC2

We create an EC2 instance and pass on the data we have inside the user_data.sh. It is a simple flask application with a few endpoints, like: /, /health, /db_info.

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
  owners = ["099720109477"]
}

resource "aws_instance" "web" {
  ami                         = data.aws_ami.ubuntu.id
  instance_type               = var.instance_type
  subnet_id                   = var.subnet_id
  vpc_security_group_ids      = [var.web_security_group_id]
  associate_public_ip_address = false

  user_data = templatefile("${path.module}/templates/user_data.sh", {
    db_host     = var.db_host
    db_username = var.db_username
    db_password = var.db_password
    db_name     = var.db_name
  })

  tags = {
    Name        = "${var.project_name}-web-server"
    Environment = var.environment
  }
}

RDS

This Terraform code sets up a private MySQL database (RDS instance) in AWS, fully isolated in your VPC's private subnets for security.

resource "aws_db_subnet_group" "main" {
  name       = "${var.project_name}-db-subnet-group"
  subnet_ids = var.private_subnet_ids
  tags = {
    Name        = "${var.project_name}-db-subnet-group"
    Environment = var.environment
  }
}

resource "aws_db_instance" "main" {
  identifier             = "${var.project_name}-db"
  allocated_storage      = var.allocated_storage
  storage_type           = "gp2"
  engine                 = "mysql"
  engine_version         = var.engine_version
  instance_class         = var.instance_class
  db_name                = var.db_name
  username               = var.db_username
  password               = var.db_password
  parameter_group_name   = "default.mysql8.0"
  skip_final_snapshot    = true
  vpc_security_group_ids = [var.db_security_group_id]
  db_subnet_group_name   = aws_db_subnet_group.main.name
  publicly_accessible    = false

  tags = {
    Name        = "${var.project_name}-rds"
    Environment = var.environment
  }
}

Secrets

Using secrets, we generate a secure, random password and store it in AWS Secrets Manager, so it's never exposed in our code or Terraform state.

resource "random_password" "db_password" {
  length           = 16
  special          = true
  override_special = "!#$%&*()-_=+[]{}<>:?"
}

resource "random_id" "suffix" {
  byte_length = 4
}

resource "aws_secretsmanager_secret" "db_password" {
  name        = "${var.project_name}-${var.environment}-db-password-${random_id.suffix.hex}"
  description = "Database password for ${var.project_name}"

  tags = {
    Name        = "${var.project_name}-db-password"
    Environment = var.environment
  }
}

resource "aws_secretsmanager_secret_version" "db_password" {
  secret_id = aws_secretsmanager_secret.db_password.id
  secret_string = jsonencode({
    username = var.db_username
    password = random_password.db_password.result
    engine   = "mysql"
    host     = ""
  })
}

Security Group

We configure three security groups to control network access for a typical web application:

  • ALB Security Group: Allows HTTP traffic (port 80) from anywhere (the internet) to your Application Load Balancer. It also allows all outbound traffic from the ALB.​

  • Web Server Security Group: Only allows HTTP traffic (port 80) from the ALB (so only the load balancer can reach your web servers). It also allows SSH (port 22) from anywhere, but in a private setup, SSH access would typically be restricted to a bastion or VPN.​

  • Database Security Group: Only allows MySQL traffic (port 3306) from the web server security group, so only your web servers can connect to the database. All outbound traffic is allowed.

These groups enforce a layered security model: the internet reaches —→ ALB — (talks to) —→ web servers — (talks to) —→database. With no direct public access to web or database servers.

Refer to the code in github repo linked at the top.

VPC

No explanation for the configuration. Refer to the repo for the information about the connection between all our components.

ALB

We sets up an Application Load Balancer to distribute HTTP traffic to our web servers.

resource "aws_lb" "main" {
  name               = "${var.project_name}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [var.alb_security_group_id]
  subnets            = var.public_subnet_ids

  tags = {
    Name        = "${var.project_name}-alb"
    Environment = var.environment
  }
}

resource "aws_lb_target_group" "web" {
  name     = "${var.project_name}-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = var.vpc_id

  health_check {
    path                = "/"
    protocol            = "HTTP"
    matcher             = "200"
    interval            = 30
    timeout             = 5
    healthy_threshold   = 3
    unhealthy_threshold = 3
  }

  tags = {
    Name        = "${var.project_name}-tg"
    Environment = var.environment
  }
}

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.web.arn
  }
}

resource "aws_lb_target_group_attachment" "web" {
  target_group_arn = aws_lb_target_group.web.arn
  target_id        = var.target_id
  port             = 80
}

Driver file - root main.tf

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

provider "aws" {
  region = var.aws_region
}

# Secrets Module
module "secrets" {
  source = "./modules/secrets"

  project_name = var.project_name
  environment  = var.environment
  db_username  = var.db_username
}

# VPC Module
module "vpc" {
  source = "./modules/vpc"

  project_name         = var.project_name
  environment          = var.environment
  aws_region           = var.aws_region
  vpc_cidr             = var.vpc_cidr
  public_subnet_cidrs  = var.public_subnet_cidrs
  private_subnet_cidrs = var.private_subnet_cidrs
}

# Security Groups Module
module "security_groups" {
  source = "./modules/security_groups"

  project_name = var.project_name
  environment  = var.environment
  vpc_id       = module.vpc.vpc_id
}

# RDS Module
module "rds" {
  source = "./modules/rds"

  project_name         = var.project_name
  environment          = var.environment
  private_subnet_ids   = module.vpc.private_subnet_ids
  db_security_group_id = module.security_groups.db_sg_id
  db_name              = var.db_name
  db_username          = var.db_username
  db_password          = module.secrets.db_password
  instance_class       = var.db_instance_class
  allocated_storage    = var.db_allocated_storage
  engine_version       = var.db_engine_version
}

# EC2 Module (Now in Private Subnet)
module "ec2" {
  source = "./modules/ec2"

  project_name          = var.project_name
  environment           = var.environment
  instance_type         = var.ec2_instance_type
  subnet_id             = module.vpc.private_subnet_ids[0]
  web_security_group_id = module.security_groups.web_sg_id
  db_host               = module.rds.db_endpoint
  db_username           = var.db_username
  db_password           = module.secrets.db_password
  db_name               = var.db_name
}

# ALB Module
module "alb" {
  source = "./modules/alb"

  project_name          = var.project_name
  environment           = var.environment
  vpc_id                = module.vpc.vpc_id
  public_subnet_ids     = module.vpc.public_subnet_ids
  private_subnet_ids    = module.vpc.private_subnet_ids
  target_id             = module.ec2.instance_id
  alb_security_group_id = module.security_groups.alb_sg_id
}

After cloning the project, run:

terraform init
terraform validate
terraform plan
terraform apply --auto-approve

After verification, make sure to:

terraform destroy --auto-approve

Verification

Endpoint verification:

Checking the endpoint: /health


Video Reference:


Arigato!

More from this blog

Code Companions

32 posts