π End-to-End DevOps Project on AWS
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