diff --git a/Z2/z2/README.md b/Z2/z2/README.md new file mode 100644 index 0000000..40f9f38 --- /dev/null +++ b/Z2/z2/README.md @@ -0,0 +1,123 @@ +# Notes App — Kubernetes Deployment + +A simple web application where you can write, view, and delete notes. Built with Flask (Python) and PostgreSQL. + +--- + +## What the app does + +- Add a note using the text box on the main page +- View all saved notes +- Delete any note with a button click +- Notes are stored in a PostgreSQL database and survive pod restarts + +--- + +## Containers used + +| Container | Image | Description | +|-----------|-------|-------------| +| `notes-web` | `notes-web:latest` (built locally) | Flask web app — serves the UI and handles requests | +| `postgres` | `postgres:15-alpine` | PostgreSQL database — stores notes | + +--- + +## Kubernetes objects + +| Object | Name | Description | +|--------|------|-------------| +| Namespace | `notes-app` | Isolates all app objects from other workloads | +| Deployment | `notes-web` | Runs 2 replicas of the Flask web app | +| StatefulSet | `postgres` | Runs 1 PostgreSQL pod with stable identity | +| PersistentVolume | `postgres-pv` | 1Gi volume backed by host path `/mnt/notes-postgres-data` | +| PersistentVolumeClaim | `postgres-pvc` | Claims the PV for use by the StatefulSet | +| Service (NodePort) | `notes-web-service` | Exposes the web app on port `30080` | +| Service (ClusterIP) | `postgres-service` | Internal access to PostgreSQL for the web app | +| Service (Headless) | `postgres-headless` | Required by the StatefulSet for stable DNS | + +--- + +## Networking and volumes + +**Services:** +- `notes-web-service` — NodePort, exposes port `30080` externally → routes to pod port `5000` +- `postgres-service` — ClusterIP, internal only, port `5432` → the web app connects to this +- `postgres-headless` — Headless service (no ClusterIP) used by the StatefulSet + +**Volume:** +- `postgres-pv` uses a `hostPath` at `/mnt/notes-postgres-data` on the node +- Data persists even if the PostgreSQL pod is restarted or recreated + +--- + +## Container configuration + +**notes-web:** +- Environment variables set the database connection: `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASS` +- `imagePullPolicy: Never` — uses the locally built image (no registry needed) +- Runs on port `5000` + +**postgres:** +- `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` configure the database on first start +- Data directory `/var/lib/postgresql/data` is mounted from the PVC + +--- + +## How to use + +### 1. Prepare (build image, create host directory) + +```bash +chmod +x prepare-app.sh start-app.sh stop-app.sh show-url.sh +./prepare-app.sh +``` + +### 2. Start the application + +```bash +./start-app.sh +``` + +### 3. Open in browser + +```bash +./show-url.sh +``` + +Or open manually: +- **Minikube:** `minikube service notes-web-service -n notes-app` +- **Other:** `http://:30080` + +### 4. Stop (keep objects, scale down) + +```bash +kubectl scale deployment notes-web -n notes-app --replicas=0 +kubectl scale statefulset postgres -n notes-app --replicas=0 +``` + +### 5. Delete everything + +```bash +./stop-app.sh +``` + +--- + +## File overview + +``` +z2/ +├── app/ +│ ├── app.py # Flask application +│ ├── requirements.txt # Python dependencies +│ └── Dockerfile # Image definition for notes-web +├── namespace.yaml # Namespace: notes-app +├── deployment.yaml # Deployment: notes-web (Flask) +├── statefulset.yaml # StatefulSet + PV + PVC (PostgreSQL) +├── service.yaml # Services: notes-web-service, postgres-service +├── prepare-app.sh # Build image, create host directory +├── start-app.sh # Apply all Kubernetes objects +├── stop-app.sh # Delete all Kubernetes objects +├── show-url.sh # Print the URL to open the app +└── README.md # This file +``` diff --git a/Z2/z2/app/Dockerfile b/Z2/z2/app/Dockerfile new file mode 100644 index 0000000..dfc6db1 --- /dev/null +++ b/Z2/z2/app/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py . + +EXPOSE 5000 + +CMD ["python", "app.py"] diff --git a/Z2/z2/app/app.py b/Z2/z2/app/app.py new file mode 100644 index 0000000..7a97015 --- /dev/null +++ b/Z2/z2/app/app.py @@ -0,0 +1,111 @@ +import os +import psycopg2 +from flask import Flask, request, redirect, url_for, render_template_string + +app = Flask(__name__) + +DB_HOST = os.environ.get("DB_HOST", "postgres-service") +DB_PORT = os.environ.get("DB_PORT", "5432") +DB_NAME = os.environ.get("DB_NAME", "notesdb") +DB_USER = os.environ.get("DB_USER", "notesuser") +DB_PASS = os.environ.get("DB_PASS", "notespass") + +HTML = """ + + + + Notes App + + + +

📝 Notes App

+ {% if error %}

{{ error }}

{% endif %} +
+

+ +
+

Notes ({{ notes|length }})

+ {% for note in notes %} +
+
+ +
+

{{ note[1] }}

+ {{ note[2] }} +
+ {% endfor %} + {% if not notes %}

No notes yet.

{% endif %} + + +""" + +def get_conn(): + return psycopg2.connect( + host=DB_HOST, port=DB_PORT, + dbname=DB_NAME, user=DB_USER, password=DB_PASS + ) + +def init_db(): + conn = get_conn() + cur = conn.cursor() + cur.execute(""" + CREATE TABLE IF NOT EXISTS notes ( + id SERIAL PRIMARY KEY, + content TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + conn.commit() + cur.close() + conn.close() + +@app.route("/") +def index(): + try: + init_db() + conn = get_conn() + cur = conn.cursor() + cur.execute("SELECT id, content, created_at FROM notes ORDER BY created_at DESC") + notes = cur.fetchall() + cur.close() + conn.close() + return render_template_string(HTML, notes=notes, error=None) + except Exception as e: + return render_template_string(HTML, notes=[], error=f"DB error: {e}") + +@app.route("/add", methods=["POST"]) +def add(): + content = request.form.get("content", "").strip() + if content: + conn = get_conn() + cur = conn.cursor() + cur.execute("INSERT INTO notes (content) VALUES (%s)", (content,)) + conn.commit() + cur.close() + conn.close() + return redirect(url_for("index")) + +@app.route("/delete/", methods=["POST"]) +def delete(note_id): + conn = get_conn() + cur = conn.cursor() + cur.execute("DELETE FROM notes WHERE id = %s", (note_id,)) + conn.commit() + cur.close() + conn.close() + return redirect(url_for("index")) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) diff --git a/Z2/z2/app/requirements.txt b/Z2/z2/app/requirements.txt new file mode 100644 index 0000000..aa3415e --- /dev/null +++ b/Z2/z2/app/requirements.txt @@ -0,0 +1,2 @@ +flask==3.0.0 +psycopg2-binary==2.9.9 diff --git a/Z2/z2/deployment.yaml b/Z2/z2/deployment.yaml new file mode 100644 index 0000000..5fa254a --- /dev/null +++ b/Z2/z2/deployment.yaml @@ -0,0 +1,34 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notes-web + namespace: notes-app + labels: + app: notes-web +spec: + replicas: 2 + selector: + matchLabels: + app: notes-web + template: + metadata: + labels: + app: notes-web + spec: + containers: + - name: notes-web + image: notes-web:latest + imagePullPolicy: Never + ports: + - containerPort: 5000 + env: + - name: DB_HOST + value: "postgres-service" + - name: DB_PORT + value: "5432" + - name: DB_NAME + value: "notesdb" + - name: DB_USER + value: "notesuser" + - name: DB_PASS + value: "notespass" diff --git a/Z2/z2/namespace.yaml b/Z2/z2/namespace.yaml new file mode 100644 index 0000000..0ba1356 --- /dev/null +++ b/Z2/z2/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: notes-app diff --git a/Z2/z2/port-forward.log b/Z2/z2/port-forward.log new file mode 100644 index 0000000..1f3b0a8 --- /dev/null +++ b/Z2/z2/port-forward.log @@ -0,0 +1,134 @@ +nohup: ignoring input +Unable to listen on port 8080: Listeners failed to create with the following errors: [unable to create listener: Error listen tcp4 1Handling connection for 8080 +Handling connection for 8080 +E0422 08:15:48.485727 143453 portforward.go:424] "Unhandled Error" err="an error occurred forwarding 8080 -> 5000: error forwarding port 5000 to pod 4ee1b087afb63a44c5fc2569518897054db6a76a1f05c60e69cb7fba21218b5f, uid : Error response from daemon: No such container: 4ee1b087afb63a44c5fc2569518897054db6a76a1f05c60e69cb7fba21218b5f" +E0422 08:15:48.485729 143453 portforward.go:424] "Unhandled Error" err="an error occurred forwarding 8080 -> 5000: error forwarding port 5000 to pod 4ee1b087afb63a44c5fc2569518897054db6a76a1f05c60e69cb7fba21218b5f, uid : Error response from daemon: No such container: 4ee1b087afb63a44c5fc2569518897054db6a76a1f05c60e69cb7fba21218b5f" +error: lost connection to pod +s "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): namespaces "notes-app" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +Error from server (NotFound): services "notes-web-service" not found +port-forward crashed, retrying in 2s... +error: unable to forward port because pod is not running. Current status=Pending +port-forward crashed, retrying in 2s... +Unable to listen on port 8080: Listeners failed to create with the following errors: [unable to create listener: Error listen tcp4 127.0.0.1:8080: bind: address already in use unable to create listener: Error listen tcp6 [::1]:8080: bind: address already in use] +error: unable to listen on any of the requested ports: [{8080 5000}] +port-forward crashed, retrying in 2s... +Unable to listen on port 8080: Listeners failed to create with the following errors: [unable to create listener: Error listen tcp4 127.0.0.1:8080: bind: address already in use unable to create listener: Error listen tcp6 [::1]:8080: bind: address already in use] +error: unable to listen on any of the requested ports: [{8080 5000}] +port-forward crashed, retrying in 2s... +Forwarding from 127.0.0.1:8080 -> 5000 +Forwarding from [::1]:8080 -> 5000 +Handling connection for 8080 +Handling connection for 8080 +Handling connection for 8080 +Handling connection for 8080 +Handling connection for 8080 +Handling connection for 8080 +Handling connection for 8080 +E0422 08:16:00.690760 146224 portforward.go:404] "Unhandled Error" err="error copying from local connection to remote stream: writeto tcp6 [::1]:8080->[::1]:39644: read tcp6 [::1]:8080->[::1]:39644: read: connection reset by peer" diff --git a/Z2/z2/prepare-app.sh b/Z2/z2/prepare-app.sh new file mode 100644 index 0000000..8ce21ab --- /dev/null +++ b/Z2/z2/prepare-app.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +echo "==> Building Docker image for notes-web..." +docker build -t notes-web:latest ./app + +echo "==> Loading image into Minikube (if using Minikube)..." +if command -v minikube &>/dev/null; then + minikube image load notes-web:latest + echo " Image loaded into Minikube." +else + echo " Minikube not found, skipping image load." +fi + +echo "==> Creating host directory for PersistentVolume..." +sudo mkdir -p /mnt/notes-postgres-data +sudo chmod 777 /mnt/notes-postgres-data + +echo "" +echo "Preparation complete. Run ./start-app.sh to start the application." diff --git a/Z2/z2/service.yaml b/Z2/z2/service.yaml new file mode 100644 index 0000000..4262362 --- /dev/null +++ b/Z2/z2/service.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Service +metadata: + name: notes-web-service + namespace: notes-app +spec: + type: NodePort + selector: + app: notes-web + ports: + - port: 80 + targetPort: 5000 + nodePort: 30080 + +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres-service + namespace: notes-app +spec: + type: ClusterIP + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 diff --git a/Z2/z2/show-url.sh b/Z2/z2/show-url.sh new file mode 100644 index 0000000..6bae7f7 --- /dev/null +++ b/Z2/z2/show-url.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +if command -v minikube &>/dev/null; then + echo "==> Minikube detected. Opening service URL..." + minikube service notes-web-service -n notes-app --url +else + NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}') + echo "==> App available at: http://${NODE_IP}:30080" +fi diff --git a/Z2/z2/start-app.sh b/Z2/z2/start-app.sh new file mode 100644 index 0000000..904486d --- /dev/null +++ b/Z2/z2/start-app.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -e + +echo "==> Creating Namespace..." +kubectl apply -f namespace.yaml + +echo "==> Creating PersistentVolume, PersistentVolumeClaim and StatefulSet (PostgreSQL)..." +kubectl apply -f statefulset.yaml + +echo "==> Waiting for PostgreSQL to be ready..." +kubectl rollout status statefulset/postgres -n notes-app --timeout=120s + +echo "==> Creating Deployment (Notes web app)..." +kubectl apply -f deployment.yaml + +echo "==> Creating Services..." +kubectl apply -f service.yaml + +echo "==> Waiting for web app to be ready..." +kubectl rollout status deployment/notes-web -n notes-app --timeout=120s + +echo "" +echo "Application is running." +echo "Run ./show-url.sh or see README.md for how to open the app in your browser." diff --git a/Z2/z2/statefulset.yaml b/Z2/z2/statefulset.yaml new file mode 100644 index 0000000..66c3a53 --- /dev/null +++ b/Z2/z2/statefulset.yaml @@ -0,0 +1,79 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: postgres-pv + namespace: notes-app +spec: + capacity: + storage: 1Gi + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: manual + hostPath: + path: /mnt/notes-postgres-data + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-pvc + namespace: notes-app +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + storageClassName: manual + +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgres + namespace: notes-app +spec: + serviceName: postgres-headless + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:15-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_DB + value: "notesdb" + - name: POSTGRES_USER + value: "notesuser" + - name: POSTGRES_PASSWORD + value: "notespass" + volumeMounts: + - name: postgres-storage + mountPath: /var/lib/postgresql/data + volumes: + - name: postgres-storage + persistentVolumeClaim: + claimName: postgres-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres-headless + namespace: notes-app +spec: + clusterIP: None + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 diff --git a/Z2/z2/status.sh b/Z2/z2/status.sh new file mode 100644 index 0000000..713c2b0 --- /dev/null +++ b/Z2/z2/status.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +echo -e "\n\033[1;34m=== CLUSTER STATUS REPORT ===\033[0m" +date +echo "" + +echo "=== NAMESPACE ===" +kubectl get namespace notes-app + +echo "=== DEPLOYMENT ===" +kubectl get deployment -n notes-app + +echo "=== STATEFULSET ===" +kubectl get statefulset -n notes-app + +echo "=== PV ===" +kubectl get pv postgres-pv + +echo "=== PVC ===" +kubectl get pvc -n notes-app + +echo "=== SERVICES ===" +kubectl get svc -n notes-app + +echo "=== PODS ===" +kubectl get pods -n notes-app -o wide diff --git a/Z2/z2/stop-app.sh b/Z2/z2/stop-app.sh new file mode 100644 index 0000000..d9e14ef --- /dev/null +++ b/Z2/z2/stop-app.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +echo "==> Deleting all objects in namespace notes-app..." +kubectl delete -f service.yaml --ignore-not-found +kubectl delete -f deployment.yaml --ignore-not-found +kubectl delete -f statefulset.yaml --ignore-not-found +kubectl delete -f namespace.yaml --ignore-not-found + +echo "" +echo "All Kubernetes objects removed." +echo "Note: The host directory /mnt/notes-postgres-data was NOT deleted." +echo "To also remove it, run: sudo rm -rf /mnt/notes-postgres-data"