Terraform is the standard for provisioning cloud infrastructure at scale. In this walkthrough, we’ll deploy a fully functional EC2 web server on AWS using three of Terraform.

In this lab, we provision an EC2 instance on AWS running Amazon Linux 2 with Apache installed, lock down access using two dedicated security groups for SSH and web traffic, dynamically fetch the latest Amazon Linux 2 AMI using a Terraform datasource, and print the instance IP and DNS to the terminal once deployment is complete.

Project Structure

Splitting configuration across multiple files isn’t required since Terraform merges everything at runtime. But keeping resources in separate files makes the project easier to navigate, maintain, and scale. Each file has a single responsibility, which is a habit worth building early.

I. Generate the Key Pair

Before writing a single line of Terraform, we need an SSH key pair in AWS. This is what gives us secure access to the EC2 instance after it’s provisioned. Rather than clicking through the console, we generate it directly from the CLI — faster and consistent with the infrastructure-as-code mindset.

aws ec2 create-key-pair \
--key-name project-key \
--query 'KeyMaterial' \
--output text > project-key.pem
chmod 400 project-key.pem

II. Write the Configuration Files

1. Provider Configuration — provider.tf

terraform {} block This block tells Terraform what version it needs to run and which provider to download.

provider "aws" {} block This is where we configure the AWS provider itself. The only thing we need to tell it is which region to deploy resources in — and instead of hardcoding "us-east-1" directly here, we reference a variable var.project_region.

2. Input Variables — variables.tf

variable "project_region" This tells Terraform which AWS region to deploy everything in. Instead of hardcoding "us-east-1" directly inside your resource files, you define it here once and reference it everywhere.

variable "project_instance_type" This controls the size of the EC2 instance — how much CPU and memory it gets. t3.micro is the default here, which is small and cheap, perfect for a lab.

variable "project_keypair" This is the name of the SSH key pair that gets attached to the EC2 instance. Without it, there’s no way to SSH into the server. We created project-key earlier from the CLI — this variable makes sure Terraform knows which key to associate with the instance at launch.

3. Security Groups — security-groups.tf

AWS blocks all traffic to an EC2 instance by default. To make the server reachable, we need to create two resource blocks — one that allows SSH access so we can connect to the instance from our terminal, and one that allows web traffic so browsers can reach the Apache server.

project-sg-ssh The first resource block creates the SSH security group. The ingress block is the incoming traffic rule and opens port 22, which is the port SSH uses to establish a secure connection between your machine and the server. The cidr_blocks = ["0.0.0.0/0"] means any IP address can attempt a connection on that port. This is fine for a lab but in production you would lock this down to your own IP only. The egress block controls outbound traffic from the instance. Setting protocol to -1 and ports to 0-0 means all outbound traffic is allowed so the instance can freely reach the internet to pull packages and updates.

project-sg-web The second resource block creates the web traffic security group. It has two ingress blocks because HTTP and HTTPS run on different ports. Port 80 handles regular HTTP traffic and port 443 handles encrypted HTTPS traffic. Both ports are opened so the server can respond to requests over either protocol. Without this resource block attached to the instance, Apache would be installed and running but every browser request would be blocked at the network level before it ever reached the server. The egress block is the same as the SSH group and allows all outbound traffic.

4. AMI— ami.tf

Instead of hardcoding an AMI ID, we use a data block to fetch it automatically.

most_recent = true grabs the latest available image. owners = ["amazon"] ensures we are only pulling from official Amazon-published images. The filter block narrows the search down to Amazon Linux 2 HVM images. Once resolved, the AMI ID is referenced in the EC2 instance as data.aws_ami.project-ami.id.

5. EC2 instance — ec2.tf

The resource "aws_instance" block creates the actual EC2 instance on AWS.

ami is the operating system image. Terraform pulls the latest Amazon Linux 2 ID automatically from the data block we defined earlier. data.aws_ami.project-ami.id is how Terraform references that result.data means we are reading from a datasource, aws_ami is the resource type, project-ami is the name we gave it, and .id is the actual AMI ID value that gets returned.

instance_type is the size of the server. We set this to t3.micro in our variables

key_name is the SSH key that gets loaded onto the instance at launch. This is how we authenticate when connecting to the server.

user_data runs the install.sh script automatically on first boot. The file("${path.module}/install.sh") function reads the contents of the script file and passes it to AWS as plain text. AWS then executes it on the instance the first time it starts. By the time the instance is ready, Apache is already installed and serving traffic.

vpc_security_group_ids attaches both security groups as a list. One allows SSH, the other allows web traffic.

tags gives the instance the name Project-WebServer in the AWS console.

User data— install.sh

This script runs automatically on the instance the first time it boots.

yum update -y updates all existing system packages to their latest version before installing anything new.

yum install -y httpd installs Apache. On Amazon Linux, Apache is called httpd which stands for HTTP daemon.

systemctl enable httpd tells the system to start Apache automatically every time the instance reboots. Without this, Apache would stop running after a restart.

systemctl start httpd starts Apache immediately without waiting for a reboot.

echo '<h1>Project WebServer</h1>' | sudo tee /var/www/html/index.html creates a simple HTML file in /var/www/html which is the default folder Apache serves files from. When you visit the server’s IP in a browser, Apache looks in that folder and returns the file.

6. Output — output.tf

The output "public_ip" block prints the EC2 instance’s public IP address to the terminal once terraform apply finishes. The value = aws_instance.project-ec2.public_ip is how Terraform references the IP. aws_instance is the resource type, project-ec2 is the name we gave it, and .public_ip is the attribute that holds the IP address assigned by AWS. Once you have that IP you can paste it directly into your browser to access the server.

III. Deployment

1. Initialize

terraform init

Downloads the AWS provider and sets up the backend. Always run this first.

2. Validate

terraform validate

Checks all files for syntax errors before running anything.

3. Plan

terraform plan

Previews what Terraform will create.

4. Apply

bash

terraform apply 

Provisions all resources on AWS. Your public_ip print automatically when done.

IV. Clean Up

Destroy lab resources when you’re done.terraform destroy tears down every resource Terraform created

terraform destroy -auto-approve
DevOps
Terraform
Cloud
AWS