In today's cloud-centric world, infrastructure management has shifted from manual configurations to automation with Infrastructure as Code (IaC). Terraform, developed by HashiCorp, is a powerful IaC tool that allows us to define and provision data center infrastructure using a declarative configuration language. In this post, I'll walk you through a project where I used Terraform to deploy a scalable web application on AWS, leveraging a range of services including VPCs, EC2 instances, S3 buckets, and an Application Load Balancer (ALB).
Project Overview
The goal of this project is to set up a simple yet scalable web application hosted on AWS. The infrastructure includes:
- A Virtual Private Cloud (VPC) with custom subnets.
- An Internet Gateway and Route Table for connectivity.
- Security Groups to manage access.
- EC2 instances running Apache servers.
- An S3 bucket for storage.
- An Application Load Balancer to distribute traffic across the instances.
1. Setting Up Terraform and AWS Provider
To start with Terraform, we need to configure the provider, which in this case is AWS. The provider.tf
file specifies the AWS provider and the region where the infrastructure will be deployed.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.11.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
This configuration ensures that Terraform uses the correct provider and region. It's crucial to keep the provider version updated to take advantage of the latest features and security patches.
2. VPC and Subnet Configuration
Next, we define the Virtual Private Cloud (VPC) where all our resources will reside. The VPC is isolated and helps secure our instances and other resources.
resource "aws_vpc" "myvpc" {
cidr_block = var.cidr
}
We also create two subnets in different availability zones to ensure high availability. These subnets will host our EC2 instances.
hcl
resource "aws_subnet" "sub1" {
vpc_id = aws_vpc.myvpc.id
cidr_block = "10.0.0.0/24"
availability_zone = "us-east-1a"
map_public_ip_on_launch = true
}
resource "aws_subnet" "sub2" {
vpc_id = aws_vpc.myvpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1b"
map_public_ip_on_launch = true
}
These subnets are public, allowing us to access the instances directly over the internet.
3. Internet Gateway and Route Table
To connect our VPC to the internet, we need an Internet Gateway. The gateway is associated with the VPC, and a route table is configured to direct internet-bound traffic through the gateway.
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.myvpc.id
}
resource "aws_route_table" "RT" {
vpc_id = aws_vpc.myvpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
}
resource "aws_route_table_association" "rta1" {
subnet_id = aws_subnet.sub1.id
route_table_id = aws_route_table.RT.id
}
resource "aws_route_table_association" "rta2" {
subnet_id = aws_subnet.sub2.id
route_table_id = aws_route_table.RT.id
}
This setup ensures that any traffic from our instances can reach the internet, and vice versa.
4. Security Groups
Security groups act as virtual firewalls, controlling inbound and outbound traffic to our instances. In this project, I created a security group that allows HTTP (port 80) and SSH (port 22) traffic from any IP address.
resource "aws_security_group" "webSg" {
name_prefix = "web-sg"
vpc_id = aws_vpc.myvpc.id
ingress {
description = "HTTP from VPC"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "SSH"
from_port = 22
to_port = 22
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 = "Web-sg"
}
}
While allowing SSH access from anywhere isn't a best practice, it's done here for simplicity. In a production environment, restrict SSH access to specific IP addresses or use a bastion host.
5. Deploying EC2 Instances
Two EC2 instances are deployed in different subnets, ensuring redundancy. Each instance runs an Apache server with a custom user data script that installs Apache, creates a simple HTML file, and starts the service.
resource "aws_instance" "webserver1" {
ami = "ami-0261755bbcb8c4a84"
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.webSg.id]
subnet_id = aws_subnet.sub1.id
user_data = base64encode(file("userdata.sh"))
}
resource "aws_instance" "webserver2" {
ami = "ami-0261755bbcb8c4a84"
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.webSg.id]
subnet_id = aws_subnet.sub2.id
user_data = base64encode(file("userdata1.sh"))
}
The user data scripts (userdata.sh
and userdata1.sh
) are executed when the instance launches, automating the setup process. Here's an example of one of the scripts:
#!/bin/bash
apt update
apt install -y apache2
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
cat <<EOF > /var/www/html/index.html
<!DOCTYPE html>
<html>
<head>
<title>My Portfolio</title>
</head>
<body>
<h1>Terraform Project Server 1</h1>
<h2>Instance ID: <span style="color:green">$INSTANCE_ID</span></h2>
</body>
</html>
EOF
systemctl start apache2
systemctl enable apache2
This script updates the package lists, installs Apache, and creates a simple HTML file that displays the instance ID.
6. Setting Up the S3 Bucket
An S3 bucket is also created for storage needs, which can be useful for storing logs, backups, or static assets.
resource "aws_s3_bucket" "example" {
bucket = "aUnique-name-for-s3-terraform"
}
In this project, the S3 bucket is created with a unique name and can be used for various purposes depending on the application's needs.
7. Configuring the Application Load Balancer (ALB)
The Application Load Balancer (ALB) is set up to distribute traffic across the two EC2 instances. This ensures that the application remains available even if one instance fails.
resource "aws_lb" "myalb" {
name = "myalb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.webSg.id]
subnets = [aws_subnet.sub1.id, aws_subnet.sub2.id]
tags = {
Name = "web"
}
}
resource "aws_lb_target_group" "tg" {
name = "myTG"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.myvpc.id
health_check {
path = "/"
port = "traffic-port"
}
}
resource "aws_lb_target_group_attachment" "attach1" {
target_group_arn = aws_lb_target_group.tg.arn
target_id = aws_instance.webserver1.id
port = 80
}
resource "aws_lb_target_group_attachment" "attach2" {
target_group_arn = aws_lb_target_group.tg.arn
target_id = aws_instance.webserver2.id
port = 80
}
resource "aws_lb_listener" "listener" {
load_balancer_arn = aws_lb.myalb.arn
port = 80
protocol = "HTTP"
default_action {
target_group_arn = aws_lb_target_group.tg.arn
type = "forward"
}
}
The ALB is configured to listen on port 80 and forward traffic to the target group, which contains the two EC2 instances. By distributing the load between the instances, the ALB ensures higher availability and better fault tolerance.
- Monitoring and Scaling Monitoring the health of our infrastructure is crucial for maintaining the reliability and performance of the application. In this project, AWS CloudWatch is used for monitoring metrics like CPU utilization, network traffic, and instance health. By setting up CloudWatch Alarms, we can trigger actions such as auto-scaling when specific thresholds are reached.
For example, if the CPU utilization of an EC2 instance exceeds a certain percentage, an auto-scaling policy can be triggered to launch additional instances. This ensures that the application can handle increased traffic without degradation in performance.
- Automating with Terraform Modules As projects grow, so does the complexity of managing infrastructure. Terraform modules allow us to break down our configurations into reusable components. In this project, I created modules for the VPC, EC2 instances, and ALB, making the infrastructure more modular and easier to manage.
Here's an example of how the VPC module might look:
hcl
Copy code
module "vpc" {
source = "./modules/vpc"
cidr = var.cidr
}
By using modules, we can easily replicate environments (e.g., staging, production) with consistent configurations, reducing the risk of errors.
Conclusion
This Terraform project demonstrates how to automate the deployment of a scalable web application on AWS. By leveraging Terraform's capabilities, we can quickly spin up infrastructure that is consistent, reliable, and easily scalable. The use of modules, monitoring tools like CloudWatch, and automated scaling policies further enhances the robustness of the infrastructure.
Whether you're building a simple web application or a complex multi-tier architecture, Terraform offers the flexibility and power to manage your infrastructure as code. As cloud environments continue to evolve, tools like Terraform will remain essential for managing infrastructure efficiently.
This project has been a valuable learning experience, reinforcing the importance of automation, scalability, and monitoring in cloud environments. I encourage anyone interested in cloud infrastructure to explore Terraform and experiment with deploying their own projects on AWS.
If you have any questions or suggestions, feel free to reach out or leave a comment. Happy coding!