Kubernetes demo
+This frontend sends names to the backend API and loads them back from PostgreSQL running in a StatefulSet.
+ +diff --git a/z1/1.txt b/z1/1.txt deleted file mode 100644 index 56a6051..0000000 --- a/z1/1.txt +++ /dev/null @@ -1 +0,0 @@ -1 \ No newline at end of file diff --git a/z1/README.md b/z1/README.md new file mode 100644 index 0000000..c1f26b1 --- /dev/null +++ b/z1/README.md @@ -0,0 +1,100 @@ +# Zadanie 1 + +## Opis aplikacie + +Tato aplikacia je jednoducha webova aplikacia nasadena pomocou Docker Compose. Umoznuje zapisat meno cez webove rozhranie do databazy PostgreSQL a nasledne zobrazit zoznam ulozenych zaznamov. Aplikacia obsahuje frontend, backend, databazu a webove rozhranie Adminer na pracu s databazou. + +## Potrebny software + +- Linux +- Docker +- Docker Compose plugin (`docker compose`) + +## Pouzite kontajnery + +- `nginx:latest` - webovy server pre staticke subory frontendu +- `node:18` - backend aplikacie postaveny zo suboru `backend/Dockerfile` +- `postgres:15` - relacna databaza PostgreSQL +- `adminer` - webove rozhranie na pracu s databazou + +## Siete a zvazky + +Docker Compose vytvori predvolenu virtualnu siet, v ktorej spolu komunikujú sluzby: + +- `web` +- `backend` +- `db` +- `adminer` + +Pouzity pomenovany trvaly zvazok: + +- `db_data` - uklada databazove data PostgreSQL, aby zostali zachovane aj po zastaveni aplikacie + +## Konfiguracia kontajnerov + +- `web` bezi v kontajneri s Nginx a spristupnuje frontend na porte `8080` +- `backend` bezi v Node.js kontajneri a je dostupny na porte `5000` +- `db` bezi ako PostgreSQL databaza s premennymi `POSTGRES_USER`, `POSTGRES_PASSWORD` a `POSTGRES_DB` +- `adminer` je dostupny na porte `8081` +- vsetky sluzby maju nastavene `restart: always` +- backend zavisi od databazy a Adminer zavisi od databazy + +## Navod na pouzitie + +Priecinok projektu: + +```bash +cd z1 +``` + +Spustenie aplikacie: + +```bash +./start-app.sh +``` + +Zastavenie aplikacie: + +```bash +./stop-app.sh +``` + +Alternativne je mozne aplikaciu spustit aj priamo cez Docker Compose: + +```bash +docker compose up -d +``` + +## Pristup cez webovy prehliadac + +- Hlavna aplikacia: `http://localhost:8080` +- Backend API: `http://localhost:5000` +- Adminer: `http://localhost:8081` + +Prihlasovacie udaje do PostgreSQL: + +- system: `PostgreSQL` +- server: `db` +- username: `user` +- password: `password` +- database: `mydb` + +## Priklad prace s aplikaciou + +1. Otvorte `http://localhost:8080` +2. Zadajte meno do formulara +3. Kliknite na tlacidlo `Save & Show` +4. Udaj sa ulozi do databazy a zobrazi v zozname + +## Zdroje + +- Docker dokumentacia +- Docker Compose dokumentacia +- Nginx oficialny image na Docker Hub +- Node.js oficialny image na Docker Hub +- PostgreSQL oficialny image na Docker Hub +- Adminer oficialny image na Docker Hub + +## Pouzitie umelej inteligencie + +Pri priprave dokumentacie a pomocnych skriptov bola pouzita umele inteligencia vo forme AI agenta Codex. diff --git a/z1/prepare-app.sh b/z1/prepare-app.sh new file mode 100644 index 0000000..ba8ee01 --- /dev/null +++ b/z1/prepare-app.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -e + +docker compose build +docker compose create diff --git a/z1/remove-app.sh b/z1/remove-app.sh new file mode 100644 index 0000000..15bf78e --- /dev/null +++ b/z1/remove-app.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e + +docker compose down -v --remove-orphans \ No newline at end of file diff --git a/z1/start-app.sh b/z1/start-app.sh new file mode 100644 index 0000000..30dd13d --- /dev/null +++ b/z1/start-app.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +echo "Running app ..." +docker compose up -d +echo "The app is available at http://localhost:8080" +echo "Adminer is available at http://localhost:8081" +echo "Backend API is available at http://localhost:5000" diff --git a/z1/stop-app.sh b/z1/stop-app.sh new file mode 100644 index 0000000..ce79245 --- /dev/null +++ b/z1/stop-app.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e + +docker compose stop diff --git a/z2/README.md b/z2/README.md new file mode 100644 index 0000000..9770d6a --- /dev/null +++ b/z2/README.md @@ -0,0 +1,112 @@ +# Zadanie 2 - Kubernetes web application + +This project deploys a simple web application into Kubernetes. The user enters a name in the browser, the frontend sends it to the backend API, and the backend stores the value in PostgreSQL. The saved names are then displayed back in the browser. + +## Used containers + +- `z2-frontend:latest` - custom Nginx image serving the static frontend and proxying API requests to the backend service. +- `z2-backend:latest` - custom Node.js and Express API image that stores and reads user data from PostgreSQL. +- `postgres:15-alpine` - database container running inside a StatefulSet with persistent storage. + +## Kubernetes objects + +- `Namespace zkt26-z2` - isolates all application resources into one namespace. +- `Deployment backend-deployment` - runs the backend API container. +- `Deployment frontend-deployment` - runs the frontend Nginx container. +- `Service backend-service` - exposes the backend inside the cluster. +- `Service frontend-service` - exposes the frontend on NodePort `30080`. +- `Service postgres-service` - stable network endpoint for PostgreSQL. +- `PersistentVolume postgres-pv` - host storage for PostgreSQL data. +- `PersistentVolumeClaim postgres-pvc` - binds storage for the database pod. +- `StatefulSet postgres-statefulset` - runs PostgreSQL with persistent data. + +## Networks and volumes + +The application uses the default Kubernetes cluster networking. Pods communicate with each other through Kubernetes services: + +- `frontend-service` for browser access +- `backend-service` for frontend to backend communication +- `postgres-service` for backend to database communication + +Persistent data is stored using: + +- `PersistentVolume postgres-pv` +- `PersistentVolumeClaim postgres-pvc` + +The volume uses `hostPath` at `/tmp/zkt26-postgres-data`, so the database data remains available even after the pod restarts. + +## Container configuration + +The frontend container is based on Nginx and includes a custom `nginx.conf` that proxies `/api/*` requests to the backend service. + +The backend container uses environment variables for the database connection: + +- `DB_HOST=postgres-service` +- `DB_PORT=5432` +- `DB_USER=user` +- `DB_PASSWORD=password` +- `DB_NAME=mydb` + +The PostgreSQL container is configured with: + +- `POSTGRES_USER=user` +- `POSTGRES_PASSWORD=password` +- `POSTGRES_DB=mydb` + +## How to prepare, start, stop and delete the application + +Run the commands from the `z2` directory: + +```bash +chmod +x prepare-app.sh start-app.sh stop-app.sh +./prepare-app.sh +./start-app.sh +``` + +To stop and delete the application: + +```bash +./stop-app.sh +``` + +## How to pause the application + +The application can be paused by scaling deployments and the stateful set to zero: + +```bash +kubectl scale deployment frontend-deployment --replicas=0 -n zkt26-z2 +kubectl scale deployment backend-deployment --replicas=0 -n zkt26-z2 +kubectl scale statefulset postgres-statefulset --replicas=0 -n zkt26-z2 +``` + +To start it again: + +```bash +kubectl scale deployment frontend-deployment --replicas=1 -n zkt26-z2 +kubectl scale deployment backend-deployment --replicas=1 -n zkt26-z2 +kubectl scale statefulset postgres-statefulset --replicas=1 -n zkt26-z2 +``` + +## How to open the web application + +After starting the application in a standard Kubernetes environment, open the browser at: + +- `http://localhost:30080` + +If you are using Minikube in WSL with the Docker driver, get the real browser URL with: + +```bash +minikube service frontend-service -n zkt26-z2 --url +``` + +Keep that terminal open and open the printed URL in the browser. + +If your Kubernetes environment does not expose NodePort on localhost directly, or if you want a fixed local port, use: + +```bash +kubectl port-forward service/frontend-service 8080:80 -n zkt26-z2 +``` + +Then open: + +- `http://localhost:8080` diff --git a/z2/backend/Dockerfile b/z2/backend/Dockerfile new file mode 100644 index 0000000..a0294c6 --- /dev/null +++ b/z2/backend/Dockerfile @@ -0,0 +1,11 @@ +FROM node:18-alpine + +WORKDIR /app + +COPY server.js . + +RUN npm init -y && npm install express pg cors + +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/z2/backend/server.js b/z2/backend/server.js new file mode 100644 index 0000000..a35e3fe --- /dev/null +++ b/z2/backend/server.js @@ -0,0 +1,91 @@ +const express = require("express"); +const cors = require("cors"); +const { Pool } = require("pg"); + +const app = express(); +const port = Number(process.env.PORT || 3000); + +app.use(cors()); +app.use(express.json()); + +const pool = new Pool({ + host: process.env.DB_HOST || "postgres-service", + user: process.env.DB_USER || "user", + password: process.env.DB_PASSWORD || "password", + database: process.env.DB_NAME || "mydb", + port: Number(process.env.DB_PORT || 5432) +}); + +async function prepareDatabase() { + await pool.query(` + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL + ) + `); +} + +async function waitForDatabase(maxAttempts = 20, delayMs = 3000) { + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + try { + await prepareDatabase(); + console.log(`Database is ready after attempt ${attempt}.`); + return; + } catch (error) { + console.error(`Database is not ready yet (attempt ${attempt}/${maxAttempts}):`, error.message); + + if (attempt === maxAttempts) { + throw error; + } + + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } +} + +app.get("/health", async (_req, res) => { + try { + await pool.query("SELECT 1"); + res.json({ status: "ok" }); + } catch (error) { + console.error("Healthcheck failed:", error); + res.status(500).json({ status: "error" }); + } +}); + +app.post("/save", async (req, res) => { + const name = (req.body.name || "").trim(); + + if (!name) { + return res.status(400).send("Name is required"); + } + + try { + await pool.query("INSERT INTO users(name) VALUES($1)", [name]); + return res.send(`Saved to DB: ${name}`); + } catch (error) { + console.error("Insert failed:", error); + return res.status(500).send("Error"); + } +}); + +app.get("/users", async (_req, res) => { + try { + const result = await pool.query("SELECT * FROM users ORDER BY id ASC"); + return res.json(result.rows); + } catch (error) { + console.error("Select failed:", error); + return res.status(500).send("Error"); + } +}); + +waitForDatabase() + .then(() => { + app.listen(port, () => { + console.log(`Backend running on port ${port}`); + }); + }) + .catch((error) => { + console.error("Database initialization failed:", error); + process.exit(1); + }); diff --git a/z2/deployment.yaml b/z2/deployment.yaml new file mode 100644 index 0000000..6342f07 --- /dev/null +++ b/z2/deployment.yaml @@ -0,0 +1,60 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend-deployment + namespace: zkt26-z2 +spec: + replicas: 1 + selector: + matchLabels: + app: backend + template: + metadata: + labels: + app: backend + spec: + containers: + - name: backend + image: z2-backend:v2 + imagePullPolicy: Never + ports: + - containerPort: 3000 + env: + - name: DB_HOST + value: postgres-service + - name: DB_PORT + value: "5432" + - name: DB_USER + value: user + - name: DB_PASSWORD + value: password + - name: DB_NAME + value: mydb + readinessProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 5 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend-deployment + namespace: zkt26-z2 +spec: + replicas: 1 + selector: + matchLabels: + app: frontend + template: + metadata: + labels: + app: frontend + spec: + containers: + - name: frontend + image: z2-frontend:latest + imagePullPolicy: Never + ports: + - containerPort: 80 diff --git a/z2/frontend/Dockerfile b/z2/frontend/Dockerfile new file mode 100644 index 0000000..4a9c5b5 --- /dev/null +++ b/z2/frontend/Dockerfile @@ -0,0 +1,6 @@ +FROM nginx:1.27-alpine + +COPY index.html /usr/share/nginx/html/index.html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 diff --git a/z2/frontend/index.html b/z2/frontend/index.html new file mode 100644 index 0000000..f4f4ddb --- /dev/null +++ b/z2/frontend/index.html @@ -0,0 +1,173 @@ + + +
+ + +This frontend sends names to the backend API and loads them back from PostgreSQL running in a StatefulSet.
+ +