This commit is contained in:
Bohdan Kapliuk 2026-04-08 12:36:10 +03:00
parent 437a05ee15
commit 2718263509
19 changed files with 740 additions and 1 deletions

View File

@ -1 +0,0 @@
1

100
z1/README.md Normal file
View File

@ -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.

6
z1/prepare-app.sh Normal file
View File

@ -0,0 +1,6 @@
#!/bin/bash
set -e
docker compose build
docker compose create

5
z1/remove-app.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/bash
set -e
docker compose down -v --remove-orphans

9
z1/start-app.sh Normal file
View File

@ -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"

5
z1/stop-app.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/bash
set -e
docker compose stop

112
z2/README.md Normal file
View File

@ -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`

11
z2/backend/Dockerfile Normal file
View File

@ -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"]

91
z2/backend/server.js Normal file
View File

@ -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);
});

60
z2/deployment.yaml Normal file
View File

@ -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

6
z2/frontend/Dockerfile Normal file
View File

@ -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

173
z2/frontend/index.html Normal file
View File

@ -0,0 +1,173 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kubernetes Demo App</title>
<style>
:root {
color-scheme: light;
--bg: #f4efe6;
--panel: #fffaf2;
--text: #1f2933;
--accent: #bf6d2c;
--accent-dark: #8e4b17;
--border: #e6d7c3;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Trebuchet MS", "Segoe UI", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top, rgba(191, 109, 44, 0.18), transparent 35%),
linear-gradient(180deg, #f8f2e9 0%, var(--bg) 100%);
display: grid;
place-items: center;
padding: 24px;
}
.card {
width: min(640px, 100%);
background: var(--panel);
border: 1px solid var(--border);
border-radius: 20px;
padding: 28px;
box-shadow: 0 20px 50px rgba(72, 49, 24, 0.12);
}
h1 {
margin-top: 0;
margin-bottom: 8px;
font-size: clamp(2rem, 5vw, 2.8rem);
}
p {
margin-top: 0;
color: #52606d;
}
.row {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin: 24px 0;
}
input {
flex: 1 1 260px;
border: 1px solid var(--border);
border-radius: 12px;
padding: 14px 16px;
font-size: 1rem;
}
button {
border: 0;
border-radius: 12px;
padding: 14px 18px;
background: var(--accent);
color: white;
font-size: 1rem;
cursor: pointer;
transition: transform 0.15s ease, background 0.15s ease;
}
button:hover {
background: var(--accent-dark);
transform: translateY(-1px);
}
.status {
min-height: 24px;
margin-bottom: 12px;
color: var(--accent-dark);
font-weight: 600;
}
ul {
margin: 0;
padding-left: 20px;
}
li + li {
margin-top: 6px;
}
</style>
</head>
<body>
<main class="card">
<h1>Kubernetes demo</h1>
<p>This frontend sends names to the backend API and loads them back from PostgreSQL running in a StatefulSet.</p>
<div class="row">
<input id="name" placeholder="Enter name">
<button onclick="sendAndLoad()">Save and show</button>
</div>
<div id="status" class="status"></div>
<ul id="list"></ul>
</main>
<script>
async function loadUsers() {
const status = document.getElementById("status");
try {
status.textContent = "Loading users...";
const res = await fetch("/api/users");
const data = await res.json();
const list = document.getElementById("list");
list.innerHTML = "";
data.forEach((user) => {
const li = document.createElement("li");
li.innerText = user.name;
list.appendChild(li);
});
status.textContent = "Users loaded successfully.";
} catch (error) {
console.error(error);
status.textContent = "Cannot connect to backend.";
}
}
async function sendAndLoad() {
const status = document.getElementById("status");
const nameInput = document.getElementById("name");
const name = nameInput.value.trim();
if (!name) {
status.textContent = "Please enter a name first.";
return;
}
try {
status.textContent = "Saving user...";
await fetch("/api/save", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ name })
});
nameInput.value = "";
await loadUsers();
} catch (error) {
console.error(error);
status.textContent = "Saving failed.";
}
}
window.onload = loadUsers;
</script>
</body>
</html>

20
z2/frontend/nginx.conf Normal file
View File

@ -0,0 +1,20 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend-service:3000/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

4
z2/namespace.yaml Normal file
View File

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: zkt26-z2

18
z2/prepare-app.sh Normal file
View File

@ -0,0 +1,18 @@
#!/bin/bash
set -e
echo "Building backend image..."
docker build -t z2-backend:v2 ./backend
echo "Building frontend image..."
docker build -t z2-frontend:latest ./frontend
if command -v minikube >/dev/null 2>&1; then
echo "Loading images into minikube..."
minikube image load z2-backend:v2
minikube image load z2-frontend:latest
else
echo "minikube not found, skipping image load step."
fi
echo "Preparation finished."

37
z2/service.yaml Normal file
View File

@ -0,0 +1,37 @@
apiVersion: v1
kind: Service
metadata:
name: backend-service
namespace: zkt26-z2
spec:
selector:
app: backend
ports:
- port: 3000
targetPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: frontend-service
namespace: zkt26-z2
spec:
type: NodePort
selector:
app: frontend
ports:
- port: 80
targetPort: 80
nodePort: 30080
---
apiVersion: v1
kind: Service
metadata:
name: postgres-service
namespace: zkt26-z2
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432

11
z2/start-app.sh Normal file
View File

@ -0,0 +1,11 @@
#!/bin/bash
set -e
kubectl apply -f namespace.yaml
kubectl apply -f statefulset.yaml
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
echo "Application started."
echo "Frontend NodePort is 30080."
echo "For Minikube on WSL use: minikube service frontend-service -n zkt26-z2 --url"

63
z2/statefulset.yaml Normal file
View File

@ -0,0 +1,63 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgres-pv
spec:
storageClassName: ""
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /tmp/zkt26-postgres-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: zkt26-z2
spec:
storageClassName: ""
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
volumeName: postgres-pv
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres-statefulset
namespace: zkt26-z2
spec:
serviceName: postgres-service
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_USER
value: user
- name: POSTGRES_PASSWORD
value: password
- name: POSTGRES_DB
value: mydb
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc

9
z2/stop-app.sh Normal file
View File

@ -0,0 +1,9 @@
#!/bin/bash
set -e
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 "Application stopped."