diff --git a/README.md b/README.md new file mode 100644 index 0000000..4123e8e --- /dev/null +++ b/README.md @@ -0,0 +1,180 @@ +# 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://: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.