diff --git a/z2/Dockerfile b/z2/Dockerfile new file mode 100644 index 0000000..1f10b13 --- /dev/null +++ b/z2/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"] \ No newline at end of file diff --git a/z2/README.md b/z2/README.md new file mode 100644 index 0000000..a74cd23 --- /dev/null +++ b/z2/README.md @@ -0,0 +1,137 @@ +# Assignment 2 - Kubernetes + +## Application description +This assignment demonstrates how to deploy a simple application in Kubernetes. + +The application consists of: +- a frontend +- a backend +- a PostgreSQL database + +The frontend displays a simple web page. +The backend provides a simple Flask service. +The database runs in PostgreSQL with persistent storage. + +## Kubernetes objects used + +### Namespace +- `z2app` + +The namespace is used to isolate all resources of the application. + +### Deployments +- `frontend-deployment` +- `backend-deployment` + +The frontend and backend are deployed using `Deployment` because they are stateless components. + +### StatefulSet +- `postgres` + +The PostgreSQL database is deployed using `StatefulSet` because it requires persistent storage. + +### Services +- `frontend-service` +- `backend-service` +- `postgres-service` + +Services are used for communication between components and for exposing the frontend. + +### Persistent storage +- `PersistentVolume` (`postgres-pv`) +- `PersistentVolumeClaim` (`postgres-pvc`) + +These resources are used to store PostgreSQL data persistently. + +## Container images used +- `nginx:alpine` for frontend +- `python:3.11-slim` for backend +- `postgres:15` for database + +## Application architecture +- Frontend runs on port 80 +- Backend runs on port 5000 +- PostgreSQL runs on port 5432 + +The frontend is exposed through `frontend-service`. +The backend is available through `backend-service`. +The database is available through `postgres-service`. + +## Files included +- `deployment.yaml` +- `service.yaml` +- `statefulset.yaml` +- `prepare-app.sh` +- `start-app.sh` +- `stop-app.sh` +- `README.md` + +## How to prepare the application + +Run: + +./prepare-app.sh + +## How to start the application + +Run: + +./start-app.sh + +## How to stop the application + +Run: + +./stop-app.sh + +## How to verify that the application is running + +# Check the pods: + +kubectl get pods -n z2app + +# Check the services: + +kubectl get svc -n z2app + +# Check persistent storage: + +kubectl get pv +kubectl get pvc -n z2app + +## How to access the frontend + +# Use port-forward: + +kubectl port-forward -n z2app service/frontend-service 8080:80 + +# Then open in browser: + +http://localhost:8080 + +## How to access the backend + +# Use port-forward: + +kubectl port-forward -n z2app service/backend-service 5000:5000 + +# Then test: + +curl http://localhost:5000 +curl http://localhost:5000/health + +## Notes + +When the application starts, pods may first appear as ContainerCreating or Pending. +After a few seconds they should become Running. + +## Summary + +This assignment shows how to deploy a multi-component application in Kubernetes using: + +# Namespace +# Deployment +# StatefulSet +# Service +# PersistentVolume +# PersistentVolumeClaim \ No newline at end of file diff --git a/z2/app.py b/z2/app.py new file mode 100644 index 0000000..4831f8b --- /dev/null +++ b/z2/app.py @@ -0,0 +1,93 @@ +from flask import Flask, request, jsonify +from flask_cors import CORS +import os +import psycopg2 +import time + +app = Flask(__name__) +CORS(app) + +DB_HOST = os.getenv("DB_HOST", "db") +DB_NAME = os.getenv("DB_NAME", "notesdb") +DB_USER = os.getenv("DB_USER", "notesuser") +DB_PASSWORD = os.getenv("DB_PASSWORD", "notessecret") +DB_PORT = os.getenv("DB_PORT", "5432") + +def get_connection(): + return psycopg2.connect( + host=DB_HOST, + database=DB_NAME, + user=DB_USER, + password=DB_PASSWORD, + port=DB_PORT + ) + +def init_db(): + for _ in range(20): + try: + conn = get_connection() + cur = conn.cursor() + cur.execute(""" + CREATE TABLE IF NOT EXISTS notes ( + id SERIAL PRIMARY KEY, + content TEXT NOT NULL + ); + """) + conn.commit() + cur.close() + conn.close() + return + except Exception: + time.sleep(2) + raise Exception("Database not ready") + +@app.route("/api/health", methods=["GET"]) +def health(): + return jsonify({"status": "ok"}) + +@app.route("/api/notes", methods=["GET"]) +def get_notes(): + conn = get_connection() + cur = conn.cursor() + cur.execute("SELECT id, content FROM notes ORDER BY id DESC;") + rows = cur.fetchall() + cur.close() + conn.close() + return jsonify([{"id": r[0], "content": r[1]} for r in rows]) + +@app.route("/api/notes", methods=["POST"]) +def add_note(): + data = request.get_json() + content = data.get("content", "").strip() + + if not content: + return jsonify({"error": "Content is required"}), 400 + + conn = get_connection() + cur = conn.cursor() + cur.execute("INSERT INTO notes (content) VALUES (%s) RETURNING id;", (content,)) + note_id = cur.fetchone()[0] + conn.commit() + cur.close() + conn.close() + + return jsonify({"id": note_id, "content": content}), 201 + +@app.route("/api/notes/", methods=["DELETE"]) +def delete_note(note_id): + conn = get_connection() + cur = conn.cursor() + cur.execute("DELETE FROM notes WHERE id = %s RETURNING id;", (note_id,)) + deleted = cur.fetchone() + conn.commit() + cur.close() + conn.close() + + if deleted is None: + return jsonify({"error": "Note not found"}), 404 + + return jsonify({"message": "Note deleted successfully"}) + +if __name__ == "__main__": + init_db() + app.run(host="0.0.0.0", port=5000) \ No newline at end of file diff --git a/z2/deployment.yaml b/z2/deployment.yaml new file mode 100644 index 0000000..2becf82 --- /dev/null +++ b/z2/deployment.yaml @@ -0,0 +1,77 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend-deployment + namespace: z2app +spec: + replicas: 1 + selector: + matchLabels: + app: backend + template: + metadata: + labels: + app: backend + spec: + containers: + - name: backend + image: python:3.11-slim + command: ["/bin/sh", "-c"] + args: + - pip install flask psycopg2-binary && python /app/app.py + ports: + - containerPort: 5000 + env: + - name: DB_HOST + value: postgres-service + - name: DB_PORT + value: "5432" + - name: DB_NAME + value: appdb + - name: DB_USER + value: appuser + - name: DB_PASSWORD + value: apppass + volumeMounts: + - name: backend-code + mountPath: /app/app.py + subPath: app.py + volumes: + - name: backend-code + configMap: + name: backend-code-config +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend-deployment + namespace: z2app +spec: + replicas: 1 + selector: + matchLabels: + app: frontend + template: + metadata: + labels: + app: frontend + spec: + containers: + - name: frontend + image: nginx:alpine + ports: + - containerPort: 80 + volumeMounts: + - name: frontend-html + mountPath: /usr/share/nginx/html/index.html + subPath: index.html + - name: frontend-nginx + mountPath: /etc/nginx/conf.d/default.conf + subPath: default.conf + volumes: + - name: frontend-html + configMap: + name: frontend-html-config + - name: frontend-nginx + configMap: + name: frontend-nginx-config \ No newline at end of file diff --git a/z2/index.html b/z2/index.html new file mode 100644 index 0000000..0d9412a --- /dev/null +++ b/z2/index.html @@ -0,0 +1,250 @@ + + + + + + Notes App + + + +
+

Notes App

+

Simple Docker project with Nginx, Flask and PostgreSQL.

+ +
+ + +
+ +
+ +

Saved notes

+ + + +
+ + + + \ No newline at end of file diff --git a/z2/nginx.conf b/z2/nginx.conf new file mode 100644 index 0000000..65caef6 --- /dev/null +++ b/z2/nginx.conf @@ -0,0 +1,10 @@ +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } +} diff --git a/z2/prepare-app.sh b/z2/prepare-app.sh new file mode 100644 index 0000000..fe683df --- /dev/null +++ b/z2/prepare-app.sh @@ -0,0 +1,3 @@ +#!/bin/bash +set -e +echo "No local build needed for this simplified version." \ No newline at end of file diff --git a/z2/requirements.txt b/z2/requirements.txt new file mode 100644 index 0000000..d5ba281 --- /dev/null +++ b/z2/requirements.txt @@ -0,0 +1,3 @@ +flask +psycopg2-binary +flask-cors diff --git a/z2/service.yaml b/z2/service.yaml new file mode 100644 index 0000000..abff10a --- /dev/null +++ b/z2/service.yaml @@ -0,0 +1,281 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres-service + namespace: z2app +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 +--- +apiVersion: v1 +kind: Service +metadata: + name: backend-service + namespace: z2app +spec: + selector: + app: backend + ports: + - port: 5000 + targetPort: 5000 +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend-service + namespace: z2app +spec: + type: NodePort + selector: + app: frontend + ports: + - port: 80 + targetPort: 80 + nodePort: 30080 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: frontend-html-config + namespace: z2app +data: + index.html: | + + + + + + Task Notes App + + + +
+

Task Notes App

+

Simple notes/tasks app running on Kubernetes.

+
+ + +
+ +
+ + + + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: frontend-nginx-config + namespace: z2app +data: + default.conf: | + server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://backend-service:5000/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + } +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: backend-code-config + namespace: z2app +data: + app.py: | + import os + import psycopg2 + from flask import Flask, request, jsonify + + app = Flask(__name__) + + DB_HOST = os.getenv("DB_HOST", "postgres-service") + DB_PORT = os.getenv("DB_PORT", "5432") + DB_NAME = os.getenv("DB_NAME", "appdb") + DB_USER = os.getenv("DB_USER", "appuser") + DB_PASSWORD = os.getenv("DB_PASSWORD", "apppass") + + def get_conn(): + return psycopg2.connect( + host=DB_HOST, + port=DB_PORT, + dbname=DB_NAME, + user=DB_USER, + password=DB_PASSWORD + ) + + def init_db(): + conn = get_conn() + cur = conn.cursor() + cur.execute(""" + CREATE TABLE IF NOT EXISTS tasks ( + id SERIAL PRIMARY KEY, + text TEXT NOT NULL + ) + """) + conn.commit() + cur.close() + conn.close() + + @app.after_request + def add_cors_headers(response): + response.headers["Access-Control-Allow-Origin"] = "*" + response.headers["Access-Control-Allow-Headers"] = "Content-Type" + response.headers["Access-Control-Allow-Methods"] = "GET,POST,DELETE,OPTIONS" + return response + + @app.route("/") + def home(): + return "Backend is running" + + @app.route("/health") + def health(): + return {"status": "ok"} + + @app.route("/tasks", methods=["GET"]) + def get_tasks(): + conn = get_conn() + cur = conn.cursor() + cur.execute("SELECT id, text FROM tasks ORDER BY id DESC") + rows = cur.fetchall() + cur.close() + conn.close() + return jsonify([{"id": r[0], "text": r[1]} for r in rows]) + + @app.route("/tasks", methods=["POST"]) + def create_task(): + data = request.get_json() + text = data.get("text", "").strip() + + if not text: + return jsonify({"error": "Text is required"}), 400 + + conn = get_conn() + cur = conn.cursor() + cur.execute("INSERT INTO tasks (text) VALUES (%s) RETURNING id", (text,)) + new_id = cur.fetchone()[0] + conn.commit() + cur.close() + conn.close() + + return jsonify({"id": new_id, "text": text}), 201 + + @app.route("/tasks/", methods=["DELETE"]) + def delete_task(task_id): + conn = get_conn() + cur = conn.cursor() + cur.execute("DELETE FROM tasks WHERE id = %s", (task_id,)) + conn.commit() + cur.close() + conn.close() + return jsonify({"deleted": task_id}) + + if __name__ == "__main__": + init_db() + app.run(host="0.0.0.0", port=5000) \ No newline at end of file diff --git a/z2/start-app.sh b/z2/start-app.sh new file mode 100644 index 0000000..f11313a --- /dev/null +++ b/z2/start-app.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +kubectl create namespace z2app || true +kubectl apply -f statefulset.yaml +kubectl apply -f deployment.yaml +kubectl apply -f service.yaml + +echo "Application started." +kubectl get pods -n z2app \ No newline at end of file diff --git a/z2/statefulset.yaml b/z2/statefulset.yaml new file mode 100644 index 0000000..624331b --- /dev/null +++ b/z2/statefulset.yaml @@ -0,0 +1,59 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: postgres-pv +spec: + capacity: + storage: 1Gi + accessModes: + - ReadWriteOnce + hostPath: + path: /tmp/postgres-data +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-pvc + namespace: z2app +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgres + namespace: z2app +spec: + serviceName: postgres-service + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:15 + ports: + - containerPort: 5432 + env: + - name: POSTGRES_DB + value: appdb + - name: POSTGRES_USER + value: appuser + - name: POSTGRES_PASSWORD + value: apppass + volumeMounts: + - name: postgres-storage + mountPath: /var/lib/postgresql/data + volumes: + - name: postgres-storage + persistentVolumeClaim: + claimName: postgres-pvc \ No newline at end of file diff --git a/z2/stop-app.sh b/z2/stop-app.sh new file mode 100644 index 0000000..2c7ecb3 --- /dev/null +++ b/z2/stop-app.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +kubectl delete -f service.yaml --ignore-not-found=true +kubectl delete -f deployment.yaml --ignore-not-found=true +kubectl delete -f statefulset.yaml --ignore-not-found=true +kubectl delete namespace z2app --ignore-not-found=true + +echo "Application stopped." \ No newline at end of file