Compare commits

...

No commits in common. "8c315a8126fa0b5865bd219771733eec09f9e12e" and "387b46267344b3e0d98889450f1783e8a4e1c251" have entirely different histories.

20 changed files with 2412 additions and 384 deletions

View File

@ -0,0 +1,33 @@
{
"permissions": {
"allow": [
"Bash(cp \"../cloud assiagment/backend/app.py\" backend/)",
"Bash(cp \"../cloud assiagment/backend/requirements.txt\" backend/)",
"Bash(cp \"../cloud assiagment/frontend/index.html\" frontend/)",
"Bash(cp \"../cloud assiagment/frontend/style.css\" frontend/)",
"Bash(cp \"../cloud assiagment/frontend/app.js\" frontend/)",
"Bash(python -m py_compile backend/app.py)",
"Bash(command -v kubectl)",
"Bash(kubectl version *)",
"Bash(kubectl apply *)",
"Bash(python -c \"import yaml,sys; list\\(yaml.safe_load_all\\(open\\('namespace.yaml'\\)\\)\\)\")",
"Bash(python -c \"import yaml,sys; list\\(yaml.safe_load_all\\(open\\('statefulset.yaml'\\)\\)\\)\")",
"Bash(python -c \"import yaml,sys; list\\(yaml.safe_load_all\\(open\\('service.yaml'\\)\\)\\)\")",
"Bash(python -c \"import yaml,sys; list\\(yaml.safe_load_all\\(open\\('deployment.yaml'\\)\\)\\)\")",
"Bash(chmod +x prepare-app.sh)",
"Bash(bash -n prepare-app.sh)",
"Bash(bash -n start-app.sh)",
"Bash(bash -n stop-app.sh)",
"Bash(python -c 'import yaml,sys; docs=list\\(yaml.safe_load_all\\(open\\('\\\\''__TRACKED_VAR__'\\\\''\\)\\)\\); print\\(f'\\\\'' {len\\(docs\\)} document\\(s\\):'\\\\''\\); [print\\(f'\\\\'' [{i}] kind={d.get\\(\"kind\"\\)}, name={d.get\\(\"metadata\",{}\\).get\\(\"name\"\\)}'\\\\''\\) for i,d in enumerate\\(docs\\) if d]')",
"Bash(docker version *)",
"Bash(minikube status *)",
"Bash(scoop install *)",
"Bash(minikube version *)",
"Bash(\"/c/Program Files/Docker/Docker/Docker Desktop.exe\")",
"Bash(./prepare-app.sh)",
"Bash(./start-app.sh)",
"Bash(./stop-app.sh)",
"Bash(minikube stop *)"
]
}
}

9
.gitignore vendored
View File

@ -1 +1,8 @@
docs/superpowers/
*.log
.DS_Store
Thumbs.db
.idea/
.vscode/
__pycache__/
*.pyc
.env

332
README.md
View File

@ -1,252 +1,180 @@
# Task Manager - Docker Web Application
# Task Manager on Kubernetes
A simple task manager web application deployed as a multi-container Docker system. Users can create, complete, and delete tasks through a web interface.
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
- **Linux** with Docker installed (Docker Engine 20.10+)
- **Docker Compose** v2 (optional, for `docker compose` deployment)
- Ports **80** must be available on the host machine
- **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)
## Application Description
## Containers used
The Task Manager is a web-based CRUD application for managing personal tasks. It consists of three services working together:
| 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. |
- A **frontend** web interface served by Nginx where users interact with the application
- A **backend** REST API built with Flask (Python) that handles business logic
- A **PostgreSQL database** that stores task data persistently
## Kubernetes objects
Users can:
- Add new tasks
- Mark tasks as completed (toggle checkbox)
- Delete tasks
- View all tasks in a list sorted by creation date
| 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. |
## Architecture
## Virtual networks
```
Browser (port 80)
|
v
+--------+ +-------+ +------------+
| Nginx | ----> | Flask | ----> | PostgreSQL |
| :80 | API | :5000 | SQL | :5432 |
+--------+ proxy +-------+ +------------+
static REST API persistent
files (gunicorn) volume
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
```
## Virtual Networks
## Container configuration performed
| Network Name | Driver | Purpose |
|-------------------|--------|------------------------------------------------------|
| taskapp-network | bridge | Connects all 3 containers so they can communicate |
- **`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.
All containers are attached to `taskapp-network`. Only Nginx exposes a port (80) to the host. Flask and PostgreSQL are accessible only within the Docker network.
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.
## Named Volumes
## Usage
| Volume Name | Mount Point | Purpose |
|----------------|------------------------------------|----------------------------------|
| taskapp-pgdata | /var/lib/postgresql/data (in db) | Persists database data across container restarts and stops |
### Prepare the application
Stopping and restarting the application preserves all task data thanks to this volume.
## Containers
### 1. taskapp-nginx (Frontend)
- **Image:** Custom, built from `nginx:alpine`
- **Port:** 80 (host) -> 80 (container)
- **Role:** Serves static HTML/CSS/JS files and reverse-proxies `/api/*` requests to the Flask backend
- **Restart policy:** `unless-stopped`
- **Configuration:** Custom `nginx.conf` with `proxy_pass` directive for API routing and `large_client_header_buffers 4 32k` for handling large cookies
### 2. taskapp-flask (Backend)
- **Image:** Custom, built from `python:3.12-slim`
- **Port:** 5000 (internal only, not exposed to host)
- **Role:** REST API server handling task CRUD operations
- **Restart policy:** `unless-stopped`
- **Configuration:** Environment variables for database connection (`DB_HOST`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`)
- **WSGI server:** Gunicorn with 2 workers
- **Auto-initialization:** Creates the `tasks` table on startup if it does not exist
### 3. taskapp-db (Database)
- **Image:** `postgres:15` (from Docker Hub)
- **Port:** 5432 (internal only, not exposed to host)
- **Role:** Stores task data (id, title, completed status, creation timestamp)
- **Restart policy:** `unless-stopped`
- **Configuration:** Environment variables (`POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`)
- **Volume:** `taskapp-pgdata` mounted at `/var/lib/postgresql/data`
## Container Configuration Details
- **Nginx** is configured via `frontend/nginx.conf` which sets up static file serving and reverse proxy rules
- **Flask** reads database credentials from environment variables passed at container runtime
- **PostgreSQL** is configured via standard Postgres environment variables; data is stored on a named volume
## Usage Instructions
### Prepare the Application
Build images and create Docker resources:
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
### Start the application
Run all containers:
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
```
Output:
```
Starting app...
App is running!
The app is available at http://localhost:80
```
The script then opens your default browser at `http://<minikube-ip>:30080`.
### View in Web Browser
### Pause / stop the application
Open your web browser and navigate to:
```
http://localhost:80
```
You will see the Task Manager interface where you can add, complete, and delete tasks.
### Stop the Application
Stop all containers (data is preserved):
`stop-app.sh` removes every Kubernetes object (Deployments, Services, StatefulSet, PVC, PV, Namespace, Secret, ConfigMap):
```bash
./stop-app.sh
```
### Remove the Application
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.
Remove all containers, images, networks, and volumes:
### Delete everything (including data)
```bash
./remove-app.sh
```
### Alternative: Docker Compose
You can also use Docker Compose instead of the shell scripts:
```bash
# Start
docker compose up -d --build
# Stop (preserves data)
docker compose down
# Remove everything including volumes
docker compose down -v --rmi all
```
## API Endpoints
| Method | Endpoint | Description |
|--------|-------------------|------------------------|
| GET | /api/tasks | List all tasks |
| POST | /api/tasks | Create a new task |
| PUT | /api/tasks/:id | Toggle task completion |
| DELETE | /api/tasks/:id | Delete a task |
## Project Structure
```
.
├── backend/
│ ├── Dockerfile # Python/Flask image definition
│ ├── requirements.txt # Python dependencies
│ └── app.py # Flask REST API application
├── frontend/
│ ├── Dockerfile # Nginx image definition
│ ├── nginx.conf # Nginx configuration (static files + reverse proxy)
│ ├── index.html # Main HTML page
│ ├── style.css # Styles
│ └── app.js # Frontend JavaScript (fetch API calls)
├── docker-compose.yaml # Docker Compose configuration
├── prepare-app.sh # Script to build images and create resources
├── start-app.sh # Script to start all containers
├── stop-app.sh # Script to stop all containers
├── remove-app.sh # Script to remove all traces of the app
└── README.md # This file
```
## Example of Working with the Application
```bash
# Prepare everything needed for the application
./prepare-app.sh
# Output:
# Preparing app...
# Building backend image...
# Building frontend image...
# Creating network...
# Creating volume...
# App prepared successfully.
# Start the application
./start-app.sh
# Output:
# Starting app...
# Starting database...
# Starting backend...
# Starting frontend...
#
# App is running!
# The app is available at http://localhost:80
# Open web browser and work with the application at http://localhost:80
# - Add tasks using the input field
# - Mark tasks as completed by clicking the checkbox
# - Delete tasks by clicking the X button
# Stop the application (data is preserved)
./stop-app.sh
# Output:
# Stopping app...
# App stopped.
minikube ssh -- sudo rm -rf /mnt/data/taskapp-db
```
# Start again - all tasks are still there
./start-app.sh
## Viewing the application in a web browser
# Remove everything related to the application
./remove-app.sh
# Output:
# Removing app...
# App removed.
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
- [Docker Documentation](https://docs.docker.com/)
- [Nginx Docker Image](https://hub.docker.com/_/nginx)
- [PostgreSQL Docker Image](https://hub.docker.com/_/postgres)
- [Python Docker Image](https://hub.docker.com/_/python)
- [Flask Documentation](https://flask.palletsprojects.com/)
- [Gunicorn Documentation](https://gunicorn.org/)
- [Nginx Reverse Proxy Guide](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/)
- 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 application was designed and implemented with the assistance of **Claude** (Anthropic), an AI assistant. Claude was used for:
This Kubernetes deployment was designed and implemented with the assistance of **Claude** (Anthropic), an AI assistant. Claude was used for:
- Designing the application architecture and service composition
- Writing application source code (Python/Flask backend, HTML/CSS/JS frontend)
- Writing Dockerfiles and Docker Compose configuration
- Writing shell scripts for application lifecycle management
- Writing this documentation
- 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.6 (Anthropic) via Claude Code CLI
**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.

View File

@ -2,6 +2,10 @@ 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
@ -9,4 +13,6 @@ 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"]

View File

@ -105,6 +105,20 @@ def delete_task(task_id):
return jsonify({"result": "ok"})
@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
if __name__ == "__main__":
init_db()
app.run(host="0.0.0.0", port=5000)

126
deployment.yaml Normal file
View File

@ -0,0 +1,126 @@
# 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

View File

@ -1,47 +0,0 @@
services:
db:
image: postgres:15
container_name: taskapp-db
restart: unless-stopped
environment:
POSTGRES_DB: taskapp
POSTGRES_USER: taskapp
POSTGRES_PASSWORD: taskapp123
volumes:
- taskapp-pgdata:/var/lib/postgresql/data
networks:
- taskapp-network
flask:
build: ./backend
image: taskapp-backend
container_name: taskapp-flask
restart: unless-stopped
environment:
DB_HOST: db
DB_NAME: taskapp
DB_USER: taskapp
DB_PASSWORD: taskapp123
depends_on:
- db
networks:
- taskapp-network
nginx:
build: ./frontend
image: taskapp-frontend
container_name: taskapp-nginx
restart: unless-stopped
ports:
- "80:80"
depends_on:
- flask
networks:
- taskapp-network
volumes:
taskapp-pgdata:
networks:
taskapp-network:
driver: bridge

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,276 @@
# Design — Task Manager on Kubernetes (zkt25 / z2)
**Date:** 2026-04-29
**Author:** Brazing Technology (bvilborg@brazing-technology.com), with Claude Opus 4.7
**Course assignment:** Task 2 — Kubernetes (due 2026-03-31, late submission)
**Source application:** First assignment — `cloud assiagment/` (Docker-based 3-tier task manager)
## 1. Goal
Migrate the existing 3-tier Task Manager application from Docker Compose to Kubernetes, satisfying every requirement of the course assignment:
- ≥ 1 `Namespace`; all other objects belong to it.
- ≥ 1 `Deployment`.
- ≥ 1 `StatefulSet` with its `PersistentVolume` and `PersistentVolumeClaim`.
- ≥ 1 `Service`.
- Mandatory files at repo root: `start-app.sh`, `stop-app.sh`, `prepare-app.sh`, `deployment.yaml`, `service.yaml`, `statefulset.yaml`, `Dockerfile(s)`, `README.md`.
- Documentation covering: app description, containers, K8s objects, networks/volumes, container configuration, lifecycle instructions, web access instructions.
## 2. Target environment
- **Cluster:** minikube (local, Windows host)
- **kubectl:** assumed installed and configured (matches assignment wording)
- **Docker:** images built directly into minikube's docker daemon via `eval $(minikube -p minikube docker-env)` — no registry, no `minikube image load` step
This choice is portable: the same scripts and YAML run unchanged on any minikube install (Linux/Mac/Windows). The graders need only `minikube start && ./prepare-app.sh && ./start-app.sh`.
## 3. Application (unchanged from assignment 1)
A Task Manager web app:
- **Frontend** — Nginx serving static HTML/CSS/JS, reverse-proxying `/api/*` to the backend.
- **Backend** — Flask (Python) REST API on Gunicorn, CRUD on `tasks` (id, title, completed, created_at). Auto-creates the table on startup.
- **Database** — PostgreSQL 15.
Endpoints: `GET/POST /api/tasks`, `PUT /api/tasks/:id`, `DELETE /api/tasks/:id`. **New:** `GET /api/health` (returns 200 if DB reachable) — used by the readiness probe.
## 4. Architecture
```
Browser ─► minikube service ─► NodePort 30080
┌──────────────────────┐
│ Service: web (NodePort)│
└──────────┬───────────┘
┌───────▼────────┐
│ Deployment: │
│ web (nginx, 2) │
└───────┬────────┘
│ /api/* (proxy_pass http://api:5000)
┌───────▼────────┐
│ Service: api │ (ClusterIP)
└───────┬────────┘
┌───────▼────────┐
│ Deployment: │
│ api (flask, 2) │
└───────┬────────┘
│ TCP 5432 (db.taskapp.svc:5432)
┌───────▼────────┐
│ Service: db │ (headless, ClusterIP None)
└───────┬────────┘
┌───────▼────────┐
│ StatefulSet: │
│ db (postgres,1)│── PVC ◄── PV (hostPath, 1Gi)
└────────────────┘
```
## 5. Kubernetes object inventory
| # | Object | Name | Purpose |
|---|--------|------|---------|
| 1 | Namespace | `taskapp` | Isolates all resources |
| 2 | Secret | `db-credentials` | `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` |
| 3 | ConfigMap | `nginx-config` | Holds `nginx.conf` (mounted at `/etc/nginx/nginx.conf`) |
| 4 | PersistentVolume | `db-pv` | 1 Gi, `hostPath: /mnt/data/taskapp-db`, `Retain` reclaim, `manual` storageClass |
| 5 | PersistentVolumeClaim | `db-pvc` | Binds to `db-pv`; consumed by StatefulSet pod |
| 6 | StatefulSet | `db` | 1 replica, `postgres:15`, mounts PVC at `/var/lib/postgresql/data`, env from Secret, `pg_isready` probes |
| 7 | Service | `db` | Headless (`clusterIP: None`), TCP 5432 |
| 8 | Deployment | `api` | 2 replicas, `taskapp-api:v1`, env from Secret, `GET /api/health` probes, resource requests/limits |
| 9 | Service | `api` | ClusterIP, TCP 5000 |
| 10 | Deployment | `web` | 2 replicas, `taskapp-web:v1`, ConfigMap-mounted nginx.conf, `GET /` readiness, resource requests/limits |
| 11 | Service | `web` | NodePort 30080 → 80 |
**Required by assignment:** Namespace ✓, Deployment ✓ (web, api), StatefulSet+PV+PVC ✓, Service ✓ (web, api, db).
**Engineering polish (item B from brainstorming):** Secret, ConfigMap, probes, resource requests/limits, multi-replica stateless tier.
## 6. Networking
- **Cluster DNS** — every Service is reachable inside the namespace by its short name: `web`, `api`, `db`. Across namespaces it would be `<svc>.taskapp.svc.cluster.local`.
- **Pod-to-pod** — handled by the CNI; no manual config.
- **Headless Service for `db`** — pairs with the StatefulSet so `db-0.db.taskapp.svc.cluster.local` is a stable DNS name. kube-proxy does not load-balance headless Services; clients connect directly to a pod.
- **External access** — only `web` is exposed (NodePort 30080). `api` and `db` are ClusterIP-only and not reachable from outside the cluster.
- **Web access for the user**`minikube service web -n taskapp` opens the browser at the right URL automatically; alternative is `minikube ip` + `:30080`.
## 7. Storage
- **PV**`db-pv`, 1 Gi, `hostPath: /mnt/data/taskapp-db` on the minikube node. Reclaim policy `Retain` so deleting the PVC does **not** wipe the underlying directory. StorageClass `manual` (matches the PVC's `storageClassName`).
- **PVC**`db-pvc`, requests 1 Gi `ReadWriteOnce`, storageClassName `manual`. The StatefulSet pod's volume mount references the PVC by name (claim ref, not a volumeClaimTemplate, since we want a pre-bound static PV).
- **Why hostPath, not a StorageClass-driven dynamic PV** — assignment explicitly requires PV and PVC objects. Static provisioning is the textbook fit. (Note: in production, dynamic provisioning is preferred.)
- **Initialization**`prepare-app.sh` runs `minikube ssh -- sudo mkdir -p /mnt/data/taskapp-db && sudo chmod 777 …` so the directory exists with permissions before the PV binds.
## 8. Configuration & secrets
- **DB password** — stored in Secret `db-credentials` (base64-encoded in YAML). Both Postgres (`POSTGRES_PASSWORD`) and Flask (`DB_PASSWORD`) read it via `envFrom: secretRef`.
- **DB host/port for Flask** — plain env vars in the Deployment manifest (`DB_HOST=db`, `DB_PORT=5432`); not secret.
- **`nginx.conf`** — held in ConfigMap `nginx-config`, mounted into the web container at `/etc/nginx/nginx.conf` (single-file mount via `subPath`). Tweaking the proxy block does not require rebuilding the image.
## 9. Container configuration
- **`taskapp-web` (Nginx)** — built from `nginx:alpine` + the static frontend files. The default `nginx.conf` is replaced at runtime by the ConfigMap mount. Listens on 80.
- **`taskapp-api` (Flask)** — built from `python:3.12-slim` + Flask + Gunicorn (2 workers). Reads DB credentials from env (Secret-sourced), creates the `tasks` table on startup if missing. Listens on 5000.
- **`postgres:15`** — official image, unmodified. Env from Secret. Volume mount at `/var/lib/postgresql/data`. Liveness and readiness use `pg_isready -U $POSTGRES_USER`.
## 10. File layout (repo root)
```
qubernetees/
├── README.md # documentation (assignment-required)
├── prepare-app.sh # build images into minikube; create PV directory
├── start-app.sh # kubectl apply in dependency order; wait for rollouts; open browser
├── stop-app.sh # kubectl delete (full teardown — assignment wording)
├── namespace.yaml # Namespace + Secret + ConfigMap
├── statefulset.yaml # PV + PVC + StatefulSet (no Service — see §10 note)
├── deployment.yaml # api + web Deployments
├── service.yaml # ALL Services: web (NodePort) + api (ClusterIP) + db (headless)
├── nginx.conf # source for the ConfigMap
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── app.py # + new /api/health endpoint
└── frontend/
├── Dockerfile
├── index.html
├── style.css
└── app.js
```
**Note on `service.yaml`:** the assignment maps file → object type strictly (`service.yaml` is "configuration file for object type Service"), so **all three Services** live in `service.yaml`: `web` (NodePort), `api` (ClusterIP), `db` (headless ClusterIP). The headless-service-next-to-its-StatefulSet pattern is idiomatic but ignored here in favor of the assignment's prescribed file layout.
**Note on `statefulset.yaml`:** holds PV + PVC + StatefulSet only. The `db` Service is in `service.yaml`. Object dependency at apply time (Service must exist before the StatefulSet pod starts so DNS resolves) is handled by ordering inside `start-app.sh`.
## 11. Lifecycle scripts
### `prepare-app.sh`
Per the assignment, this script "compiles images **and creates permanent volumes**". So image builds **and** PV creation happen here. The PV is cluster-scoped (no namespace prerequisite), so it can be applied before `start-app.sh` runs.
```
1. minikube status (or `minikube start` if not running)
2. eval "$(minikube -p minikube docker-env)"
3. docker build -t taskapp-api:v1 backend/
4. docker build -t taskapp-web:v1 frontend/
5. minikube ssh -- "sudo mkdir -p /mnt/data/taskapp-db && sudo chmod 777 /mnt/data/taskapp-db"
6. kubectl apply -f statefulset.yaml --dry-run=client -o yaml \
| kubectl apply -f - # NO — see decision below
6. kubectl apply -f statefulset.yaml # creates PV (cluster-scoped) — PVC and StatefulSet
# are namespaced and will fail-soft if NS missing,
# but we keep this here for "creating permanent volumes"
# per assignment wording
7. echo "App prepared."
```
**Apply-strategy decision:** `statefulset.yaml` contains the cluster-scoped PV plus the namespaced PVC and StatefulSet. Running `kubectl apply -f statefulset.yaml` before the namespace exists would fail on the namespaced objects. Two clean options:
- **(a)** Split the PV into its own file (e.g., `pv.yaml`) so `prepare-app.sh` applies only the PV. Cleaner, but adds an extra file beyond the assignment's mandatory set.
- **(b)** Apply `namespace.yaml` first inside `prepare-app.sh`, then apply `statefulset.yaml`. The namespace+PV live after prepare; PVC and StatefulSet are also created in prepare; `start-app.sh` then applies only `deployment.yaml` and `service.yaml`.
We pick **(b)**. It satisfies the assignment wording ("creating permanent volumes" — PV+PVC are both created in prepare), and `start-app.sh` still satisfies "create all Kubernetes objects" since `kubectl apply` is idempotent and re-running it on already-existing PV/PVC/StatefulSet is a no-op (resources are reconciled, not duplicated).
Final `prepare-app.sh`:
```
1. minikube status (or `minikube start` if not running)
2. eval "$(minikube -p minikube docker-env)"
3. docker build -t taskapp-api:v1 backend/
4. docker build -t taskapp-web:v1 frontend/
5. minikube ssh -- "sudo mkdir -p /mnt/data/taskapp-db && sudo chmod 777 /mnt/data/taskapp-db"
6. kubectl apply -f namespace.yaml # Namespace + Secret + ConfigMap (prereq for PVC)
7. kubectl apply -f statefulset.yaml # PV + PVC + StatefulSet
8. echo "App prepared."
```
### `start-app.sh`
Per assignment: "commands for kubectl to create all Kubernetes objects". Re-applies everything (idempotent). Resources already created by `prepare-app.sh` are reconciled with no side effect.
```
1. kubectl apply -f namespace.yaml # idempotent
2. kubectl apply -f statefulset.yaml # idempotent
3. kubectl apply -f service.yaml # web + api + db Services
4. kubectl apply -f deployment.yaml # web + api Deployments
5. kubectl -n taskapp rollout status statefulset/db
6. kubectl -n taskapp rollout status deployment/api
7. kubectl -n taskapp rollout status deployment/web
8. echo "App is running."
9. minikube service web -n taskapp # opens browser at http://<minikube-ip>:30080
```
Note: `service.yaml` is applied *before* `deployment.yaml` so the api and db Services exist before pods try to resolve them.
### `stop-app.sh`
Full teardown (matches assignment wording "drop the created Kubernetes objects"):
```
1. kubectl delete -f service.yaml --ignore-not-found
2. kubectl delete -f deployment.yaml --ignore-not-found
3. kubectl delete -f statefulset.yaml --ignore-not-found # also deletes PV+PVC+db Service
4. kubectl delete -f namespace.yaml --ignore-not-found # also deletes Secret+ConfigMap
5. echo "App stopped and removed."
```
The hostPath data on the node remains (`Retain` reclaim policy). It can be wiped manually with `minikube ssh -- sudo rm -rf /mnt/data/taskapp-db` if desired — that command is documented in the README, not in any script.
## 12. Health checks
| Container | Liveness | Readiness | Initial delay |
|-----------|----------|-----------|---------------|
| `db` | `exec: pg_isready -U $POSTGRES_USER` | same | 10 s / 5 s |
| `api` | `httpGet: /api/health :5000` | same | 5 s / 5 s |
| `web` | `httpGet: / :80` | same | 2 s / 1 s |
The `api` `/api/health` endpoint executes `SELECT 1` against the DB and returns 200 only if it succeeds — this means rolling updates wait for real DB reachability, not just Flask startup.
## 13. Resource limits
| Container | requests cpu / mem | limits cpu / mem |
|-----------|--------------------|------------------|
| `db` | 100m / 128Mi | 500m / 512Mi |
| `api` | 50m / 64Mi | 250m / 256Mi |
| `web` | 25m / 32Mi | 100m / 128Mi |
Conservative; fits comfortably in a 4 GiB minikube VM with overhead for system pods.
## 14. Documentation (README.md)
The README covers, in order:
1. What the application does (one-paragraph plus screenshots-optional).
2. Containers used (web/api/db) — short description each.
3. Kubernetes objects (the table from §5, with one-line "what it does" for each).
4. Virtual networks — cluster DNS, the four Services, headless Service rationale.
5. Named volumes — the PV, the PVC, hostPath path, reclaim policy.
6. Container configuration performed (env vars, ConfigMap mount, image build context).
7. Instructions: prepare → start → web access → stop.
8. How to view in browser (`minikube service web -n taskapp`).
9. Sources (assignment 1 + Kubernetes docs).
10. Use of AI (Claude Opus 4.7, Anthropic — disclosed per academic-integrity convention from assignment 1).
## 15. Out of scope (YAGNI)
- Ingress, HPA, NetworkPolicy, PodDisruptionBudget — not required, no demo value.
- TLS — assignment doesn't ask, browser access is over plain HTTP on a NodePort.
- Multiple DB replicas / streaming replication — out of scope for the assignment.
- CI/CD, Helm chart, Kustomize overlays — over-engineering for a single-environment school project.
## 16. Risks & mitigations
| Risk | Mitigation |
|------|-----------|
| Image not visible to minikube | `prepare-app.sh` runs `eval $(minikube docker-env)` before `docker build` so the image lands in minikube's daemon. |
| PV directory missing on node | `prepare-app.sh` creates `/mnt/data/taskapp-db` via `minikube ssh`. |
| `api` starts before `db` is reachable | Readiness probe on `/api/health` includes a DB ping; rolling update waits. App also tolerates and retries on connect failure at boot. |
| User runs `start-app.sh` twice | All operations are idempotent (`kubectl apply`); script is safe to re-run. |
| User stops then starts → data lost? | PV reclaim policy is `Retain`, so the underlying hostPath dir survives. On re-create the same PV is bound by the same PVC selector. |
## 17. Oral evaluation talking points
(For grading; not for the README.)
1. *Why StatefulSet for Postgres, not Deployment?* — stable pod identity (`db-0`), stable storage, ordered startup; Postgres can't tolerate two pods racing on the same data dir.
2. *Why headless Service for `db`?* — gives StatefulSet pods stable DNS; kube-proxy does not load-balance, which is what stateful clients want.
3. *Why a Secret instead of plain env in YAML?* — separate object, base64-encoded, can be replaced with sealed-secrets / vault; the YAML can be committed without the password (production direction).
4. *Why a ConfigMap for `nginx.conf`?* — separates config from image; tweaking the proxy block does not require a rebuild.
5. *Why two replicas on `web` and `api`?* — stateless = horizontally scalable; demonstrates that Deployment ≠ "one pod".
6. *Why `Retain` on the PV?* — deleting the PVC won't wipe the underlying directory; safer default; allows operator review before reuse.
7. *Why static PV (not StorageClass dynamic provisioning)?* — assignment explicitly asks for PV+PVC objects; static is the textbook match. In production we'd use a StorageClass.

View File

@ -1,8 +1,14 @@
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 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;"]

View File

@ -1,20 +0,0 @@
server {
listen 80;
server_name localhost;
large_client_header_buffers 4 32k;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://flask:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

View File

@ -1,120 +1,178 @@
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Outfit', sans-serif;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f0f2f5;
color: #1a1a2e;
background: #f4f5f8;
color: #111;
min-height: 100vh;
display: flex;
justify-content: center;
padding-top: 60px;
align-items: flex-start;
padding: 40px 16px;
}
.container {
width: 100%;
max-width: 520px;
padding: 0 16px;
max-width: 420px;
background: #f4f5f8;
position: relative;
}
h1 {
font-size: 28px;
font-size: 26px;
font-weight: 700;
margin-bottom: 24px;
text-align: center;
margin-bottom: 32px;
color: #111;
display: flex;
align-items: center;
gap: 8px;
}
h1::after {
content: '👋';
font-size: 24px;
}
#task-form {
display: flex;
gap: 8px;
margin-bottom: 24px;
flex-direction: column;
gap: 16px;
margin-bottom: 40px;
}
#task-input {
flex: 1;
padding: 12px 16px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 16px;
width: 100%;
padding: 18px 20px;
border: 1.5px solid #111;
border-radius: 16px;
font-size: 15px;
font-weight: 500;
outline: none;
transition: border-color 0.2s;
background: #fff;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(0,0,0,0.02);
}
#task-input::placeholder {
color: #999;
font-weight: 400;
}
#task-input:focus {
border-color: #4a6cf7;
box-shadow: 0 6px 16px rgba(0,0,0,0.06);
transform: translateY(-1px);
}
#task-form button {
padding: 12px 24px;
background: #4a6cf7;
padding: 18px 24px;
background: #F06A59;
color: #fff;
border: none;
border-radius: 8px;
border-radius: 16px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
transition: all 0.2s ease;
box-shadow: 0 8px 20px rgba(240, 106, 89, 0.25);
}
#task-form button:hover {
background: #3a5ce5;
background: #e55c4b;
transform: translateY(-2px);
box-shadow: 0 12px 24px rgba(240, 106, 89, 0.35);
}
#task-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 16px;
}
.task-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
gap: 16px;
padding: 20px;
background: #fff;
border-radius: 8px;
margin-bottom: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
transition: opacity 0.2s;
border: 1.5px solid #111;
border-radius: 20px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.04);
transition: all 0.2s ease;
}
.task-item:hover {
transform: translateY(-3px);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
}
.task-item.completed {
opacity: 0.6;
background: #fdfdfd;
border-color: #ddd;
box-shadow: none;
}
.task-item.completed:hover {
transform: none;
}
.task-item.completed .task-title {
text-decoration: line-through;
opacity: 0.5;
color: #888;
}
.task-checkbox {
width: 20px;
height: 20px;
width: 22px;
height: 22px;
cursor: pointer;
accent-color: #4a6cf7;
accent-color: #F06A59;
border-radius: 6px;
}
.task-title {
flex: 1;
font-size: 16px;
font-weight: 600;
color: #111;
word-break: break-word;
}
.task-delete {
background: none;
background: #111;
color: #fff;
border: none;
color: #e74c3c;
font-size: 18px;
width: 32px;
height: 32px;
border-radius: 50%;
font-size: 16px;
font-weight: bold;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
opacity: 0.6;
transition: opacity 0.2s;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.task-delete:hover {
opacity: 1;
background: #F06A59;
transform: scale(1.1) rotate(90deg);
}
.empty {
text-align: center;
color: #888;
font-size: 14px;
margin-top: 16px;
font-size: 15px;
font-weight: 500;
margin-top: 32px;
padding: 32px 24px;
border: 1.5px dashed #ccc;
border-radius: 20px;
background: rgba(255, 255, 255, 0.5);
}

68
namespace.yaml Normal file
View File

@ -0,0 +1,68 @@
# 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;
}
}
}

38
nginx.conf Normal file
View File

@ -0,0 +1,38 @@
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;
}
}
}

View File

@ -1,19 +1,47 @@
#!/bin/bash
echo "Preparing app..."
#!/usr/bin/env bash
set -euo pipefail
# Build Docker images
echo "Building backend image..."
docker build -t taskapp-backend ./backend
echo "[prepare-app] Preparing Task Manager for Kubernetes..."
echo "Building frontend image..."
docker build -t taskapp-frontend ./frontend
# 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
# Create network (ignore error if it already exists)
echo "Creating network..."
docker network create taskapp-network 2>/dev/null || true
# 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)"
# Create named volume (ignore error if it already exists)
echo "Creating volume..."
docker volume create taskapp-pgdata 2>/dev/null || true
# 3. Build the two custom images
echo "[prepare-app] Building taskapp-api:v1..."
docker build -t taskapp-api:v1 backend/
echo "App prepared successfully."
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."

View File

@ -1,17 +0,0 @@
#!/bin/bash
echo "Removing app..."
# Stop and remove containers
docker stop taskapp-nginx taskapp-flask taskapp-db 2>/dev/null
docker rm taskapp-nginx taskapp-flask taskapp-db 2>/dev/null
# Remove images
docker rmi taskapp-backend taskapp-frontend 2>/dev/null
# Remove network
docker network rm taskapp-network 2>/dev/null
# Remove volume
docker volume rm taskapp-pgdata 2>/dev/null
echo "App removed."

53
service.yaml Normal file
View File

@ -0,0 +1,53 @@
# 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

View File

@ -1,39 +1,37 @@
#!/bin/bash
echo "Starting app..."
#!/usr/bin/env bash
set -euo pipefail
# Start PostgreSQL
echo "Starting database..."
docker run -d \
--name taskapp-db \
--network taskapp-network \
--restart unless-stopped \
-e POSTGRES_DB=taskapp \
-e POSTGRES_USER=taskapp \
-e POSTGRES_PASSWORD=taskapp123 \
-v taskapp-pgdata:/var/lib/postgresql/data \
postgres:15
echo "[start-app] Starting Task Manager on Kubernetes..."
# Start Flask backend
echo "Starting backend..."
docker run -d \
--name taskapp-flask \
--network taskapp-network \
--restart unless-stopped \
-e DB_HOST=taskapp-db \
-e DB_NAME=taskapp \
-e DB_USER=taskapp \
-e DB_PASSWORD=taskapp123 \
taskapp-backend
# 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.
# Start Nginx frontend
echo "Starting frontend..."
docker run -d \
--name taskapp-nginx \
--network taskapp-network \
--restart unless-stopped \
-p 80:80 \
taskapp-frontend
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 "App is running!"
echo "The app is available at http://localhost:80"
echo "[start-app] App is running!"
echo "[start-app] Opening browser at the web service URL..."
minikube service web -n taskapp

98
statefulset.yaml Normal file
View File

@ -0,0 +1,98 @@
# 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

View File

@ -1,7 +1,18 @@
#!/bin/bash
echo "Stopping app..."
#!/usr/bin/env bash
set -euo pipefail
docker stop taskapp-nginx taskapp-flask taskapp-db 2>/dev/null
docker rm taskapp-nginx taskapp-flask taskapp-db 2>/dev/null
echo "[stop-app] Removing Task Manager from Kubernetes..."
echo "App stopped."
# 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.)"