1096 lines
45 KiB
Markdown
1096 lines
45 KiB
Markdown
# 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 | |