othe ffiles
هذا الالتزام موجود في:
20
.ghaymah.json
Normal file
20
.ghaymah.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"id": "00000000-0000-0000-0000-000000000001",
|
||||||
|
"name": "nodeapp-production",
|
||||||
|
"projectId": "00000000-0000-0000-0000-000000000002",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"expose": true,
|
||||||
|
"number": 5000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"publicAccess": {
|
||||||
|
"enabled": true,
|
||||||
|
"domain": "auto"
|
||||||
|
},
|
||||||
|
"resourceTier": "t1",
|
||||||
|
"container": {
|
||||||
|
"image": "docker.io/your-username/nodeapp:production",
|
||||||
|
"pullSecretName": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
15
metrics.json
Normal file
15
metrics.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"timestamp": "2026-07-29T02:06:33.694428Z",
|
||||||
|
"latency_ms": 486.49,
|
||||||
|
"uptime": true,
|
||||||
|
"status_code": 200,
|
||||||
|
"dns_ms": 0.48,
|
||||||
|
"ssl_valid": false,
|
||||||
|
"ssl_expires": "Sep 15 13:10:47 2026 GMT",
|
||||||
|
"ssl_days_remaining": null,
|
||||||
|
"search_ms": 473.48,
|
||||||
|
"search_status_code": 200,
|
||||||
|
"overall": false
|
||||||
|
}
|
||||||
|
]
|
||||||
6
q1-deploy-monitor/Dockerfile
Normal file
6
q1-deploy-monitor/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
FROM node:16.3.0-alpine
|
||||||
|
COPY . /app
|
||||||
|
RUN mkdir -p /logs && touch /logs/logs.txt
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm install
|
||||||
|
CMD ["node", "/app/app.js"]
|
||||||
82
q1-deploy-monitor/app.js
Normal file
82
q1-deploy-monitor/app.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
const port = 5000
|
||||||
|
const logDir = '/logs'
|
||||||
|
const logFile = path.join(logDir, 'logs.txt')
|
||||||
|
|
||||||
|
if (!fs.existsSync(logDir)) {
|
||||||
|
fs.mkdirSync(logDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatsFromLogFile() {
|
||||||
|
if (!fs.existsSync(logFile)) {
|
||||||
|
return { requestCount: 0, averageResponseTime: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(logFile, 'utf8')
|
||||||
|
const lines = content.split('\n').filter(Boolean)
|
||||||
|
const requestCount = lines.length
|
||||||
|
|
||||||
|
if (requestCount === 0) {
|
||||||
|
return { requestCount: 0, averageResponseTime: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalResponseTime = lines.reduce((total, line) => {
|
||||||
|
const parts = line.trim().split(' ')
|
||||||
|
const duration = parseFloat(parts[1])
|
||||||
|
return total + (Number.isNaN(duration) ? 0 : duration)
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestCount,
|
||||||
|
averageResponseTime: totalResponseTime / requestCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use('/dashboard', express.static(path.join(__dirname, 'dashboard'), { index: false }))
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const startTime = Date.now()
|
||||||
|
const requestTime = new Date(startTime).toISOString()
|
||||||
|
|
||||||
|
res.on('finish', () => {
|
||||||
|
const durationSeconds = (Date.now() - startTime) / 1000
|
||||||
|
const line = `${req.path} ${durationSeconds.toFixed(3)} ${requestTime} ${res.statusCode}\n`
|
||||||
|
|
||||||
|
fs.appendFile(logFile, line, (err) => {
|
||||||
|
if (err) console.error('Failed to write request log:', err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/dashboard', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'dashboard', 'index.html'))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/stats', (req, res) => {
|
||||||
|
const { requestCount, averageResponseTime } = getStatsFromLogFile()
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
requestCount,
|
||||||
|
averageResponseTime,
|
||||||
|
status: 'up'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.send('Hello World from GHAYMAH')
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.send('The app up and runing')
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Example app listening at http://localhost:${port}`)
|
||||||
|
console.log(`Dashboard available at http://localhost:${port}/dashboard`)
|
||||||
|
})
|
||||||
49
q1-deploy-monitor/dashboard.js
Normal file
49
q1-deploy-monitor/dashboard.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
const requestCountEl = document.getElementById('requestCount')
|
||||||
|
const averageTimeEl = document.getElementById('averageTime')
|
||||||
|
const appStatusEl = document.getElementById('appStatus')
|
||||||
|
const statusBadgeEl = document.getElementById('statusBadge')
|
||||||
|
const statusTextEl = document.getElementById('statusText')
|
||||||
|
const lastUpdatedEl = document.getElementById('lastUpdated')
|
||||||
|
|
||||||
|
function setStatus(isUp) {
|
||||||
|
statusBadgeEl.classList.remove('up', 'down')
|
||||||
|
appStatusEl.classList.remove('up', 'down')
|
||||||
|
|
||||||
|
if (isUp) {
|
||||||
|
statusBadgeEl.classList.add('up')
|
||||||
|
appStatusEl.classList.add('up')
|
||||||
|
statusTextEl.textContent = 'Online'
|
||||||
|
appStatusEl.textContent = 'Up'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
statusBadgeEl.classList.add('down')
|
||||||
|
appStatusEl.classList.add('down')
|
||||||
|
statusTextEl.textContent = 'Offline'
|
||||||
|
appStatusEl.textContent = 'Down'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchStats() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/stats')
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Stats request failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
requestCountEl.textContent = data.requestCount.toLocaleString()
|
||||||
|
averageTimeEl.textContent = `${data.averageResponseTime.toFixed(3)}s`
|
||||||
|
setStatus(data.status === 'up')
|
||||||
|
lastUpdatedEl.textContent = `Last updated: ${new Date().toLocaleTimeString()}`
|
||||||
|
} catch (error) {
|
||||||
|
requestCountEl.textContent = '—'
|
||||||
|
averageTimeEl.textContent = '—'
|
||||||
|
setStatus(false)
|
||||||
|
lastUpdatedEl.textContent = 'Last updated: failed to connect'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchStats()
|
||||||
|
setInterval(fetchStats, 5000)
|
||||||
13
q1-deploy-monitor/health-check.py
Normal file
13
q1-deploy-monitor/health-check.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from urllib.request import urlopen
|
||||||
|
from urllib.error import HTTPError
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
with urlopen('https://my-application-2f85feb054d8.hosted.ghaymah.systems/health') as response:
|
||||||
|
print(f"Status Code: {response.status}")
|
||||||
|
|
||||||
|
except HTTPError as error:
|
||||||
|
print(f"Error Status Code: {error.code}")
|
||||||
|
time.sleep(30)
|
||||||
50
q1-deploy-monitor/index.html
Normal file
50
q1-deploy-monitor/index.html
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Server Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="/dashboard/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="dashboard">
|
||||||
|
<header class="header">
|
||||||
|
<div>
|
||||||
|
<h1>Server Dashboard</h1>
|
||||||
|
<p class="subtitle">Live monitoring for your Node.js application</p>
|
||||||
|
</div>
|
||||||
|
<div class="status-badge" id="statusBadge">
|
||||||
|
<span class="status-dot"></span>
|
||||||
|
<span id="statusText">Checking...</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="cards">
|
||||||
|
<section class="card">
|
||||||
|
<p class="card-label">Total Requests</p>
|
||||||
|
<p class="card-value" id="requestCount">—</p>
|
||||||
|
<p class="card-hint">All requests received by the server</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<p class="card-label">Average Response Time</p>
|
||||||
|
<p class="card-value" id="averageTime">—</p>
|
||||||
|
<p class="card-hint">Mean duration in seconds</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<p class="card-label">Application Status</p>
|
||||||
|
<p class="card-value status-value" id="appStatus">—</p>
|
||||||
|
<p class="card-hint">Based on health check</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<span id="lastUpdated">Last updated: —</span>
|
||||||
|
<span>Auto-refresh every 5 seconds</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/dashboard/dashboard.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
18
q1-deploy-monitor/package.json
Normal file
18
q1-deploy-monitor/package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "nodeapp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "app.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.17.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.1.14"
|
||||||
|
}
|
||||||
|
}
|
||||||
158
q1-deploy-monitor/style.css
Normal file
158
q1-deploy-monitor/style.css
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
padding: 0.65rem 1.1rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(15, 23, 42, 0.7);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #64748b;
|
||||||
|
box-shadow: 0 0 0 0 rgba(100, 116, 139, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.up .status-dot {
|
||||||
|
background: #22c55e;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.down .status-dot {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.up {
|
||||||
|
border-color: rgba(34, 197, 94, 0.35);
|
||||||
|
color: #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.down {
|
||||||
|
border-color: rgba(239, 68, 68, 0.35);
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: rgba(30, 41, 59, 0.85);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.15);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||||
|
transition: transform 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: rgba(96, 165, 250, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-label {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-value {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f8fafc;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value.up {
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value.down {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hint {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.5);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 8px rgba(34, 197, 94, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
body {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
0
q2-postmortem/postmortem-report.md
Normal file
0
q2-postmortem/postmortem-report.md
Normal file
175
q3-cicd/workflow.yml
Normal file
175
q3-cicd/workflow.yml
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
name: CI/CD - Ghaymah Cloud
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, staging]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, staging]
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOCKERHUB_REPOSITORY: ${{ secrets.DOCKERHUB_REPOSITORY }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
name: Build and Push to Docker Hub
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
outputs:
|
||||||
|
environment: ${{ steps.env.outputs.environment }}
|
||||||
|
image_uri: ${{ steps.image.outputs.image_uri }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
|
|
||||||
|
- name: Set deployment environment
|
||||||
|
id: env
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.ref_name }}" = "main" ]; then
|
||||||
|
echo "environment=production" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "environment=staging" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3.6.0
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./nodeapp
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPOSITORY }}:production
|
||||||
|
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPOSITORY }}:staging
|
||||||
|
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPOSITORY }}:${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Set deployment image
|
||||||
|
id: image
|
||||||
|
run: |
|
||||||
|
IMAGE_BASE="docker.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPOSITORY }}"
|
||||||
|
if [ "${{ steps.env.outputs.environment }}" = "production" ]; then
|
||||||
|
echo "image_uri=${IMAGE_BASE}:production" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "image_uri=${IMAGE_BASE}:staging" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build Docker Image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: docker build ./nodeapp
|
||||||
|
|
||||||
|
deploy-staging:
|
||||||
|
name: Deploy to Staging
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-and-push
|
||||||
|
if: github.event_name == 'push' && github.ref_name == 'staging'
|
||||||
|
environment:
|
||||||
|
name: staging
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
|
- name: Install Ghaymah CLI
|
||||||
|
run: curl -sSL https://cli.ghaymah.systems/install.sh | bash
|
||||||
|
|
||||||
|
- name: Login to Ghaymah
|
||||||
|
run: $HOME/ghaymah/bin/gy auth login --email "${{ secrets.GHAYMAH_EMAIL }}" --password "${{ secrets.GHAYMAH_PW }}"
|
||||||
|
|
||||||
|
- name: Create Ghaymah configuration
|
||||||
|
working-directory: nodeapp
|
||||||
|
env:
|
||||||
|
IMAGE_URI: ${{ needs.build-and-push.outputs.image_uri }}
|
||||||
|
PROJECT_ID: ${{ secrets.GHAYMAH_PROJECT_ID }}
|
||||||
|
APP_ID: ${{ secrets.GHAYMAH_APP_ID_STAGING }}
|
||||||
|
PULL_SECRET: ${{ secrets.DOCKERHUB_PULL_SECRET_NAME }}
|
||||||
|
run: |
|
||||||
|
jq -n \
|
||||||
|
--arg id "${APP_ID}" \
|
||||||
|
--arg name "nodeapp-staging" \
|
||||||
|
--arg projectId "${PROJECT_ID}" \
|
||||||
|
--arg image "${IMAGE_URI}" \
|
||||||
|
--arg pullSecret "${PULL_SECRET}" \
|
||||||
|
'{
|
||||||
|
id: $id,
|
||||||
|
name: $name,
|
||||||
|
projectId: $projectId,
|
||||||
|
ports: [{ expose: true, number: 5000 }],
|
||||||
|
publicAccess: {
|
||||||
|
enabled: true,
|
||||||
|
domain: "auto"
|
||||||
|
},
|
||||||
|
resourceTier: "t1",
|
||||||
|
container: {
|
||||||
|
image: $image,
|
||||||
|
pullSecretName: $pullSecret
|
||||||
|
}
|
||||||
|
}' > .ghaymah.json
|
||||||
|
|
||||||
|
cat .ghaymah.json
|
||||||
|
|
||||||
|
- name: Deploy to Ghaymah
|
||||||
|
working-directory: nodeapp
|
||||||
|
run: $HOME/ghaymah/bin/gy resource app launch --env staging
|
||||||
|
|
||||||
|
deploy-production:
|
||||||
|
name: Deploy to Production
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-and-push
|
||||||
|
if: github.event_name == 'push' && github.ref_name == 'main'
|
||||||
|
environment:
|
||||||
|
name: production
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
|
- name: Install Ghaymah CLI
|
||||||
|
run: curl -sSL https://cli.ghaymah.systems/install.sh | bash
|
||||||
|
|
||||||
|
- name: Login to Ghaymah
|
||||||
|
run: $HOME/ghaymah/bin/gy auth login --email "${{ secrets.GHAYMAH_EMAIL }}" --password "${{ secrets.GHAYMAH_PW }}"
|
||||||
|
|
||||||
|
- name: Create Ghaymah configuration
|
||||||
|
working-directory: nodeapp
|
||||||
|
env:
|
||||||
|
IMAGE_URI: ${{ needs.build-and-push.outputs.image_uri }}
|
||||||
|
PROJECT_ID: ${{ secrets.GHAYMAH_PROJECT_ID }}
|
||||||
|
APP_ID: ${{ secrets.GHAYMAH_APP_ID_PRODUCTION }}
|
||||||
|
PULL_SECRET: ${{ secrets.DOCKERHUB_PULL_SECRET_NAME }}
|
||||||
|
run: |
|
||||||
|
jq -n \
|
||||||
|
--arg id "${APP_ID}" \
|
||||||
|
--arg name "nodeapp-production" \
|
||||||
|
--arg projectId "${PROJECT_ID}" \
|
||||||
|
--arg image "${IMAGE_URI}" \
|
||||||
|
--arg pullSecret "${PULL_SECRET}" \
|
||||||
|
'{
|
||||||
|
id: $id,
|
||||||
|
name: $name,
|
||||||
|
projectId: $projectId,
|
||||||
|
ports: [{ expose: true, number: 5000 }],
|
||||||
|
publicAccess: {
|
||||||
|
enabled: true,
|
||||||
|
domain: "auto"
|
||||||
|
},
|
||||||
|
resourceTier: "t1",
|
||||||
|
container: {
|
||||||
|
image: $image,
|
||||||
|
pullSecretName: $pullSecret
|
||||||
|
}
|
||||||
|
}' > .ghaymah.json
|
||||||
|
|
||||||
|
cat .ghaymah.json
|
||||||
|
|
||||||
|
- name: Deploy to Ghaymah
|
||||||
|
working-directory: nodeapp
|
||||||
|
run: $HOME/ghaymah/bin/gy resource app launch --env production
|
||||||
ثنائية
q4-scalability/architecture.png
Normal file
ثنائية
q4-scalability/architecture.png
Normal file
ملف ثنائي غير معروض.
|
بعد العرض: | الارتفاع: | الحجم: 1.4 MiB |
170
q4-scalability/calculations.md
Normal file
170
q4-scalability/calculations.md
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# Q4 — قابلية التوسع وتوزيع الأحمال
|
||||||
|
|
||||||
|
## 1. Architecture Diagram
|
||||||
|
|
||||||
|
راجع الملف `architecture.png` في هذا المجلد.
|
||||||
|
|
||||||
|
يوضّح المخطط تدفّق **15,000 req/s** عبر:
|
||||||
|
|
||||||
|
| الطبقة | المكوّن | الدور |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| Edge | Ghaymah Load Balancer (L7) | توزيع الطلبات، SSL، Health Checks |
|
||||||
|
| Compute | Auto Scaling Group — 43 حاوية | معالجة الطلبات (500 req/s لكل حاوية) |
|
||||||
|
| Stateful | Ghaymah Block Storage | تخزين دائم للـ stateful workloads |
|
||||||
|
| Cache/CDN | Object Storage / CDN | الملفات الثابتة والوسائط |
|
||||||
|
| Ops | Monitoring & Alerts | مقاييس، تنبيهات، قرارات التوسع |
|
||||||
|
| Warm Pool | Cold Start Pool | حاويات مُسخَّنة مسبقاً لتقليل زمن الإقلاع |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. حساب عدد الحاويات
|
||||||
|
|
||||||
|
### المعطيات
|
||||||
|
|
||||||
|
| المتغير | القيمة |
|
||||||
|
|---------|--------|
|
||||||
|
| إجمالي الحمل | **15,000 req/s** |
|
||||||
|
| سعة الحاوية الواحدة | **500 req/s** |
|
||||||
|
| هامش الأمان | **30%** |
|
||||||
|
|
||||||
|
### المنطق
|
||||||
|
|
||||||
|
الهامش 30% يعني **عدم تشغيل الحاويات فوق 70%** من سعتها القصوى، لترك مساحة للذروات والتقلبات:
|
||||||
|
|
||||||
|
```
|
||||||
|
السعة الفعلية القابلة للتخطيط = 500 × (1 − 0.30) = 500 × 0.70 = 350 req/s لكل حاوية
|
||||||
|
```
|
||||||
|
|
||||||
|
### الحساب
|
||||||
|
|
||||||
|
```
|
||||||
|
عدد الحاويات = ⌈ 15,000 ÷ 350 ⌉
|
||||||
|
= ⌈ 42.857 ⌉
|
||||||
|
= 43 حاوية
|
||||||
|
```
|
||||||
|
|
||||||
|
### التحقق
|
||||||
|
|
||||||
|
```
|
||||||
|
43 × 350 = 15,050 req/s ✅ (يغطي 15,000 req/s مع هامش 30%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ملخص
|
||||||
|
|
||||||
|
| البند | القيمة |
|
||||||
|
|-------|--------|
|
||||||
|
| الحد الأدنى نظرياً (بدون هامش) | 30 حاوية |
|
||||||
|
| **العدد الموصى به (مع هامش 30%)** | **43 حاوية** |
|
||||||
|
| السعة الإجمالية الفعلية | 15,050 req/s |
|
||||||
|
| نسبة الاستخدام عند 15K req/s | ≈ 99.7% من السعة المُخطَّطة |
|
||||||
|
|
||||||
|
> **ملاحظة:** إذا قصدت إضافة 30% فوق العدد الأساسي: `30 × 1.30 = 39 حاوية`.
|
||||||
|
> في سياق SRE، هامش السعة (capacity headroom) هو التفسير الأدق — أي 43 حاوية.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. استراتيجية Cold Start للحاويات الجديدة
|
||||||
|
|
||||||
|
عند التوسع الأفقي (scale-out)، الحاوية الجديدة تحتاج وقتاً قبل أن تستقبل حركة. الاستراتيجية المقترحة:
|
||||||
|
|
||||||
|
### أ) Warm Pool (مجموعة تسخين)
|
||||||
|
|
||||||
|
- الإبقاء على **3–5 حاويات جاهزة** في حالة `standby` (مُشغَّلة ومُسخَّنة، لكن خارج rotation الـ Load Balancer).
|
||||||
|
- عند ارتفاع الحمل، تُضاف فوراً إلى الـ LB **بدون** انتظار pull للصورة أو boot.
|
||||||
|
|
||||||
|
### ب) Pre-pull الصور
|
||||||
|
|
||||||
|
- تخزين صورة Docker في **Ghaymah Internal Registry** داخل نفس المنطقة.
|
||||||
|
- تقليل زمن `image pull` من دقائق إلى ثوانٍ.
|
||||||
|
|
||||||
|
### ج) Readiness Probe تدريجي
|
||||||
|
|
||||||
|
```
|
||||||
|
Startup Probe → Readiness Probe → إضافة للـ LB
|
||||||
|
(30s) (HTTP /health)
|
||||||
|
```
|
||||||
|
|
||||||
|
- لا تُوجَّه الطلبات للحاوية حتى تمر `/health` بنجاح.
|
||||||
|
- يمنع إرسال حركة لحاوية لم تكتمل تهيئتها.
|
||||||
|
|
||||||
|
### د) Graceful Scale-In
|
||||||
|
|
||||||
|
- عند التقليص: إرسال `SIGTERM` → انتظار إنهاء الطلبات الجارية (drain) → إزالة من LB → إيقاف.
|
||||||
|
- يمنع قطع الطلبات أثناء التوسع العكسي.
|
||||||
|
|
||||||
|
### هـ) Predictive Scaling
|
||||||
|
|
||||||
|
- مراقبة CPU / RPS / latency.
|
||||||
|
- بدء التوسع **قبل** الوصول للحد (مثلاً عند 60% utilization) وليس عند 90%.
|
||||||
|
|
||||||
|
### ف) Application Warm-up
|
||||||
|
|
||||||
|
- عند الإقلاع: تحميل cache محلي، pre-connect لقاعدة البيانات، JIT warm-up.
|
||||||
|
- endpoint `/warmup` يُستدعى تلقائياً بعد readiness.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Ghaymah Block Storage للـ Stateful Workloads
|
||||||
|
|
||||||
|
### ما هو Block Storage؟
|
||||||
|
|
||||||
|
تخزين **كتلة (block-level)** يُ attach كقرص افتراضي (volume) لحاوية أو خدمة — مثل `/dev/vdb` — ويحتفظ بالبيانات **بعد** إعادة تشغيل أو استبدال الحاوية.
|
||||||
|
|
||||||
|
### متى نستخدمه؟
|
||||||
|
|
||||||
|
| Stateful | Stateless (لا يحتاج Block Storage) |
|
||||||
|
|----------|-------------------------------------|
|
||||||
|
| PostgreSQL / MySQL | API servers (43 حاوية أعلاه) |
|
||||||
|
| Redis persistence (AOF/RDB) | Static assets → Object Storage |
|
||||||
|
| File uploads / media processing | Session في Redis مشترك |
|
||||||
|
| Logs طويلة الأمد | |
|
||||||
|
|
||||||
|
### كيف يُستخدم على غيمة؟
|
||||||
|
|
||||||
|
1. **إنشاء Volume**
|
||||||
|
- حجم مناسب (مثلاً 100 GB SSD) في نفس منطقة التطبيق.
|
||||||
|
|
||||||
|
2. **Mount على الحاوية Stateful**
|
||||||
|
```text
|
||||||
|
/data/postgresql ← Ghaymah Block Volume (persistent)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **فصل Compute عن Storage**
|
||||||
|
- حاويات الـ API **stateless** — تتوسع أفقياً بحرية.
|
||||||
|
- قاعدة البيانات **stateful** — volume واحد (أو cluster مع replication).
|
||||||
|
|
||||||
|
4. **Snapshots & Backup**
|
||||||
|
- Ghaymah Backup: snapshots دورية للـ volume.
|
||||||
|
- استرداد Point-in-Time عند الفشل.
|
||||||
|
|
||||||
|
5. **High Availability**
|
||||||
|
- Primary DB على volume + Replica على volume منفصل.
|
||||||
|
- Failover تلقائي عند سقوط Primary.
|
||||||
|
|
||||||
|
### في مخططنا (architecture.png)
|
||||||
|
|
||||||
|
```
|
||||||
|
Load Balancer → 43 App Containers (stateless)
|
||||||
|
↓
|
||||||
|
Stateful DB Pod ← Ghaymah Block Storage (persistent volume)
|
||||||
|
```
|
||||||
|
|
||||||
|
- الـ **43 حاوية** لا تخزّن state محلياً.
|
||||||
|
- كل البيانات الدائمة على **Block Storage** م attached لطبقة DB/Cache الم persistent.
|
||||||
|
|
||||||
|
### الفوائد
|
||||||
|
|
||||||
|
| الفائدة | الشرح |
|
||||||
|
|---------|--------|
|
||||||
|
| Persistence | البيانات تبقى بعد restart/redeploy |
|
||||||
|
| Performance | IOPS مضمون لقواعد البيانات |
|
||||||
|
| Scalability | فصل التوسع الأفقي (API) عن التخزين (DB) |
|
||||||
|
| DR | Snapshots + restore سريع |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## المراجع
|
||||||
|
|
||||||
|
- [Ghaymah Cloud](https://ghaymah.systems/)
|
||||||
|
- [Ghaymah CLI — `gy resource app launch`](https://cli.ghaymah.systems/)
|
||||||
|
- Health & Retry: HTTP/TCP probes — down-nodes تخرج فوراً من rotation
|
||||||
المرجع في مشكلة جديدة
حظر مستخدم