Files
cluster/k3s-cluster-migratieplan.md
T
2026-10-13 12:00:13 +02:00

1096 lines
45 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Migratieplan: Proxmox/Stacks → k3s-cluster met Argo CD
Complete migratie van je huidige stacks-repo naar een GitOps-managed k3s-cluster op 3 Optiplexen (i5-6500T, i3-8100T, + Proxmox-VM of 3e Optiplex), inclusief Debian-installatie, k3s, Argo CD, en migratie van alle services.
**Structuur:** Deel A (Fase 0–3) is de afgeronde basisinstallatie. **Deel B** legt uit hoe diensten bereikbaar zijn (poorten, hostnames, TLS). **Deel C** is het stap-voor-stap uitvoeringsplan — beginnend bij Argo CD, dan elke dienst via de Gitea-cluster-repo. **Bijlagen A–E** bevatten de achtergronddetails per onderdeel.
---
## Fase 0 — Voorbereiding (vóór de Optiplexen binnenkomen)
- [ ] Backup huidige stacks-repo (git clone, incl. LFS indien aanwezig)
- [ ] Exporteer/configs van draaiende services: AdGuard (yaml), dashboards, Renovate-config
- [ ] Inventariseer huidige stack: welke services draaien waar, welke ports, welke volumes
- [ ] Reserveer statische IP's voor 3 cluster-nodes + 1 "spare" (DHCP-reserveringen of static via router)
- [ ] Bedenk een naamgevingsschema: bijv. k3s-node-01 t/m -03
- [ ] DNS-plan: welke hostnames wil je straks (adguard.example.com, homarr.example.com, etc.)
- [ ] Zorg dat je een USB-stick (8 GB+) hebt voor Debian-installatie
---
## Fase 1 — Debian 13 installeren op de eerste Optiplex (k3s-node-01)
### 1.1 Installatiemedia maken
- Download Debian 13 netinst ISO
- Schrijf naar USB-stick (Rufus/balenaEtcher)
- Boot de Optiplex van USB (F12 = boot menu bij Dell)
### 1.2 Installatie
1. **Graphical install** → taal naar eigen voorkeur (English is prima voor server)
2. **Hostname:** `k3s-node-01`
3. **Gebruiker:** eigen account, sterke wachtwoord (geen root-login, sudo is veiliger)
4. **Partitionering:** "Guided - use entire disk" → aparte `/boot` niet nodig, alles in één partitie is prima. Geen swap-partitie nodig (komt later via zram)
5. **Softwareselectie:** vink alles uit behalve:
- ✅ SSH server
- ❌ desktop environments
- ❌ print server
6. GRUB naar primaire schijf, reboot
### 1.3 Basisconfiguratie na installatie
```bash
# Als sudo-user inloggen
sudo apt update && sudo apt upgrade -y
# Handige tools
sudo apt install -y curl git htop vim net-tools
# Zram swap (voorkomt disk-gebruik, prima voor 8GB nodes)
sudo apt install -y systemd-zram-generator
echo -e "[zram0]\nzram-size = min(ram / 2, 4096)" | sudo tee /etc/systemd/zram-generator.conf
# Timezone
sudo timedatectl set-timezone Europe/Amsterdam
```
### 1.4 Unattended-upgrades (automatische security-updates)
Cluster-nodes willen nooit verwaarloosd worden, maar je wil ook niet wekelijks handmatig patchen. De golden middle: **security-updates automatisch, de rest handmatig**.
```bash
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades # kies "Ja"
```
Configuratie met verstandige defaults voor k3s-nodes:
```bash
sudo tee /etc/apt/apt.conf.d/50unattended-upgrades-local > /dev/null <<'EOF'
Unattended-Upgrade::Origins-Pattern {
"origin=Debian,codename=${distro_codename},label=Debian-Security";
"origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
};
// Alleen security-updates, geen gewone package-upgrades:
Unattended-Upgrade::Package-Blacklist {
};
// GEEN automatische reboot — kured regelt dat (zie hieronder)
Unattended-Upgrade::Automatic-Reboot "false";
// Mail/rapportage optioneel (logcheck via /var/log/unattended-upgrades/)
Unattended-Upgrade::Automatic-Reboot-Time "02:00";
EOF
```
Waarom geen `Automatic-Reboot true`? Bij een etcd-quorum wil je nooit dat alle 3 nodes tegelijk besluiten te rebooten. Daarvoor is **kured** (Kubernetes Reboot Daemon) de standaardoplossing:
- kured detecteert dat een node een reboot nodig heeft (`/var/run/reboot-required` door Debian aangemaakt bij kernel-updates)
- Hij **draine't en reboot't nodes één voor één**, netjes binnen je quorum
- Installeer via Argo CD in `infrastructure/kured/`:
```bash
sudo kubectl apply -f https://github.com/kubereboot/kured/releases/download/1.17.1/kured-1.17.1.yaml
# Optioneel: reboot-window vastleggen zodat het alleen 's nachts gebeurt
# --reboot-days=mon --start-time=2am --end-time=6am (via de helm chart values)
```
Samen is dit het complete patroon: unattended-upgrades patcht security-gaten dagelijks zonder reboot, en kured plant de nodige reboots netjes gespreid en 's nachts.
Extra tip: pin de k3s-versie zelf expliciet (`INSTALL_K3S_VERSION` of de versioned download-URL) — k3s-updates wil je bewust via een PR in je repo doen, niet automatisch, zodat je cluster-versie en je Git-repo synchroon blijven.
### 1.5 Netwerk statisch maken (belangrijk voor cluster-node)
```bash
# Check interfacenaam
ip a
# Statische IP via systemd-networkd of Netplan — gebruik degene die je installatie heeft
# Netplan-voorbeeld (Ubuntu-stijl):
sudo tee /etc/netplan/01-static.yaml > /dev/null <<EOF
network:
version: 2
ethernets:
enp2s0: # <-- vervang door jouw interfacenaam
dhcp4: no
addresses: [192.168.1.101/24]
routes:
- to: default
via: 192.168.1.1
nameservers:
addresses: [192.168.1.1, 1.1.1.1]
EOF
sudo netplan apply
```
*(Debian gebruikt `ifupdown` (`/etc/network/interfaces`) — kies het pad dat bij jouw setup past; het punt is: statisch IP, geen DHCP-afhankelijkheid voor de node zelf)*
### 1.6 BIOS-instellingen
- **AC Power Recovery:** "On" (automatisch opstarten na stroomuitval)
- **Wake on LAN:** aan (optioneel, handig)
- **Virtualisatie (VT-x/VT-d):** aan (voor toekomstige workloads)
- **Secure Boot:** uit (voorkomt gedoe met kernel-modules zoals Longhorn/zram)
- **Deep Sleep/S4:** uit (server moet altijd bereikbaar zijn)
### 1.7 Tailscale op de nodes (beheer-toegang via je Tailnet)
Elke node krijgt `tailscaled` voor SSH/kubectl-toegang vanaf je Tailnet — geen port-forwards of publieke beheer-poorten nodig.
```bash
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --hostname=k3s-node-01 --ssh
# Eén van de nodes (bijv. node-01) adverteert daarnaast je LAN-subnet:
sudo tailscale up --advertise-routes=192.168.1.0/24 --hostname=k3s-node-01 --ssh
# Accepteer de route in de Tailscale-admin-console (Machines → node-01 → Edit route settings)
```
**Wat dit oplevert:**
- SSH en kubectl vanaf elke locatie via MagicDNS (`k3s-node-01.<tailnet>.ts.net`), incl. Tailscale-SSH met device-authenticatie
- Beheer-UI's (Argo CD, Grafana, k3s-metrics) bereikbaar via Tailnet-i.p.v. publiek op DuckDNS
- Via de subnet-router is ook je Proxmox-UI, AdGuard-fallback-LXC e.d. remote bereikbaar
**Wat je NIET door het Tailnet haalt:**
- Cluster-intern verkeer (k3s-nodes onderling) en NFS blijven op je LAN — Tailscale is alleen de beheer-overlay, geen transport voor workload-verkeer
- AdGuard DNS-port 53 blijft via je LAN/DHCP-regels, niet via Tailscale
**Etcd-communicatie blijft op LAN-IP's** — verander niets aan `--node-ip`; Tailnet-IP's zijn alleen extra interfaces.
### 1.8 SSH-toegang regelen
```bash
# Vanaf je eigen machine
ssh-copy-id [email protected]
# Test sudo zonder wachtwoord-prompt (optioneel voor ansible later)
sudo visudo # voeg toe: user ALL=(ALL) NOPASSWD: ALL
```
---
## Fase 2 — k3s installeren op node 01 (met cluster-init)
### 2.1 Waarom `--cluster-init`?
Je wil straks HA met 3 servers, dus **start meteen met embedded etcd** — dan hoef je later niet te migreren.
### 2.2 Installatie
```bash
curl -sfL https://get.k3s.io | sh -s - server \
--cluster-init \
--node-ip 192.168.1.101 \
--node-taint CriticalAddonsOnly=true:NoExecute \
--disable traefik # zie opmerking hieronder
```
> **Traefik:** k3s levert standaard Traefik als ingress. Je kunt hem laten staan (aanrader om te beginnen) of uitzetten als je bijv. nginx-ingress wil. Voor dit plan: **laten staan** (weghalen uit bovenstaand commando) tenzij je reden hebt.
### 2.3 Verificatie
```bash
# Node is klaar?
sudo kubectl get nodes
# Verwacht: k3s-node-01 Ready control-plane,etcd,master
# Kubeconfig voor je eigen account
sudo mkdir -p ~/.kube
sudo k3s kubectl config view --raw | sudo tee ~/.kube/config > /dev/null
sudo chown -R $USER ~/.kube
```
### 2.4 Node-token bewaren (nodig voor node 02/03)
```bash
sudo cat /var/lib/rancher/k3s/server/node-token
# Bewaar deze veilig (password manager)
```
### 2.5 Automatische etcd-snapshots naar Proxmox (NFS)
Op je Proxmox: maak een NFS-share (bijv. `/zfs-pool/data/k3s-backups`).
```bash
# Op k3s-node-01: herstart k3s met snapshot-config
sudo tee /etc/rancher/k3s/config.yaml > /dev/null <<EOF
etcd-snapshot-schedule-cron: "0 */6 * * *"
etcd-snapshot-snapshot-retention: 14
etcd-s3: false
EOF
# Of naar S3-compatible (MinIO op Proxmox VM) — geavanceerder, kan later
sudo systemctl restart k3s
```
---
## Fase 3 — Node 02 en 03 toevoegen als servers (HA-quorum)
Herhaal **Fase 1 volledig** op de tweede Optiplex (i3-8100T) en op je derde node (derde Optiplex óf een VM op Proxmox).
> ⚠️ **Derde node via Proxmox-VM:** een VM telt als "één fysiek punt van faal" als je meerdere nodes op dezelfde Proxmox-host zet. Voor een écht HA-quorum wil je idealiter 3 fysieke machines. Heb je maar 2 Optiplexen + Proxmox: accepteer de risico-afweging (Proxmox is vaak stabieler dan een nieuwe goedkope Mini) of wacht op een 3e fysieke node. De cluster werkt in beide gevallen.
### 3.1 Join als server
```bash
curl -sfL https://get.k3s.io | sh -s - server \
--server https://192.168.1.101:6443 \
--token <NODE_TOKEN> \
--node-ip 192.168.1.102
```
### 3.2 Verificatie op node 01
```bash
sudo kubectl get nodes
# Verwacht: 3 nodes, alle Ready, alle met control-plane,etcd,master rol
```
### 3.3 Labels zetten (handig voor later)
```bash
sudo kubectl label nodes k3s-node-01 node-type=intel-big
sudo kubectl label nodes k3s-node-02 node-type=intel-small
# etc.
```
---
# Deel B — Hoe wordt een dienst bereikbaar? (het poorten-verhaal)
Voordat het stappenplan komt, eerst het antwoord op de kernvraag: **hoe kan het dat honderd pods allemaal "poort 80" gebruiken terwijl een node maar één IP heeft?** Het antwoord heeft drie lagen:
## B.1 Pods: elk hun eigen cluster-IP
- Elke **pod** krijgt een eigen privé-IP uit het clusternetwerk (bij k3s: `10.42.x.x`)
- Pod A en pod B die allebei intern op poort 80 luisteren: **geen conflict** — het zijn aparte IP's, net zoals twee laptops thuis allebei poort 80 kunnen gebruiken
- Deze IP's zijn alleen binnen het cluster bereikbaar en vervallen bij herstart — daarom praat je er (bijna) nooit rechtstreeks tegen
## B.2 Services: vaste naam + virtueel IP binnen het cluster
- Een **Service** is een vast virtueel IP (ClusterIP) + DNS-naam (`homarr.homarr.svc.cluster.local`) dat naar de bijbehorende pods load-balancet
- Pods komen en gaan, de Service blijft — dit is het "telefoonnummer" binnen het cluster
- Nog steeds géén node-poort in gebruik: dit verkeer blijft binnen het clusternetwerk
## B.3 Ingress (Traefik): één deur, onderscheid op naam
- **Traefik is de enige die op de node op poort 80 en 443 luistert** (via de svclb/klipper-helper, precies zoals je in de logs zag bij `svclb-traefik`)
- Alle webapp-diensten delen dus die twee poorten. Het onderscheid wordt gemaakt op **hostname**: de browser vraagt `https://homarr.zb-kw.duckdns.org`, en Traefik leest de hostname (HTTP Host-header vóór de omleiding, SNI binnen TLS) en kijkt in z'n Ingress-regels: "welke Ingress hoort bij `homarr.zb-kw.duckdns.org`?" → routeert naar de Service `homarr` in namespace `homarr` → en die brengt het naar de pod
- Concreet: **één node-IP, twee poorten (80/443), oneindig veel diensten** — elk met z'n eigen hostname
## B.4 Diensten die géén HTTP zijn
Wat niet via Host-header kan (protocol zonder hostnames), krijgt z'n eigen poort op de node:
| Dienst | Protocol/poort | Oplossing |
|---|---|---|
| AdGuard Home | DNS, 53/udp+tcp | Service type `LoadBalancer` → klipper bindt 53 op de node(s) |
| Gitea SSH | SSH, 22 | `LoadBalancer` op poort 2222 (of 22) op de node |
| Databases, Kafka e.d. | alleen intern | ClusterIP Service — komt nooit op de node |
Regel: **alles met een browser-URL loopt via Traefik op 80/443; de rest krijgt een eigen, unieke node-poort.**
## B.5 Intern vs. extern — zelfde namen, zelfde certificaat
- **Intern:** je AdGuard geeft een DNS-rewrite: `homarr.zb-kw.duckdns.org` → IP van een cluster-node. Je browser praat dus rechtstreeks met Traefik in je LAN.
- **Extern:** je router forward 80/443 naar hetzelfde node-IP. Van buiten kom je op dezelfde Traefik binnen, met dezelfde hostname en hetzelfde wildcard-certificaat.
- TLS wordt overal door **Traefik zelf** afgehandeld (terminated), met het wildcard-certificaat `*.zb-kw.duckdns.org`. Interne pods ontvangen verkeer via HTTP binnen het cluster (het clusternetwerk is privé).
- Voordeel van deze aanpak: één DNS-aanpak, één certificaat, geen verschil tussen binnen en buiten.
## B.6 Waar nginx proxy manager in het verhaal zit (interim → afbouw)
Nu forward je router 80/443 naar **NPM op Proxmox**. Dit plan rekent daarmee:
1. **Interim-fase (nu → alle services gemigreerd):** NPM blijft op 80/443 van de router, maar per gemigreerde dienst zet je de NPM-proxy-host om van "naar de oude LXC/VM" naar **"forward naar cluster-node-IP poort 80"**. Traefik neemt het vanaf daar over op hostname. Zo werk je dienst-voor-dienst om zonder DNS-risico.
2. **Definitief (Stap 9):** router-forward 80/443 gaat direct naar het cluster-node-IP. NPM verliest z'n rol voor de DuckDNS-hostnames — behalve eventueel voor diensten die bewust buiten het cluster blijven (zoals HAOS), en ook die kan Traefik naar een LAN-IP routeren. Daarna NPM afbouwen.
---
# Deel C — Stappenplan: van cluster naar bereikbare diensten met TLS
Doorloop deze stappen op volgorde. Wat al is uitgevoerd staat met ✅ en dient ter verificatie.
## Stap 1 — TLS-fundering afronden ✅ (grotendeels gedaan)
- [x] cert-manager geïnstalleerd in namespace `cert-manager`
- [x] DuckDNS-webhook geïnstalleerd (helm chart `ebrianne/cert-manager-webhook-duckdns`)
- [x] Token-secret `duckdns-token` in namespace `cert-manager`
- [x] ClusterIssuer `duckdns` — `READY True`
- [x] Wildcard-Certificate `zb-kw-wildcard` in namespace `argocd` (secret `zb-kw-wildcard-tls`)
- [ ] **RBAC voor de webhook** — bewaar dit in de repo:
```yaml
# infrastructure/cert-manager/webhook-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: cert-manager-webhook-duckdns-token-reader
namespace: cert-manager
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["duckdns-token"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: cert-manager-webhook-duckdns-token-reader
namespace: cert-manager
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: cert-manager-webhook-duckdns-token-reader
subjects:
- kind: ServiceAccount
name: cert-manager-webhook-duckdns
namespace: cert-manager
```
- [ ] Verificatie: `kubectl get certificate -n argocd` → `READY True` (na het RBAC-fix herpakt de challenge zichzelf)
**Belangrijk voor latere namespaces:** het wildcard-secret staat nu alleen in `argocd`. Voor elke nieuwe namespace maak je een kleine `Certificate`-resource mee (zelfde `issuerRef`, zelfde `dnsNames: ["*.zb-kw.duckdns.org"]`, zelfde `secretName`) in die namespace — cert-manager vernieuwt ze automatisch. Zo heeft elke namespace z'n eigen TLS-secret bij Traefik.
## Stap 2 — Argo CD op z'n eigen hostname (het voorbeeld voor alle volgende)
### 2.1 argocd-server in HTTP-mode (Traefik doet de TLS)
```bash
sudo kubectl patch deployment argocd-server -n argocd \
-p '{"spec":{"template":{"spec":{"containers":[{"name":"argocd-server","env":[{"name":"ARGOCD_INSECURE","value":"true"}]}]}}}}'
```
### 2.2 Ingress-manifest (in de repo als `services/argocd/ingress.yaml`)
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd
namespace: argocd
spec:
rules:
- host: argocd.zb-kw.duckdns.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
number: 80 # poort 80 van de svc = HTTP-backend
tls:
- hosts: [argocd.zb-kw.duckdns.org]
secretName: zb-kw-wildcard-tls
```
### 2.3 Testen vóórdat je DNS aanpast — vanaf de node zelf:
```bash
curl -vk --resolve argocd.zb-kw.duckdns.org:443:192.168.2.<node-ip> https://argocd.zb-kw.duckdns.org
```
Krijg je het Argo CD-login-scherm, dan werkt de hele keten: Traefik → Ingress → Service → pod, mét het geldige wildcard-certificaat.
## Stap 3 — Interne DNS: rewrite in AdGuard
Zodat je binnen je LAN `https://argocd.zb-kw.duckdns.org` kunt gebruiken:
- AdGuard (de LXC **én** later de in-cluster instantie) → *DNS rewrites*:
- `argocd.zb-kw.duckdns.org` → `192.168.2.<node-ip>`
- Per dienst één rewrite (DuckDNS-rewrites ondersteunen geen wildcard) — bij ~10 diensten is dat een keer typen
- Test vanaf je laptop: `nslookup argocd.zb-kw.duckdns.org` geeft het node-IP; de browser opent de UI
- **Geen kip-en-ei met cert-manager:** de DNS-01-challenge loopt via de DuckDNS-API en checkt TXT-records, niet je lokale A-records
## Stap 4 — NPM ombouwen tot doorgeefluik (interim)
- In NPM: proxy-host `argocd.zb-kw.duckdns.org` aanmaken/wijzigen → forward naar `192.168.2.<node-ip>:80` (HTTP — Traefik doet de rest, inclusief de HTTPS-redirect op hostname)
- Zo werkt de dienst ook van buiten zolang de router nog naar NPM wijst
- Voor elke volgende gemigreerde dienst: verwijder de oude NPM-host, zet de nieuwe forward naar het node-IP — 1 minuut werk
- NPM's eigen Let's Encrypt-certs zijn voor deze hostnames overbodig geworden; laat ze verlopen, verwijder ze pas als NPM niets meer bedient
## Stap 5 — De cluster-repo op Gitea wordt de bron van alles
Alle YAML's hieronder leven in **dezelfde repo** (`gitea.zb-kw.duckdns.org/willemjan/cluster`). Vanaf de node apply je daarna nog maar **één manifest** (de bootstrap) — de rest trekt Argo CD zelf.
### 5.1 Repo-structuur
```
cluster/
├── bootstrap/
│ └── root-app.yaml # de enige die je handmatig applyt (Stap 6)
├── apps/ # één Argo CD-Application per dienst
│ ├── argocd.yaml # self-management (Stap 6)
│ ├── adguard.yaml
│ └── homarr.yaml
├── infrastructure/
│ ├── cert-manager/
│ │ ├── cert-manager.yaml # al geïnstalleerd — ter documentatie in Git
│ │ ├── duckdns-issuer.yaml
│ │ ├── webhook-rbac.yaml
│ │ └── wildcard-cert-argocd.yaml
│ └── storage/
│ └── nfs-provisioner.yaml # zie Bijlage A
└── services/ # de echte workloads
├── argocd/
│ └── ingress.yaml
├── adguard/
│ ├── namespace.yaml
│ ├── deployment.yaml
│ ├── service.yaml # incl. LoadBalancer voor poort 53
│ ├── ingress.yaml
│ └── cert.yaml
└── homarr/…
```
### 5.2 Het handmatige deel: root-app applyen vanaf de node
```bash
sudo kubectl apply -f https://gitea.zb-kw.duckdns.org/willemjan/cluster/raw/branch/main/bootstrap/root-app.yaml
```
```yaml
# bootstrap/root-app.yaml — de "app-of-apps"
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root
namespace: argocd
spec:
project: default
source:
repoURL: https://gitea.zb-kw.duckdns.org/willemjan/cluster.git
targetRevision: main
path: apps # elke yaml hierin = één Application
directory:
recurse: true
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true # wat uit Git verdwijnt, verdwijnt uit het cluster
selfHeal: true # handmatige kubectl-wijzigingen worden teruggedraaid
```
Vanaf dit moment is **Git de waarheid**: een commit op `main` verandert het cluster binnen ~3 minuten (Argo CD's poll-interval).
### 5.3 Repo-toegang voor Argo CD
Argo CD heeft leesrechten op de repo nodig. In de Argo CD UI: *Settings → Repositories → Connect repo using https* met je Gitea-gebruiker + access-token (verplicht bij een private repo; check met *Test*). Zie ook Bijlage C.
## Stap 6 — Argo CD beheert zichzelf (GitOps sluiten)
1. Commit de bestaende Argo CD-installatie + ingress in de repo onder `services/argocd/`
2. Maak `apps/argocd.yaml`:
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: argocd
namespace: argocd
spec:
project: default
source:
repoURL: https://gitea.zb-kw.duckdns.org/willemjan/cluster.git
targetRevision: main
path: services/argocd
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated: {prune: true, selfHeal: true}
```
3. Commit → de root-app pikt het bestand op → Argo CD beheert zichzelf vanaf nu vanuit Git. **GitOps is rond.**
## Stap 7 — Standaardrecept: een dienst toevoegen (geldt voor elke volgende)
Altijd dezelfde volgorde:
1. **Map + manifests** in de repo (`services/<naam>/`): namespace, deployment, service, ingress (+ PVC/secret) — templates hieronder
2. **Application-manifest** in `apps/<naam>.yaml` (kopieer `apps/argocd.yaml`, pas `path: services/<naam>` aan)
3. **Commit & push** → Argo CD pakt het op (of klik Sync)
4. **DNS-rewrite** in AdGuard: `<naam>.zb-kw.duckdns.org` → node-IP
5. **NPM** (interim): forward `<naam>.zb-kw.duckdns.org` → node-IP:80 — of sla over als je al in Stap 9 zit
6. **Test** intern (browser) en extern (via de router)
### Template: de complete app in één bestand
```yaml
# services/<naam>/app.yaml
apiVersion: v1
kind: Namespace
metadata:
name: <naam>
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: <naam>-config
namespace: <naam>
spec:
storageClassName: nfs-client # NFS vanaf Proxmox — Bijlage A
accessModes: [ReadWriteMany]
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: <naam>
namespace: <naam>
spec:
selector:
matchLabels: {app: <naam>}
template:
metadata:
labels: {app: <naam>}
spec:
containers:
- name: <naam>
image: <image>:<tag> # pin de tag! (Renovate houdt ze bij)
ports: [{containerPort: <poort>}]
volumeMounts: [{name: config, mountPath: /config}]
volumes:
- name: config
persistentVolumeClaim: {claimName: <naam>-config}
---
apiVersion: v1
kind: Service
metadata:
name: <naam>
namespace: <naam>
spec:
selector: {app: <naam>}
ports: [{port: 80, targetPort: <poort>}] # intern op 80 zetten is handig
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: <naam>
namespace: <naam>
spec:
rules:
- host: <naam>.zb-kw.duckdns.org
http:
paths:
- path: /
pathType: Prefix
backend:
service: {name: <naam>, port: {number: 80}}
tls:
- hosts: [<naam>.zb-kw.duckdns.org]
secretName: zb-kw-wildcard-tls
```
En eenmalig per namespace de bijbehorende Certificate (zelfde wildcard, in deze namespace):
```yaml
# services/<naam>/cert.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: zb-kw-wildcard
namespace: <naam>
spec:
secretName: zb-kw-wildcard-tls
dnsNames: ["*.zb-kw.duckdns.org"]
issuerRef: {name: duckdns, kind: ClusterIssuer}
```
### Voor niet-HTTP-diensten (zoals AdGuard): extra Service-blok
```yaml
# toevoegen in service.yaml — DNS op de node zelf
apiVersion: v1
kind: Service
metadata:
name: adguard-dns
namespace: adguard
spec:
type: LoadBalancer # klipper bindt poort 53 op de node(s)
selector: {app: adguard}
ports:
- {name: dns-tcp, port: 53, targetPort: 53, protocol: TCP}
- {name: dns-udp, port: 53, targetPort: 53, protocol: UDP}
```
(Zie B.4: alleen voor diensten zonder browser-URL.)
## Stap 8 — Diensten migreren in volgorde van laag → hoog risico
Details per dienst in **Bijlage D**:
1. [ ] **Argo CD** (Stap 2–6) — bijna af
2. [ ] **AdGuard Home** in-cluster — mét de fallback-LXC als tweede DNS tijdens de omschakeling (DNS is je kritiekste dienst: eerst redundantie, dan migratie)
3. [ ] **Homarr** (dashboard — lichte dienst, mooie tweede)
4. [ ] **Authentik** (identiteitsprovider — de SSO-laag voor alle overige diensten; vóór de rest migreren)
5. [ ] **Renovate** (verhuist als CronJob of deployment)
6. [ ] **Immich/Nextcloud-achtige diensten** — zorgvuldig met de NFS-datamigratie (Bijlage A/D)
7. [ ] **Gitea zelf, als laatste** (de kip-en-ei, Bijlage D): Gitea verhuist pas als alles al via Git beheerd wordt én er een veilige fallback is
## Stap 9 — Router omschakelen: het cluster wordt de front-deur (definitief)
Als alle diensten in het cluster zitten:
- [ ] Router port-forwards **80/tcp en 443/tcp** omzetten van NPM-VM → **cluster-node-IP**
- [ ] Test extern: alle `*.zb-kw.duckdns.org`-hostnames
- [ ] NPM afbouwen: doorgeef-hosts verwijderen; houd NPM alleen aan voor wat bewust buiten het cluster blijft (HAOS) — óf routeer dat via een Traefik-IngressRoute naar het LAN-IP van de HAOS-VM
- [ ] Bewaak in Uptime Kuma: al je hostnames (extern én intern)
- [ ] Optioneel later: een VIP/MetalLB voor 80/443 zodat het niet aan één node kleeft
## Stap 10 — Testen, chaos en monitoring (afsluiting)
Zie **Bijlage E**: kube-prometheus-stack, Beszel-agent op de nodes, Uptime Kuma, backup-strategie en de chaos-test (node uit, Proxmox-reboot i.v.m. NFS, certificaatvernieuwing droog oefenen).
---
# Bijlagen — service-specifieke en achtergronddetails
## Bijlage A — Storage: NFS vanaf Proxmox
Je Proxmox-server wordt de centrale storage-backend: k3s mount een NFS-export en PVC's worden subdirectories daarop. Voordeel: een pod kan naar elke node verhuizen en z'n data blijft bereikbaar.
**Stap 1 — NFS-server direct op de Proxmox-host:**
We draaien de NFS-server **rechtstreeks op de Proxmox-host** (niet in een LXC/VM). Reden: `nfs-kernel-server` in een unprivileged container is een bekend breekpunt (nfsd-kernelmodule in user namespaces), en de ZFS-pool is toch al aan de host gebonden. Je immich/nextcloud/fileshare-LXC's blijven gewoon bestaan — dit is alleen voor de k3s-storage-export.
```bash
# 1. Op de Proxmox-host: dataset aanmaken voor de k3s-data
zfs create zfs-pool/data/k3s-storage
# Optioneel: quota erop zetten zodat k3s je pool niet volzuigt
zfs set quota=200G zfs-pool/data/k3s-storage
# 2. NFS-server installeren op de host
apt install -y nfs-kernel-server
# 3. Export aanmaken — beperk tot je cluster-subnet!
echo "/zfs-pool/data/k3s-storage 192.168.2.0/24(rw,sync,no_subtree_check,no_root_squash)" >> /etc/exports
# 4. Export activeren
exportfs -ra
exportfs -v # controleer dat de export er staat
# 5. Firewall-check (indien pve-firewall actief): poort 2049/tcp open voor cluster-subnet
# ook rpcbind/nfs-helperdiensten: `systemctl status nfs-server`
```
**Waarom deze plek (`zfs-pool/data/...`):** data in `/zfs-pool/data/` leeft op je ZFS-pool, buiten elke container of VM om — hij overleeft recreaties en is direct backupbaar met je bestaande ZFS-snapshots/replicatie. Precies hetzelfde patroon als je immich/nextcloud/fileshare-datasets.
**Belangrijk:** de Proxmox-host doet nu óók dienst als NFS-server — dat is een extra service op je hypervisor. Voor een homelab prima, maar besef: herstart je Proxmox, dan bevriezen in-cluster pods met NFS-PVC's tijdelijk (writes blokkeren tot NFS terug is). Dit scenario zit in de chaos-test van Fase 8.
**Stap 2 — NFS als StorageClass in k3s via `nfs-subdir-external-provisioner`:**
```yaml
# infrastructure/storage/nfs-provisioner.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-subdir-external-provisioner
namespace: kube-system
spec:
selector:
matchLabels: {app: nfs-subdir-external-provisioner}
template:
metadata:
labels: {app: nfs-subdir-external-provisioner}
spec:
serviceAccountName: nfs-subdir-external-provisioner
containers:
- name: nfs-subdir-external-provisioner
image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.7.0
volumeMounts: [{name: nfs, mountPath: /persistentvolumes}]
env:
- {name: PROVISIONER_NAME, value: k8s-sigs.io/nfs-subdir}
- {name: NFS_SERVER, value: 192.168.2.254} # <-- Proxmox-host IP
- {name: NFS_PATH, value: /zfs-pool/data/k3s-storage}
volumes:
- name: nfs
nfs: {server: 192.168.2.254, path: /zfs-pool/data/k3s-storage}
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-client
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: k8s-sigs.io/nfs-subdir
parameters:
archiveOnDelete: "true"
reclaimPolicy: Retain
volumeBindingMode: Immediate
```
*(De RBAC-regels voor de provisioner horen er ook bij — gebruik de officiële helm chart `nfs-subdir-external-provisioner` voor de complete set; hier ter illustratie het principe)*
**Wat dit je geeft:**
- PVC's landen als mappen op de Proxmox-NFS: backupbaar op één plek, uitbreidbaar met een grote schijf
- Pods zijn vrij verplaatsbaar tussen nodes (geen nodeSelector-pinning meer voor storage-redenen)
- Minder RAM-overhead dan Longhorn
**Trade-off om te kennen:**
- Proxmox wordt een dependency: valt de NFS-server uit, dan verliezen pods toegang tot hun data. Pods blijven draaien maar freezes bij writes tot NFS terug is. Voor een homelab acceptabel; je chaos-test in Fase 8 moet dit scenario bevatten.
- Longhorn (replicated block storage op de nodes zelf) blijft het alternatief zonder deze dependency, maar is zwaarder en vraagt meer van je 128 GB SSD's. NFS-via-Proxmox past beter bij jouw setup — en je had de Proxmox toch al als storage-backend gepland.
## Bijlage B — Cert-manager met DuckDNS (al uitgevoerd — ter naslag in de repo)
Je huidige DuckDNS-domein werkt gewoon door in het cluster. Belangrijkste punt: **HTTP-01-validatie werkt alleen op poort 80/443 van buiten bereikbaar**, en DuckDNS ondersteunt wildcards via **DNS-01** — cert-manager heeft een officiële DuckDNS-webhook.
**Stap 1 — cert-manager installeren** (Argo CD-managed in `infrastructure/cert-manager/`):
```bash
sudo kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.yaml
```
**Stap 2 — DuckDNS-webhook installeren (ebrianne/cert-manager-webhook-duckdns):**
De ClusterIssuer praat met een webhook-pod die de DNS-01-challenge via de DuckDNS-API uitvoert. Installeer eerst de webhook:
```bash
# 1. Token-secret aanmaken (haal je token van duckdns.org)
sudo kubectl create secret generic duckdns-token \
-n cert-manager --from-literal=token=<JOUW_DUCKDNS_TOKEN>
# 2. Webhook installeren via helm
helm install cert-manager-webhook-duckdns \
--namespace cert-manager \
--set duckdns.token='<JOUW_DUCKDNS_TOKEN>' \
ebrianne/cert-manager-webhook-duckdns
# 3. Verifieer de APIService
sudo kubectl get apiservice | grep duckdns
# Verwacht: v1alpha1.acme.duckdns.org Available
```
**Stap 3 — DuckDNS-issuer (werkt voor wildcard én losse hostnames):**
```yaml
# infrastructure/cert-manager/duckdns-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: duckdns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: duckdns-account-key
solvers:
- dns01:
webhook:
groupName: acme.duckdns.org # default van de ebrianne-webhook
solverName: duckdns
config:
apiTokenSecretRef: # verwijst naar je token-secret
name: duckdns-token
key: token
```
De webhook haalt het te valideren domein uit de Challenge zelf — je `Certificate`-manifest bepaalt dus wat er uitgegeven wordt. Geen `subdomain`-config nodig.
**Aanpak per hostname — twee opties:**
1. **Wildcard (`*.zb-kw.duckdns.org`) — aanrader:** één Certificate-resource met `dnsNames: ["*.zb-kw.duckdns.org"]`, door al je Ingresses hergebruikt via één `secretName`. Eenvoudiger beheer en DNS-01 werkt ook voor hostnames die niet publiek routen.
2. **Losse subdomeinen** (zoals nu): `adguard.zb-kw.duckdns.org`, `auth.zb-kw.duckdns.org`, enz. — elke hostname z'n eigen Certificate. Werkt net zo goed, alleen meer resources om te beheren.
**Tip:** test de eerste issuance met het **staging**-endpoint (`https://acme-staging-v02.api.letsencrypt.org/directory`) om Let's Encrypt rate-limits te omzeilen, en schakel pas naar productie als de flow werkt.
**Poorten uitsturen:** DuckDNS-verwijzingen moeten naar je publieke IP wijzen (zoals nu al), en je router forwarded 80/443 naar het LoadBalancer-IP van Traefik. Voor DNS-only diensten zoals AdGuard (poort 53) gebruik je DNS-redirection in je router, geen port-forward — meer daarover in de AdGuard-sectie.
**Belangrijk:** gebruik **DNS-01**, niet HTTP-01 — DuckDNS-webhook + wildcard werkt zonder dat je iets extra op poort 80 beschikbaar hoeft te stellen, en ook achter CGNAT/dynamisch IP zolang de DNS-verwijzing klopt.
---
## Bijlage C — Argo CD: installatie- en beheerdetails
### 5.1 Argo CD installeren
```bash
sudo kubectl create namespace argocd
sudo kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# UI bereikbaar maken
sudo kubectl patch svc argocd-server -n argocd -p '{"spec":{"type":"LoadBalancer"}}'
# LoadBalancer = k3s klipper → krijgt een IP uit je node-range (prima voor homelab)
# Admin-wachtwoord ophalen
sudo kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
```
### 5.2 Pin Argo CD op één node (prettig voor performance)
```yaml
# infrastructure/argocd/values.yaml of patch
spec:
template:
spec:
nodeSelector:
node-type: intel-big
```
### 5.3 App-of-apps patroon (aanrader)
Maak één "root-app" die alle andere apps beheert:
```yaml
# argocd/root-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root
namespace: argocd
spec:
project: default
source:
repoURL: https://gitea.zb-kw.duckdns.org/je-gebruikersnaam/cluster.git
targetRevision: main
path: infrastructure
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
```
### 5.4 Repo-toegang via SSH-key of token
- Maak in Gitea een read-only deploy-token of SSH-key aan
- Configureer dit in Argo CD: `argocd repo add git@gitea...:user/cluster.git --ssh-private-key-path ...`
### 5.5 Sync-policy per app
Begin met `automated: {prune: true, selfHeal: true}` voor alles in `infrastructure/` — daarna per service kijken of je wilt dat Argo CD ook live veranderingen teruggedraait (selfHeal kan irritant zijn bij debugging).
---
## Bijlage D — Service-specifieke migratiedetails
Migreer in deze volgorde: lichte/kritiek-loze services eerst, pas later de diensten waar je huisnetwerk vanaf hangt.
### 6.1 AdGuard Home — met redundante tweede instantie buiten het cluster
**Waarom eerste:** klein, geen dependencies, direct nut — én je DNS-redundantie-plan is hier het belangrijkste onderdeel.
**De architectuur (zoals je nu al hebt, maar dan hybride):**
| Instantie | Draait op | Rol |
|---|---|---|
| adguard-cluster | k3s (namespace `networking`) | Primair, GitOps-beheerd |
| adguard-fallback | LXC op Proxmox | Secundair, DNS-fallback als het cluster (her)start of platligt |
**Waarom dit slim is:** DNS is de kritieke dienst van je netwerk — valt die weg, dan lijkt "alles" plat, inclusief de diensten die gewoon draaien. Een tweede AdGuard buiten het cluster overleeft cluster-upgrades, node-reboots en etcd-herstel.
**Config-sync tussen de twee instanties (je huidige aanpak doorzetten):**
AdGuard Home heeft geen ingebouwde sync, maar er zijn twee beproefde patronen:
1. **GitOps-sync (aanrader in jouw setup):** de in-cluster AdGuard haalt zijn config uit je cluster-repo (PVC op NFS), en een kleine cron/sync-job rsync't de `AdGuardHome.yaml` naar de LXC-instantie (bijv. via ssh/scp of een gedeelde NFS-mount op de LXC). Aanpassingen doe je via de UI van de cluster-instantie; de sync-propagateert naar de fallback.
2. **AdGuardHome-Sync tool:** een community-tool die config van een master naar replica's pusht (incl. filters, rewrites, clients). Draait als sidecar of aparte deployment in-cluster, wijst naar je LXC-instantie als replica.
Kies optie 2 als je weinig wilt bouwen; optie 1 als je alles in je eigen tooling wil houden.
**Router/DHCP-configuratie:** zet in je DHCP-config **beide** DNS-servers: eerst het cluster-IP (LoadBalancer of hostPort op een vaste node), dan de LXC-IP als fallback. Clients vallen dan automatisch terug op de tweede AdGuard als de eerste niet antwoordt — precies de redundancy die je wil.
**Deployment in-cluster (core):**
```yaml
# services/adguard/deployment.yaml — DNS-ports via hostPort of LoadBalancer
apiVersion: apps/v1
kind: Deployment
metadata:
name: adguard-home
namespace: networking
spec:
replicas: 1
selector:
matchLabels: {app: adguard-home}
template:
metadata:
labels: {app: adguard-home}
spec:
containers:
- name: adguard-home
image: adguard/adguardhome:v0.107.62
ports:
- {containerPort: 53, protocol: UDP, hostPort: 53}
- {containerPort: 53, protocol: TCP, hostPort: 53}
- {containerPort: 3000} # admin UI eerste setup, daarna 80
volumeMounts:
- {name: config, mountPath: /opt/adguardhome/conf}
volumes:
- name: config
persistentVolumeClaim: {claimName: adguard-config}
```
*(DNS op poort 53: gebruik `hostPort` op de node met vaste nodeSelector, óf de klipper LoadBalancer met extra IP. In beide gevallen: de router/DHCP verwijst naar dat IP. Zet de pod vast op één node zolang je hostPort gebruikt.)*
**Belangrijk voor AdGuard:** zet je router-DNS pas om naar de nieuwe IP's nadát beide instanties getest zijn — de LXC-fallback maakt deze overstap veel veiliger dan bij een enkele instantie.
### 6.2 Homarr (dashboard)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: homarr
namespace: dashboards
spec:
replicas: 1
selector:
matchLabels: {app: homarr}
template:
metadata:
labels: {app: homarr}
spec:
containers:
- name: homarr
image: ghcr.io/ajnart/homarr:latest
ports: [{containerPort: 7575}]
volumeMounts:
- name: data
mountPath: /data
- name: config
mountPath: /app/data/configs
volumes:
- name: data
persistentVolumeClaim: {claimName: homarr-data}
- name: config
persistentVolumeClaim: {claimName: homarr-config}
---
apiVersion: v1
kind: Service
metadata: {name: homarr, namespace: dashboards}
spec:
selector: {app: homarr}
ports: [{port: 80, targetPort: 7575}]
---
# Ingress met cert-manager TLS: homarr.zb-kw.duckdns.org
```
### 6.3 Authentik (nieuw — nog nergens draaiend)
Authentik bestaat uit 4 componenten: server, worker, PostgreSQL en Redis.
```yaml
# services/authentik/ - samengevat, gebruik de officiële Helm chart voor volledige setup
# Helm chart: authentik/authentik
# Belangrijkste values:
authentik:
secret_key: "<via sealed-secrets of SOPS>"
error_reporting: false
server:
ingress:
host: auth.zb-kw.duckdns.org
postgresql:
enabled: true
redis:
enabled: true
resources:
server: {requests: {cpu: 250m, memory: 512Mi}}
worker: {requests: {cpu: 250m, memory: 512Mi}}
```
**Resource-planning:** Authentik + postgres + redis samen ~1,5-2 GB RAM. Zet de postgres-PVC op je node met de meeste vrije ruimte, of gebruik Longhorn zodra actief. Pin op één node zolang je local-path storage gebruikt.
**Volgorde:** configureer Authentik eerst met één lokale admin-user, zet daarna pas SSO-integraties op andere services.
### 6.4 Renovate
Renovate als **CronJob** in-cluster (of gebruik Mend-hosted — hosted is minder werk):
```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: renovate
namespace: ci
spec:
schedule: "0 3 * * *" # elke nacht 03:00
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: renovate
image: renovate/renovate:39
env:
- name: RENOVATE_TOKEN
valueFrom:
secretKeyRef: {name: renovate-token, key: token}
- name: RENOVATE_PLATFORM
value: "gitea"
- name: RENOVATE_ENDPOINT
value: "https://gitea.zb-kw.duckdns.org/api/v1"
- name: RENOVATE_REPOSITORIES
value: "[]"
- name: RENOVATE_AUTODISCOVER
value: "true"
resources:
requests: {cpu: 250m, memory: 512Mi}
limits: {memory: 2Gi}
```
**Let op:** Renovate draait in-cluster die zijn eigen cluster-repo beheert — dat werkt prima (PR's op Gitea, Argo CD past toe na merge), maar zorg dat Renovate niet zijn eigen deploy platgooit door een broken image-tag te PR'en; gebruik Renovate's `minimumReleaseAge` of pin de cluster-repo op major-versions via `packageRules`.
### 6.5 Overige services uit je stacks-repo
Voor elke bestaande service (pi-hole-achtigen, uptime-kuma, etc.):
1. Container-image en config uit huidige repo halen
2. Omzetten naar Deployment + Service + Ingress + PVC
3. Als Argo CD draait: gewoon yaml in de juiste `services/<naam>/`-map committen → Argo CD pakt het op
4. DNS/router pas omschakelen als de nieuwe variant getest is
---
### D.6 — Gitea zelf migreren (de kip-en-ei-vraag)
**Gitea kan** in-cluster draaien, maar er is een bootstrapping-probleem: Argo CD heeft je Git-repo nodig om te weten wat hij moet deployen. Staat de repo in het cluster zelf, dan heb je een circular dependency.
**Aanrader:** Gitea op Proxmox laten staan (of een aparte lichte VM), niet in-cluster. Redenen:
1. CI/CD-infrastructuur buiten je cluster = je kunt het cluster altijd herbouwen vanuit Git
2. Gitea is niet zwaar, een kleine VM volstaat
3. Wil je Gitea tóch in-cluster: gebruik Argo CD's fallback naar een lokale bootstrap-kustomization als bron onbereikbaar is — werkt, maar extra complexiteit
---
## Bijlage E — Testing, monitoring en afsluiting
### 8.1 Monitoring
**Beszel uitbreiden naar de cluster-nodes:**
Je draait al Beszel voor je Proxmox-node — de agent kan gewoon op elke k3s-node meedraaien (klein systemd-proces, conflicteert niet met k3s):
```bash
# Op elke node (haal de installer/hub-key uit je Beszel-hub)
curl -sL https://get.beszel.dev -o /tmp/beszel.sh && bash /tmp/beszel.sh
# Voeg de node daarna in de hub toe als "system" met z'n LAN- of Tailnet-IP
```
- Zo behoud je één vertrouwd dashboard voor **alle hardware** (Proxmox + 3 Optiplexen) op OS-niveau
- Gebruik de Tailnet-IP's of MagicDNS-namen (`k3s-node-01.<tailnet>.ts.net`) als connectie-adres — dan werkt monitoring ook remote, en je hoeft geen SSH-keys van buiten je Tailnet toe te staan
- Beszel bewaakt alleen nodes; voor **pods/workloads in Kubernetes** geldt het volgende:
**Kubernetes-native monitoring (optioneel, later):**
- [ ] kube-prometheus-stack in `monitoring/` — pas toevoegen als je RAM-headroom hebt (kost zelf ~1-1,5 GB)
- [ ] Alert naar je telefoon (Alertmanager → Telegram/Discord webhook)
- [ ] Uptime Kuma op Proxmox-VM (buiten het cluster) om de cluster-diensten zelf te bewaken — Bewaakt samen met Beszel-hub de "van buitenaf"-view
- [ ] Alerteer in Uptime Kuma ook op je AdGuard-fallback-LXC (DNS is je kritiekste dienst)
### 8.2 Backups
- [ ] etcd-snapshots (Fase 2.5) — verifiëer dat ze verschijnen en test een restore!
```bash
sudo k3s etcd-snapshot restore /var/lib/rancher/k3s/server/db/snapshots/<naam>
```
- [ ] Velero voor workload-backups (optioneel, later)
- [ ] Gitea-data op Proxmox: Proxmox Backup Server of vzdump cron
### 8.3 Chaos-test vóór productie
- [ ] Start één node opnieuw op → cluster blijft beheerbaar, pods reschedule
- [ ] Trek de stekker van één node → zelfde test, zonder nette shutdown
- [ ] DNS werkt nog: cluster-AdGuard uit → clients vallen automatisch terug op de LXC-fallback (DHCP verwijst naar beide)
### 8.4 Router/DNS-omschakeling (laatste stap)
- [ ] DHCP DNS-server → cluster-IP's
- [ ] Router-advertenties/port-forwards omzetten naar LoadBalancer-IP's van k3s
- [ ] Alles één week dubbel draaien (oude stack + k3s parallel) alvorens oude stack af te bouwen
---
---
## Samengevat: volgorde in één blik
**Basis (afgerond):** Fase 0–3 — Debian op de nodes, k3s met `--cluster-init`, quorum van 3.
**Hoe bereikbaarheid werkt:** één deur (Traefik op node-IP 80/443), onderscheid op hostname, TLS via wildcard-certificaat; niet-HTTP-diensten krijgen een eigen node-poort (B.1 t/m B.6).
| # | Stap | Resultaat |
|---|------|-----------|
| 1 | TLS-fundering (cert-manager + DuckDNS-webhook + RBAC + wildcard) | Cluster kan zelf geldige certificaten halen ✅ |
| 2 | Argo CD-ingress + `ARGOCD_INSECURE` | `argocd.zb-kw.duckdns.org` werkt op de node |
| 3 | AdGuard DNS-rewrites | Hostnames resolven intern naar het cluster |
| 4 | NPM als doorgeefluik naar node:80 | Interim: alles blijft ook van buiten bereikbaar |
| 5 | Cluster-repo op Gitea + root-app (bootstrap) | Eén `kubectl apply` vanaf de node |
| 6 | Argo CD self-management | GitOps gesloten — Git is de waarheid |
| 7 | Standaardrecept per dienst (app.yaml + cert.yaml + apps/x.yaml) | Elke nieuwe dienst is een commit |
| 8 | Migreren: AdGuard → Homarr → Authentik → Renovate → data-diensten → Gitea | Diensten verhuizen laag→hoog risico |
| 9 | Router 80/443 → cluster-node-IP, NPM afbouwen | Cluster is de front-deur, intern én extern |
| 10 | Monitoring, backups, chaos-test | Produktief vertrouwen |