Project Overview

Imagine a world where infrastructure provisions itself, deployments happen at the speed of light, and applications run seamlessly in containers. Sounds like DevOps heaven, right? Well, that’s exactly what we’ve built! This project is a Flask API running on AWS EC2, with a PostgreSQL database, fully automated using Terraform, Docker, and GitHub Actions. No more manual configurations—just pure DevOps wizardry! 🧙‍♂️✨


Why This Project?

We set out to create a fully automated deployment pipeline with a few key objectives:
Automate Deployments – Because clicking buttons is overrated.

Infrastructure as Code (IaC) – Terraform makes provisioning as easy as writing poetry.

Containerization – So our app runs smoothly, anywhere, anytime.

AWS Deployment – Because cloud is king, and EC2 is our throne.


🛠 Tech Stack & Tools Used

Infrastructure & Deployment

🔹 AWS EC2 → Our app’s luxurious penthouse in the cloud.

🔹 Terraform → Automates infrastructure like a charm.

🔹 GitHub Actions → Our deployment butler, making CI/CD effortless.

🔹 Docker & Docker Compose → Containers keep our app lightweight and portable.

🔹 SSH (appleboy/ssh-action) → Because deploying securely is non-negotiable.

Backend & Database

🔹 Flask → Python’s sleek and simple web framework.

🔹 PostgreSQL → The brain storing all our precious data.

Version Control & Registry

🔹 Git & GitHub → Our code’s happy place.

🔹 Docker Hub → The treasure chest for our container images.

Security & Networking

🔹 IAM Roles & Policies → Guarding AWS like a bouncer at an exclusive club.

🔹 Security Groups → Keeping unwanted traffic out like a medieval fortress.


📌 Project Setup & Installation Guide

This guide will walk you through setting up the project from scratch. No magic wands required, just follow these steps! 🧙‍♂️

🛠 Prerequisites

Before you dive in, ensure you have these installed:

🔹 On Your Local Machine:

Git → Version control is life.

Docker & Docker Compose → Containerization makes life easier.

Python 3.x & pip → Flask won’t run without it.

Terraform → Because writing YAML files is old school.

AWS CLI → Talking to AWS like a pro.

🔹 On AWS EC2 Instance:

Ubuntu 22.04 (or latest stable version).

Docker & Docker Compose installed.

PostgreSQL installed manually or via Docker.


📂 Cloning the Repositories

Now let’s grab the code and start the fun!

# Clone the main project repo
git clone https://github.com/DestinyObs/ictg_task.git
cd ictg_task

# Clone the Terraform automation repo
git clone https://github.com/DestinyObs/ictg_automate_v2.git
cd ictg_automate_v2

⚙️ Setting Up Infrastructure on AWS with Terraform

Terraform is our infrastructure wizard, automating EC2 provisioning and security configurations so we don’t have to click around AWS like it’s 2010.

🏗️ Provisioning an EC2 Instance with Terraform

🔹 Steps to Deploy:

1️⃣ Navigate to the Terraform automation repository:

cd ictg_automate_v2

2️⃣ Initialize Terraform:

terraform init

3️⃣ Validate the configuration:

terraform validate

4️⃣ Preview what Terraform is about to create:

terraform plan

5️⃣ Apply and deploy the infrastructure:

terraform apply -auto-approve

💡 After execution, Terraform will provide the EC2 instance details like IP address and security settings.


🛠️ 1. Infrastructure Setup (Terraform)

🔑 1.1 Generating an SSH Key Pair

Before launching the EC2 instance, we need an SSH key to securely connect to it.

resource "tls_private_key" "ec2_key" {
  algorithm = "RSA"
  rsa_bits  = 2048
}

📌 Explanation:
✅ Generates an RSA 2048-bit private key for SSH authentication. No more passwords—only secure keys!


🔑 1.2 Creating a Key Pair in AWS

To allow SSH access, we need to register the public key with AWS.

resource "aws_key_pair" "ictg_key" {
  key_name   = var.key_name
  public_key = tls_private_key.ec2_key.public_key_openssh
}

📌 Explanation:
✅ This creates an AWS Key Pair using our generated SSH key.
✅ The private key remains local for secure SSH connections.


📂 1.3 Storing the Private Key Locally

Terraform saves the private key to a file for authentication.

resource "local_file" "ssh_key" {
  content  = tls_private_key.ec2_key.private_key_pem
  filename = "${path.module}/ictg_automate_key.pem"
}

📌 Important: Keep this key safe—losing it is like losing your house keys, but worse.


🔒 1.4 Security Group Configuration

A Security Group defines what network traffic is allowed.

resource "aws_security_group" "ictg_automate_sg" {
  name        = var.security_group_name
  description = "Allow SSH, HTTP, and necessary ports"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"] # Open for SSH (Restrict in production)
  }

  ingress {
    from_port   = 8000
    to_port     = 8000
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"] # Backend API access
  }

  ingress {
    from_port   = 5173
    to_port     = 5173
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"] # Frontend access
  }

  ingress {
    from_port   = 5432
    to_port     = 5432
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"] # PostgreSQL database access
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"] # Allow all outbound traffic
  }
}

📌 Explanation:
✅ SSH (22) → Remote access.
✅ Backend (8000) → API communication.
✅ Frontend (5173) → Web app access.
✅ Database (5432) → PostgreSQL connectivity.
✅ Outbound rule → Allows all outgoing traffic for smooth operations.


💻 1.5 Creating the EC2 Instance

Finally, let’s provision our EC2 instance with Terraform.

resource "aws_instance" "app_server" {
  ami           = var.ami_id
  instance_type = var.instance_type
  key_name      = aws_key_pair.ictg_key.key_name
  security_groups = [aws_security_group.ictg_automate_sg.name]

  user_data = file("${path.module}/app-setup.sh")

  provisioner "file" {
    source      = "app-setup.sh"
    destination = "/home/ubuntu/app-setup.sh"
  }

  provisioner "remote-exec" {
    inline = [
      "chmod +x /home/ubuntu/app-setup.sh",
      "sudo /home/ubuntu/app-setup.sh"
    ]
  }

  connection {
    type        = "ssh"
    user        = "ubuntu"
    private_key = tls_private_key.ec2_key.private_key_pem
    host        = self.public_ip
  }

  tags = {
    Name = "ICTG App Server"
  }
}

📌 Explanation:
✅ Uses an Ubuntu AMI (adjust as needed).
✅ Attaches the Security Group.
✅ Uploads and executes a setup script (app-setup.sh).
✅ Establishes an SSH connection.


📍 1.6 Outputting Important Details

Terraform outputs key details so you don’t have to dig around AWS.

output "instance_public_ip" {
  description = "Public IP of the EC2 instance"
  value       = aws_instance.app_server.public_ip
}

output "private_key_file" {
  description = "Path to the private key file"
  value       = local_file.ssh_key.filename
}

output "security_group_id" {
  description = "ID of the created security group"
  value       = aws_security_group.ictg_automate_sg.id
}

📌 Explanation:
✅ Public IP → Needed to access the server.
✅ Private Key File → Used for SSH login.
✅ Security Group ID → For managing network rules.


Image description

🚀 Next Steps: Now that our infrastructure is live, let’s move on to containerizing the application with Docker! 🐳

🐳 2. Containerizing the Application (Docker Setup)

The application is containerized using Docker for consistency and portability.

🔹 Steps to Build and Run Locally:

1️⃣ Navigate to the project directory:

cd ictg_task

2️⃣ Build and start the containers using Docker Compose:

docker-compose up --build -d

3️⃣ Verify that the containers are running:

docker ps

🔹 Docker Compose Configuration

version: "3.8"
services:
  backend:
    build:
      context: ./backend
    container_name: backend_service
    ports:
      - "8000:8000"
    depends_on:
      - database
    env_file:
      - ./backend/.env
    networks:
      - devops-network
    restart: unless-stopped

  frontend:
    build:
      context: ./frontend
    container_name: frontend_service
    ports:
      - "5173:5173"
    env_file:
      - ./frontend/.env
    networks:
      - devops-network
    restart: unless-stopped

  database:
    image: postgres:latest
    container_name: postgres_database
    ports:
      - "5432:5432"
    volumes:
      - ./postgres_data:/var/lib/postgresql/data
    env_file:
      - ./backend/.env
    networks:
      - devops-network
    restart: unless-stopped

networks:
  devops-network:
    driver: bridge

📌 Explanation:
✅ Backend (Flask API) → Runs on port 8000.
✅ Frontend (React/Vue) → Runs on port 5173.
✅ Database (PostgreSQL) → Runs on port 5432.
✅ Uses Environment Variables from .env files.
✅ Ensures services restart if they fail (restart: unless-stopped).

Image description

🚀 3. Deploying the Application to EC2

Once Terraform provisions the EC2 instance, GitHub Actions will handle automatic deployment.

🔹 Manual Deployment via SSH (Optional Alternative to CI/CD)

If needed, you can manually deploy using SSH:
1️⃣ SSH into the EC2 instance:

ssh -i your-key.pem ubuntu@

2️⃣ Pull the latest Docker images from Docker Hub:

docker pull destinyobs/ictg_task-backend:latest
docker pull destinyobs/ictg_task-frontend:latest

3️⃣ Run the application using Docker:

docker-compose up -d

🎯 4. Verifying the Deployment

Once deployed, test the setup:
✅ Backend:

curl -X GET http://:8000/health

Expected Response: { "status": "ok" }

✅ Frontend: Open http://:5173 in a browser.

🪔 3. CI/CD Pipeline Setup for EC2 Deployment Using GitHub Actions

To streamline the deployment process of our application to an AWS EC2 instance, we leverage GitHub Actions. This setup ensures that every push to the main branch triggers an automated build and deployment of our backend and frontend applications.


Workflow Overview

This GitHub Actions workflow:
✅ Triggers on every push to the main branch.
✅ Builds Docker images for the frontend and backend.
✅ Pushes the images to Docker Hub.
✅ Connects to the EC2 instance via SSH.
✅ Deploys the updated containers automatically.


Workflow Breakdown

1. Define the Workflow

name: Deploy to EC2

on:
  push:
    branches:
      - main

This triggers the workflow when a push occurs on the main branch.

2. Define the Deployment Job

jobs:
  deploy:
    runs-on: ubuntu-latest

Defines a job called deploy that runs on an Ubuntu-based GitHub Actions runner.

3. Checkout the Repository

steps:
  - name: Checkout Repository
    uses: actions/checkout@v3

Fetches the repository code for further processing.

4. Authenticate with Docker Hub

- name: Log in to Docker Hub
    uses: docker/login-action@v2
    with:
      username: ${{ secrets.DOCKER_USERNAME }}
      password: ${{ secrets.DOCKER_PASSWORD }}

Logs into Docker Hub securely using GitHub Secrets.

5. Build and Push Docker Images

- name: Build and Push Backend Image
    run: |
      docker build -t ${{ secrets.DOCKER_USERNAME }}/ictg_task-backend:latest ./backend
      docker push ${{ secrets.DOCKER_USERNAME }}/ictg_task-backend:latest

  - name: Build and Push Frontend Image
    run: |
      docker build -t ${{ secrets.DOCKER_USERNAME }}/ictg_task-frontend:latest ./frontend
      docker push ${{ secrets.DOCKER_USERNAME }}/ictg_task-frontend:latest

Builds and pushes Docker images for both the backend and frontend.

6. Connect to EC2 and Deploy Containers

- name: Deploy to EC2
    uses: appleboy/[email protected]
    with:
      host: ${{ secrets.EC2_HOST }}
      username: ${{ secrets.EC2_USER }}
      key: ${{ secrets.EC2_PRIVATE_KEY }}
      script: |

Securely connects to the EC2 instance using SSH.

7. Remove Old Containers

docker stop backend_service frontend_service postgres_database || true
        docker rm backend_service frontend_service postgres_database || true

Stops and removes any existing containers to prevent conflicts.

8. Pull the Latest Docker Images

docker pull ${{ secrets.DOCKER_USERNAME }}/ictg_task-backend:latest
        docker pull ${{ secrets.DOCKER_USERNAME }}/ictg_task-frontend:latest

Retrieves the latest versions of the images from Docker Hub.

9. Ensure Docker Network Exists

docker network inspect devops-network >/dev/null 2>&1 || docker network create devops-network

Checks for the network and creates it if missing.

10. Run PostgreSQL Container

docker run -d \
          --name postgres_database \
          --network devops-network \
          -e POSTGRES_USER=app \
          -e POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} \
          -e POSTGRES_DB=ictg_db \
          -p 5432:5432 \
          --restart unless-stopped \
          postgres:latest

Ensures the database container runs with the correct configurations.

11. Ensure PostgreSQL Readiness

retries=10
        until docker exec postgres_database pg_isready -U app || [ $retries -eq 0 ]; do
          echo "Waiting for PostgreSQL to be ready..."
          sleep 5
          retries=$((retries - 1))
        done
        if [ $retries -eq 0 ]; then
          echo "PostgreSQL failed to start." >&2
          exit 1
        fi

Waits for PostgreSQL to be fully initialized.

12. Run Backend and Frontend Services

docker run -d \
          --name backend_service \
          --network devops-network \
          -e DATABASE_URL="postgresql://app:${{ secrets.POSTGRES_PASSWORD }}@postgres_database:5432/ictg_db" \
          -p 8000:8000 \
          --restart unless-stopped \
          ${{ secrets.DOCKER_USERNAME }}/ictg_task-backend:latest

        docker run -d \
          --name frontend_service \
          --network devops-network \
          -p 5173:5173 \
          --restart unless-stopped \
          ${{ secrets.DOCKER_USERNAME }}/ictg_task-frontend:latest

Deploys the backend and frontend services.

13. Clean Up Unused Docker Resources

docker system prune -af

Frees up disk space by removing unused containers and images.

Image description

Image description

Security Considerations

Secrets Management: All sensitive credentials are stored securely in GitHub Secrets.
Access Restrictions: The EC2 Security Group is configured to allow only necessary traffic.

Database Security: PostgreSQL password is stored securely and not hardcoded.

Why This Setup?

Fully Automated Deployment – Code push triggers deployment.
Zero Downtime – Smooth updates without disruptions.
Security First – No hardcoded credentials.
Efficient Resource Management – Cleanup ensures optimal performance.


Conclusion & Next Steps

Summary of Achievements

We successfully automated the deployment of a full-stack application using Terraform, Docker, and GitHub Actions. The key milestones included:

🔹 Infrastructure Setup – Provisioned an AWS EC2 instance using Terraform.
🔹 Containerization – Dockerized backend, frontend, and database services.
🔹 CI/CD Pipeline – Automated build and deployment using GitHub Actions.
🔹 Automated Deployment – Utilized SSH actions to update containers smoothly.

Potential Improvements

🚀 Enhanced Security: Implement IAM roles, security best practices, and database access restrictions.
🚀 Automated Scaling: Leverage AWS Auto Scaling and Load Balancing.
🚀 Monitoring & Logging: Integrate Prometheus, Grafana, and centralized logging.
🚀 Infrastructure as Code Refinement: Use remote state management (S3) and modularized configurations.
🚀 Nginx Reverse Proxy & SSL: Implement Nginx for traffic management and SSL security.

This project provides a strong foundation for continuous deployment and scaling in a DevOps-oriented environment. 🚀


Note: This setup is a foundational approach as I continue expanding my expertise in this field. I hope this guide proves useful to fellow DevOps enthusiasts tackling similar challenges!