End-to-End Web Application Deployment Using Terraform
Augustine Tetteh Ozor
Cloud DevOps Engineer | 2x AWS Certified | AWS, Kubernetes, Docker, Terraform, Jenkins, and CI/CD Pipelines | AWS Community Builder
Pre-deployment Procedures
1. Install Terraform
sudo apt-get update && sudo apt-get install -y gnupg software-properties-common
wget -O- https://apt.releases.hashicorp.com/gpg | \
gpg --dearmor | \
sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null
gpg --no-default-keyring \
--keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg \
--fingerprint
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update
sudo apt-get install terraform -y
2. Configure AWS Credentials
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
aws --version
3. Write Terraform Configuration Files
4. Initialize Terraform
terraform init
5. Format and Validate Configuration
terraform validate
6. Generate and Review an Execution Plan
terraform plan
7. Apply the Configuration
terraform apply
?Architecture Components:
1. VPC (Virtual Private Cloud):
? ? - Create a VPC to logically isolate the application environment.
?? - Cidr block This block determines the IP range available for subnets and other resources in the VPC
?? - Subnets:
???? - Public Subnets for web servers and load balancers (allowing internet access).
???? - Private Subnets for RDS (isolating database traffic).
?? - Internet Gateway for outbound traffic from web servers.
?? - NAT Gateway for private subnet instances (like RDS) to access the internet for updates.
vpc.tf
?
data "aws_availability_zones" "available" {
state = "available"
}
# Main vpc
resource "aws_vpc" "levelup_vpc" {
cidr_block = var.LEVELUP_VPC_CIDR_BLOC
enable_dns_support = "true"
enable_dns_hostnames = "true"
tags = {
Name = "${var.ENVIRONMENT}-vpc"
}
}
# Public subnets
#public Subnet 1
resource "aws_subnet" "levelup_vpc_public_subnet_1" {
vpc_id = aws_vpc.levelup_vpc.id
cidr_block = var.LEVELUP_VPC_PUBLIC_SUBNET1_CIDR_BLOCK
availability_zone = data.aws_availability_zones.available.names[0]
map_public_ip_on_launch = "true"
tags = {
Name = "${var.ENVIRONMENT}-levelup-vpc-public-subnet-1"
}
}
#public Subnet 2
resource "aws_subnet" "levelup_vpc_public_subnet_2" {
vpc_id = aws_vpc.levelup_vpc.id
cidr_block = var.LEVELUP_VPC_PUBLIC_SUBNET2_CIDR_BLOCK
availability_zone = data.aws_availability_zones.available.names[1]
map_public_ip_on_launch = "true"
tags = {
Name = "${var.ENVIRONMENT}-levelup-vpc-public-subnet-2"
}
}
# private subnet 1
resource "aws_subnet" "levelup_vpc_private_subnet_1" {
vpc_id = aws_vpc.levelup_vpc.id
cidr_block = var.LEVELUP_VPC_PRIVATE_SUBNET1_CIDR_BLOCK
availability_zone = data.aws_availability_zones.available.names[0]
tags = {
Name = "${var.ENVIRONMENT}-levelup-vpc-private-subnet-1"
}
}
# private subnet 2
resource "aws_subnet" "levelup_vpc_private_subnet_2" {
vpc_id = aws_vpc.levelup_vpc.id
cidr_block = var.LEVELUP_VPC_PRIVATE_SUBNET2_CIDR_BLOCK
availability_zone = data.aws_availability_zones.available.names[1]
tags = {
Name = "${var.ENVIRONMENT}-levelup-vpc-private-subnet-2"
}
}
# internet gateway
resource "aws_internet_gateway" "levelup_igw" {
vpc_id = aws_vpc.levelup_vpc.id
tags = {
Name = "${var.ENVIRONMENT}-levelup-vpc-internet-gateway"
}
}
# ELastic IP for NAT Gateway
resource "aws_eip" "levelup_nat_eip" {
domain = "vpc" # This should be a string value
depends_on = [aws_internet_gateway.levelup_igw]
}
# NAT gateway for private ip address
resource "aws_nat_gateway" "levelup_ngw" {
allocation_id = aws_eip.levelup_nat_eip.id
subnet_id = aws_subnet.levelup_vpc_public_subnet_1.id
depends_on = [aws_internet_gateway.levelup_igw]
tags = {
Name = "${var.ENVIRONMENT}-levelup-vpc-NAT-gateway"
}
}
# Route Table for public Architecture
resource "aws_route_table" "public" {
vpc_id = aws_vpc.levelup_vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.levelup_igw.id
}
tags = {
Name = "${var.ENVIRONMENT}-levelup-public-route-table"
}
}
# Route table for Private subnets
resource "aws_route_table" "private" {
vpc_id = aws_vpc.levelup_vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_nat_gateway.levelup_ngw.id
}
tags = {
Name = "${var.ENVIRONMENT}-levelup-private-route-table"
}
}
# Route Table association with public subnets
resource "aws_route_table_association" "to_public_subnet1" {
subnet_id = aws_subnet.levelup_vpc_public_subnet_1.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "to_public_subnet2" {
subnet_id = aws_subnet.levelup_vpc_public_subnet_2.id
route_table_id = aws_route_table.public.id
}
# Route table association with private subnets
resource "aws_route_table_association" "to_private_subnet1" {
subnet_id = aws_subnet.levelup_vpc_private_subnet_1.id
route_table_id = aws_route_table.private.id
}
resource "aws_route_table_association" "to_private_subnet2" {
subnet_id = aws_subnet.levelup_vpc_private_subnet_2.id
route_table_id = aws_route_table.private.id
}
provider "aws" {
region = var.AWS_REGION
}
#Output Specific to Custom VPC
output "my_vpc_id" {
description = "VPC ID"
value = aws_vpc.levelup_vpc.id
}
output "private_subnet1_id" {
description = "Subnet ID"
value = aws_subnet.levelup_vpc_private_subnet_1.id
}
output "private_subnet2_id" {
description = "Subnet ID"
value = aws_subnet.levelup_vpc_private_subnet_2.id
}
output "public_subnet1_id" {
description = "Subnet ID"
value = aws_subnet.levelup_vpc_public_subnet_1.id
}
output "public_subnet2_id" {
description = "Subnet ID"
value = aws_subnet.levelup_vpc_private_subnet_2.id
}
?variable.tf
variable "AWS_REGION" {
type = string
default = "us-east-2"
}
variable "LEVELUP_VPC_CIDR_BLOC" {
description = "The CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "LEVELUP_VPC_PUBLIC_SUBNET1_CIDR_BLOCK" {
description = "The CIDR block for the VPC"
type = string
default = "10.0.101.0/24"
}
variable "LEVELUP_VPC_PUBLIC_SUBNET2_CIDR_BLOCK" {
description = "The CIDR block for the VPC"
type = string
default = "10.0.102.0/24"
}
variable "LEVELUP_VPC_PRIVATE_SUBNET1_CIDR_BLOCK" {
description = "The CIDR block for the VPC"
type = string
default = "10.0.1.0/24"
}
variable "LEVELUP_VPC_PRIVATE_SUBNET2_CIDR_BLOCK" {
description = "The CIDR block for the VPC"
type = string
default = "10.0.2.0/24"
}
variable "ENVIRONMENT" {
description = "AWS VPC Environment Name"
type = string
default = "Development"
}
领英推荐
2. RDS (Relational Database Service):
?? - A primary RDS instance with Multi-AZ replication for high availability and disaster recovery.
?? - Place the RDS in private subnets to enhance security.
?? - Create security groups allowing access only from web servers and specific port access (e.g., MySQL on port 3306).
rds.tf
#Call VPC Module First to get the Subnet IDs
module "levelup-vpc" {
source = "../vpc"
ENVIRONMENT = var.ENVIRONMENT
AWS_REGION = var.AWS_REGION
}
#Define Subnet Group for RDS Service
resource "aws_db_subnet_group" "levelup-rds-subnet-group" {
name = "${var.ENVIRONMENT}-levelup-db-snet"
description = "Allowed subnets for DB cluster instances"
subnet_ids = [
"${module.levelup-vpc.private_subnet1_id}",
"${module.levelup-vpc.private_subnet2_id}",
]
tags = {
Name = "${var.ENVIRONMENT}_levelup_db_subnet"
}
}
#Define Security Groups for RDS Instances
resource "aws_security_group" "levelup-rds-sg" {
name = "${var.ENVIRONMENT}-levelup-rds-sg"
description = "Created by LevelUp"
vpc_id = module.levelup-vpc.my_vpc_id
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
cidr_blocks = ["${var.RDS_CIDR}"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.ENVIRONMENT}-levelup-rds-sg"
}
}
resource "aws_db_instance" "levelup-rds" {
identifier = "${var.ENVIRONMENT}-levelup-rds"
allocated_storage = var.LEVELUP_RDS_ALLOCATED_STORAGE
storage_type = "gp2"
engine = var.LEVELUP_RDS_ENGINE
engine_version = var.LEVELUP_RDS_ENGINE_VERSION
instance_class = var.DB_INSTANCE_CLASS
backup_retention_period = var.BACKUP_RETENTION_PERIOD
publicly_accessible = var.PUBLICLY_ACCESSIBLE
username = var.LEVELUP_RDS_USERNAME
password = var.LEVELUP_RDS_PASSWORD
vpc_security_group_ids = [aws_security_group.levelup-rds-sg.id]
db_subnet_group_name = aws_db_subnet_group.levelup-rds-subnet-group.name
multi_az = "false"
}
output "rds_prod_endpoint" {
value = aws_db_instance.levelup-rds.endpoint
}
variable.tf
variable "AWS_REGION" {
type = string
default = "us-east-2"
}
variable "BACKUP_RETENTION_PERIOD" {
default = "7"
}
variable "PUBLICLY_ACCESSIBLE" {
default = "true"
}
variable "LEVELUP_RDS_USERNAME" {
default = "testdb"
}
variable "LEVELUP_RDS_PASSWORD" {
default = "testdb12345"
}
variable "LEVELUP_RDS_ALLOCATED_STORAGE" {
type = string
default = "20"
}
variable "LEVELUP_RDS_ENGINE" {
type = string
default = "mysql"
}
variable "LEVELUP_RDS_ENGINE_VERSION" {
type = string
default = "8.0.35"
}
variable "DB_INSTANCE_CLASS" {
type = string
default = "db.t3.micro"
}
variable "RDS_CIDR" {
description = "The CIDR block for the VPC"
type = string
default = "0.0.0.0/0"
}
variable "ENVIRONMENT" {
description = "AWS VPC Environment Name"
type = string
default = "Development"
}
variable "vpc_private_subnet1" {
description = "AWS VPC Environment Name"
type = string
default = ""
}
variable "vpc_private_subnet2" {
description = "AWS VPC Environment Name"
type = string
default = ""
}
variable "vpc_id" {
description = "AWS VPC Environment Name"
type = string
default = ""
}
3. Web Servers (EC2 with NGINX):
?? - EC2 instances running NGINX in an Auto Scaling Group to handle traffic spikes.
?? - Instances should be placed in public subnets, with access managed by security groups to allow HTTP/HTTPS traffic.
?? - Use User Data to install and configure NGINX on EC2 at launch.
instance.tf
module "levelup-vpc" {
source = "../module/vpc"
ENVIRONMENT = var.ENVIRONMENT
AWS_REGION = var.AWS_REGION
}
module "levelup-rds" {
source = "../module/rds"
ENVIRONMENT = var.ENVIRONMENT
AWS_REGION = var.AWS_REGION
vpc_private_subnet1 = var.vpc_private_subnet1
vpc_private_subnet2 = var.vpc_private_subnet2
vpc_id = var.vpc_id
}
resource "aws_security_group" "levelup_webservers"{
tags = {
Name = "${var.ENVIRONMENT}-levelup-webservers"
}
name = "${var.ENVIRONMENT}-levelup-webservers"
description = "Created by Levelup"
vpc_id = var.vpc_id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["${var.SSH_CIDR_WEB_SERVER}"]
}
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"]
}
}
#Resource key pair
resource "aws_key_pair" "level_key" {
key_name = "level_key"
public_key = file(var.public_key_path)
}
resource "aws_launch_configuration" "launch_config_webserver" {
name = "launch_config_webserver"
image_id = lookup(var.AMIS, var.AWS_REGION)
instance_type = var.INSTANCE_TYPE
user_data = "#!/bin/bash\napt-get update\napt-get -y install net-tools nginx\nMYIP=`ifconfig | grep -E '(inet 10)|(addr:10)' | awk '{ print $2 }' | cut -d ':' -f2`\necho 'Hello Team\nThis is my IP: '$MYIP > /var/www/html/index.html"
security_groups = [aws_security_group.levelup_webservers.id]
key_name = aws_key_pair.level_key.key_name
root_block_device {
volume_type = "gp2"
volume_size = "20"
}
}
resource "aws_autoscaling_group" "levelup_webserver" {
name = "levelup_WebServers"
max_size = 2
min_size = 1
health_check_grace_period = 30
health_check_type = "EC2"
desired_capacity = 1
force_delete = true
launch_configuration = aws_launch_configuration.launch_config_webserver.name
vpc_zone_identifier = ["${var.vpc_public_subnet1}", "${var.vpc_public_subnet2}"]
target_group_arns = [aws_lb_target_group.load-balancer-target-group.arn]
}
#Application load balancer for app server
resource "aws_lb" "levelup-load-balancer" {
name = "${var.ENVIRONMENT}-levelup-lb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.levelup_webservers_alb.id]
subnets = ["${var.vpc_public_subnet1}", "${var.vpc_public_subnet2}"]
}
# Add Target Group
resource "aws_lb_target_group" "load-balancer-target-group" {
name = "load-balancer-target-group"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
}
# Adding HTTP listener
resource "aws_lb_listener" "webserver_listner" {
load_balancer_arn = aws_lb.levelup-load-balancer.arn
port = "80"
protocol = "HTTP"
default_action {
target_group_arn = aws_lb_target_group.load-balancer-target-group.arn
type = "forward"
}
}
output "load_balancer_output" {
value = aws_lb.levelup-load-balancer.dns_name
}
variable.tf
variable "SSH_CIDR_WEB_SERVER" {
type = string
default = "0.0.0.0/0"
}
variable "INSTANCE_TYPE" {
default = "t2.micro"
}
variable "AMIS" {
type = map
default = {
us-east-1 = "ami-0f40c8f97004632f9"
us-east-2 = "ami-05692172625678b4e"
us-west-2 = "ami-02c8896b265d8c480"
eu-west-1 = "ami-0cdd3aca00188622e"
}
}
variable "AWS_REGION" {
type = string
default = "us-east-2"
}
variable "ENVIRONMENT" {
description = "AWS VPC Environment Name"
type = string
default = "Development"
}
variable "public_key_path" {
description = "Public key path"
default = "~/.ssh/level_key.pub"
}
variable "vpc_private_subnet1" {
description = "AWS VPC Environment Name"
type = string
default = ""
}
variable "vpc_private_subnet2" {
description = "AWS VPC Environment Name"
type = string
default = ""
}
variable "vpc_id" {
description = "AWS VPC Environment Name"
type = string
default = ""
}
variable "vpc_public_subnet1" {
description = "AWS VPC Environment Name"
type = string
default = ""
}
variable "vpc_public_subnet2" {
description = "AWS VPC Environment Name"
type = string
default = ""
}
4. Load Balancers:
?? - An Application Load Balancer (ALB) distributes traffic across web servers, ensuring high availability.
?? - The ALB should be in public subnets with proper routing to EC2 instances within the Auto Scaling Group.
alb.tf
resource "aws_security_group" "levelup_webservers_alb" {
tags = {
Name = "${var.ENVIRONMENT}-levelup-webservers-ALB"
}
name = "${var.ENVIRONMENT}-levelup-webservers-ALB"
description = "Created by levelup"
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"]
}
}
5. Security Groups:
?? - Each resource (EC2, RDS, ALB) requires a dedicated Security Group.
?? - Web server SG: Allows HTTP (80) and HTTPS (443) inbound.
?? - RDS SG: Only allows traffic from web server SG over the database port (e.g., 3306)
?
Clean Up the Deployment
terraform destroy