Second Assignment

This commit is contained in:
Nitheesh Kumar Subramanian 2026-04-29 09:47:09 +02:00
parent 2443ce80f7
commit 3821bf2d1e
16 changed files with 1080 additions and 0 deletions

BIN
thesis.pdf Normal file

Binary file not shown.

249
z2/README.md Normal file
View 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
View 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
View 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
View 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
View 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>

Binary file not shown.

27
z2/k8s/configmap.yaml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 "======================================================"