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

45 KiB
Raw Permalink Blame History

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

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

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades   # kies "Ja"

Configuratie met verstandige defaults voor k3s-nodes:

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/:
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)

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

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

# 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

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

# 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)

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

# 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

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

sudo kubectl get nodes
# Verwacht: 3 nodes, alle Ready, alle met control-plane,etcd,master rol

3.3 Labels zetten (handig voor later)

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)

  • cert-manager geïnstalleerd in namespace cert-manager
  • DuckDNS-webhook geïnstalleerd (helm chart ebrianne/cert-manager-webhook-duckdns)
  • Token-secret duckdns-token in namespace cert-manager
  • ClusterIssuer duckdns — READY True
  • Wildcard-Certificate zb-kw-wildcard in namespace argocd (secret zb-kw-wildcard-tls)
  • RBAC voor de webhook — bewaar dit in de repo:
# 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)

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)

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:

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

sudo kubectl apply -f https://gitea.zb-kw.duckdns.org/willemjan/cluster/raw/branch/main/bootstrap/root-app.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:
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}
  1. 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

# 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):

# 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

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

# 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:

# 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/):

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:

# 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):

# 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

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)

# 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:

# 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):

# 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)

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.

# 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):

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):

# 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!
    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