Skip to main content

Command Palette

Search for a command to run...

πŸš€ End-to-End DevOps Project on AWS

Updated
β€’6 min read

CI/CD Pipeline with Terraform, Docker, GitHub Actions & EKS


πŸ“Œ Introduction

In this project, I implemented a complete production-style DevOps workflow on AWS, starting from application development to automated deployment on Amazon EKS using Terraform, Docker, GitHub Actions, and Kubernetes.

This project demonstrates how modern DevOps teams:

  • Provision infrastructure using Infrastructure as Code

  • Build and push container images automatically

  • Deploy applications to Kubernetes with zero manual intervention


🧱 Tech Stack Used

  • AWS – EKS, EC2, VPC, IAM, ECR, ELB

  • Terraform – Infrastructure as Code

  • Docker – Containerization

  • GitHub Actions – CI/CD automation

  • Kubernetes – Application orchestration

  • Python (Flask) – Backend API

  • HTML / CSS / JS – Frontend


πŸ—οΈ Project Architecture

User
  ↓
AWS Load Balancer (via Kubernetes Service)
  ↓
EKS Cluster
  ↓
Kubernetes Deployment
  ↓
Docker Container (Flask App)

πŸ”Ή PHASE 1: Application Development


Backend – Flask API

app.py

from flask import Flask, jsonify, send_from_directory

app = Flask(__name__, static_folder="static")

PRODUCTS = [
    {"id": 1, "name": "Running Shoes", "price": 2999, "image": "shoes.jpg"},
    {"id": 2, "name": "Leather Bag", "price": 4599, "image": "bag.jpg"},
    {"id": 3, "name": "Smart Watch", "price": 6999, "image": "watch.jpg"}
]

@app.route("/")
def home():
    return send_from_directory(app.static_folder, "index.html")

@app.route("/products")
def products():
    return jsonify(PRODUCTS), 200

@app.route("/health")
def health():
    return jsonify({"status": "UP"})

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

βœ” Provides REST API
βœ” Health endpoint for Kubernetes readiness


Frontend – Static Website

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>DevOps Shop CI/CD Live</title>
  <link rel="stylesheet" href="/static/style.css" />
</head>
<body>
  <header class="header">
    <h1>πŸ›’ DevOps Shop</h1>
  </header>

  <main id="products" class="product-container"></main>

  <script src="/static/script.js"></script>
</body>
</html>

script.js

fetch("/products")
  .then(response => response.json())
  .then(products => {
    const container = document.getElementById("products");

    products.forEach(product => {
      const card = document.createElement("div");
      card.className = "product-card";

      card.innerHTML = `
        <img src="/static/images/${product.image}" />
        <h3>${product.name}</h3>
        <p>β‚Ή${product.price}</p>
        <button>Add to Cart</button>
      `;

      container.appendChild(card);
    });
  });

style.css

body {
    font-family: Arial, sans-serif;
    background: #f4f4f4;
    margin: 0;
}

header {
    background: #0f172a;
    color: white;
    padding: 15px;
    text-align: center;
}

.product-container {
    display: flex;
    gap: 20px;
    padding: 20px;
    justify-content: center;
}

.product-card {
    background: white;
    padding: 15px;
    width: 200px;
    border-radius: 8px;
    text-align: center;
}

πŸ”Ή PHASE 2: Containerization with Docker


Dockerfile

FROM python:3.10-slim

WORKDIR /app

COPY . .

RUN pip install flask

EXPOSE 5000

CMD ["python", "app.py"]

βœ” Lightweight
βœ” Production-ready
βœ” Portable across environments


πŸ”Ή PHASE 3: Infrastructure as Code (Terraform)


Provider Configuration – provider.tf

provider "aws" {
  region = "ap-south-1"
}

VPC – vpc.tf

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "devops-shop-vpc"
  }
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "devops-shop-igw"
  }
}

resource "aws_subnet" "public_1" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "${var.aws_region}a"
  map_public_ip_on_launch = true

  tags = {
    Name = "devops-shop-public-1"
  }
}

resource "aws_subnet" "public_2" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = "${var.aws_region}b"
  map_public_ip_on_launch = true

  tags = {
    Name = "devops-shop-public-2"
  }
}

resource "aws_route_table" "public_rt" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "devops-shop-public-rt"
  }
}

resource "aws_route_table_association" "public_1_assoc" {
  subnet_id      = aws_subnet.public_1.id
  route_table_id = aws_route_table.public_rt.id
}

resource "aws_route_table_association" "public_2_assoc" {
  subnet_id      = aws_subnet.public_2.id
  route_table_id = aws_route_table.public_rt.id
}

βœ” Custom networking
βœ” Enterprise standard


EKS Cluster – eks.tf

resource "aws_eks_cluster" "main" {
  name     = "devops-shop-eks"
  role_arn = aws_iam_role.eks_cluster_role.arn

  vpc_config {
    subnet_ids = [
      aws_subnet.public_1.id,
      aws_subnet.public_2.id
    ]
  }

  depends_on = [
    aws_iam_role_policy_attachment.eks_cluster_policy
  ]

  tags = {
    Name = "devops-shop-eks"
  }
}

Node Group – nodegroup.tf

resource "aws_eks_node_group" "main" {
  cluster_name    = aws_eks_cluster.main.name
  node_group_name = "devops-shop-node-group"
  node_role_arn  = aws_iam_role.eks_node_role.arn
  subnet_ids     = [
    aws_subnet.public_1.id,
    aws_subnet.public_2.id
  ]

  scaling_config {
    desired_size = 1
    max_size     = 1
    min_size     = 1
  }

  instance_types = ["t3.small"]

  ami_type = "AL2023_x86_64_STANDARD"

  depends_on = [
    aws_iam_role_policy_attachment.eks_worker_node_policy,
    aws_iam_role_policy_attachment.eks_cni_policy,
    aws_iam_role_policy_attachment.eks_ecr_policy
  ]

  tags = {
    Name = "devops-shop-node-group"
  }
}

IAM Role

resource "aws_iam_role" "eks_cluster_role" {
  name = "devops-shop-eks-cluster-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "eks.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "eks_cluster_policy" {
  role       = aws_iam_role.eks_cluster_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
}

############################
# EKS Node Group IAM Role
############################

resource "aws_iam_role" "eks_node_role" {
  name = "devops-shop-eks-node-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_attachment" "eks_worker_node_policy" {
  role       = aws_iam_role.eks_node_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
}

resource "aws_iam_role_policy_attachment" "eks_cni_policy" {
  role       = aws_iam_role.eks_node_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
}

resource "aws_iam_role_policy_attachment" "eks_ecr_policy" {
  role       = aws_iam_role.eks_node_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
}

βœ” Enables Kubernetes LoadBalancer
βœ” Least-privilege focused


πŸ”Ή PHASE 4: Kubernetes Deployment


Deployment – deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: devops-shop
spec:
  replicas: 2
  selector:
    matchLabels:
      app: devops-shop
  template:
    metadata:
      labels:
        app: devops-shop
    spec:
      containers:
      - name: app
        image: <AWS_ACCOUNT_ID>.dkr.ecr.ap-south-1.amazonaws.com/devops-shop:latest
        ports:
        - containerPort: 5000

Service – service.yaml

apiVersion: v1
kind: Service
metadata:
  name: devops-shop-service
spec:
  type: LoadBalancer
  selector:
    app: devops-shop
  ports:
  - port: 80
    targetPort: 5000

βœ” Auto-creates AWS Load Balancer
βœ” Publicly accessible


πŸ”Ή PHASE 5: CI/CD with GitHub Actions


GitHub Actions Workflow – docker-ecr.yml

name: Build and Push Docker Image to ECR

on:
  push:
    branches:
      - main

jobs:
  build-and-push:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source code
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Login to Amazon ECR
        uses: aws-actions/amazon-ecr-login@v2

      - name: Create ECR repository if not exists
        run: |
          aws ecr describe-repositories --repository-names ${{ secrets.ECR_REPO_NAME }} \
          || aws ecr create-repository --repository-name ${{ secrets.ECR_REPO_NAME }}

      - name: Build Docker image
        run: |
          docker build -t ${{ secrets.ECR_REPO_NAME }}:${{ github.sha }} ./backend


      - name: Tag Docker image
        run: |
          docker tag ${{ secrets.ECR_REPO_NAME }}:${{ github.sha }} \
          ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.ECR_REPO_NAME }}:${{ github.sha }}


      - name: Push Docker image to ECR
        run: |
          docker push \
          ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.ECR_REPO_NAME }}:${{ github.sha }}


      - name: Update kubeconfig for EKS
        run: |
          aws eks update-kubeconfig \
            --name devops-shop-eks \
            --region ${{ secrets.AWS_REGION }}

      - name: Install kubectl
        uses: azure/setup-kubectl@v4
        with:
          version: 'latest'


      - name: Deploy to EKS (Rolling Update)
        run: |
          kubectl set image deployment/devops-shop \
            devops-shop=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.ECR_REPO_NAME }}:${{ github.sha }}

      - name: Verify rollout status
        run: |
          kubectl rollout status deployment/devops-shop --timeout=300s

βœ” Fully automated
βœ” No manual deployments


βœ… Final Outcome

  • Infrastructure provisioned using Terraform

  • Application containerized with Docker

  • CI/CD pipeline implemented using GitHub Actions

  • Application deployed on AWS EKS

  • Public access via AWS Load Balancer

  • Production-grade DevOps workflow


🎯 What This Project Proves

  • Strong understanding of real DevOps practices

  • Cloud-native mindset

  • Automation-first approach

  • Interview & production ready skills

More from this blog

Rasika DevOps

13 posts