adding task files
هذا الالتزام موجود في:
16
q3-cicd/Dockerfile
Normal file
16
q3-cicd/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["python", "app.py"]
|
||||
254
q3-cicd/README.md
Normal file
254
q3-cicd/README.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# Ghaymah SRE - CI/CD Pipeline Project
|
||||
|
||||
## Overview
|
||||
|
||||
This project demonstrates a complete CI/CD pipeline using **GitHub Actions**, **Docker**, **Docker Hub**, and **AWS EC2**.
|
||||
|
||||
The pipeline automatically builds a Docker image, pushes it to Docker Hub, waits for manual approval before production deployment, and deploys the application to an EC2 instance.
|
||||
|
||||
---
|
||||
|
||||
# Project Architecture
|
||||
|
||||
Developer
|
||||
│
|
||||
▼
|
||||
GitHub Repository
|
||||
│
|
||||
▼
|
||||
GitHub Actions
|
||||
│
|
||||
▼
|
||||
Build Docker Image
|
||||
│
|
||||
▼
|
||||
Push Image to Docker Hub
|
||||
│
|
||||
▼
|
||||
Manual Approval
|
||||
│
|
||||
▼
|
||||
Deploy to AWS EC2
|
||||
│
|
||||
▼
|
||||
Docker Container
|
||||
│
|
||||
▼
|
||||
Running Flask Application
|
||||
|
||||
---
|
||||
|
||||
# Technologies Used
|
||||
|
||||
- Git
|
||||
- GitHub
|
||||
- GitHub Actions
|
||||
- Docker
|
||||
- Docker Hub
|
||||
- AWS EC2 (Amazon Linux 2023)
|
||||
- Python Flask
|
||||
|
||||
---
|
||||
|
||||
# Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── .github
|
||||
│ └── workflows
|
||||
│ └── docker-build-push.yml
|
||||
├── Dockerfile
|
||||
├── requirements.txt
|
||||
├── app.py
|
||||
├── templates/
|
||||
├── static/
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# CI/CD Workflow
|
||||
|
||||
The GitHub Actions workflow performs the following steps:
|
||||
|
||||
1. Trigger on every push to the main branch.
|
||||
2. Checkout the repository.
|
||||
3. Log in to Docker Hub using GitHub Secrets.
|
||||
4. Build the Docker image.
|
||||
5. Push the image to Docker Hub.
|
||||
6. Wait for manual approval using the Production Environment.
|
||||
7. Deploy the latest image to the EC2 server.
|
||||
8. Verify that the application is running successfully.
|
||||
|
||||
---
|
||||
|
||||
# GitHub Secrets
|
||||
|
||||
The following repository secrets were configured:
|
||||
|
||||
| Secret | Description |
|
||||
|---------|-------------|
|
||||
| DOCKER_USERNAME | Docker Hub username |
|
||||
| DOCKER_TOKEN | Docker Hub Access Token |
|
||||
|
||||
---
|
||||
|
||||
# Manual Approval
|
||||
|
||||
A GitHub Environment named **production** was created.
|
||||
|
||||
Deployment to production requires manual approval before execution.
|
||||
|
||||
This prevents accidental deployments and provides an additional safety layer.
|
||||
|
||||
---
|
||||
|
||||
# Docker Image
|
||||
|
||||
The application image is stored in Docker Hub.
|
||||
|
||||
Image name:
|
||||
|
||||
```
|
||||
moustafamedhat97/ghaymah-api
|
||||
```
|
||||
|
||||
Each build is tagged using the Git commit SHA.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
moustafamedhat97/ghaymah-api:0367751868ea7532b1b22f744baa5e49851158f6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Deployment
|
||||
|
||||
The deployment process performs the following:
|
||||
|
||||
- Pull latest Docker image
|
||||
- Stop old container (if exists)
|
||||
- Remove old container
|
||||
- Start new container
|
||||
- Expose port 8080
|
||||
- Restart application
|
||||
|
||||
Deployment command:
|
||||
|
||||
```bash
|
||||
docker pull moustafamedhat97/ghaymah-api:latest
|
||||
|
||||
docker stop ghaymah-api || true
|
||||
|
||||
docker rm ghaymah-api || true
|
||||
|
||||
docker run -d \
|
||||
--name ghaymah-api \
|
||||
-p 8080:8080 \
|
||||
moustafamedhat97/ghaymah-api:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Application Health Check
|
||||
|
||||
The application exposes a health endpoint:
|
||||
|
||||
```
|
||||
GET /health
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
http://<EC2-Public-IP>:8080/health
|
||||
```
|
||||
|
||||
Expected response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status":"UP"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Dashboard
|
||||
|
||||
The deployed application provides a dashboard displaying:
|
||||
|
||||
- Application Status
|
||||
- Response Time
|
||||
- Total Requests
|
||||
- Last Update Time
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
http://<EC2-Public-IP>:8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Difference Between Staging and Production
|
||||
|
||||
## Staging
|
||||
|
||||
- Used for testing before release.
|
||||
- Mirrors the production environment.
|
||||
- Safe for validation and QA.
|
||||
- Can contain test data.
|
||||
|
||||
## Production
|
||||
|
||||
- Live environment.
|
||||
- Used by end users.
|
||||
- Requires high availability.
|
||||
- Requires manual approval before deployment.
|
||||
|
||||
---
|
||||
|
||||
# Ghaymah CLI Integration
|
||||
|
||||
The original requirement requested deployment using the Ghaymah CLI.
|
||||
|
||||
Since the CLI was not available in the execution environment, deployment was simulated using Docker commands.
|
||||
|
||||
The integration steps would normally be:
|
||||
|
||||
1. Install Ghaymah CLI.
|
||||
2. Authenticate using:
|
||||
|
||||
```bash
|
||||
ghaymah login
|
||||
```
|
||||
|
||||
3. Configure the target project.
|
||||
|
||||
4. Deploy the application:
|
||||
|
||||
```bash
|
||||
ghaymah deploy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Result
|
||||
|
||||
The CI/CD pipeline was successfully implemented.
|
||||
|
||||
Achievements:
|
||||
|
||||
- Docker image successfully built.
|
||||
- Image pushed to Docker Hub.
|
||||
- Manual approval configured.
|
||||
- Application deployed to AWS EC2.
|
||||
- Docker container started successfully.
|
||||
- Flask application became accessible.
|
||||
- Health checks passed.
|
||||
- Dashboard successfully displayed through the browser.
|
||||
|
||||
---
|
||||
|
||||
70
q3-cicd/app.py
Normal file
70
q3-cicd/app.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from flask import Flask, jsonify, render_template, request
|
||||
import time
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Application start time
|
||||
start_time = time.time()
|
||||
|
||||
# Metrics
|
||||
request_count = 0
|
||||
last_response_time = 0
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
request.start_time = time.perf_counter()
|
||||
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
global request_count
|
||||
global last_response_time
|
||||
|
||||
request_count += 1
|
||||
last_response_time = round(
|
||||
(time.perf_counter() - request.start_time) * 1000, 2
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def home():
|
||||
return render_template("dashboard.html")
|
||||
|
||||
|
||||
@app.route("/health")
|
||||
def health():
|
||||
return jsonify({
|
||||
"status": "UP",
|
||||
"message": "Application is healthy",
|
||||
"uptime_seconds": round(time.time() - start_time, 2),
|
||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"version": "1.0.0"
|
||||
}), 200
|
||||
|
||||
|
||||
@app.route("/metrics")
|
||||
def metrics():
|
||||
return jsonify({
|
||||
"status": "UP",
|
||||
"requests": request_count,
|
||||
"response_time_ms": last_response_time,
|
||||
"uptime_seconds": round(time.time() - start_time, 2)
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/info")
|
||||
def api_info():
|
||||
return jsonify({
|
||||
"application": "Ghaymah SRE API",
|
||||
"language": "Python",
|
||||
"framework": "Flask",
|
||||
"version": "1.0.0",
|
||||
"author": "Moustafa Medhat"
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8080)
|
||||
1
q3-cicd/requirements.txt
Normal file
1
q3-cicd/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
Flask==3.1.0
|
||||
151
q3-cicd/templates/dashboard.html
Normal file
151
q3-cicd/templates/dashboard.html
Normal file
@@ -0,0 +1,151 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ghaymah SRE Dashboard</title>
|
||||
|
||||
<style>
|
||||
*{
|
||||
margin:0;
|
||||
padding:0;
|
||||
box-sizing:border-box;
|
||||
font-family:Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
body{
|
||||
background:#f5f7fb;
|
||||
}
|
||||
|
||||
.container{
|
||||
width:900px;
|
||||
margin:40px auto;
|
||||
}
|
||||
|
||||
h1{
|
||||
text-align:center;
|
||||
margin-bottom:30px;
|
||||
color:#2c3e50;
|
||||
}
|
||||
|
||||
.cards{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
gap:20px;
|
||||
}
|
||||
|
||||
.card{
|
||||
flex:1;
|
||||
background:white;
|
||||
border-radius:10px;
|
||||
padding:25px;
|
||||
text-align:center;
|
||||
box-shadow:0 5px 15px rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
.title{
|
||||
color:#666;
|
||||
font-size:18px;
|
||||
margin-bottom:15px;
|
||||
}
|
||||
|
||||
.value{
|
||||
font-size:34px;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
.up{
|
||||
color:green;
|
||||
}
|
||||
|
||||
.down{
|
||||
color:red;
|
||||
}
|
||||
|
||||
.footer{
|
||||
margin-top:35px;
|
||||
text-align:center;
|
||||
color:#777;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<h1>🚀 Ghaymah API Dashboard</h1>
|
||||
|
||||
<div class="cards">
|
||||
|
||||
<div class="card">
|
||||
<div class="title">Status</div>
|
||||
<div id="status" class="value">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="title">Response Time</div>
|
||||
<div id="response" class="value">0 ms</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="title">Requests</div>
|
||||
<div id="requests" class="value">0</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Last Update:
|
||||
<span id="time">--</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
async function loadMetrics(){
|
||||
|
||||
try{
|
||||
|
||||
const response = await fetch('/metrics');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const status=document.getElementById("status");
|
||||
|
||||
status.innerHTML=data.status;
|
||||
|
||||
status.className="value";
|
||||
|
||||
if(data.status==="UP")
|
||||
status.classList.add("up");
|
||||
else
|
||||
status.classList.add("down");
|
||||
|
||||
document.getElementById("response").innerHTML=data.response_time_ms+" ms";
|
||||
|
||||
document.getElementById("requests").innerHTML=data.requests;
|
||||
|
||||
document.getElementById("time").innerHTML=
|
||||
new Date().toLocaleTimeString();
|
||||
|
||||
}
|
||||
|
||||
catch(error){
|
||||
|
||||
document.getElementById("status").innerHTML="DOWN";
|
||||
|
||||
document.getElementById("status").className="value down";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
loadMetrics();
|
||||
|
||||
setInterval(loadMetrics,30000);
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
59
q3-cicd/workflow.yml
Normal file
59
q3-cicd/workflow.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
name: Build, Push and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Push Docker Image
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build Docker Image
|
||||
run: |
|
||||
docker build -t ${{ secrets.DOCKER_USERNAME }}/ghaymah-api:${{ github.sha }} .
|
||||
|
||||
- name: Push Docker Image
|
||||
run: |
|
||||
docker push ${{ secrets.DOCKER_USERNAME }}/ghaymah-api:${{ github.sha }}
|
||||
|
||||
deploy-production:
|
||||
name: Deploy to Production
|
||||
needs: build
|
||||
runs-on: self-hosted
|
||||
|
||||
environment:
|
||||
name: production
|
||||
|
||||
steps:
|
||||
- name: Pull Latest Image
|
||||
run: |
|
||||
docker pull ${{ secrets.DOCKER_USERNAME }}/ghaymah-api:${{ github.sha }}
|
||||
|
||||
- name: Stop Existing Container
|
||||
run: |
|
||||
docker stop ghaymah-api || true
|
||||
docker rm ghaymah-api || true
|
||||
|
||||
- name: Run New Container
|
||||
run: |
|
||||
docker run -d \
|
||||
--name ghaymah-api \
|
||||
--restart unless-stopped \
|
||||
-p 8080:8080 \
|
||||
${{ secrets.DOCKER_USERNAME }}/ghaymah-api:${{ github.sha }}
|
||||
|
||||
- name: Verify Deployment
|
||||
run: |
|
||||
docker ps
|
||||
المرجع في مشكلة جديدة
حظر مستخدم