⏱ Duration: 5 Hours
📚 Learning Objectives
- Create Terraform configuration for AWS infrastructure
- Set up VPC with networking components
- Configure EC2 instance for application deployment
- Create security groups with proper access rules
- Build CI/CD pipeline with GitHub Actions
📖 Core Concepts (2 Hours)
Terraform Project Structure
terraform/
├── main.tf # Provider and main config
├── variables.tf # Input variables
├── outputs.tf # Output values
├── vpc.tf # VPC and networking
├── security_groups.tf # Security groups
├── ec2.tf # EC2 instance
└── terraform.tfvars # Variable values (git-ignored)Main Configuration (main.tf)
# terraform/main.tf
terraform {
required_version = ">= 1.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# Remote state configuration (recommended for team projects)
# backend "s3" {
# bucket = "your-terraform-state-bucket"
# key = "capstone/terraform.tfstate"
# region = "us-east-1"
# encrypt = true
# dynamodb_table = "terraform-locks"
# }
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Project = "DevOps-Capstone"
Environment = var.environment
ManagedBy = "Terraform"
}
}
}
# Data source for latest Amazon Linux 2 AMI
data "aws_ami" "amazon_linux_2" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}Variables Configuration (variables.tf)
# terraform/variables.tf
variable "aws_region" {
description = "AWS region for resources"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Environment name"
type = string
default = "production"
}
variable "project_name" {
description = "Project name for resource naming"
type = string
default = "capstone"
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "public_subnet_cidr" {
description = "CIDR block for public subnet"
type = string
default = "10.0.1.0/24"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
}
variable "key_name" {
description = "SSH key pair name"
type = string
}
variable "docker_image" {
description = "Docker image to deploy"
type = string
default = "your-dockerhub-username/capstone-app:latest"
}
variable "allowed_ssh_cidr" {
description = "CIDR blocks allowed for SSH access"
type = list(string)
default = ["0.0.0.0/0"] # Restrict in production!
}VPC and Networking (vpc.tf)
# terraform/vpc.tf
# VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw"
}
}
# Public Subnet
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidr
availability_zone = "${var.aws_region}a"
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-subnet"
}
}
# Route Table
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.project_name}-public-rt"
}
}
# Route Table Association
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}Security Groups (security_groups.tf)
# terraform/security_groups.tf
# Security Group for EC2 Instance
resource "aws_security_group" "app" {
name = "${var.project_name}-app-sg"
description = "Security group for capstone application"
vpc_id = aws_vpc.main.id
# SSH access
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = var.allowed_ssh_cidr
}
# HTTP access
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Application port
ingress {
description = "Application"
from_port = 8000
to_port = 8000
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# All outbound traffic
egress {
description = "All outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-app-sg"
}
}EC2 Instance (ec2.tf)
# terraform/ec2.tf
# EC2 Instance
resource "aws_instance" "app" {
ami = data.aws_ami.amazon_linux_2.id
instance_type = var.instance_type
key_name = var.key_name
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.app.id]
# User data script to install Docker and run container
user_data = <<-EOF
#!/bin/bash
set -e
# Update system
yum update -y
# Install Docker
amazon-linux-extras install docker -y
systemctl start docker
systemctl enable docker
usermod -a -G docker ec2-user
# Install Docker Compose
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
# Pull and run application
docker pull ${var.docker_image}
docker run -d \
--name capstone-app \
--restart unless-stopped \
-p 80:8000 \
-p 8000:8000 \
-e ENVIRONMENT=production \
-e APP_VERSION=1.0.0 \
${var.docker_image}
# Log completion
echo "Setup complete" > /var/log/user-data-complete.log
EOF
root_block_device {
volume_size = 20
volume_type = "gp3"
encrypted = true
}
tags = {
Name = "${var.project_name}-app-server"
}
}
# Elastic IP (optional - for static IP)
resource "aws_eip" "app" {
instance = aws_instance.app.id
domain = "vpc"
tags = {
Name = "${var.project_name}-app-eip"
}
}Outputs (outputs.tf)
# terraform/outputs.tf
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}
output "public_subnet_id" {
description = "Public subnet ID"
value = aws_subnet.public.id
}
output "instance_id" {
description = "EC2 instance ID"
value = aws_instance.app.id
}
output "instance_public_ip" {
description = "EC2 instance public IP"
value = aws_eip.app.public_ip
}
output "instance_public_dns" {
description = "EC2 instance public DNS"
value = aws_instance.app.public_dns
}
output "app_url" {
description = "Application URL"
value = "http://${aws_eip.app.public_ip}:8000"
}
output "health_check_url" {
description = "Health check URL"
value = "http://${aws_eip.app.public_ip}:8000/health"
}GitHub Actions CI/CD Pipeline
# .github/workflows/cicd.yml
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
DOCKER_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/capstone-app
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r app/requirements.txt
- name: Run tests
run: |
pytest tests/ -v --tb=short
build:
name: Build and Push Docker Image
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
push: true
tags: |
${{ env.DOCKER_IMAGE }}:latest
${{ env.DOCKER_IMAGE }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
name: Deploy to EC2
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Deploy to EC2
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.EC2_HOST }}
username: ec2-user
key: ${{ secrets.EC2_SSH_KEY }}
script: |
# Pull latest image
docker pull ${{ env.DOCKER_IMAGE }}:latest
# Stop and remove old container
docker stop capstone-app || true
docker rm capstone-app || true
# Run new container
docker run -d \
--name capstone-app \
--restart unless-stopped \
-p 80:8000 \
-p 8000:8000 \
-e ENVIRONMENT=production \
-e APP_VERSION=${{ github.sha }} \
${{ env.DOCKER_IMAGE }}:latest
# Clean up old images
docker image prune -f
# Verify deployment
sleep 10
curl -f http://localhost:8000/health || exit 1🔬 Hands-on Lab (2.5 Hours)
Lab 1: Create Terraform Configuration
# Navigate to terraform directory
cd capstone-project/terraform
# Create all terraform files (copy content from above sections)
# main.tf, variables.tf, outputs.tf, vpc.tf, security_groups.tf, ec2.tf
# Create terraform.tfvars (DO NOT commit this file!)
cat > terraform.tfvars << 'EOF'
aws_region = "us-east-1"
environment = "production"
project_name = "capstone"
key_name = "your-key-pair-name"
docker_image = "your-dockerhub-username/capstone-app:latest"
allowed_ssh_cidr = ["YOUR_IP/32"] # Replace with your IP
EOF
# Add to .gitignore
echo "terraform.tfvars" >> ../.gitignore
echo "*.tfstate*" >> ../.gitignore
echo ".terraform/" >> ../.gitignoreLab 2: Deploy Infrastructure
# Initialize Terraform
terraform init
# Validate configuration
terraform validate
# Format code
terraform fmt
# Preview changes
terraform plan -out=tfplan
# Apply changes (creates infrastructure)
terraform apply tfplan
# Get outputs
terraform output
# Note the app_url and health_check_url
echo "Application URL: $(terraform output -raw app_url)"
echo "Health Check: $(terraform output -raw health_check_url)"Lab 3: Configure GitHub Secrets
# Required GitHub Secrets for CI/CD pipeline:
# Settings → Secrets and variables → Actions → New repository secret
# 1. DOCKERHUB_USERNAME - Your Docker Hub username
# 2. DOCKERHUB_TOKEN - Docker Hub access token (not password!)
# 3. EC2_HOST - EC2 public IP from terraform output
# 4. EC2_SSH_KEY - Private SSH key for EC2 access
# Generate Docker Hub access token:
# Docker Hub → Account Settings → Security → New Access Token
# Get SSH key:
cat ~/.ssh/your-key-pair.pem
# Add secrets via GitHub CLI (optional):
gh secret set DOCKERHUB_USERNAME
gh secret set DOCKERHUB_TOKEN
gh secret set EC2_HOST
gh secret set EC2_SSH_KEY < ~/.ssh/your-key-pair.pemLab 4: Create and Test CI/CD Pipeline
# Create GitHub Actions workflow
mkdir -p .github/workflows
cat > .github/workflows/cicd.yml << 'EOF'
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
DOCKER_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/capstone-app
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install -r app/requirements.txt
- run: pytest tests/ -v
build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
push: true
tags: ${{ env.DOCKER_IMAGE }}:latest
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.EC2_HOST }}
username: ec2-user
key: ${{ secrets.EC2_SSH_KEY }}
script: |
docker pull ${{ env.DOCKER_IMAGE }}:latest
docker stop capstone-app || true
docker rm capstone-app || true
docker run -d --name capstone-app -p 80:8000 ${{ env.DOCKER_IMAGE }}:latest
EOF
# Commit and push to trigger pipeline
git add .
git commit -m "Add CI/CD pipeline"
git push origin main
# Monitor pipeline in GitHub Actions tab
echo "Check pipeline: https://github.com/YOUR_USERNAME/capstone-project/actions"✅ Day 3 Checklist
- Created Terraform configuration files
- Configured VPC with public subnet and internet gateway
- Set up security groups with proper rules
- Deployed EC2 instance with Docker
- Created GitHub Actions CI/CD workflow
- Configured GitHub Secrets for deployment
- Tested infrastructure deployment