diff --git a/thesis.pdf b/thesis.pdf new file mode 100644 index 0000000..fca143b Binary files /dev/null and b/thesis.pdf differ diff --git a/z2/README.md b/z2/README.md new file mode 100644 index 0000000..63d5634 --- /dev/null +++ b/z2/README.md @@ -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://: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'` | diff --git a/z2/app/Dockerfile b/z2/app/Dockerfile new file mode 100644 index 0000000..daf63d2 --- /dev/null +++ b/z2/app/Dockerfile @@ -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"] diff --git a/z2/app/app.py b/z2/app/app.py new file mode 100644 index 0000000..1da072d --- /dev/null +++ b/z2/app/app.py @@ -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/") +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/") +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) diff --git a/z2/app/requirements.txt b/z2/app/requirements.txt new file mode 100644 index 0000000..8bed5ac --- /dev/null +++ b/z2/app/requirements.txt @@ -0,0 +1,3 @@ +flask==3.0.3 +psycopg2-binary==2.9.9 +gunicorn==22.0.0 diff --git a/z2/app/templates/index.html b/z2/app/templates/index.html new file mode 100644 index 0000000..6fa444e --- /dev/null +++ b/z2/app/templates/index.html @@ -0,0 +1,205 @@ + + + + + + K8s Todo App + + + + +
+
+
+
+ +
+
{{ done_count }}/{{ total }} done
+
+
+ +
+ + +
+ +
+ {% if todos %} + {% for todo in todos %} +
+ +
+
+ {{ todo[1] }} + {{ todo[3].strftime('%b %d') if todo[3] else '' }} + + + +
+ {% endfor %} + {% else %} +
+
📋
+

No tasks yet. Add one above!

+
+ {% endif %} +
+ + +
+ + diff --git a/z2/k8s-todo-app-documentation.docx b/z2/k8s-todo-app-documentation.docx new file mode 100644 index 0000000..3267a00 Binary files /dev/null and b/z2/k8s-todo-app-documentation.docx differ diff --git a/z2/k8s/configmap.yaml b/z2/k8s/configmap.yaml new file mode 100644 index 0000000..d74c3a2 --- /dev/null +++ b/z2/k8s/configmap.yaml @@ -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; diff --git a/z2/k8s/deployment.yaml b/z2/k8s/deployment.yaml new file mode 100644 index 0000000..b1ef632 --- /dev/null +++ b/z2/k8s/deployment.yaml @@ -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!" diff --git a/z2/k8s/namespace.yaml b/z2/k8s/namespace.yaml new file mode 100644 index 0000000..1f68015 --- /dev/null +++ b/z2/k8s/namespace.yaml @@ -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 diff --git a/z2/k8s/service.yaml b/z2/k8s/service.yaml new file mode 100644 index 0000000..3bd28e4 --- /dev/null +++ b/z2/k8s/service.yaml @@ -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://: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 diff --git a/z2/k8s/statefulset.yaml b/z2/k8s/statefulset.yaml new file mode 100644 index 0000000..1cad181 --- /dev/null +++ b/z2/k8s/statefulset.yaml @@ -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 diff --git a/z2/prepare-app.sh b/z2/prepare-app.sh new file mode 100644 index 0000000..168c460 --- /dev/null +++ b/z2/prepare-app.sh @@ -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 "======================================================" diff --git a/z2/sql/init.sql b/z2/sql/init.sql new file mode 100644 index 0000000..59973f6 --- /dev/null +++ b/z2/sql/init.sql @@ -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); diff --git a/z2/start-app.sh b/z2/start-app.sh new file mode 100644 index 0000000..d4d2ab6 --- /dev/null +++ b/z2/start-app.sh @@ -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 "") +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 "======================================================" diff --git a/z2/stop-app.sh b/z2/stop-app.sh new file mode 100644 index 0000000..822ec9b --- /dev/null +++ b/z2/stop-app.sh @@ -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 "======================================================"