commit 703c229dd241ec29aab5f84042b070950d8b0921 Author: Mihai Cimpoesu Date: Sun Aug 16 21:34:36 2026 +0300 Initial commit: imcu.ro HA k3s cluster manifests - 3-node HA cluster (k3s v1.36.3+k3s1, embedded etcd) on Hetzner - Netbird VPN mesh (wt0) for all cluster traffic - Longhorn distributed storage (2 replicas, /var/lib/longhorn/) - Traefik ingress (3 replicas, LoadBalancer on all node IPs) - cert-manager with Let's Encrypt (auto-renewing TLS) - Gitea (git + container registry) with SQLite on Longhorn - 4 migrated workloads: wolsey, school-games (3 variants), randomly - All HelmCharts and workload manifests as YAML - deploy.sh for one-shot cluster setup diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8a7fa8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Sensitive files - never commit +*.env +.env +secrets/ +*.key +*.pem + +# OS +.DS_Store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..48f285b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,326 @@ +# Agents & Services + +Operational reference for all workloads and system services in the imcu.ro cluster. + +## System Services + +### Netbird VPN + +| Property | Value | +|---|---| +| Purpose | WireGuard mesh connecting all k3s nodes | +| Control plane | 167.233.140.254 (netbird.imcu.ro) | +| Interface | wt0 | +| Client version | v0.77.0 | +| Setup key | `E5496A84-6B50-4B13-943A-102B36D5D190` (reusable) | +| Management URL | https://netbird.imcu.ro:443 | +| Local auth | Embedded Dex IdP, password auth | +| TLS | Let's Encrypt | + +**Per-node commands:** +```bash +netbird status # show connection state +netbird up --management-url https://netbird.imcu.ro:443 --hostname --disable-client-routes --disable-server-routes --disable-dns +netbird down +``` + +**VPN IPs:** +- k3s-fi-01: 100.121.136.204 +- k3s-de-01: 100.121.191.170 +- k3s-de-02: 100.121.49.137 + +### k3s + +| Property | Value | +|---|---| +| Version | v1.36.3+k3s1 | +| Mode | HA (embedded etcd, 3 control-plane nodes) | +| Data dir | /var/lib/rancher/k3s | +| Kubeconfig | /etc/rancher/k3s/k3s.yaml | +| Binaries | k3s, kubectl (symlink), crictl (symlink), ctr (symlink) | +| Service | systemd `k3s.service` | + +**Useful commands:** +```bash +k3s kubectl get nodes +k3s kubectl get pods -A +systemctl status k3s +journalctl -u k3s -f +``` + +### Longhorn + +| Property | Value | +|---|---| +| Version | v1.12.1 | +| Namespace | longhorn-system | +| Storage class | longhorn (default) | +| Default replicas | 2 | +| Disk path | /var/lib/longhorn/ | +| UI | ClusterIP 10.43.255.96:80 (SSH tunnel only) | +| Managed via | k3s HelmChart in kube-system | + +**HelmChart values:** +```yaml +defaultSettings: + defaultDataPath: /var/lib/longhorn/ + defaultReplicaCount: 2 +persistence: + defaultClass: true + defaultClassReplicaCount: 2 +``` + +**Accessing the UI:** +```bash +ssh -L 8080:10.43.255.96:80 root@89.167.46.12 +# Open http://localhost:8080 +``` + +**Adding a disk to a node:** +1. Attach new volume in Hetzner Cloud +2. Mount it: `mount /dev/disk/by-id/scsi- /mnt/longhorn-disk2` +3. Add to `/etc/fstab` for persistence +4. In Longhorn UI: Node > Edit > Add Disk (path `/mnt/longhorn-disk2`) + +### Traefik + +| Property | Value | +|---|---| +| Namespace | kube-system | +| Replicas | 3 (one per node) | +| Service type | LoadBalancer (external IPs: all 3 node public IPs) | +| IngressClass | traefik (default) | +| Managed via | k3s HelmChart in kube-system | + +**Ports:** +- 80/TCP (HTTP, redirects to HTTPS) +- 443/TCP (HTTPS) + +### cert-manager + +| Property | Value | +|---|---| +| Version | v1.18.2 | +| Namespace | cert-manager | +| ClusterIssuer | letsencrypt-prod | +| ACME server | https://acme-v02.api.letsencrypt.org/directory | +| Email | admin@imcu.ro | +| Challenge | HTTP-01 via Traefik ingress | +| Managed via | k3s HelmChart in kube-system | + +**All ingresses must have:** +```yaml +metadata: + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod +``` + +**Check cert status:** +```bash +k3s kubectl get certificate -A +k3s kubectl describe certificate -n +``` + +## Application Workloads + +### Gitea + +| Property | Value | +|---|---| +| Namespace | gitea | +| Image | gitea/gitea:1.27.2-rootless | +| URL | https://gitea.imcu.ro | +| SSH | gitea.imcu.ro:30022 (NodePort) | +| Database | SQLite (on Longhorn PVC) | +| Storage | 10 Gi Longhorn PVC `gitea-data` | +| Admin user | mcimpoesu (mihai@imcu.ro) | +| Registry | Built-in (enabled) | +| Packages | Enabled | + +**Container registry:** +```bash +docker login gitea.imcu.ro +# Username: mcimpoesu +# Password: +docker push gitea.imcu.ro/homeschool/: +``` + +**Image repositories in Gitea:** +- homeschool/games (tags: 2a4f03df, 7d3a57d) +- homeschool/school (tag: e76d244) +- homeschool/school-games (tag: 20260112T132336) +- homeschool/randomly (tag: latest) + +**Create access token:** +```bash +k3s kubectl exec -n gitea deploy/gitea -- gitea admin user generate-access-token \ + --username mcimpoesu --token-name "" --scopes "read:repository,write:package,read:package" +``` + +**Gitea config location:** `/etc/gitea/app.ini` inside the container (mounted from PVC `config` subPath). + +### Wolsey + +| Property | Value | +|---|---| +| Namespace | default | +| Image | gitea.imcu.ro/homeschool/school:e76d244 | +| URL | https://wolsey.imcu.ro | +| Storage | 10 Gi Longhorn PVC `wolsey-content` (mounted at /content, readOnly) | +| Content size | 6.5 GB (markdown-based educational content) | +| Port | 3000 | + +**Environment:** +- PORT=3000 +- HOSTNAME=0.0.0.0 +- NODE_ENV=production +- CONTENT_ROOT=/content + +### School Games (production) + +| Property | Value | +|---|---| +| Namespace | games | +| Image | gitea.imcu.ro/homeschool/school-games:20260112T132336 | +| URL | https://school.imcu.ro | +| Port | 3000 | + +### School Games (develop) + +| Property | Value | +|---|---| +| Namespace | default | +| Image | gitea.imcu.ro/homeschool/games:7d3a57d | +| URL | https://develop.imcu.ro | +| Port | 3000 | + +### School Games (default) + +| Property | Value | +|---|---| +| Namespace | default | +| Image | gitea.imcu.ro/homeschool/games:2a4f03df | +| URL | none (internal service only) | +| Port | 3000 | + +### Randomly + +| Property | Value | +|---|---| +| Namespace | randomly | +| Image | gitea.imcu.ro/homeschool/randomly:latest | +| URL | https://randomly.imcu.ro | +| Port | 3000 | + +**Environment:** +- PORT=3000 +- HOSTNAME=0.0.0.0 +- NODE_ENV=production + +## Image Pull Secrets + +All namespaces pulling from Gitea registry use secret `gitea-registry`: + +```bash +k3s kubectl create secret docker-registry gitea-registry \ + --namespace= \ + --docker-server=gitea.imcu.ro \ + --docker-username=mcimpoesu \ + --docker-password= \ + --docker-email=mihai@imcu.ro +``` + +Present in namespaces: `default`, `games`, `randomly`. + +## School (decommissioned) + +| Property | Value | +|---|---| +| IP | 157.90.228.70 | +| Hostname | school | +| k3s | v1.36.3+k3s1 (still running, system pods only) | +| Status | All workloads migrated, data preserved as backup | + +**Backup data on school:** +- `/opt/gitea/` — 255 MB (Gitea data) +- `/opt/registry-data/` — 411 MB (old standalone registry) +- `/mnt/HC_Volume_106055137/school-content/` — 6.5 GB (Wolsey content) + +School is reachable via SSH from all k3s nodes (public IP) but is not on the VPN. + +## Cloudflare DNS + +| Property | Value | +|---|---| +| Zone | imcu.ro | +| Zone ID | 0fca28c6b103c6a730f5c34de3543503 | +| Nameservers | neil.ns.cloudflare.com, may.ns.cloudflare.com | +| API token | Stored in environment as CF_API_TOKEN (Zone:DNS:Edit permission) | + +**Manage DNS via API:** +```bash +export CF_API_TOKEN="" +# List records +curl -s -X GET "https://api.cloudflare.com/client/v4/zones/0fca28c6b103c6a730f5c34de3543503/dns_records?per_page=100" \ + -H "Authorization: Bearer $CF_API_TOKEN" | python3 -m json.tool +``` + +## Common Operations + +### Deploy a new service + +1. Create namespace if needed: `k3s kubectl create ns ` +2. Create image pull secret if pulling from Gitea registry +3. Apply deployment + service manifest +4. Apply ingress with cert-manager annotation: + ```yaml + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + spec: + ingressClassName: traefik + tls: + - hosts: [] + secretName: -tls + ``` +5. Add DNS A records (3x, one per node IP) via Cloudflare API + +### Add a new node to the cluster + +1. Provision Hetzner server with Ubuntu +2. Install Netbird: `curl -fsSL https://pkgs.netbird.io/install.sh | sh` +3. Enroll: `netbird login --management-url https://netbird.imcu.ro:443 --setup-key E5496A84-6B50-4B13-943A-102B36D5D190 --hostname ` +4. Bring up tunnel: `netbird up --management-url https://netbird.imcu.ro:443 --hostname --disable-client-routes --disable-server-routes --disable-dns` +5. Mount Hetzner volume at `/var/lib/longhorn`, update fstab +6. Install k3s: + ```bash + curl -sfL https://get.k3s.io | sh -s - server \ + --server https://100.121.136.204:6443 \ + --token \ + --node-ip \ + --node-external-ip \ + --flannel-iface wt0 \ + --disable traefik + ``` +7. Distribute SSH keys + +### Back up Gitea + +```bash +# Dump Gitea data +k3s kubectl exec -n gitea deploy/gitea -- gitea dump -c /etc/gitea/app.ini +# Copy dump out +k3s kubectl cp gitea/:/data/gitea-dump-*.zip ./gitea-backup.zip +``` + +### Update a deployment image + +```bash +# Push new image to Gitea registry +docker push gitea.imcu.ro/homeschool/: + +# Update deployment +k3s kubectl set image deploy/ =gitea.imcu.ro/homeschool/: -n +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..8324e9b --- /dev/null +++ b/README.md @@ -0,0 +1,248 @@ +# imcu.ro HA Kubernetes Cluster + +3-node highly available Kubernetes cluster running on Hetzner Cloud, connected via Netbird VPN mesh, with distributed storage via Longhorn. + +## Cluster Nodes + +| Node | Public IP | VPN IP (wt0) | Location | Volume | +|---|---|---|---|---| +| k3s-fi-01 | 89.167.46.12 | 100.121.136.204 | Helsinki, FI | scsi-0HC_Volume_106631386 | +| k3s-de-01 | 91.99.15.157 | 100.121.191.170 | Falkenstein, DE | scsi-0HC_Volume_106631392 | +| k3s-de-02 | 2.28.55.101 | 100.121.49.137 | Falkenstein, DE | scsi-0HC_Volume_106631403 | + +All nodes: Ubuntu 26.04 LTS, 2 vCPU, 3.7 Gi RAM, 100 GB Hetzner volume (mounted at `/var/lib/longhorn`). + +## Network Topology + +``` + Internet + | + Cloudflare DNS + (imcu.ro zone) + | + +---------------+---------------+ + | | | + 89.167.46.12 91.99.15.157 2.28.55.101 + k3s-fi-01 k3s-de-01 k3s-de-02 + | | | + +------- wt0 (Netbird VPN) -----+ + 100.121.x.x mesh + | + k3s control plane + (embedded etcd, HA) +``` + +- **All k3s traffic** (API server, etcd, flannel, node communication) runs over the Netbird VPN (`wt0` interface). +- **Public IPs** are only used for: ingress traffic (Traefik LoadBalancer), Gitea SSH (NodePort 30022), and SSH admin access. +- **Netbird control plane** runs on `167.233.140.254` at `netbird.imcu.ro`. + +## Core Components + +| Component | Version | Purpose | +|---|---|---| +| k3s | v1.36.3+k3s1 | Kubernetes distribution with embedded etcd (HA) | +| Longhorn | v1.12.1 | Distributed block storage (2 replicas, `/var/lib/longhorn/`) | +| Traefik | 3.x | Ingress controller (3 replicas, LoadBalancer on all node IPs) | +| cert-manager | v1.18.2 | TLS certificate management (Let's Encrypt, auto-renewing) | + +All three are installed as k3s-native `HelmChart` resources in `kube-system` namespace. + +## Storage + +- **Longhorn** is the default StorageClass (`longhorn`). +- Each node contributes its 100 GB Hetzner volume as a Longhorn disk at `/var/lib/longhorn/`. +- Volumes are replicated across **2 of 3 nodes** (configurable per-PVC). +- To add more disks: mount a new volume at `/mnt/longhorn-disk2`, add it via Longhorn UI. + +### Persistent Volumes + +| PVC | Namespace | Size | Content | +|---|---|---|---| +| gitea-data | gitea | 10 Gi | Gitea SQLite DB, git repos, packages | +| wolsey-content | default | 10 Gi | Wolsey static content (6.5 GB) | + +## Workloads + +| Service | URL | Namespace | Image | Storage | +|---|---|---|---|---| +| Gitea | https://gitea.imcu.ro | gitea | gitea/gitea:1.27.2-rootless | 10 GB Longhorn | +| Gitea SSH | gitea.imcu.ro:30022 | gitea | (same pod) | NodePort 30022 | +| Gitea Registry | gitea.imcu.ro/v2 | gitea | (built-in) | — | +| Wolsey | https://wolsey.imcu.ro | default | homeschool/school:e76d244 | 10 GB Longhorn | +| School Games (prod) | https://school.imcu.ro | games | homeschool/school-games:20260112T132336 | — | +| School Games (dev) | https://develop.imcu.ro | default | homeschool/games:7d3a57d | — | +| School Games (default) | — | default | homeschool/games:2a4f03df | — | +| Randomly | https://randomly.imcu.ro | randomly | homeschool/randomly:latest | — | + +## DNS (Cloudflare) + +All A records point to all 3 node public IPs (round-robin): + +| Record | Type | Value | +|---|---|---| +| gitea.imcu.ro | A x3 | 89.167.46.12, 91.99.15.157, 2.28.55.101 | +| school.imcu.ro | A x3 | 89.167.46.12, 91.99.15.157, 2.28.55.101 | +| wolsey.imcu.ro | A x3 | 89.167.46.12, 91.99.15.157, 2.28.55.101 | +| develop.imcu.ro | A x3 | 89.167.46.12, 91.99.15.157, 2.28.55.101 | +| randomly.imcu.ro | A x3 | 89.167.46.12, 91.99.15.157, 2.28.55.101 | +| *.imcu.ro | A x3 | 89.167.46.12, 91.99.15.157, 2.28.55.101 | +| netbird.imcu.ro | A | 167.233.140.254 | + +## TLS Certificates + +All managed by cert-manager via `letsencrypt-prod` ClusterIssuer (HTTP-01 challenge via Traefik). Auto-renewed 30 days before expiry. + +| Domain | Secret | Namespace | +|---|---|---| +| gitea.imcu.ro | gitea-tls | gitea | +| wolsey.imcu.ro | wolsey-imcu-ro-tls | default | +| school.imcu.ro | school-imcu-ro-tls | games | +| develop.imcu.ro | develop-imcu-ro-tls | default | +| randomly.imcu.ro | randomly-imcu-ro-tls | randomly | + +## Admin Access + +### SSH + +All k3s nodes have SSH keys distributed. From any node you can SSH to any other using VPN IPs: + +``` +ssh k3s-fi-01 # 100.121.136.204 +ssh k3s-de-01 # 100.121.191.170 +ssh k3s-de-02 # 100.121.49.137 +ssh school # 157.90.228.70 (old server, public IP) +``` + +### kubectl + +```bash +ssh root@89.167.46.12 +export KUBECONFIG=/etc/rancher/k3s/k3s.yaml +k3s kubectl get nodes +``` + +### Longhorn UI + +SSH tunnel (UI is not publicly exposed): + +```bash +ssh -L 8080:10.43.255.96:80 root@89.167.46.12 +# Open http://localhost:8080 +``` + +### Gitea + +- Web: https://gitea.imcu.ro +- SSH: `git clone ssh://git@gitea.imcu.ro:30022/homeschool/repo.git` +- Registry: `docker login gitea.imcu.ro` (use Gitea access token as password) + +## Old Server (school) + +`157.90.228.70` (hostname `school`) is the previous single-node k3s cluster. All workloads have been migrated to the HA cluster. It still runs only system pods (kube-system, cert-manager) and preserves original data as backup: + +| Path | Size | Content | +|---|---|---| +| /opt/gitea/ | 255 MB | Gitea data backup | +| /opt/registry-data/ | 411 MB | Old standalone registry | +| /mnt/HC_Volume_106055137/school-content/ | 6.5 GB | Wolsey content backup | + +Can be decommissioned or repurposed once the migration is confirmed stable. + +## Bootstrap (reference) + +The cluster was built in this order: + +1. Netbird client installed on all 3 nodes, enrolled with setup key, tunnels up on `wt0` +2. Hetzner volumes mounted at `/var/lib/longhorn` on all 3 nodes (fstab updated) +3. k3s installed with `--cluster-init` on k3s-fi-01, then joined k3s-de-01 and k3s-de-02 +4. Longhorn installed via k3s HelmChart (2 replicas, default data path `/var/lib/longhorn/`) +5. Traefik installed via k3s HelmChart (3 replicas, LoadBalancer) +6. cert-manager installed via k3s HelmChart, ClusterIssuer created +7. Gitea deployed, data migrated from school via tar over SSH +8. All other workloads migrated from school, images pushed to Gitea registry + +### k3s install flags + +**Node 1 (cluster-init):** +```bash +curl -sfL https://get.k3s.io | sh -s - server \ + --cluster-init \ + --node-ip 100.121.136.204 \ + --node-external-ip 89.167.46.12 \ + --flannel-iface wt0 \ + --advertise-address 100.121.136.204 \ + --tls-san netbird.imcu.ro \ + --tls-san 100.121.136.204 \ + --tls-san 100.121.191.170 \ + --tls-san 100.121.49.137 \ + --disable traefik +``` + +**Nodes 2 & 3 (join):** +```bash +curl -sfL https://get.k3s.io | sh -s - server \ + --server https://100.121.136.204:6443 \ + --token \ + --node-ip \ + --node-external-ip \ + --flannel-iface wt0 \ + --disable traefik +``` + +## SSH Key Mesh + +All k3s nodes have ed25519 SSH keys distributed to each other and to school. Config at `/root/.ssh/config` on each node uses VPN IPs for k3s nodes and public IP for school. + +``` +Host k3s-fi-01 HostName 100.121.136.204 +Host k3s-de-01 HostName 100.121.191.170 +Host k3s-de-02 HostName 100.121.49.137 +Host school HostName 157.90.228.70 +``` + +## Repository Structure (GitOps) + +This repo lives at `gitea.imcu.ro/cluster/k3s-cluster`. It contains all manifests needed to recreate the cluster from scratch. + +``` +k3s-cluster/ +├── README.md # This file (architecture overview) +├── AGENTS.md # Operational reference (per-service details) +├── deploy.sh # Apply all manifests to the cluster +├── .gitignore +├── helmcharts/ # k3s HelmChart resources (system components) +│ ├── traefik.yaml # Ingress controller (3 replicas, LoadBalancer) +│ ├── cert-manager.yaml # TLS certificate manager (Let's Encrypt) +│ └── longhorn.yaml # Distributed storage (2 replicas, /var/lib/longhorn/) +├── cluster-config/ # Cluster-wide configuration +│ ├── namespaces.yaml # Application namespaces (gitea, games, randomly) +│ ├── clusterissuer.yaml # Let's Encrypt ClusterIssuer +│ └── image-pull-secret-template.txt # How to create gitea-registry secrets +└── workloads/ # Application manifests (deploy + svc + ingress + pvc) + ├── gitea/gitea.yaml + ├── wolsey/wolsey.yaml + ├── school-games-prod/school-games-prod.yaml + ├── school-games-dev/school-games-dev.yaml + ├── school-games-default/school-games-default.yaml + └── randomly/randomly.yaml +``` + +### How to update a service + +1. Edit the manifest in `workloads//.yaml` +2. Apply: `k3s kubectl apply -f workloads//.yaml` +3. Commit and push to keep the repo in sync + +### How to update a HelmChart (system component) + +1. Edit the manifest in `helmcharts/.yaml` (e.g. bump `version:` for cert-manager) +2. Apply: `k3s kubectl apply -f helmcharts/.yaml` +3. k3s HelmChart controller will automatically reconcile the change +4. Commit and push + +### How to deploy from scratch + +```bash +# From any k3s node: +KUBECONFIG=/etc/rancher/k3s/k3s.yaml ./deploy.sh +``` diff --git a/cluster-config/clusterissuer.yaml b/cluster-config/clusterissuer.yaml new file mode 100644 index 0000000..bf4a6f7 --- /dev/null +++ b/cluster-config/clusterissuer.yaml @@ -0,0 +1,14 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + email: admin@imcu.ro + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: letsencrypt-prod + solvers: + - http01: + ingress: + class: traefik diff --git a/cluster-config/image-pull-secret-template.txt b/cluster-config/image-pull-secret-template.txt new file mode 100644 index 0000000..552ab88 --- /dev/null +++ b/cluster-config/image-pull-secret-template.txt @@ -0,0 +1,17 @@ +# Image Pull Secret Template +# +# Create this secret in each namespace that pulls from the Gitea registry. +# Replace with a valid Gitea access token (scopes: read:repository, read:package, write:package). +# +# kubectl create secret docker-registry gitea-registry \ +# --namespace= \ +# --docker-server=gitea.imcu.ro \ +# --docker-username=mcimpoesu \ +# --docker-password= \ +# --docker-email=mihai@imcu.ro +# +# Present in namespaces: default, games, randomly, gitea (if needed) +# +# To generate a new Gitea token: +# kubectl exec -n gitea deploy/gitea -- gitea admin user generate-access-token \ +# --username mcimpoesu --token-name "" --scopes "read:repository,write:package,read:package" diff --git a/cluster-config/namespaces.yaml b/cluster-config/namespaces.yaml new file mode 100644 index 0000000..2bc1137 --- /dev/null +++ b/cluster-config/namespaces.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: gitea +--- +apiVersion: v1 +kind: Namespace +metadata: + name: games +--- +apiVersion: v1 +kind: Namespace +metadata: + name: randomly diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..60c391c --- /dev/null +++ b/deploy.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Deploy script - applies all manifests to the cluster +# Usage: SSH to k3s-fi-01 (or any node), then run: +# KUBECONFIG=/etc/rancher/k3s/k3s.yaml ./deploy.sh +# +# Or from local machine with kubectl configured: +# ./deploy.sh + +set -e + +KUBECTL="${KUBECTL:-k3s kubectl}" +if ! command -v k3s &>/dev/null; then + KUBECTL="kubectl" +fi + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "=== Applying namespaces ===" +$KUBECTL apply -f "$DIR/cluster-config/namespaces.yaml" + +echo "=== Applying cluster config ===" +$KUBECTL apply -f "$DIR/cluster-config/clusterissuer.yaml" + +echo "=== Applying HelmCharts ===" +$KUBECTL apply -f "$DIR/helmcharts/" + +echo "=== Applying workloads ===" +for f in "$DIR"/workloads/*/*.yaml; do + echo " -> $(basename "$f")" + $KUBECTL apply -f "$f" +done + +echo "" +echo "=== Done. Check status with: ===" +echo " $KUBECTL get pods -A" +echo " $KUBECTL get ingress -A" +echo " $KUBECTL get certificate -A" diff --git a/helmcharts/cert-manager.yaml b/helmcharts/cert-manager.yaml new file mode 100644 index 0000000..4828329 --- /dev/null +++ b/helmcharts/cert-manager.yaml @@ -0,0 +1,14 @@ +apiVersion: helm.cattle.io/v1 +kind: HelmChart +metadata: + name: cert-manager + namespace: kube-system +spec: + repo: https://charts.jetstack.io + chart: cert-manager + version: v1.18.2 + targetNamespace: cert-manager + createNamespace: true + valuesContent: |- + crds: + enabled: true diff --git a/helmcharts/longhorn.yaml b/helmcharts/longhorn.yaml new file mode 100644 index 0000000..92e42bd --- /dev/null +++ b/helmcharts/longhorn.yaml @@ -0,0 +1,17 @@ +apiVersion: helm.cattle.io/v1 +kind: HelmChart +metadata: + name: longhorn + namespace: kube-system +spec: + repo: https://charts.longhorn.io + chart: longhorn + targetNamespace: longhorn-system + createNamespace: true + valuesContent: |- + defaultSettings: + defaultDataPath: /var/lib/longhorn/ + defaultReplicaCount: 2 + persistence: + defaultClass: true + defaultClassReplicaCount: 2 diff --git a/helmcharts/traefik.yaml b/helmcharts/traefik.yaml new file mode 100644 index 0000000..8d9f1bf --- /dev/null +++ b/helmcharts/traefik.yaml @@ -0,0 +1,17 @@ +apiVersion: helm.cattle.io/v1 +kind: HelmChart +metadata: + name: traefik + namespace: kube-system +spec: + repo: https://traefik.github.io/charts + chart: traefik + targetNamespace: kube-system + valuesContent: |- + deployment: + replicas: 3 + service: + type: LoadBalancer + ingressClass: + enabled: true + isDefaultClass: true diff --git a/workloads/gitea/gitea.yaml b/workloads/gitea/gitea.yaml new file mode 100644 index 0000000..b97c4fa --- /dev/null +++ b/workloads/gitea/gitea.yaml @@ -0,0 +1,203 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: gitea + name: gitea + namespace: gitea +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: gitea + strategy: + type: Recreate + template: + metadata: + labels: + app: gitea + spec: + containers: + - env: + - name: GITEA__server__DOMAIN + value: gitea.imcu.ro + - name: GITEA__server__SSH_DOMAIN + value: gitea.imcu.ro + - name: GITEA__server__ROOT_URL + value: https://gitea.imcu.ro/ + - name: GITEA__server__SSH_PORT + value: '30022' + - name: GITEA__server__SSH_LISTEN_PORT + value: '2222' + - name: GITEA__database__DB_TYPE + value: sqlite3 + - name: GITEA__packages__ENABLED + value: 'true' + - name: GITEA__actions__ENABLED + value: 'true' + - name: GITEA__security__INSTALL_LOCK + value: 'true' + - name: GITEA__service__DISABLE_REGISTRATION + value: 'true' + - name: GITEA__service__REQUIRE_SIGNIN_VIEW + value: 'false' + - name: GITEA__service__DEFAULT_ALLOW_CREATE_ORGANIZATION + value: 'true' + - name: GITEA__service__SHOW_REGISTRATION_BUTTON + value: 'false' + - name: GITEA__service__ENABLE_CAPTCHA + value: 'true' + - name: GITEA__service__EMAIL_DOMAIN_ALLOWLIST + value: imcu.ro,danubian.io,gmail.com + - name: GITEA__admin__DEFAULT_EMAIL_NOTIFICATIONS + value: disabled + - name: GITEA__oauth2__ENABLED + value: 'true' + image: gitea/gitea:1.27.2-rootless + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: /api/healthz + port: 3000 + scheme: HTTP + initialDelaySeconds: 60 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + name: gitea + ports: + - containerPort: 3000 + name: http + protocol: TCP + - containerPort: 2222 + name: ssh + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /api/healthz + port: 3000 + scheme: HTTP + initialDelaySeconds: 30 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: '1' + memory: 1Gi + requests: + cpu: 100m + memory: 256Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/lib/gitea + name: gitea-data + - mountPath: /etc/gitea + name: gitea-data + subPath: config + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - name: gitea-data + persistentVolumeClaim: + claimName: gitea-data +--- +apiVersion: v1 +kind: Service +metadata: + name: gitea + namespace: gitea +spec: + clusterIP: 10.43.169.153 + clusterIPs: + - 10.43.169.153 + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http + port: 3000 + protocol: TCP + targetPort: 3000 + - name: ssh + port: 2222 + protocol: TCP + targetPort: 2222 + selector: + app: gitea + sessionAffinity: None + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + name: gitea-ssh + namespace: gitea +spec: + clusterIP: 10.43.220.233 + clusterIPs: + - 10.43.220.233 + externalTrafficPolicy: Cluster + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: ssh + nodePort: 30022 + port: 2222 + protocol: TCP + targetPort: 2222 + selector: + app: gitea + sessionAffinity: None + type: NodePort +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + finalizers: + - kubernetes.io/pvc-protection + name: gitea-data + namespace: gitea +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: longhorn + volumeMode: Filesystem + volumeName: pvc-6949d930-c76f-4bd8-9bd1-13fbad019d37 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gitea + namespace: gitea +spec: + ingressClassName: traefik + rules: + - host: gitea.imcu.ro + http: + paths: + - backend: + service: + name: gitea + port: + number: 3000 + path: / + pathType: Prefix + tls: + - hosts: + - gitea.imcu.ro + secretName: gitea-tls diff --git a/workloads/randomly/randomly.yaml b/workloads/randomly/randomly.yaml new file mode 100644 index 0000000..a72e3ff --- /dev/null +++ b/workloads/randomly/randomly.yaml @@ -0,0 +1,119 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: randomly + name: randomly + namespace: randomly +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: randomly + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + app: randomly + spec: + containers: + - env: + - name: PORT + value: '3000' + - name: HOSTNAME + value: 0.0.0.0 + - name: NODE_ENV + value: production + image: gitea.imcu.ro/homeschool/randomly:latest + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 3000 + scheme: HTTP + initialDelaySeconds: 15 + periodSeconds: 20 + successThreshold: 1 + timeoutSeconds: 1 + name: randomly + ports: + - containerPort: 3000 + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 3000 + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 50m + memory: 128Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + imagePullSecrets: + - name: gitea-registry + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: randomly-service + namespace: randomly +spec: + clusterIP: 10.43.57.214 + clusterIPs: + - 10.43.57.214 + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - port: 80 + protocol: TCP + targetPort: 3000 + selector: + app: randomly + sessionAffinity: None + type: ClusterIP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: randomly-ingress + namespace: randomly +spec: + ingressClassName: traefik + rules: + - host: randomly.imcu.ro + http: + paths: + - backend: + service: + name: randomly-service + port: + number: 80 + path: / + pathType: Prefix + tls: + - hosts: + - randomly.imcu.ro + secretName: randomly-imcu-ro-tls diff --git a/workloads/school-games-default/school-games-default.yaml b/workloads/school-games-default/school-games-default.yaml new file mode 100644 index 0000000..5bd146f --- /dev/null +++ b/workloads/school-games-default/school-games-default.yaml @@ -0,0 +1,66 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: school-games + name: school-games + namespace: default +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: school-games + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + app: school-games + spec: + containers: + - env: + - name: PORT + value: '3000' + image: gitea.imcu.ro/homeschool/games:2a4f03df7e2e43af116a033e4cca55bbb3b30d89 + imagePullPolicy: IfNotPresent + name: school-games + ports: + - containerPort: 3000 + protocol: TCP + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + imagePullSecrets: + - name: gitea-registry + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: school-games-service + namespace: default +spec: + clusterIP: 10.43.59.103 + clusterIPs: + - 10.43.59.103 + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - port: 80 + protocol: TCP + targetPort: 3000 + selector: + app: school-games + sessionAffinity: None + type: ClusterIP diff --git a/workloads/school-games-dev/school-games-dev.yaml b/workloads/school-games-dev/school-games-dev.yaml new file mode 100644 index 0000000..70b4a35 --- /dev/null +++ b/workloads/school-games-dev/school-games-dev.yaml @@ -0,0 +1,96 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: school-games-develop + name: school-games-develop + namespace: default +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: school-games-develop + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + app: school-games-develop + spec: + containers: + - env: + - name: PORT + value: '3000' + image: gitea.imcu.ro/homeschool/games:7d3a57d7fbc4939763b05dc37e6e146ab6e14a26 + imagePullPolicy: IfNotPresent + name: school-games + ports: + - containerPort: 3000 + protocol: TCP + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + imagePullSecrets: + - name: gitea-registry + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: school-games-develop-service + namespace: default +spec: + clusterIP: 10.43.92.195 + clusterIPs: + - 10.43.92.195 + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - port: 80 + protocol: TCP + targetPort: 3000 + selector: + app: school-games-develop + sessionAffinity: None + type: ClusterIP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: school-games-develop-ingress + namespace: default +spec: + ingressClassName: traefik + rules: + - host: develop.imcu.ro + http: + paths: + - backend: + service: + name: school-games-develop-service + port: + number: 80 + path: /games + pathType: Prefix + - backend: + service: + name: school-games-develop-service + port: + number: 80 + path: / + pathType: Prefix + tls: + - hosts: + - develop.imcu.ro + secretName: develop-imcu-ro-tls diff --git a/workloads/school-games-prod/school-games-prod.yaml b/workloads/school-games-prod/school-games-prod.yaml new file mode 100644 index 0000000..2b1e84b --- /dev/null +++ b/workloads/school-games-prod/school-games-prod.yaml @@ -0,0 +1,96 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: school-games + name: school-games + namespace: games +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: school-games + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + app: school-games + spec: + containers: + - env: + - name: PORT + value: '3000' + image: gitea.imcu.ro/homeschool/school-games:20260112T132336 + imagePullPolicy: IfNotPresent + name: school-games + ports: + - containerPort: 3000 + protocol: TCP + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + imagePullSecrets: + - name: gitea-registry + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: school-games-service + namespace: games +spec: + clusterIP: 10.43.115.23 + clusterIPs: + - 10.43.115.23 + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - port: 80 + protocol: TCP + targetPort: 3000 + selector: + app: school-games + sessionAffinity: None + type: ClusterIP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: school-games-ingress + namespace: games +spec: + ingressClassName: traefik + rules: + - host: school.imcu.ro + http: + paths: + - backend: + service: + name: school-games-service + port: + number: 80 + path: /games + pathType: Prefix + - backend: + service: + name: school-games-service + port: + number: 80 + path: / + pathType: Exact + tls: + - hosts: + - school.imcu.ro + secretName: school-imcu-ro-tls diff --git a/workloads/wolsey/wolsey.yaml b/workloads/wolsey/wolsey.yaml new file mode 100644 index 0000000..dbae538 --- /dev/null +++ b/workloads/wolsey/wolsey.yaml @@ -0,0 +1,146 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: wolsey + name: wolsey + namespace: default +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: wolsey + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + app: wolsey + spec: + containers: + - env: + - name: PORT + value: '3000' + - name: HOSTNAME + value: 0.0.0.0 + - name: NODE_ENV + value: production + - name: CONTENT_ROOT + value: /content + image: gitea.imcu.ro/homeschool/school:e76d244 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 3000 + scheme: HTTP + initialDelaySeconds: 15 + periodSeconds: 20 + successThreshold: 1 + timeoutSeconds: 1 + name: wolsey + ports: + - containerPort: 3000 + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 3000 + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: '1' + memory: 768Mi + requests: + cpu: 50m + memory: 128Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /content + name: content + readOnly: true + dnsPolicy: ClusterFirst + imagePullSecrets: + - name: gitea-registry + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - name: content + persistentVolumeClaim: + claimName: wolsey-content +--- +apiVersion: v1 +kind: Service +metadata: + name: wolsey-service + namespace: default +spec: + clusterIP: 10.43.31.159 + clusterIPs: + - 10.43.31.159 + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - port: 80 + protocol: TCP + targetPort: 3000 + selector: + app: wolsey + sessionAffinity: None + type: ClusterIP +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + finalizers: + - kubernetes.io/pvc-protection + name: wolsey-content + namespace: default +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: longhorn + volumeMode: Filesystem + volumeName: pvc-06d36938-7042-4bcf-aa28-a12e695e0779 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: wolsey-ingress + namespace: default +spec: + ingressClassName: traefik + rules: + - host: wolsey.imcu.ro + http: + paths: + - backend: + service: + name: wolsey-service + port: + number: 80 + path: / + pathType: Prefix + tls: + - hosts: + - wolsey.imcu.ro + secretName: wolsey-imcu-ro-tls