250 lines
9.0 KiB
Markdown
250 lines
9.0 KiB
Markdown
# K8s Todo App
|
|
|
|
A full-stack **Todo List** web application deployed on **Kubernetes** using **Minikube**.
|
|
The app stores tasks in a PostgreSQL database and serves a modern web UI via a Python Flask backend.
|
|
|
|
---
|
|
|
|
## What the Application Does
|
|
|
|
Users can:
|
|
- **Add** new tasks via a text input form
|
|
- **Mark tasks as done** by clicking the circle toggle button
|
|
- **Delete** tasks with the ✕ button
|
|
- View a live progress counter (e.g. `2/5 done`)
|
|
|
|
All data is persisted to a PostgreSQL database backed by a Kubernetes `PersistentVolume`.
|
|
|
|
---
|
|
|
|
## Architecture Overview
|
|
|
|
```
|
|
Browser
|
|
│
|
|
▼ NodePort :30080
|
|
┌──────────────────────────────────────┐
|
|
│ Namespace: todo-app │
|
|
│ │
|
|
│ ┌────────────────────┐ │
|
|
│ │ Deployment │ │
|
|
│ │ todo-app (x2 pods)│ ──────────► │─── postgres-service:5432
|
|
│ │ Flask + Gunicorn │ │ │
|
|
│ └────────────────────┘ │ ┌──────────────────┐
|
|
│ │ │ StatefulSet │
|
|
│ │ │ postgres (x1) │
|
|
│ │ │ PostgreSQL 16 │
|
|
│ │ │ │ │
|
|
│ │ │ PVC ──► PV │
|
|
│ │ │ (hostPath) │
|
|
│ │ └──────────────────┘
|
|
└──────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Container Images Used
|
|
|
|
| Image | Version | Purpose |
|
|
|-------|---------|---------|
|
|
| `todo-app` | `latest` (built locally) | Flask web server + Gunicorn WSGI |
|
|
| `postgres` | `16-alpine` | Relational database |
|
|
| `busybox` | `1.36` | Init container — waits for PostgreSQL before app starts |
|
|
|
|
### `todo-app` (custom image)
|
|
- Built from `app/Dockerfile` using Python 3.12-slim
|
|
- Runs a **Flask** application with **Gunicorn** (2 workers)
|
|
- Connects to PostgreSQL via environment variables
|
|
- Exposes port `5000`
|
|
- Runs as a non-root user (`appuser`) for security
|
|
|
|
### `postgres:16-alpine`
|
|
- Official PostgreSQL image (Alpine variant for smaller size)
|
|
- Initialises the `tododb` database using a mounted SQL ConfigMap
|
|
- Data is persisted via a PersistentVolumeClaim
|
|
|
|
---
|
|
|
|
## Kubernetes Objects
|
|
|
|
| Object | Name | Description |
|
|
|--------|------|-------------|
|
|
| `Namespace` | `todo-app` | Isolates all app resources from the rest of the cluster |
|
|
| `Deployment` | `todo-app` | Runs 2 replicas of the Flask web app with rolling updates |
|
|
| `StatefulSet` | `postgres` | Manages the PostgreSQL pod with stable network identity |
|
|
| `PersistentVolume` | `postgres-pv` | 2Gi host-path volume on the minikube node |
|
|
| `PersistentVolumeClaim` | `postgres-pvc` | Binds to `postgres-pv`, used by the StatefulSet |
|
|
| `Service` | `todo-app-service` | NodePort (30080) — exposes the Flask app to the browser |
|
|
| `Service` | `postgres-service` | ClusterIP — internal access to PostgreSQL on port 5432 |
|
|
| `Service` | `postgres-headless` | Headless — required by StatefulSet for stable DNS |
|
|
| `Secret` | `postgres-secret` | Stores DB credentials (username, password, DB name) |
|
|
| `ConfigMap` | `postgres-init-sql` | Holds the SQL init script run by PostgreSQL on first boot |
|
|
|
|
---
|
|
|
|
## Networks and Volumes
|
|
|
|
### Virtual Networks (Services)
|
|
|
|
- **`todo-app-service`** — `NodePort` type. Maps external port `30080` → pod port `5000`. Accessible at `http://<minikube-ip>:30080`.
|
|
- **`postgres-service`** — `ClusterIP` type. Only reachable inside the cluster at `postgres-service.todo-app.svc.cluster.local:5432`.
|
|
- **`postgres-headless`** — `ClusterIP: None`. A DNS-only headless service required by the StatefulSet so each pod gets a stable DNS entry (`postgres-0.postgres-headless.todo-app.svc.cluster.local`).
|
|
|
|
### Persistent Volumes
|
|
|
|
- **`postgres-pv`** — `hostPath` pointing to `/mnt/data/postgres` inside the minikube VM. Survives pod restarts and redeployments.
|
|
- **`postgres-pvc`** — Claims 2Gi from `postgres-pv`. Mounted at `/var/lib/postgresql/data` inside the PostgreSQL container.
|
|
|
|
---
|
|
|
|
## Container Configuration
|
|
|
|
### Flask App (`todo-app`)
|
|
- `DB_HOST` — hostname of the PostgreSQL service (`postgres-service.todo-app.svc.cluster.local`)
|
|
- `DB_NAME` — database name (`tododb`)
|
|
- `DB_USER` / `DB_PASS` — injected from the `postgres-secret` Kubernetes Secret
|
|
- **Liveness probe**: `GET /health` every 10s — restarts the pod if it hangs
|
|
- **Readiness probe**: `GET /health` every 5s — holds traffic until the app is truly ready
|
|
- **Init container**: `busybox` runs `nc -z postgres-service 5432` in a loop until PostgreSQL accepts connections before the main container starts
|
|
|
|
### PostgreSQL (`postgres`)
|
|
- `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` — injected from `postgres-secret`
|
|
- `PGDATA=/var/lib/postgresql/data/pgdata` — prevents permission errors on mounted volumes
|
|
- SQL init script from ConfigMap is placed at `/docker-entrypoint-initdb.d/init.sql` and runs automatically on first start
|
|
- **Liveness probe**: `pg_isready -U postgres`
|
|
- **Readiness probe**: `pg_isready -U postgres`
|
|
|
|
---
|
|
|
|
## Step-by-Step Instructions
|
|
|
|
### Prerequisites — Install Everything on Ubuntu
|
|
|
|
Run the following commands in sequence in your terminal:
|
|
|
|
```bash
|
|
# 1. Update system
|
|
sudo apt-get update && sudo apt-get upgrade -y
|
|
|
|
# 2. Install Docker
|
|
sudo apt-get install -y ca-certificates curl gnupg
|
|
sudo install -m 0755 -d /etc/apt/keyrings
|
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
|
|
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
|
sudo chmod a+r /etc/apt/keyrings/docker.gpg
|
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
|
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
|
|
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
|
sudo apt-get update
|
|
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
|
|
sudo usermod -aG docker $USER
|
|
newgrp docker
|
|
|
|
# 3. Install kubectl
|
|
curl -LO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
|
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
|
|
kubectl version --client
|
|
|
|
# 4. Install minikube
|
|
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
|
|
sudo install minikube-linux-amd64 /usr/local/bin/minikube
|
|
minikube version
|
|
```
|
|
|
|
### Prepare the Application (run once)
|
|
|
|
```bash
|
|
# Navigate to the project directory
|
|
cd k8s-todo-app
|
|
|
|
# Make all scripts executable
|
|
chmod +x prepare-app.sh start-app.sh stop-app.sh
|
|
|
|
# Run the preparation script (starts minikube, builds Docker image, creates host dir)
|
|
bash prepare-app.sh
|
|
```
|
|
|
|
### Start the Application
|
|
|
|
```bash
|
|
bash start-app.sh
|
|
```
|
|
|
|
This script creates all Kubernetes objects in order:
|
|
1. Namespace
|
|
2. ConfigMap
|
|
3. PersistentVolume, PVC, Secret, StatefulSet (PostgreSQL)
|
|
4. Services
|
|
5. Deployment (Flask app)
|
|
|
|
### View the Application in a Browser
|
|
|
|
```bash
|
|
# Option A — get the URL automatically
|
|
minikube service todo-app-service -n todo-app
|
|
|
|
# Option B — open directly
|
|
minikube service todo-app-service -n todo-app --url
|
|
# Copy the URL and open it in your browser
|
|
# e.g. http://192.168.49.2:30080
|
|
```
|
|
|
|
### Useful Commands While Running
|
|
|
|
```bash
|
|
# See all running pods
|
|
kubectl get pods -n todo-app
|
|
|
|
# See all objects
|
|
kubectl get all -n todo-app
|
|
|
|
# Watch pods in real time
|
|
kubectl get pods -n todo-app -w
|
|
|
|
# View Flask app logs
|
|
kubectl logs -l app=todo-app -n todo-app --tail=50
|
|
|
|
# View PostgreSQL logs
|
|
kubectl logs postgres-0 -n todo-app --tail=50
|
|
|
|
# Connect to PostgreSQL shell
|
|
kubectl exec -it postgres-0 -n todo-app -- psql -U postgres -d tododb
|
|
|
|
# Scale up/down the web app
|
|
kubectl scale deployment todo-app -n todo-app --replicas=3
|
|
```
|
|
|
|
### Pause / Delete the Application
|
|
|
|
```bash
|
|
# Delete all Kubernetes objects (data on PV is preserved)
|
|
bash stop-app.sh
|
|
|
|
# To also remove all stored database data
|
|
minikube ssh -- "sudo rm -rf /mnt/data/postgres"
|
|
|
|
# To stop minikube entirely
|
|
minikube stop
|
|
|
|
# To delete the minikube cluster completely
|
|
minikube delete
|
|
```
|
|
|
|
### Re-deploy After Stop
|
|
|
|
```bash
|
|
bash start-app.sh # no need to run prepare-app.sh again unless minikube was deleted
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
| Problem | Fix |
|
|
|---------|-----|
|
|
| `ImagePullBackOff` on todo-app | Re-run `bash prepare-app.sh` to rebuild image inside minikube |
|
|
| Postgres pod in `Pending` | Run `kubectl describe pv postgres-pv` — check hostPath exists |
|
|
| App shows "Cannot connect to database" | Wait 30s, postgres is still initialising; check `kubectl logs postgres-0 -n todo-app` |
|
|
| Can't reach browser URL | Run `minikube service todo-app-service -n todo-app` to get the correct URL |
|
|
| Permission error on PV | Run `minikube ssh -- 'sudo chmod 777 /mnt/data/postgres'` |
|