zkt25z1/docs/superpowers/plans/2026-04-29-k8s-taskapp-plan.md
Brazing Technology 387b462673 update qubernetees
2026-04-29 14:52:01 +05:30

1365 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Task Manager on Kubernetes — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Migrate the existing 3-tier Task Manager (Nginx + Flask + Postgres) from `cloud assiagment/` to Kubernetes on minikube, satisfying every course-assignment requirement (Namespace, Deployment ×2, StatefulSet+PV+PVC, Services, lifecycle scripts, README).
**Architecture:** All resources in namespace `taskapp`. Stateless tiers (web, api) as Deployments with 2 replicas. Postgres as a 1-replica StatefulSet bound to a manually-defined hostPath PV via a PVC. Web exposed via NodePort 30080 (browser access through `minikube service`). DB password in a Secret; `nginx.conf` in a ConfigMap.
**Tech Stack:** Kubernetes 1.28+, kubectl, minikube, Docker, Nginx Alpine, Python 3.12 + Flask + Gunicorn, PostgreSQL 15, bash.
**Spec reference:** `docs/superpowers/specs/2026-04-29-k8s-taskapp-design.md` — read it before starting if you're a fresh agent.
**Source-of-truth files copied from first assignment:** `C:\Users\gigis\DEVapps\cloud assiagment\backend\*`, `…\frontend\*`. The first assignment is a complete, working Docker version of the same app.
**Working directory:** `C:\Users\gigis\DEVapps\qubernetees\` (Windows host; bash via Git Bash). Use Unix-style forward-slash paths in scripts.
**Note on git:** the working directory is NOT a git repo. Decision: do `git init` once at the start of Task 1; commit at the end of each task. This gives a clean history for the oral evaluation.
**Note on cluster availability:** the agent executing this plan may or may not have minikube installed and running. The plan's verification commands assume minikube is available. If `minikube status` fails, the agent should report this back to the user rather than try to install minikube — that's the user's environment to set up. End-to-end checks (Task 13) MAY be deferred to the user if the cluster isn't reachable in the executor's environment.
---
## File structure (target end state)
```
qubernetees/
├── README.md
├── prepare-app.sh
├── start-app.sh
├── stop-app.sh
├── namespace.yaml
├── statefulset.yaml
├── deployment.yaml
├── service.yaml
├── nginx.conf
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── app.py
└── frontend/
├── Dockerfile
├── index.html
├── style.css
└── app.js
```
Total: 8 root files + 2 subdirectories with 4+3 files = 15 files. The plan creates each one in dependency order.
---
## Task 1: Initialize repo and copy frontend/backend source from first assignment
**Files:**
- Create: `qubernetees/.gitignore`
- Copy: `cloud assiagment/backend/{app.py,requirements.txt}``qubernetees/backend/`
- Copy: `cloud assiagment/frontend/{index.html,style.css,app.js}``qubernetees/frontend/`
- [ ] **Step 1: Initialize git repo**
```bash
cd "C:/Users/gigis/DEVapps/qubernetees"
git init
git config user.email "bvilborg@brazing-technology.com"
git config user.name "Brazing Technology"
```
- [ ] **Step 2: Create `.gitignore`**
Content:
```
*.log
.DS_Store
Thumbs.db
.idea/
.vscode/
__pycache__/
*.pyc
.env
```
- [ ] **Step 3: Create directories and copy source files**
```bash
mkdir -p backend frontend
cp "../cloud assiagment/backend/app.py" backend/
cp "../cloud assiagment/backend/requirements.txt" backend/
cp "../cloud assiagment/frontend/index.html" frontend/
cp "../cloud assiagment/frontend/style.css" frontend/
cp "../cloud assiagment/frontend/app.js" frontend/
```
- [ ] **Step 4: Verify all files copied**
```bash
ls -la backend/ frontend/
```
Expected: `backend/app.py`, `backend/requirements.txt`, `frontend/index.html`, `frontend/style.css`, `frontend/app.js` all present and non-empty.
- [ ] **Step 5: Commit**
```bash
git add .gitignore backend/ frontend/
git commit -m "chore: initial repo, copy source from assignment 1"
```
---
## Task 2: Add `/api/health` endpoint to backend
The readiness probe needs a meaningful health check. Add `GET /api/health` that returns 200 only if the DB is reachable.
**Files:**
- Modify: `qubernetees/backend/app.py`
- [ ] **Step 1: Read existing `app.py`**
```bash
cat backend/app.py
```
Confirmed pattern (already verified during planning): the file defines `get_db()` returning a `psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor)`. DB_CONFIG reads `DB_HOST`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` from env. Use the same helper.
- [ ] **Step 2: Add the health endpoint**
Place the new route **after the `delete_task` route** (line ~105) and **before** the `if __name__ == "__main__":` block (line ~108). Insert this verbatim:
```python
@app.route("/api/health", methods=["GET"])
def health():
try:
conn = get_db()
cur = conn.cursor()
cur.execute("SELECT 1")
cur.fetchone()
cur.close()
conn.close()
return jsonify({"status": "ok"}), 200
except Exception as exc:
return jsonify({"status": "unhealthy", "error": str(exc)}), 503
```
- [ ] **Step 3: Verify Python syntax**
```bash
python -m py_compile backend/app.py
```
Expected: no output (success). If syntax error, fix and re-run.
- [ ] **Step 4: Commit**
```bash
git add backend/app.py
git commit -m "feat(backend): add /api/health endpoint for k8s readiness probe"
```
---
## Task 3: Create backend Dockerfile
**Files:**
- Create: `qubernetees/backend/Dockerfile`
- [ ] **Step 1: Write Dockerfile**
Content:
```dockerfile
FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev gcc \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
# Run init_db() once (creates the tasks table) then start gunicorn.
# Same pattern as the first assignment's Dockerfile.
CMD ["sh", "-c", "python -c 'from app import init_db; init_db()' && gunicorn --bind 0.0.0.0:5000 --workers 2 app:app"]
```
If the first assignment's `requirements.txt` already includes `gunicorn`, this works as-is. Otherwise add `gunicorn` to `backend/requirements.txt`.
- [ ] **Step 2: Verify gunicorn is in requirements**
```bash
grep -i gunicorn backend/requirements.txt
```
If not present:
```bash
echo "gunicorn==22.0.0" >> backend/requirements.txt
```
- [ ] **Step 3: Lint Dockerfile syntax**
```bash
docker build --no-cache -t taskapp-api:lint-check backend/ 2>&1 | tail -5
```
Expected: build completes successfully (or fails only if the Docker daemon isn't running — that's OK, we'll build for real in Task 11). The point is that `docker` accepts the Dockerfile syntax.
If Docker daemon isn't running, skip the build and move on; the syntax will be checked when `prepare-app.sh` runs in Task 13.
- [ ] **Step 4: Commit**
```bash
git add backend/Dockerfile backend/requirements.txt
git commit -m "feat(backend): add Dockerfile for taskapp-api image"
```
---
## Task 4: Create frontend Dockerfile and nginx.conf source file
**Files:**
- Create: `qubernetees/frontend/Dockerfile`
- Create: `qubernetees/nginx.conf` (root-level — used as ConfigMap source)
- [ ] **Step 1: Write `frontend/Dockerfile`**
Content:
```dockerfile
FROM nginx:alpine
# Static assets
COPY index.html /usr/share/nginx/html/
COPY style.css /usr/share/nginx/html/
COPY app.js /usr/share/nginx/html/
# nginx.conf is provided at runtime via ConfigMap mount.
# We do NOT copy it into the image; the default nginx.conf in the image
# would be used if the ConfigMap mount is missing.
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
- [ ] **Step 2: Write root-level `nginx.conf` (ConfigMap source)**
Content:
```nginx
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
large_client_header_buffers 4 32k;
upstream api_backend {
server api:5000;
}
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
```
Notes:
- `upstream api_backend { server api:5000; }``api` is the K8s Service name, resolved via cluster DNS.
- `large_client_header_buffers` carried over from assignment 1 (handles large cookies; harmless if unused).
- This file is the **source** for the ConfigMap; the manifest in `namespace.yaml` will inline its contents.
- [ ] **Step 3: Verify nginx.conf syntax (optional, only if nginx CLI available)**
```bash
docker run --rm -v "$(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro" nginx:alpine nginx -t
```
Expected: `nginx: configuration file /etc/nginx/nginx.conf test is successful`. If Docker isn't available, skip — `kubectl apply` won't catch nginx-syntax errors anyway; the pod readiness probe will.
- [ ] **Step 4: Commit**
```bash
git add frontend/Dockerfile nginx.conf
git commit -m "feat(frontend): add Dockerfile and nginx.conf for ConfigMap"
```
---
## Task 5: Write `namespace.yaml` (Namespace + Secret + ConfigMap)
**Files:**
- Create: `qubernetees/namespace.yaml`
- [ ] **Step 1: Generate base64 values for the Secret**
```bash
echo -n "taskapp" | base64 # POSTGRES_DB
echo -n "taskuser" | base64 # POSTGRES_USER
echo -n "taskpass" | base64 # POSTGRES_PASSWORD
```
Expected output:
- `dGFza2FwcA==`
- `dGFza3VzZXI=`
- `dGFza3Bhc3M=`
(These are weak demo credentials. The README will note this.)
- [ ] **Step 2: Inline the contents of `nginx.conf` into the ConfigMap**
The ConfigMap embeds `nginx.conf` as a multi-line YAML string. Use `|` block style (preserves newlines, strips trailing). The ConfigMap key name is `nginx.conf` (matches the `subPath` in the Deployment).
- [ ] **Step 3: Write `namespace.yaml`**
Content:
```yaml
# 1. Namespace — every other object lives in this namespace
apiVersion: v1
kind: Namespace
metadata:
name: taskapp
labels:
app: taskapp
---
# 2. Secret — DB credentials, consumed by both Postgres and Flask
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: taskapp
type: Opaque
data:
POSTGRES_DB: dGFza2FwcA==
POSTGRES_USER: dGFza3VzZXI=
POSTGRES_PASSWORD: dGFza3Bhc3M=
---
# 3. ConfigMap — nginx.conf for the web tier
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
namespace: taskapp
data:
nginx.conf: |
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
large_client_header_buffers 4 32k;
upstream api_backend {
server api:5000;
}
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
```
- [ ] **Step 4: Validate YAML with kubectl dry-run**
```bash
kubectl apply -f namespace.yaml --dry-run=client -o yaml >/dev/null
```
Expected: no errors. (Output suppressed; we just want to confirm the manifest parses and is valid.)
If kubectl isn't available locally, fall back to a YAML linter:
```bash
python -c "import yaml,sys; list(yaml.safe_load_all(open('namespace.yaml')))"
```
Expected: no exception.
- [ ] **Step 5: Commit**
```bash
git add namespace.yaml
git commit -m "feat(k8s): namespace, secret, configmap manifests"
```
---
## Task 6: Write `statefulset.yaml` (PV + PVC + StatefulSet)
**Files:**
- Create: `qubernetees/statefulset.yaml`
- [ ] **Step 1: Write `statefulset.yaml`**
Content:
```yaml
# 1. PersistentVolume — cluster-scoped, hostPath on minikube node
apiVersion: v1
kind: PersistentVolume
metadata:
name: db-pv
labels:
app: db
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
hostPath:
path: /mnt/data/taskapp-db
type: DirectoryOrCreate
---
# 2. PersistentVolumeClaim — namespaced, binds to db-pv
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: db-pvc
namespace: taskapp
spec:
accessModes:
- ReadWriteOnce
storageClassName: manual
resources:
requests:
storage: 1Gi
selector:
matchLabels:
app: db
---
# 3. StatefulSet — Postgres
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
namespace: taskapp
spec:
serviceName: db # references the headless Service in service.yaml
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: postgres
image: postgres:15
ports:
- containerPort: 5432
name: postgres
envFrom:
- secretRef:
name: db-credentials
env:
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: db-storage
mountPath: /var/lib/postgresql/data
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
readinessProbe:
exec:
command:
- sh
- -c
- 'pg_isready -U "$POSTGRES_USER"'
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
livenessProbe:
exec:
command:
- sh
- -c
- 'pg_isready -U "$POSTGRES_USER"'
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
volumes:
- name: db-storage
persistentVolumeClaim:
claimName: db-pvc
```
Design notes (DO NOT include in the file):
- `PGDATA` is set to a subdir of the mount because Postgres refuses to initialize a non-empty data dir, and `lost+found` etc. on hostPath can trigger that.
- `volumeClaimTemplates` is intentionally NOT used — we want one explicit PVC bound to one explicit PV (assignment requires "PV and PVC objects").
- The PVC selector `matchLabels: app: db` is what binds it to `db-pv` (which has matching labels). Static binding.
- [ ] **Step 2: Validate YAML**
```bash
kubectl apply -f statefulset.yaml --dry-run=client --validate=false -o yaml >/dev/null
```
(`--validate=false` skips the cluster-side schema check, which would require a real cluster connection. Client-side parsing still runs.)
Expected: no errors.
- [ ] **Step 3: Commit**
```bash
git add statefulset.yaml
git commit -m "feat(k8s): PV, PVC, StatefulSet for postgres"
```
---
## Task 7: Write `service.yaml` (web + api + db Services)
**Files:**
- Create: `qubernetees/service.yaml`
- [ ] **Step 1: Write `service.yaml`**
Content:
```yaml
# 1. web Service — NodePort, exposes the Nginx frontend to the host
apiVersion: v1
kind: Service
metadata:
name: web
namespace: taskapp
labels:
app: web
spec:
type: NodePort
selector:
app: web
ports:
- name: http
port: 80
targetPort: 80
nodePort: 30080
---
# 2. api Service — ClusterIP, internal-only access for Flask
apiVersion: v1
kind: Service
metadata:
name: api
namespace: taskapp
labels:
app: api
spec:
type: ClusterIP
selector:
app: api
ports:
- name: http
port: 5000
targetPort: 5000
---
# 3. db Service — headless (clusterIP: None), pairs with the StatefulSet
apiVersion: v1
kind: Service
metadata:
name: db
namespace: taskapp
labels:
app: db
spec:
clusterIP: None
selector:
app: db
ports:
- name: postgres
port: 5432
targetPort: 5432
```
- [ ] **Step 2: Validate YAML**
```bash
kubectl apply -f service.yaml --dry-run=client --validate=false -o yaml >/dev/null
```
Expected: no errors.
- [ ] **Step 3: Commit**
```bash
git add service.yaml
git commit -m "feat(k8s): web NodePort, api ClusterIP, db headless services"
```
---
## Task 8: Write `deployment.yaml` (api + web Deployments)
**Files:**
- Create: `qubernetees/deployment.yaml`
- [ ] **Step 1: Write `deployment.yaml`**
Content:
```yaml
# 1. api Deployment — Flask backend
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: taskapp
labels:
app: api
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: taskapp-api:v1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5000
name: http
env:
- name: DB_HOST
value: "db"
- name: DB_PORT
value: "5432"
- name: DB_NAME
valueFrom:
secretKeyRef:
name: db-credentials
key: POSTGRES_DB
- name: DB_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: POSTGRES_USER
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: POSTGRES_PASSWORD
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 250m
memory: 256Mi
readinessProbe:
httpGet:
path: /api/health
port: 5000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 6
livenessProbe:
httpGet:
path: /api/health
port: 5000
initialDelaySeconds: 30
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 3
---
# 2. web Deployment — Nginx frontend
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: taskapp
labels:
app: web
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: taskapp-web:v1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: http
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: true
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 100m
memory: 128Mi
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 1
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 15
volumes:
- name: nginx-config
configMap:
name: nginx-config
items:
- key: nginx.conf
path: nginx.conf
```
Design notes (DO NOT include in file):
- The Flask app (`app.py`) reads `DB_HOST`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` — not `POSTGRES_*`. So we map each Secret key to the right env name explicitly via `valueFrom: secretKeyRef`. (Important: `$(POSTGRES_DB)` substitution in the `env:` block does NOT work for vars sourced via `envFrom`; only prior `env:` entries can be referenced. So inline-reading from the Secret per-var is the correct pattern here.)
- `imagePullPolicy: IfNotPresent` is critical — local minikube images would otherwise be re-pulled from a registry that doesn't have them.
- The `failureThreshold: 6` on the api readiness probe means the api pod gets up to 30 s (6×5s) of DB unavailability before being marked unready. Useful during cold start when the DB is still initializing.
- `subPath: nginx.conf` mounts a single file (not a directory) over `/etc/nginx/nginx.conf`. Without `subPath`, the entire ConfigMap directory would be mounted at that path, breaking nginx.
- [ ] **Step 2: Validate YAML**
```bash
kubectl apply -f deployment.yaml --dry-run=client --validate=false -o yaml >/dev/null
```
Expected: no errors.
- [ ] **Step 3: Commit**
```bash
git add deployment.yaml
git commit -m "feat(k8s): api and web Deployments with probes and resources"
```
---
## Task 9: Write `prepare-app.sh`
**Files:**
- Create: `qubernetees/prepare-app.sh` (executable)
- [ ] **Step 1: Write `prepare-app.sh`**
Content:
```bash
#!/usr/bin/env bash
set -euo pipefail
echo "[prepare-app] Preparing Task Manager for Kubernetes..."
# 1. Ensure minikube is running
if ! minikube status >/dev/null 2>&1; then
echo "[prepare-app] minikube is not running. Starting it..."
minikube start
else
echo "[prepare-app] minikube is already running."
fi
# 2. Point local docker at minikube's daemon so images land inside the cluster
echo "[prepare-app] Switching docker context to minikube..."
eval "$(minikube -p minikube docker-env)"
# 3. Build the two custom images
echo "[prepare-app] Building taskapp-api:v1..."
docker build -t taskapp-api:v1 backend/
echo "[prepare-app] Building taskapp-web:v1..."
docker build -t taskapp-web:v1 frontend/
# 4. Create the hostPath directory on the minikube node
echo "[prepare-app] Creating /mnt/data/taskapp-db on the minikube node..."
minikube ssh -- "sudo mkdir -p /mnt/data/taskapp-db && sudo chmod 777 /mnt/data/taskapp-db"
# 5. Apply the namespace (so the PVC has a place to live) and the StatefulSet stack.
# The namespace must exist before namespaced objects (PVC, StatefulSet) can be created,
# so we apply and then poll for it to exist before continuing.
echo "[prepare-app] Creating Namespace, Secret, ConfigMap..."
kubectl apply -f namespace.yaml
echo "[prepare-app] Waiting for taskapp namespace to be available..."
for _ in $(seq 1 20); do
if kubectl get namespace taskapp >/dev/null 2>&1; then
break
fi
sleep 1
done
echo "[prepare-app] Creating PV, PVC, StatefulSet..."
kubectl apply -f statefulset.yaml
echo "[prepare-app] App prepared."
echo "[prepare-app] Run ./start-app.sh to bring up the rest."
```
- [ ] **Step 2: Make executable**
```bash
chmod +x prepare-app.sh
```
- [ ] **Step 3: Verify bash syntax**
```bash
bash -n prepare-app.sh
```
Expected: no output.
- [ ] **Step 4: Commit**
```bash
git add prepare-app.sh
git commit -m "feat(scripts): prepare-app.sh — build images, create PV"
```
---
## Task 10: Write `start-app.sh`
**Files:**
- Create: `qubernetees/start-app.sh` (executable)
- [ ] **Step 1: Write `start-app.sh`**
Content:
```bash
#!/usr/bin/env bash
set -euo pipefail
echo "[start-app] Starting Task Manager on Kubernetes..."
# Note: this script assumes ./prepare-app.sh has already been run successfully
# (images built into minikube, hostPath dir created, PV applied). Re-applying
# the prepare-stage manifests below is idempotent and safe, but does NOT build
# images or create the hostPath dir. Without prepare-app.sh, pods will fail with
# ImagePullBackOff.
echo "[start-app] Applying namespace.yaml..."
kubectl apply -f namespace.yaml
echo "[start-app] Applying statefulset.yaml..."
kubectl apply -f statefulset.yaml
echo "[start-app] Applying service.yaml..."
kubectl apply -f service.yaml
echo "[start-app] Applying deployment.yaml..."
kubectl apply -f deployment.yaml
# Wait for everything to become ready
echo "[start-app] Waiting for db StatefulSet..."
kubectl -n taskapp rollout status statefulset/db --timeout=180s
echo "[start-app] Waiting for api Deployment..."
kubectl -n taskapp rollout status deployment/api --timeout=180s
echo "[start-app] Waiting for web Deployment..."
kubectl -n taskapp rollout status deployment/web --timeout=180s
echo ""
echo "[start-app] App is running!"
echo "[start-app] Opening browser at the web service URL..."
minikube service web -n taskapp
```
- [ ] **Step 2: Make executable**
```bash
chmod +x start-app.sh
```
- [ ] **Step 3: Verify bash syntax**
```bash
bash -n start-app.sh
```
Expected: no output.
- [ ] **Step 4: Commit**
```bash
git add start-app.sh
git commit -m "feat(scripts): start-app.sh — apply all manifests, open browser"
```
---
## Task 11: Write `stop-app.sh`
**Files:**
- Create: `qubernetees/stop-app.sh` (executable)
- [ ] **Step 1: Write `stop-app.sh`**
Content:
```bash
#!/usr/bin/env bash
set -euo pipefail
echo "[stop-app] Removing Task Manager from Kubernetes..."
# Reverse order: Deployments and Services first, then StatefulSet+PV+PVC, then Namespace
kubectl delete -f deployment.yaml --ignore-not-found
kubectl delete -f service.yaml --ignore-not-found
kubectl delete -f statefulset.yaml --ignore-not-found
kubectl delete -f namespace.yaml --ignore-not-found
# The hostPath directory on the minikube node (/mnt/data/taskapp-db) is intentionally
# NOT removed: PV ReclaimPolicy is Retain, so the data survives even after the PV
# object is deleted. To wipe it manually, run:
# minikube ssh -- sudo rm -rf /mnt/data/taskapp-db
echo "[stop-app] App stopped and removed."
echo "[stop-app] (Persistent data on minikube node is retained at /mnt/data/taskapp-db.)"
```
- [ ] **Step 2: Make executable**
```bash
chmod +x stop-app.sh
```
- [ ] **Step 3: Verify bash syntax**
```bash
bash -n stop-app.sh
```
Expected: no output.
- [ ] **Step 4: Commit**
```bash
git add stop-app.sh
git commit -m "feat(scripts): stop-app.sh — full teardown"
```
---
## Task 12: Write `README.md`
**Files:**
- Create: `qubernetees/README.md`
- [ ] **Step 1: Write `README.md`**
Use this exact structure (the assignment grades documentation completeness):
```markdown
# Task Manager on Kubernetes
A web-based task manager (Nginx + Flask + PostgreSQL) deployed to a Kubernetes cluster (minikube). Migrated from the Docker Compose version in the first assignment.
## What the application does
A simple CRUD task manager. Users can:
- Add a new task by typing a title and pressing Enter.
- Mark a task as completed by toggling its checkbox.
- Delete a task by clicking the X button.
- See all tasks sorted by creation date.
The frontend is a static page served by Nginx. The backend is a Flask REST API. Tasks are stored in PostgreSQL and survive pod restarts thanks to a hostPath PersistentVolume.
## Prerequisites
- **minikube** (any recent version, tested with 1.32+)
- **kubectl** (any version compatible with the minikube cluster)
- **Docker** CLI (used by `prepare-app.sh` to build images directly into minikube's docker daemon)
- **bash** (Linux/macOS native; Git Bash on Windows)
## Containers used
| Container | Image | Description |
|-----------|-------|-------------|
| `web` | `taskapp-web:v1` (built locally from `nginx:alpine`) | Serves static HTML/CSS/JS and reverse-proxies `/api/*` to the `api` Service. `nginx.conf` is mounted from a ConfigMap. |
| `api` | `taskapp-api:v1` (built locally from `python:3.12-slim`) | Flask REST API on Gunicorn (2 workers). Exposes `GET/POST /api/tasks`, `PUT /api/tasks/:id`, `DELETE /api/tasks/:id`, `GET /api/health`. |
| `db` | `postgres:15` (Docker Hub) | Stores `tasks` table. Data lives at `/var/lib/postgresql/data` on a PersistentVolume backed by hostPath. |
## Kubernetes objects
| Object | Name | Description |
|--------|------|-------------|
| Namespace | `taskapp` | Isolates every other resource. |
| Secret | `db-credentials` | Holds `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`. Consumed by both Postgres and Flask via `envFrom`. |
| ConfigMap | `nginx-config` | Holds `nginx.conf`. Mounted into the `web` container at `/etc/nginx/nginx.conf` via `subPath`. |
| PersistentVolume | `db-pv` | 1 Gi, `hostPath: /mnt/data/taskapp-db` on the minikube node. ReclaimPolicy `Retain`. StorageClass `manual`. |
| PersistentVolumeClaim | `db-pvc` | 1 Gi, `ReadWriteOnce`, binds to `db-pv` via label selector. Mounted by the StatefulSet. |
| StatefulSet | `db` | 1 replica, `postgres:15`. Uses the PVC. Has liveness and readiness probes (`pg_isready`). |
| Deployment | `api` | 2 replicas, custom Flask image. Probes hit `GET /api/health` (which checks DB reachability). |
| Deployment | `web` | 2 replicas, custom Nginx image. Probes hit `GET /`. |
| Service | `web` | NodePort 30080 → 80. The only externally reachable Service. |
| Service | `api` | ClusterIP, port 5000. Internal only. |
| Service | `db` | Headless (`clusterIP: None`), port 5432. Internal only. Pairs with the StatefulSet for stable DNS. |
## Virtual networks
The cluster's CNI plugin handles all pod-to-pod traffic. Service discovery is via Kubernetes DNS (`kube-dns` / `coredns`):
- Inside the namespace, every Service is reachable by short name: `web`, `api`, `db`.
- The `web` Service is the only one exposed outside the cluster (NodePort 30080).
- The `db` Service is **headless** — it returns the pod IPs directly instead of load-balancing. This is the canonical pattern for StatefulSets and gives `db-0` a stable DNS identity (`db-0.db.taskapp.svc.cluster.local`).
## Named volumes
| Volume | Mount | Backed by | Reclaim policy |
|--------|-------|-----------|----------------|
| `db-pv` (PVC `db-pvc`) | `/var/lib/postgresql/data` (in `db-0`) | hostPath `/mnt/data/taskapp-db` on the minikube node | Retain |
`Retain` means deleting the PVC does not automatically wipe the underlying directory — the data survives even a full `stop-app.sh` and is reattached on the next `prepare-app.sh + start-app.sh`. To wipe it manually:
```bash
minikube ssh -- sudo rm -rf /mnt/data/taskapp-db
```
## Container configuration performed
- **`web`** — built from `nginx:alpine` plus the static frontend files. The `nginx.conf` is supplied at runtime via the `nginx-config` ConfigMap, mounted with `subPath: nginx.conf` so only that single file is overlaid (not the whole config directory). This means the proxy rule (`proxy_pass http://api_backend;`) and `large_client_header_buffers` setting can be edited by changing the ConfigMap and reloading, without rebuilding the image.
- **`api`** — built from `python:3.12-slim`, installs `libpq-dev` and `gcc` for the `psycopg2` build, then `pip install` of `requirements.txt` (Flask + Gunicorn + psycopg2). Runs Gunicorn with 2 workers on port 5000. Reads DB credentials from env vars sourced from the `db-credentials` Secret.
- **`db`** — official `postgres:15` image, unmodified. Env (DB name, user, password) sourced from the same Secret. `PGDATA` is set to `/var/lib/postgresql/data/pgdata` (a subdir) because Postgres refuses to initialize a non-empty data directory and hostPath dirs occasionally have stray entries.
Resource requests/limits are set on every container so the cluster scheduler can place pods predictably; values are conservative and tested against a 4 GiB minikube VM.
## Usage
### Prepare the application
Builds images directly into minikube's docker daemon, creates the hostPath directory on the node, and applies Namespace + Secret + ConfigMap + PV + PVC + StatefulSet.
```bash
./prepare-app.sh
```
### Start the application
Applies the Deployments and Services. (Re-applies prepare-stage resources idempotently, so it's safe to run on its own too.)
```bash
./start-app.sh
```
The script then opens your default browser at `http://<minikube-ip>:30080`.
### Pause / stop the application
`stop-app.sh` removes every Kubernetes object (Deployments, Services, StatefulSet, PVC, PV, Namespace, Secret, ConfigMap):
```bash
./stop-app.sh
```
The hostPath data on the minikube node is **retained** (PV ReclaimPolicy = `Retain`). To resume from where you left off, run `./prepare-app.sh && ./start-app.sh` again.
### Delete everything (including data)
```bash
./stop-app.sh
minikube ssh -- sudo rm -rf /mnt/data/taskapp-db
```
## Viewing the application in a web browser
After `./start-app.sh` finishes, run:
```bash
minikube service web -n taskapp
```
This opens the browser at the right URL automatically. Alternatively, navigate manually:
```bash
echo "http://$(minikube ip):30080"
```
You'll see the Task Manager interface where you can add, complete, and delete tasks.
## API endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/health` | Health check (200 if DB reachable, 503 otherwise). Used by readiness probe. |
| GET | `/api/tasks` | List all tasks. |
| POST | `/api/tasks` | Create a task. Body: `{"title": "..."}`. |
| PUT | `/api/tasks/:id` | Toggle completion. |
| DELETE | `/api/tasks/:id` | Delete a task. |
## Project structure
```
qubernetees/
├── README.md # this file
├── prepare-app.sh # build images, create PV, apply prepare-stage manifests
├── start-app.sh # apply Deployments + Services, open browser
├── stop-app.sh # full teardown
├── namespace.yaml # Namespace + Secret + ConfigMap
├── statefulset.yaml # PV + PVC + StatefulSet
├── deployment.yaml # api + web Deployments
├── service.yaml # api + web + db Services
├── nginx.conf # source of the ConfigMap (kept for readability)
├── backend/ # Flask REST API
│ ├── Dockerfile
│ ├── requirements.txt
│ └── app.py # + /api/health endpoint
└── frontend/ # static HTML/CSS/JS
├── Dockerfile
├── index.html
├── style.css
└── app.js
```
## Sources
- First assignment (Docker version of the same app) — code copied from `cloud assiagment/`
- [Kubernetes documentation](https://kubernetes.io/docs/)
- [Kubernetes StatefulSet docs](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)
- [Postgres official Docker image](https://hub.docker.com/_/postgres)
- [minikube documentation](https://minikube.sigs.k8s.io/docs/)
## Use of Artificial Intelligence
This Kubernetes deployment was designed and implemented with the assistance of **Claude** (Anthropic), an AI assistant. Claude was used for:
- Designing the K8s object topology (Namespace, Deployments, StatefulSet, PV, PVC, Services).
- Authoring the manifest files, lifecycle scripts, and this documentation.
- Reviewing the design against the assignment requirements.
**AI agent used:** Claude Opus 4.7 (Anthropic) via Claude Code CLI.
The application logic itself (Flask backend, frontend) was carried over from the first assignment.
```
- [ ] **Step 2: Validate that markdown renders cleanly**
```bash
# Lightweight check — confirm no merge markers, no broken yaml fences
grep -nE '<<<<<<<|>>>>>>>|```\s*$' README.md && echo "BAD" || echo "OK"
```
Expected: `OK`.
- [ ] **Step 3: Commit**
```bash
git add README.md
git commit -m "docs: README covering app, K8s objects, networks, volumes, lifecycle"
```
---
## Task 13: End-to-end functional verification
**Goal:** prove the whole stack works from clean slate to browser-served task manager.
**Prerequisites:** working minikube cluster, working `kubectl`, working `docker`. If any of these is unavailable in the executor's environment: **skip Task 13 entirely and proceed to Task 14**. Task 14's offline dry-run validation (manifest schema) is the no-cluster fallback — it will catch YAML and structural errors but not image-build, scheduling, or runtime issues. Surface this clearly to the user when handing off so they know the live verification is still pending on their side.
- [ ] **Step 1: Verify cluster reachable**
```bash
kubectl version --client
minikube status
docker version
```
Expected: all three commands succeed. If `minikube status` shows "Stopped" or fails, run `minikube start` first.
- [ ] **Step 2: Run prepare-app.sh**
```bash
cd "C:/Users/gigis/DEVapps/qubernetees"
./prepare-app.sh
```
Expected last line: `[prepare-app] Run ./start-app.sh to bring up the rest.`
If this fails, read the error carefully:
- "Cannot connect to the Docker daemon" → `minikube start` again to refresh docker-env.
- "no matches for kind X" → re-check that all YAMLs apply cleanly with `kubectl apply --dry-run=client -f <file>`.
- Image build failures → fix the Dockerfile in the relevant subdir.
- [ ] **Step 3: Run start-app.sh**
```bash
./start-app.sh
```
Expected:
- All four `kubectl apply` lines succeed.
- All three `rollout status` lines end with "successfully rolled out".
- A browser opens (or a URL is printed).
- [ ] **Step 4: Confirm pods are healthy**
```bash
kubectl -n taskapp get pods
```
Expected: 5 pods (`db-0`, 2× `api-…`, 2× `web-…`), all `Running`, all `1/1` ready.
- [ ] **Step 5: Smoke-test the API directly via Service URL**
```bash
URL=$(minikube service web -n taskapp --url)
curl -sf "$URL/api/health"
```
Expected: `{"status":"ok"}` with HTTP 200.
- [ ] **Step 6: Smoke-test task creation**
```bash
curl -s -X POST -H "Content-Type: application/json" \
-d '{"title":"k8s smoke test task"}' \
"$URL/api/tasks"
curl -s "$URL/api/tasks"
```
Expected: the second `curl` returns a JSON array containing the newly-created task (with `id`, `title="k8s smoke test task"`, `completed=false`, `created_at`).
- [ ] **Step 7: Open the UI manually in a browser**
```bash
echo "$URL"
```
Open that URL. Verify visually: input box accepts text, Enter creates a task, checkbox toggles completion, X deletes a task, list persists across page reloads.
- [ ] **Step 8: Verify data persistence across stop/start**
```bash
./stop-app.sh
./prepare-app.sh
./start-app.sh
URL=$(minikube service web -n taskapp --url)
curl -s "$URL/api/tasks"
```
Expected: the task created in Step 6 is still present (PV `Retain` reclaim policy preserved the data dir).
- [ ] **Step 9: Final teardown for clean evaluation state**
```bash
./stop-app.sh
```
(Optional: `minikube ssh -- sudo rm -rf /mnt/data/taskapp-db` to wipe the persisted data too.)
- [ ] **Step 10: Commit verification log (optional)**
If you captured terminal output to a file like `verification.log`, do NOT commit it (it pollutes the submission). Just confirm verification passed.
---
## Task 14: Final review and submission readiness
- [ ] **Step 1: Confirm all required files are present**
```bash
ls -la prepare-app.sh start-app.sh stop-app.sh \
deployment.yaml service.yaml statefulset.yaml namespace.yaml \
README.md nginx.conf
ls -la backend/Dockerfile frontend/Dockerfile
```
Expected: every file listed exists and is non-empty. The three `.sh` files have `x` executable bits.
- [ ] **Step 2: Spot-check that scripts pass syntax**
```bash
bash -n prepare-app.sh && bash -n start-app.sh && bash -n stop-app.sh
```
Expected: no output, exit 0.
- [ ] **Step 3: Spot-check that all YAML parses**
```bash
for f in namespace.yaml statefulset.yaml service.yaml deployment.yaml; do
echo "--- $f ---"
kubectl apply -f "$f" --dry-run=client --validate=false -o yaml >/dev/null && echo OK
done
```
Expected: four `OK` lines.
- [ ] **Step 4: Show git log to confirm clean history**
```bash
git log --oneline
```
Expected: roughly one commit per task (12-14 commits, descriptive messages).
- [ ] **Step 5: Hand off to user**
Tell the user:
- "Implementation complete. End-to-end verification: <PASSED / DEFERRED describe>."
- "All required files present in `qubernetees/`. Ready for upload to `zkt25/z2`."
- Quote the URL the app served at, if available, so they can sanity-check before submission.
---
## Total estimate
| Task | What | Estimate |
|------|------|----------|
| 1 | Init repo + copy source | 5 min |
| 2 | Add /api/health | 5 min |
| 3 | Backend Dockerfile | 5 min |
| 4 | Frontend Dockerfile + nginx.conf | 10 min |
| 5 | namespace.yaml | 10 min |
| 6 | statefulset.yaml | 15 min |
| 7 | service.yaml | 5 min |
| 8 | deployment.yaml | 15 min |
| 9 | prepare-app.sh | 10 min |
| 10 | start-app.sh | 10 min |
| 11 | stop-app.sh | 5 min |
| 12 | README.md | 20 min |
| 13 | E2E verification | 15 min |
| 14 | Final review | 5 min |
| **Total** | | **~2h15m** |