Module Project — Deploy Web App with EC2, S3, and IAM
Checking access...
In this project, you will deploy a full web application stack using Terraform. The application serves static assets from S3 and runs on EC2 instances with proper IAM roles — no hardcoded credentials.
Architecture
┌─────────────┐ │ Internet │ └──────┬──────┘ │ ┌──────┴──────┐ │ Security │ │ Group │ │ (port 80) │ └──────┬──────┘ │ ┌────────────┴────────────┐ │ │ ┌───────┴───────┐ ┌────────┴────────┐ │ EC2 Web │ │ S3 Static │ │ Instance │ │ Assets Bucket │ │ (app server) │ │ (images, css) │ └───────┬───────┘ └────────┬────────┘ │ │ └──────────┬─────────────┘ │ ┌───────┴───────┐ │ IAM Role │ │ (EC2 reads │ │ from S3) │ └───────────────┘Prerequisites
- AWS account with programmatic access configured
- Terraform installed (v1.5+)
- AWS CLI configured with
aws configure
Step 1: Terraform Configuration
Create main.tf:
provider "aws" { region = var.aws_region}
# S3 bucket for static assetsresource "aws_s3_bucket" "static_assets" { bucket = "${var.project_name}-static-${data.aws_caller_identity.current.account_id}"}
resource "aws_s3_bucket_versioning" "static_versioning" { bucket = aws_s3_bucket.static_assets.id versioning_configuration { status = "Enabled" }}
resource "aws_s3_bucket_public_access_block" "static_block" { bucket = aws_s3_bucket.static_assets.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true}
# IAM role for EC2 to access S3resource "aws_iam_role" "ec2_s3_role" { name = "${var.project_name}-ec2-s3-role"
assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Principal = { Service = "ec2.amazonaws.com" } Action = "sts:AssumeRole" } ] })}
resource "aws_iam_role_policy" "ec2_s3_policy" { name = "${var.project_name}-ec2-s3-policy" role = aws_iam_role.ec2_s3_role.id
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:ListBucket" ] Resource = [ aws_s3_bucket.static_assets.arn, "${aws_s3_bucket.static_assets.arn}/*" ] } ] })}
resource "aws_iam_instance_profile" "ec2_profile" { name = "${var.project_name}-instance-profile" role = aws_iam_role.ec2_s3_role.name}
# Security groupresource "aws_security_group" "web_sg" { name = "${var.project_name}-web-sg" description = "Allow HTTP traffic"
ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] }
ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = var.allowed_ssh_cidrs }
egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] }}
# EC2 instanceresource "aws_instance" "web_server" { ami = data.aws_ami.amazon_linux.id instance_type = var.instance_type iam_instance_profile = aws_iam_instance_profile.ec2_profile.name vpc_security_group_ids = [aws_security_group.web_sg.id] user_data = templatefile("user_data.sh", { s3_bucket = aws_s3_bucket.static_assets.bucket })
tags = { Name = "${var.project_name}-web-server" }}
# Outputsoutput "web_server_public_ip" { value = aws_instance.web_server.public_ip}
output "static_assets_bucket" { value = aws_s3_bucket.static_assets.bucket}Create variables.tf:
variable "aws_region" { description = "AWS region" type = string default = "us-east-1"}
variable "project_name" { description = "Project name used for resource naming" type = string default = "skillber-webapp"}
variable "instance_type" { description = "EC2 instance type" type = string default = "t3.micro"}
variable "allowed_ssh_cidrs" { description = "CIDR blocks allowed for SSH" type = list(string) default = ["10.0.0.0/8"]}Create user_data.sh:
#!/bin/bashyum update -yyum install -y httpdsystemctl start httpdsystemctl enable httpd
# Copy static assets from S3aws s3 sync s3://${s3_bucket}/ /var/www/html/
# Create a sample index pagecat > /var/www/html/index.html << EOF<!DOCTYPE html><html><head><title>Skillber Web App</title></head><body><h1>Welcome to Skillber</h1><p>Deployed via Terraform with EC2 + S3 + IAM</p><p>Server: $(hostname -f)</p></body></html>EOFCreate outputs.tf:
data "aws_caller_identity" "current" {}
data "aws_ami" "amazon_linux" { most_recent = true owners = ["amazon"]
filter { name = "name" values = ["al2023-ami-*-x86_64"] }}Step 2: Deploy
# Initialize Terraformterraform init
# Preview the changesterraform plan
# Apply the configurationterraform apply -auto-approve
# Upload static assets to S3aws s3 cp assets/ s3://$(terraform output -raw static_assets_bucket)/ --recursiveStep 3: Verify
# Get the public IPterraform output web_server_public_ip
# Test the web servercurl http://<public-ip>Step 4: Clean Up
# Empty the S3 bucket first (required for destroy)aws s3 rm s3://$(terraform output -raw static_assets_bucket)/ --recursive
# Destroy all resourcesterraform destroy -auto-approveCaution
The S3 bucket must be empty before Terraform can destroy it. Use aws s3 rm with --recursive to delete all objects, including all versions if versioning is enabled.
Deliverables
- Terraform code that provisions EC2 + S3 + IAM resources
- EC2 instance running a web server that reads from S3 without hardcoded credentials
- IAM role with least-privilege S3 access policy
- Working web server accessible via public IP