Second Assignment
This commit is contained in:
parent
2443ce80f7
commit
3821bf2d1e
BIN
thesis.pdf
Normal file
BIN
thesis.pdf
Normal file
Binary file not shown.
249
z2/README.md
Normal file
249
z2/README.md
Normal file
@ -0,0 +1,249 @@
|
||||
# K8s Todo App
|
||||
|
||||
A full-stack **Todo List** web application deployed on **Kubernetes** using **Minikube**.
|
||||
The app stores tasks in a PostgreSQL database and serves a modern web UI via a Python Flask backend.
|
||||
|
||||
---
|
||||
|
||||
## What the Application Does
|
||||
|
||||
Users can:
|
||||
- **Add** new tasks via a text input form
|
||||
- **Mark tasks as done** by clicking the circle toggle button
|
||||
- **Delete** tasks with the ✕ button
|
||||
- View a live progress counter (e.g. `2/5 done`)
|
||||
|
||||
All data is persisted to a PostgreSQL database backed by a Kubernetes `PersistentVolume`.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
Browser
|
||||
│
|
||||
▼ NodePort :30080
|
||||
┌──────────────────────────────────────┐
|
||||
│ Namespace: todo-app │
|
||||
│ │
|
||||
│ ┌────────────────────┐ │
|
||||
│ │ Deployment │ │
|
||||
│ │ todo-app (x2 pods)│ ──────────► │─── postgres-service:5432
|
||||
│ │ Flask + Gunicorn │ │ │
|
||||
│ └────────────────────┘ │ ┌──────────────────┐
|
||||
│ │ │ StatefulSet │
|
||||
│ │ │ postgres (x1) │
|
||||
│ │ │ PostgreSQL 16 │
|
||||
│ │ │ │ │
|
||||
│ │ │ PVC ──► PV │
|
||||
│ │ │ (hostPath) │
|
||||
│ │ └──────────────────┘
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Container Images Used
|
||||
|
||||
| Image | Version | Purpose |
|
||||
|-------|---------|---------|
|
||||
| `todo-app` | `latest` (built locally) | Flask web server + Gunicorn WSGI |
|
||||
| `postgres` | `16-alpine` | Relational database |
|
||||
| `busybox` | `1.36` | Init container — waits for PostgreSQL before app starts |
|
||||
|
||||
### `todo-app` (custom image)
|
||||
- Built from `app/Dockerfile` using Python 3.12-slim
|
||||
- Runs a **Flask** application with **Gunicorn** (2 workers)
|
||||
- Connects to PostgreSQL via environment variables
|
||||
- Exposes port `5000`
|
||||
- Runs as a non-root user (`appuser`) for security
|
||||
|
||||
### `postgres:16-alpine`
|
||||
- Official PostgreSQL image (Alpine variant for smaller size)
|
||||
- Initialises the `tododb` database using a mounted SQL ConfigMap
|
||||
- Data is persisted via a PersistentVolumeClaim
|
||||
|
||||
---
|
||||
|
||||
## Kubernetes Objects
|
||||
|
||||
| Object | Name | Description |
|
||||
|--------|------|-------------|
|
||||
| `Namespace` | `todo-app` | Isolates all app resources from the rest of the cluster |
|
||||
| `Deployment` | `todo-app` | Runs 2 replicas of the Flask web app with rolling updates |
|
||||
| `StatefulSet` | `postgres` | Manages the PostgreSQL pod with stable network identity |
|
||||
| `PersistentVolume` | `postgres-pv` | 2Gi host-path volume on the minikube node |
|
||||
| `PersistentVolumeClaim` | `postgres-pvc` | Binds to `postgres-pv`, used by the StatefulSet |
|
||||
| `Service` | `todo-app-service` | NodePort (30080) — exposes the Flask app to the browser |
|
||||
| `Service` | `postgres-service` | ClusterIP — internal access to PostgreSQL on port 5432 |
|
||||
| `Service` | `postgres-headless` | Headless — required by StatefulSet for stable DNS |
|
||||
| `Secret` | `postgres-secret` | Stores DB credentials (username, password, DB name) |
|
||||
| `ConfigMap` | `postgres-init-sql` | Holds the SQL init script run by PostgreSQL on first boot |
|
||||
|
||||
---
|
||||
|
||||
## Networks and Volumes
|
||||
|
||||
### Virtual Networks (Services)
|
||||
|
||||
- **`todo-app-service`** — `NodePort` type. Maps external port `30080` → pod port `5000`. Accessible at `http://<minikube-ip>:30080`.
|
||||
- **`postgres-service`** — `ClusterIP` type. Only reachable inside the cluster at `postgres-service.todo-app.svc.cluster.local:5432`.
|
||||
- **`postgres-headless`** — `ClusterIP: None`. A DNS-only headless service required by the StatefulSet so each pod gets a stable DNS entry (`postgres-0.postgres-headless.todo-app.svc.cluster.local`).
|
||||
|
||||
### Persistent Volumes
|
||||
|
||||
- **`postgres-pv`** — `hostPath` pointing to `/mnt/data/postgres` inside the minikube VM. Survives pod restarts and redeployments.
|
||||
- **`postgres-pvc`** — Claims 2Gi from `postgres-pv`. Mounted at `/var/lib/postgresql/data` inside the PostgreSQL container.
|
||||
|
||||
---
|
||||
|
||||
## Container Configuration
|
||||
|
||||
### Flask App (`todo-app`)
|
||||
- `DB_HOST` — hostname of the PostgreSQL service (`postgres-service.todo-app.svc.cluster.local`)
|
||||
- `DB_NAME` — database name (`tododb`)
|
||||
- `DB_USER` / `DB_PASS` — injected from the `postgres-secret` Kubernetes Secret
|
||||
- **Liveness probe**: `GET /health` every 10s — restarts the pod if it hangs
|
||||
- **Readiness probe**: `GET /health` every 5s — holds traffic until the app is truly ready
|
||||
- **Init container**: `busybox` runs `nc -z postgres-service 5432` in a loop until PostgreSQL accepts connections before the main container starts
|
||||
|
||||
### PostgreSQL (`postgres`)
|
||||
- `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` — injected from `postgres-secret`
|
||||
- `PGDATA=/var/lib/postgresql/data/pgdata` — prevents permission errors on mounted volumes
|
||||
- SQL init script from ConfigMap is placed at `/docker-entrypoint-initdb.d/init.sql` and runs automatically on first start
|
||||
- **Liveness probe**: `pg_isready -U postgres`
|
||||
- **Readiness probe**: `pg_isready -U postgres`
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Instructions
|
||||
|
||||
### Prerequisites — Install Everything on Ubuntu
|
||||
|
||||
Run the following commands in sequence in your terminal:
|
||||
|
||||
```bash
|
||||
# 1. Update system
|
||||
sudo apt-get update && sudo apt-get upgrade -y
|
||||
|
||||
# 2. Install Docker
|
||||
sudo apt-get install -y ca-certificates curl gnupg
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
|
||||
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
|
||||
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
|
||||
sudo usermod -aG docker $USER
|
||||
newgrp docker
|
||||
|
||||
# 3. Install kubectl
|
||||
curl -LO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
|
||||
kubectl version --client
|
||||
|
||||
# 4. Install minikube
|
||||
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
|
||||
sudo install minikube-linux-amd64 /usr/local/bin/minikube
|
||||
minikube version
|
||||
```
|
||||
|
||||
### Prepare the Application (run once)
|
||||
|
||||
```bash
|
||||
# Navigate to the project directory
|
||||
cd k8s-todo-app
|
||||
|
||||
# Make all scripts executable
|
||||
chmod +x prepare-app.sh start-app.sh stop-app.sh
|
||||
|
||||
# Run the preparation script (starts minikube, builds Docker image, creates host dir)
|
||||
bash prepare-app.sh
|
||||
```
|
||||
|
||||
### Start the Application
|
||||
|
||||
```bash
|
||||
bash start-app.sh
|
||||
```
|
||||
|
||||
This script creates all Kubernetes objects in order:
|
||||
1. Namespace
|
||||
2. ConfigMap
|
||||
3. PersistentVolume, PVC, Secret, StatefulSet (PostgreSQL)
|
||||
4. Services
|
||||
5. Deployment (Flask app)
|
||||
|
||||
### View the Application in a Browser
|
||||
|
||||
```bash
|
||||
# Option A — get the URL automatically
|
||||
minikube service todo-app-service -n todo-app
|
||||
|
||||
# Option B — open directly
|
||||
minikube service todo-app-service -n todo-app --url
|
||||
# Copy the URL and open it in your browser
|
||||
# e.g. http://192.168.49.2:30080
|
||||
```
|
||||
|
||||
### Useful Commands While Running
|
||||
|
||||
```bash
|
||||
# See all running pods
|
||||
kubectl get pods -n todo-app
|
||||
|
||||
# See all objects
|
||||
kubectl get all -n todo-app
|
||||
|
||||
# Watch pods in real time
|
||||
kubectl get pods -n todo-app -w
|
||||
|
||||
# View Flask app logs
|
||||
kubectl logs -l app=todo-app -n todo-app --tail=50
|
||||
|
||||
# View PostgreSQL logs
|
||||
kubectl logs postgres-0 -n todo-app --tail=50
|
||||
|
||||
# Connect to PostgreSQL shell
|
||||
kubectl exec -it postgres-0 -n todo-app -- psql -U postgres -d tododb
|
||||
|
||||
# Scale up/down the web app
|
||||
kubectl scale deployment todo-app -n todo-app --replicas=3
|
||||
```
|
||||
|
||||
### Pause / Delete the Application
|
||||
|
||||
```bash
|
||||
# Delete all Kubernetes objects (data on PV is preserved)
|
||||
bash stop-app.sh
|
||||
|
||||
# To also remove all stored database data
|
||||
minikube ssh -- "sudo rm -rf /mnt/data/postgres"
|
||||
|
||||
# To stop minikube entirely
|
||||
minikube stop
|
||||
|
||||
# To delete the minikube cluster completely
|
||||
minikube delete
|
||||
```
|
||||
|
||||
### Re-deploy After Stop
|
||||
|
||||
```bash
|
||||
bash start-app.sh # no need to run prepare-app.sh again unless minikube was deleted
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| `ImagePullBackOff` on todo-app | Re-run `bash prepare-app.sh` to rebuild image inside minikube |
|
||||
| Postgres pod in `Pending` | Run `kubectl describe pv postgres-pv` — check hostPath exists |
|
||||
| App shows "Cannot connect to database" | Wait 30s, postgres is still initialising; check `kubectl logs postgres-0 -n todo-app` |
|
||||
| Can't reach browser URL | Run `minikube service todo-app-service -n todo-app` to get the correct URL |
|
||||
| Permission error on PV | Run `minikube ssh -- 'sudo chmod 777 /mnt/data/postgres'` |
|
||||
22
z2/app/Dockerfile
Normal file
22
z2/app/Dockerfile
Normal file
@ -0,0 +1,22 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies first (layer caching)
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY app.py .
|
||||
COPY templates/ templates/
|
||||
|
||||
# Create non-root user for security
|
||||
RUN useradd -m -r appuser && chown -R appuser /app
|
||||
USER appuser
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5000
|
||||
|
||||
# Entrypoint: initialise DB then launch with gunicorn
|
||||
CMD ["sh", "-c", "python -c 'import app; app.init_db()' && gunicorn --bind 0.0.0.0:5000 --workers 2 --timeout 60 app:app"]
|
||||
102
z2/app/app.py
Normal file
102
z2/app/app.py
Normal file
@ -0,0 +1,102 @@
|
||||
import os
|
||||
import time
|
||||
import psycopg2
|
||||
from flask import Flask, render_template, request, redirect, url_for
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
DB_HOST = os.environ.get("DB_HOST", "postgres-service.todo-app.svc.cluster.local")
|
||||
DB_NAME = os.environ.get("DB_NAME", "tododb")
|
||||
DB_USER = os.environ.get("DB_USER", "postgres")
|
||||
DB_PASS = os.environ.get("DB_PASS", "postgres123")
|
||||
|
||||
|
||||
def get_db():
|
||||
for attempt in range(10):
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=DB_HOST, database=DB_NAME, user=DB_USER, password=DB_PASS
|
||||
)
|
||||
return conn
|
||||
except psycopg2.OperationalError:
|
||||
time.sleep(2)
|
||||
raise Exception("Cannot connect to database after 10 attempts")
|
||||
|
||||
|
||||
def init_db():
|
||||
conn = get_db()
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS todos (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
done BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.before_request
|
||||
def setup():
|
||||
# Only run once
|
||||
pass
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
conn = get_db()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT id, title, done, created_at FROM todos ORDER BY id DESC")
|
||||
todos = cur.fetchall()
|
||||
cur.close()
|
||||
conn.close()
|
||||
done_count = sum(1 for t in todos if t[2])
|
||||
return render_template("index.html", todos=todos, done_count=done_count, total=len(todos))
|
||||
|
||||
|
||||
@app.route("/add", methods=["POST"])
|
||||
def add():
|
||||
title = request.form.get("title", "").strip()
|
||||
if title:
|
||||
conn = get_db()
|
||||
cur = conn.cursor()
|
||||
cur.execute("INSERT INTO todos (title, done) VALUES (%s, FALSE)", (title,))
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@app.route("/toggle/<int:todo_id>")
|
||||
def toggle(todo_id):
|
||||
conn = get_db()
|
||||
cur = conn.cursor()
|
||||
cur.execute("UPDATE todos SET done = NOT done WHERE id = %s", (todo_id,))
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@app.route("/delete/<int:todo_id>")
|
||||
def delete(todo_id):
|
||||
conn = get_db()
|
||||
cur = conn.cursor()
|
||||
cur.execute("DELETE FROM todos WHERE id = %s", (todo_id,))
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@app.route("/health")
|
||||
def health():
|
||||
return {"status": "ok"}, 200
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_db()
|
||||
app.run(host="0.0.0.0", port=5000, debug=False)
|
||||
3
z2/app/requirements.txt
Normal file
3
z2/app/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
flask==3.0.3
|
||||
psycopg2-binary==2.9.9
|
||||
gunicorn==22.0.0
|
||||
205
z2/app/templates/index.html
Normal file
205
z2/app/templates/index.html
Normal file
@ -0,0 +1,205 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>K8s Todo App</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;600;700&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f0f13;
|
||||
--surface: #1a1a24;
|
||||
--surface2: #22222e;
|
||||
--border: #2e2e3e;
|
||||
--accent: #7c6af7;
|
||||
--accent2: #4fd1c5;
|
||||
--text: #e8e8f0;
|
||||
--muted: #6b6b82;
|
||||
--done-color: #4ade80;
|
||||
--danger: #f87171;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
.noise {
|
||||
position: fixed; inset: 0; pointer-events: none; z-index: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
|
||||
opacity: 0.4;
|
||||
}
|
||||
.glow {
|
||||
position: fixed; top: -200px; left: 50%; transform: translateX(-50%);
|
||||
width: 600px; height: 400px; pointer-events: none; z-index: 0;
|
||||
background: radial-gradient(ellipse at center, rgba(124,106,247,0.15) 0%, transparent 70%);
|
||||
}
|
||||
.container {
|
||||
position: relative; z-index: 1;
|
||||
max-width: 680px; margin: 0 auto;
|
||||
}
|
||||
header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
.logo {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
}
|
||||
.k8s-badge {
|
||||
background: linear-gradient(135deg, #326de6, #7c6af7);
|
||||
border-radius: 8px; padding: 6px 10px;
|
||||
font-family: 'DM Mono', monospace; font-size: 0.7rem;
|
||||
font-weight: 500; letter-spacing: 0.05em;
|
||||
color: #fff;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.5rem; font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--text), var(--muted));
|
||||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||||
}
|
||||
.stats {
|
||||
display: flex; gap: 1rem;
|
||||
}
|
||||
.stat {
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: 10px; padding: 0.5rem 1rem;
|
||||
font-family: 'DM Mono', monospace; font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.stat span { color: var(--accent); font-weight: 600; }
|
||||
|
||||
.add-form {
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: 16px; padding: 1.25rem;
|
||||
display: flex; gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.add-form:focus-within { border-color: var(--accent); }
|
||||
.add-form input {
|
||||
flex: 1; background: var(--surface2); border: 1px solid var(--border);
|
||||
border-radius: 10px; padding: 0.75rem 1rem;
|
||||
color: var(--text); font-family: 'DM Sans', sans-serif;
|
||||
font-size: 0.95rem; outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.add-form input:focus { border-color: var(--accent); }
|
||||
.add-form input::placeholder { color: var(--muted); }
|
||||
.btn {
|
||||
background: linear-gradient(135deg, var(--accent), #9b8af8);
|
||||
color: #fff; border: none; border-radius: 10px;
|
||||
padding: 0.75rem 1.5rem; font-size: 0.9rem; font-weight: 600;
|
||||
cursor: pointer; transition: opacity 0.2s, transform 0.1s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn:hover { opacity: 0.9; }
|
||||
.btn:active { transform: scale(0.97); }
|
||||
|
||||
.todo-list { display: flex; flex-direction: column; gap: 0.6rem; }
|
||||
.todo-item {
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: 14px; padding: 1rem 1.25rem;
|
||||
display: flex; align-items: center; gap: 1rem;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
animation: slideIn 0.25s ease;
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateY(-6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.todo-item:hover { border-color: var(--accent); background: var(--surface2); }
|
||||
.todo-item.done { opacity: 0.6; }
|
||||
.toggle-btn {
|
||||
width: 22px; height: 22px; border-radius: 50%;
|
||||
border: 2px solid var(--border); background: transparent;
|
||||
cursor: pointer; flex-shrink: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.toggle-btn:hover { border-color: var(--accent2); }
|
||||
.done .toggle-btn {
|
||||
background: var(--done-color); border-color: var(--done-color);
|
||||
}
|
||||
.done .toggle-btn::after {
|
||||
content: '✓'; color: #000; font-size: 0.7rem; font-weight: 700;
|
||||
}
|
||||
.todo-title {
|
||||
flex: 1; font-size: 0.95rem; line-height: 1.4;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.done .todo-title { text-decoration: line-through; color: var(--muted); }
|
||||
.todo-meta {
|
||||
font-family: 'DM Mono', monospace; font-size: 0.7rem;
|
||||
color: var(--muted); white-space: nowrap;
|
||||
}
|
||||
.delete-btn {
|
||||
background: transparent; border: none; color: var(--muted);
|
||||
cursor: pointer; font-size: 1rem; padding: 0.25rem;
|
||||
border-radius: 6px; transition: color 0.2s, background 0.2s;
|
||||
line-height: 1;
|
||||
}
|
||||
.delete-btn:hover { color: var(--danger); background: rgba(248,113,113,0.1); }
|
||||
|
||||
.empty {
|
||||
text-align: center; padding: 4rem 2rem;
|
||||
color: var(--muted); font-size: 0.95rem;
|
||||
}
|
||||
.empty .icon { font-size: 3rem; margin-bottom: 1rem; }
|
||||
|
||||
footer {
|
||||
margin-top: 3rem; text-align: center;
|
||||
font-family: 'DM Mono', monospace; font-size: 0.7rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
footer a { color: var(--accent); text-decoration: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="noise"></div>
|
||||
<div class="glow"></div>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="logo">
|
||||
<div class="k8s-badge">⎈ K8S</div>
|
||||
<h1>Todo App</h1>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div class="stat"><span>{{ done_count }}</span>/{{ total }} done</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form class="add-form" action="/add" method="post">
|
||||
<input type="text" name="title" placeholder="Add a new task..." autocomplete="off" required />
|
||||
<button type="submit" class="btn">+ Add</button>
|
||||
</form>
|
||||
|
||||
<div class="todo-list">
|
||||
{% if todos %}
|
||||
{% for todo in todos %}
|
||||
<div class="todo-item {% if todo[2] %}done{% endif %}">
|
||||
<a href="/toggle/{{ todo[0] }}" style="text-decoration:none">
|
||||
<div class="toggle-btn"></div>
|
||||
</a>
|
||||
<span class="todo-title">{{ todo[1] }}</span>
|
||||
<span class="todo-meta">{{ todo[3].strftime('%b %d') if todo[3] else '' }}</span>
|
||||
<a href="/delete/{{ todo[0] }}">
|
||||
<button class="delete-btn" title="Delete">✕</button>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty">
|
||||
<div class="icon">📋</div>
|
||||
<p>No tasks yet. Add one above!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
running on <a href="#">kubernetes</a> · flask + postgresql
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
z2/k8s-todo-app-documentation.docx
Normal file
BIN
z2/k8s-todo-app-documentation.docx
Normal file
Binary file not shown.
27
z2/k8s/configmap.yaml
Normal file
27
z2/k8s/configmap.yaml
Normal file
@ -0,0 +1,27 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: postgres-init-sql
|
||||
namespace: todo-app
|
||||
labels:
|
||||
app: postgres
|
||||
data:
|
||||
init.sql: |
|
||||
-- The database 'tododb' is created automatically via POSTGRES_DB env var.
|
||||
-- This script seeds the initial data.
|
||||
\c tododb;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS todos (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
done BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT INTO todos (title, done) VALUES
|
||||
('Deploy app to Kubernetes', TRUE),
|
||||
('Set up PostgreSQL StatefulSet', TRUE),
|
||||
('Configure PersistentVolume', FALSE),
|
||||
('Write README documentation', FALSE),
|
||||
('Test the web application', FALSE)
|
||||
ON CONFLICT DO NOTHING;
|
||||
82
z2/k8s/deployment.yaml
Normal file
82
z2/k8s/deployment.yaml
Normal file
@ -0,0 +1,82 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: todo-app
|
||||
namespace: todo-app
|
||||
labels:
|
||||
app: todo-app
|
||||
tier: frontend
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: todo-app
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: todo-app
|
||||
tier: frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: todo-app
|
||||
image: todo-app:latest
|
||||
# Use local image built into minikube — never pull from registry
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 5000
|
||||
name: http
|
||||
env:
|
||||
- name: DB_HOST
|
||||
value: "postgres-service.todo-app.svc.cluster.local"
|
||||
- name: DB_NAME
|
||||
value: "tododb"
|
||||
- name: DB_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: POSTGRES_USER
|
||||
- name: DB_PASS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: POSTGRES_PASSWORD
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "128Mi"
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "256Mi"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 5000
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 5000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
failureThreshold: 3
|
||||
# Wait for postgres to be available before starting pods
|
||||
initContainers:
|
||||
- name: wait-for-postgres
|
||||
image: busybox:1.36
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
echo "Waiting for PostgreSQL..."
|
||||
until nc -z postgres-service.todo-app.svc.cluster.local 5432; do
|
||||
echo "PostgreSQL not ready, sleeping 2s..."
|
||||
sleep 2
|
||||
done
|
||||
echo "PostgreSQL is ready!"
|
||||
7
z2/k8s/namespace.yaml
Normal file
7
z2/k8s/namespace.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: todo-app
|
||||
labels:
|
||||
app.kubernetes.io/name: todo-app
|
||||
app.kubernetes.io/managed-by: kubectl
|
||||
59
z2/k8s/service.yaml
Normal file
59
z2/k8s/service.yaml
Normal file
@ -0,0 +1,59 @@
|
||||
###############################################################
|
||||
# Service – Flask web application (NodePort for browser access)
|
||||
###############################################################
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: todo-app-service
|
||||
namespace: todo-app
|
||||
labels:
|
||||
app: todo-app
|
||||
spec:
|
||||
type: NodePort
|
||||
selector:
|
||||
app: todo-app
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 5000
|
||||
nodePort: 30080 # access via http://<minikube-ip>:30080
|
||||
|
||||
---
|
||||
###############################################################
|
||||
# Service – PostgreSQL (ClusterIP, internal only)
|
||||
###############################################################
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres-service
|
||||
namespace: todo-app
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- name: postgres
|
||||
port: 5432
|
||||
targetPort: 5432
|
||||
|
||||
---
|
||||
###############################################################
|
||||
# Headless Service – required by StatefulSet for stable DNS
|
||||
###############################################################
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres-headless
|
||||
namespace: todo-app
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
clusterIP: None
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- name: postgres
|
||||
port: 5432
|
||||
targetPort: 5432
|
||||
139
z2/k8s/statefulset.yaml
Normal file
139
z2/k8s/statefulset.yaml
Normal file
@ -0,0 +1,139 @@
|
||||
###############################################################
|
||||
# PersistentVolume – host-path storage (works on minikube)
|
||||
###############################################################
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: postgres-pv
|
||||
labels:
|
||||
type: local
|
||||
app: postgres
|
||||
spec:
|
||||
capacity:
|
||||
storage: 2Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: standard
|
||||
hostPath:
|
||||
path: /mnt/data/postgres
|
||||
type: DirectoryOrCreate
|
||||
|
||||
---
|
||||
###############################################################
|
||||
# PersistentVolumeClaim
|
||||
###############################################################
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: postgres-pvc
|
||||
namespace: todo-app
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: standard
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
|
||||
---
|
||||
###############################################################
|
||||
# Secret – credentials for PostgreSQL
|
||||
###############################################################
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: postgres-secret
|
||||
namespace: todo-app
|
||||
type: Opaque
|
||||
stringData:
|
||||
POSTGRES_USER: "postgres"
|
||||
POSTGRES_PASSWORD: "postgres123"
|
||||
POSTGRES_DB: "tododb"
|
||||
|
||||
---
|
||||
###############################################################
|
||||
# StatefulSet – PostgreSQL database
|
||||
###############################################################
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: todo-app
|
||||
labels:
|
||||
app: postgres
|
||||
tier: database
|
||||
spec:
|
||||
serviceName: "postgres-headless"
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
tier: database
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:16-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
name: postgres
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: POSTGRES_USER
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: POSTGRES_PASSWORD
|
||||
- name: POSTGRES_DB
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres-secret
|
||||
key: POSTGRES_DB
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/pgdata
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "256Mi"
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "512Mi"
|
||||
volumeMounts:
|
||||
- name: postgres-storage
|
||||
mountPath: /var/lib/postgresql/data
|
||||
- name: init-sql
|
||||
mountPath: /docker-entrypoint-initdb.d
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- postgres
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- postgres
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: postgres-storage
|
||||
persistentVolumeClaim:
|
||||
claimName: postgres-pvc
|
||||
- name: init-sql
|
||||
configMap:
|
||||
name: postgres-init-sql
|
||||
55
z2/prepare-app.sh
Normal file
55
z2/prepare-app.sh
Normal file
@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# prepare-app.sh
|
||||
# Prepares the application: builds Docker image into minikube,
|
||||
# creates the host-path directory for the PersistentVolume.
|
||||
# Run this ONCE before start-app.sh.
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
echo "======================================================"
|
||||
echo " Preparing K8s Todo App"
|
||||
echo "======================================================"
|
||||
|
||||
# ── 1. Verify tools are available ─────────────────────────
|
||||
echo ""
|
||||
echo "[1/4] Checking required tools..."
|
||||
for tool in minikube kubectl docker; do
|
||||
if ! command -v "$tool" &>/dev/null; then
|
||||
echo " ERROR: '$tool' is not installed or not in PATH."
|
||||
exit 1
|
||||
fi
|
||||
echo " ✓ $tool found"
|
||||
done
|
||||
|
||||
# ── 2. Ensure minikube is running ─────────────────────────
|
||||
echo ""
|
||||
echo "[2/4] Checking minikube status..."
|
||||
STATUS=$(minikube status --format='{{.Host}}' 2>/dev/null || echo "Stopped")
|
||||
if [ "$STATUS" != "Running" ]; then
|
||||
echo " minikube is not running. Starting minikube..."
|
||||
minikube start --driver=docker --memory=2048 --cpus=2
|
||||
echo " ✓ minikube started"
|
||||
else
|
||||
echo " ✓ minikube is already running"
|
||||
fi
|
||||
|
||||
# ── 3. Create host directory for PersistentVolume ─────────
|
||||
echo ""
|
||||
echo "[3/4] Creating PersistentVolume host directory inside minikube..."
|
||||
minikube ssh -- "sudo mkdir -p /mnt/data/postgres && sudo chmod 777 /mnt/data/postgres"
|
||||
echo " ✓ /mnt/data/postgres created inside minikube node"
|
||||
|
||||
# ── 4. Build Docker image inside minikube's Docker daemon ─
|
||||
echo ""
|
||||
echo "[4/4] Building Docker image 'todo-app:latest' inside minikube..."
|
||||
# Point Docker CLI to minikube's daemon so image is available to k8s pods
|
||||
eval "$(minikube docker-env)"
|
||||
docker build -t todo-app:latest ./app
|
||||
echo " ✓ Image 'todo-app:latest' built successfully"
|
||||
|
||||
echo ""
|
||||
echo "======================================================"
|
||||
echo " Preparation complete!"
|
||||
echo " Now run: bash start-app.sh"
|
||||
echo "======================================================"
|
||||
19
z2/sql/init.sql
Normal file
19
z2/sql/init.sql
Normal file
@ -0,0 +1,19 @@
|
||||
-- Initialize the Todo database
|
||||
CREATE DATABASE IF NOT EXISTS tododb;
|
||||
|
||||
\c tododb;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS todos (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
done BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Seed some sample data
|
||||
INSERT INTO todos (title, done) VALUES
|
||||
('Deploy app to Kubernetes', TRUE),
|
||||
('Set up PostgreSQL StatefulSet', TRUE),
|
||||
('Configure PersistentVolume', FALSE),
|
||||
('Write README documentation', FALSE),
|
||||
('Test the web application', FALSE);
|
||||
64
z2/start-app.sh
Normal file
64
z2/start-app.sh
Normal file
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# start-app.sh
|
||||
# Creates all Kubernetes objects for the Todo application.
|
||||
# Assumes: kubectl configured, minikube running, prepare-app.sh already run.
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
K8S_DIR="$(dirname "$0")/k8s"
|
||||
|
||||
echo "======================================================"
|
||||
echo " Starting K8s Todo App"
|
||||
echo "======================================================"
|
||||
|
||||
# ── 1. Namespace ───────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[1/6] Creating Namespace..."
|
||||
kubectl apply -f "$K8S_DIR/namespace.yaml"
|
||||
echo " ✓ Namespace 'todo-app' ready"
|
||||
|
||||
# ── 2. ConfigMap (init SQL) ────────────────────────────────
|
||||
echo ""
|
||||
echo "[2/6] Creating ConfigMap for database init SQL..."
|
||||
kubectl apply -f "$K8S_DIR/configmap.yaml"
|
||||
echo " ✓ ConfigMap 'postgres-init-sql' ready"
|
||||
|
||||
# ── 3. PersistentVolume + PVC + Secret + StatefulSet ──────
|
||||
echo ""
|
||||
echo "[3/6] Creating PersistentVolume, PVC, Secret and StatefulSet (PostgreSQL)..."
|
||||
kubectl apply -f "$K8S_DIR/statefulset.yaml"
|
||||
echo " ✓ PersistentVolume, PVC, Secret and StatefulSet applied"
|
||||
|
||||
# ── 4. Services ────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[4/6] Creating Services..."
|
||||
kubectl apply -f "$K8S_DIR/service.yaml"
|
||||
echo " ✓ Services created"
|
||||
|
||||
# ── 5. Wait for PostgreSQL to be ready ────────────────────
|
||||
echo ""
|
||||
echo "[5/6] Waiting for PostgreSQL StatefulSet to become ready (up to 3 min)..."
|
||||
kubectl rollout status statefulset/postgres -n todo-app --timeout=180s
|
||||
echo " ✓ PostgreSQL is ready"
|
||||
|
||||
# ── 6. Deployment (Flask app) ──────────────────────────────
|
||||
echo ""
|
||||
echo "[6/6] Creating Deployment (Flask Todo App)..."
|
||||
kubectl apply -f "$K8S_DIR/deployment.yaml"
|
||||
kubectl rollout status deployment/todo-app -n todo-app --timeout=120s
|
||||
echo " ✓ Deployment is ready"
|
||||
|
||||
# ── Summary ────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "======================================================"
|
||||
echo " Application is UP!"
|
||||
echo ""
|
||||
MINIKUBE_IP=$(minikube ip 2>/dev/null || echo "<minikube-ip>")
|
||||
echo " Open in browser: http://${MINIKUBE_IP}:30080"
|
||||
echo ""
|
||||
echo " Or run: minikube service todo-app-service -n todo-app"
|
||||
echo ""
|
||||
echo " All pods:"
|
||||
kubectl get pods -n todo-app
|
||||
echo "======================================================"
|
||||
47
z2/stop-app.sh
Normal file
47
z2/stop-app.sh
Normal file
@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# stop-app.sh
|
||||
# Deletes all Kubernetes objects created for the Todo application.
|
||||
# The PersistentVolume host directory is preserved (data survives restart).
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
K8S_DIR="$(dirname "$0")/k8s"
|
||||
|
||||
echo "======================================================"
|
||||
echo " Stopping K8s Todo App"
|
||||
echo "======================================================"
|
||||
|
||||
# Delete in reverse creation order to avoid dependency issues
|
||||
|
||||
echo ""
|
||||
echo "[1/5] Deleting Deployment..."
|
||||
kubectl delete -f "$K8S_DIR/deployment.yaml" --ignore-not-found=true
|
||||
echo " ✓ Deployment deleted"
|
||||
|
||||
echo ""
|
||||
echo "[2/5] Deleting Services..."
|
||||
kubectl delete -f "$K8S_DIR/service.yaml" --ignore-not-found=true
|
||||
echo " ✓ Services deleted"
|
||||
|
||||
echo ""
|
||||
echo "[3/5] Deleting StatefulSet, PVC, Secret and PV..."
|
||||
kubectl delete -f "$K8S_DIR/statefulset.yaml" --ignore-not-found=true
|
||||
echo " ✓ StatefulSet, PVC, Secret and PV deleted"
|
||||
|
||||
echo ""
|
||||
echo "[4/5] Deleting ConfigMap..."
|
||||
kubectl delete -f "$K8S_DIR/configmap.yaml" --ignore-not-found=true
|
||||
echo " ✓ ConfigMap deleted"
|
||||
|
||||
echo ""
|
||||
echo "[5/5] Deleting Namespace (this removes any remaining objects)..."
|
||||
kubectl delete -f "$K8S_DIR/namespace.yaml" --ignore-not-found=true
|
||||
echo " ✓ Namespace deleted"
|
||||
|
||||
echo ""
|
||||
echo "======================================================"
|
||||
echo " Application stopped and all objects removed."
|
||||
echo " Database files remain at /mnt/data/postgres inside minikube."
|
||||
echo " To fully reset: minikube ssh -- 'sudo rm -rf /mnt/data/postgres'"
|
||||
echo "======================================================"
|
||||
Loading…
Reference in New Issue
Block a user