diff --git a/.ghaymah.json b/.ghaymah.json new file mode 100644 index 0000000..4ca67ff --- /dev/null +++ b/.ghaymah.json @@ -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": "" + } +} diff --git a/metrics.json b/metrics.json new file mode 100644 index 0000000..e412dbb --- /dev/null +++ b/metrics.json @@ -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 + } +] \ No newline at end of file diff --git a/q1-deploy-monitor/Dockerfile b/q1-deploy-monitor/Dockerfile new file mode 100644 index 0000000..30882a1 --- /dev/null +++ b/q1-deploy-monitor/Dockerfile @@ -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"] diff --git a/q1-deploy-monitor/app.js b/q1-deploy-monitor/app.js new file mode 100644 index 0000000..b1a44e7 --- /dev/null +++ b/q1-deploy-monitor/app.js @@ -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`) +}) diff --git a/q1-deploy-monitor/dashboard.js b/q1-deploy-monitor/dashboard.js new file mode 100644 index 0000000..f3478d5 --- /dev/null +++ b/q1-deploy-monitor/dashboard.js @@ -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) diff --git a/q1-deploy-monitor/health-check.py b/q1-deploy-monitor/health-check.py new file mode 100644 index 0000000..a0783d1 --- /dev/null +++ b/q1-deploy-monitor/health-check.py @@ -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) diff --git a/q1-deploy-monitor/index.html b/q1-deploy-monitor/index.html new file mode 100644 index 0000000..0331e20 --- /dev/null +++ b/q1-deploy-monitor/index.html @@ -0,0 +1,50 @@ + + + + + + Server Dashboard + + + +
+
+
+

Server Dashboard

+

Live monitoring for your Node.js application

+
+
+ + Checking... +
+
+ +
+
+

Total Requests

+

+

All requests received by the server

+
+ +
+

Average Response Time

+

+

Mean duration in seconds

+
+ +
+

Application Status

+

+

Based on health check

+
+
+ + +
+ + + + diff --git a/q1-deploy-monitor/package.json b/q1-deploy-monitor/package.json new file mode 100644 index 0000000..a7719cf --- /dev/null +++ b/q1-deploy-monitor/package.json @@ -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" + } +} diff --git a/q1-deploy-monitor/style.css b/q1-deploy-monitor/style.css new file mode 100644 index 0000000..496da72 --- /dev/null +++ b/q1-deploy-monitor/style.css @@ -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; + } +} diff --git a/q2-postmortem/postmortem-report.md b/q2-postmortem/postmortem-report.md new file mode 100644 index 0000000..e69de29 diff --git a/q3-cicd/workflow.yml b/q3-cicd/workflow.yml new file mode 100644 index 0000000..58f8b73 --- /dev/null +++ b/q3-cicd/workflow.yml @@ -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 diff --git a/q4-scalability/architecture.png b/q4-scalability/architecture.png new file mode 100644 index 0000000..6ca1ebb Binary files /dev/null and b/q4-scalability/architecture.png differ diff --git a/q4-scalability/calculations.md b/q4-scalability/calculations.md new file mode 100644 index 0000000..2e5cf76 --- /dev/null +++ b/q4-scalability/calculations.md @@ -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