Wat gebeurt er als je Kubernetes cluster geen internet kan bereiken? Ik bedoel geen trage verbinding. Ik bedoel helemaal geen verbinding. Schepen op zee. Afgelegen mijnbouwlocaties. Fabrieksvloeren met air-gapped netwerken. Militaire deployments.
Voor veel mensen klinkt dat exotisch, alsof het een probleem van iemand anders is. Ik behandel het als een basis-ontwerpeis, en ik leg uit waarom het mijn homelab beter maakt terwijl ik de kabel bijna nooit echt eruit trek.
Waarom dit ertoe doet: voorbij het technische
Kubernetes offline draaien dwingt je om een vraag te beantwoorden waar de meeste cloud-native guides straal langs lopen: waar ben je eigenlijk van afhankelijk?
Een standaard Kubernetes setup heeft overal onzichtbare dependencies:
- Container registries (Docker Hub, gcr.io, quay.io)
- Helm chart repositories
- Certificate authorities
- NTP servers
- DNS resolvers
- Package repositories
- Telemetry endpoints
Geen van deze is een optionele extra. Het zijn dragende aannames die ingebakken zitten in hoe de meeste clusters werken, en meestal merk je ze pas op de dag dat er eentje wegvalt. Je cluster “werkt” totdat Docker Hub je rate-limit of je DNS forwarder omvalt, en dan werkt het ineens helemaal niet meer.
Dit is in de kern een soevereiniteit-probleem. Als je je infrastructuur niet kunt draaien zonder naar huis te bellen naar systemen die je niet controleert, bezit je het niet echt. Je huurt capaciteit en hoopt dat het contract blijft staan. Black boxes voelen als splinters, en een dependency die je niet ziet is het ergste soort black box.
Island mode: de architectuur
Ik ontwerp al mijn infrastructuur om “island mode capable” te zijn, in staat om volledig losgekoppeld van elk extern netwerk te functioneren. De eerlijke reden is niet dat ik verwacht op een onderzeeër te belanden. Het is dat de constraint betere architectuur afdwingt. Op het moment dat je besluit dat het cluster moet overleven met de kabel eruit, wordt elke slordige externe dependency iets dat je bewust moet aanpakken.
flowchart TD
subgraph island["Island Mode Cluster"]
subgraph internal["Interne Services"]
Registry["Registry<br/>(Harbor)"]
NTP["NTP<br/>(chrony)"]
DNS["DNS<br/>(CoreDNS)"]
CA["CA<br/>(cert-mgr)"]
Helm["Helm<br/>(ChartMuseum)"]
GitOps["GitOps<br/>(ArgoCD)"]
end
internal -->|"Alle dependencies lokaal opgelost"| workloads
subgraph workloads["Workloads"]
Apps["Apps draaien zonder externe calls"]
end
end
island x--x|"Geen verbinding vereist"| Internet["Internet<br/>(optioneel)"]
Het doel is makkelijk op te schrijven en irritant om te bereiken: trek de netwerkkabel eruit en alles blijft draaien. Updates stoppen. Operaties gaan door. Ik bouw het component voor component op, want elk component is een eigen kleine dependency die je naar binnen moet halen.
Component 1: lokale container registry
Begin met de meest voor de hand liggende. Je cluster heeft images nodig. Verbonden pullt het on-demand van Docker Hub of gcr.io en denk je er nooit over na. Offline faalt elk van die requests, en een node die geen image kan pullen is een node die niets kan schedulen.
Fix: mirror alles lokaal.
Harbor is wat ik draai voor self-hosted registries. Het geeft je:
- Pull-through caching (automatisch mirroren wanneer verbonden)
- Replicatie van upstream registries
- Vulnerability scanning
- Access control
# Harbor replication policy - sync wanneer verbonden
apiVersion: goharbor.io/v1beta1
kind: HarborReplicationPolicy
metadata:
name: docker-hub-mirror
spec:
srcRegistry:
url: https://registry-1.docker.io
destRegistry:
url: https://harbor.internal
trigger:
type: scheduled
scheduledTrigger:
cron: "0 2 * * *" # Nachtelijk sync wanneer verbonden
filters:
- type: name
value: "library/**" # Alleen officiële images
Verbonden synct Harbor images op een schema. Losgekoppeld serveert het wat het al gecached heeft. Dat is het hele patroon dat zich herhaalt voor elk component hieronder: wees een cache als het kan, wees de source of truth als het moet.
Het stuk dat mensen vergeten: pre-pull je dependencies.
Een pull-through cache helpt alleen als de image er minstens één keer doorheen is gegaan. Voordat je offline gaat, zorg dat elke image die je workloads nodig hebben echt in de lokale registry staat:
# Lijst alle images die momenteel draaien
kubectl get pods -A -o jsonpath="{.items[*].spec.containers[*].image}" | \
tr ' ' '\n' | sort | uniq
# Zorg dat ze allemaal gespiegeld zijn naar lokale registry
Component 2: lokale DNS
Leg de volgende dependency erbovenop. Kubernetes leunt op DNS voor service discovery, en CoreDNS handelt de cluster-interne namen prima af. Het probleem zit in alles buiten het cluster.
Probleem: elke pod die api.github.com of registry-1.docker.io probeert te resolven valt plat zonder upstream resolver, en een deel van je eigen tooling gaat er stilletjes van uit dat die namen werken.
Fix: leer CoreDNS om de externe namen te beantwoorden die jij belangrijk vindt, gericht op interne adressen:
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health
# Cluster-interne namen
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
# Bekende externe services - resolve naar intern
hosts /etc/coredns/custom.hosts {
fallthrough
}
# Forward onbekend naar lokale resolver (of fail gracefully)
forward . 10.0.0.1 {
policy sequential
health_check 5s
}
cache 30
loop
reload
loadbalance
}
custom.hosts: |
10.0.10.5 harbor.internal registry.internal
10.0.10.6 git.internal gitlab.internal
10.0.10.7 ntp.internal time.internal
Component 3: lokale tijdsynchronisatie
Deze bijt mensen die hem niet zagen aankomen. Tijdsynchronisatie doet er veel meer toe dan het lijkt. TLS certificaten valideren tegen de klok, dus een gedrifte node begint prima certificaten af te wijzen. Logs hebben timestamps nodig die je kunt vertrouwen als je een incident aan het correleren bent. Distributed systems hebben nodig dat de nodes het eens zijn over wat “nu” is.
Afgesneden van publieke NTP heb je een eigen tijdsbron nodig:
# Chrony als lokale NTP server
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: chrony
namespace: kube-system
spec:
selector:
matchLabels:
app: chrony
template:
spec:
hostNetwork: true
containers:
- name: chrony
image: harbor.internal/infra/chrony:4.3
volumeMounts:
- name: chrony-conf
mountPath: /etc/chrony
volumes:
- name: chrony-conf
configMap:
name: chrony-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: chrony-config
data:
chrony.conf: |
# Wanneer verbonden, gebruik publieke NTP
server time.cloudflare.com iburst
server pool.ntp.org iburst
# Wanneer losgekoppeld, wees de tijdsbron
local stratum 10
allow 10.0.0.0/8
# Drift file voor accuratesse wanneer losgekoppeld
driftfile /var/lib/chrony/drift
De regel die het werk doet is local stratum 10. Die vertelt chrony om als tijdsbron te fungeren, zelfs als er geen upstream is om te volgen. De nodes syncen met elkaar en blijven onderling consistent, en dat is wat TLS en je logs eigenlijk willen.
Component 4: lokale certificate authority
TLS overal betekent certificaten overal, en certificaten moeten ergens vandaan komen. Verbonden kletst cert-manager met Let’s Encrypt en vergeet je dat het probleem bestaat. Offline kan Let’s Encrypt je niet bereiken voor de ACME challenge en zit je vast.
Fix: draai je eigen CA.
# Self-signed root CA
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: internal-ca
spec:
ca:
secretName: internal-ca-root
---
# Maak het root certificaat
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: internal-ca-root
namespace: cert-manager
spec:
isCA: true
commonName: Internal Root CA
secretName: internal-ca-root
duration: 87600h # 10 jaar
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: selfsigned
kind: ClusterIssuer
Push de root CA naar elke node en client en vertrouw hem. Vanaf dat moment slaat cert-manager certificaten uit je eigen authority zonder ook maar één externe call. Je ruilt het gemak van publiek vertrouwen in voor volledige controle over je trust chain, en dat is de juiste ruil als de publieke CA je toch niet kan bereiken.
Component 5: GitOps zonder internet
Als je GitOps doet, synct ArgoCD waarschijnlijk van GitHub of GitLab.com, en offline verdwijnt die bron. Je hebt de Git server ook binnen het eiland nodig.
Optie 1: embedded GitLab
Draai GitLab in-cluster. Het is zwaar op resources, maar je krijgt alles in één doos.
Optie 2: Gitea
Lichtgewicht Git server, en een veel betere match voor edge hardware:
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea
spec:
template:
spec:
containers:
- name: gitea
image: harbor.internal/gitea/gitea:1.21
env:
- name: GITEA__server__OFFLINE_MODE
value: "true"
Wijs ArgoCD naar git.internal in plaats van github.com. Als je een verbinding hebt push je je wijzigingen als vanouds naar upstream, maar ArgoCD reconcilet hoe dan ook tegen de lokale kopie. De desired state van het cluster woont op het eiland, niet op de server van iemand anders.
Component 6: Helm charts offline
Laatste dependency in de keten. Helm charts komen normaal van remote repositories, dus offline heb je chart storage nodig die bij je woont.
ChartMuseum spreekt de Helm repository API:
# Push charts naar lokale museum wanneer verbonden
helm push mychart-1.0.0.tgz http://chartmuseum.internal
# ArgoCD gebruikt lokale charts
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
spec:
source:
repoURL: http://chartmuseum.internal
chart: mychart
targetRevision: 1.0.0
De pre-deployment checklist
Zes componenten verder wordt het pas echt. De failure mode die het meest pijn doet is de mode die je ontdekt de dag nadat je losgekoppeld bent, als niemand het kan fixen zonder een boot of een lange rit. Dus voordat je de kabel doorknipt, verifieer dat het eiland echt overeind blijft. Dit is het script dat ik draai, en ik heb het liever dat het nu tegen me schreeuwt dan dat een pod een image niet kan pullen op zee:
#!/bin/bash
# offline-readiness-check.sh
echo "=== Checking Offline Readiness ==="
# 1. Alle images lokaal beschikbaar
echo "Checking images..."
for image in $(kubectl get pods -A -o jsonpath="{..image}" | tr ' ' '\n' | sort -u); do
if ! curl -s "https://harbor.internal/v2/${image}/manifests/latest" > /dev/null; then
echo "MISSING: $image"
fi
done
# 2. DNS resolvet intern
echo "Checking DNS..."
for name in harbor.internal git.internal ntp.internal; do
if ! nslookup $name > /dev/null 2>&1; then
echo "DNS FAIL: $name"
fi
done
# 3. Tijd is gesynchroniseerd
echo "Checking NTP..."
chronyc tracking | grep -q "Leap status.*Normal" || echo "TIME SYNC ISSUE"
# 4. Certificaten zijn geldig
echo "Checking certificates..."
kubectl get certificates -A -o jsonpath='{range .items[*]}{.metadata.name}: {.status.conditions[0].type}{"\n"}{end}'
# 5. GitOps is synced
echo "Checking ArgoCD..."
argocd app list | grep -v "Synced.*Healthy" && echo "APPS NOT SYNCED"
echo "=== Check Complete ==="
Praktijkscenario’s
De stukken hierboven blijven abstract tot je ze neerzet op een plek waar de kabel echt niet reikt. Drie settings maken de constraints concreet.
Scenario 1: schip op zee
Een containerschip dat Kubernetes draait voor cargo management. Satellietconnectiviteit bestaat, maar het is duur en het valt weg als het weer omslaat.
Architectuur:
- K3s voor een lage resource footprint
- Harbor met een volledige mirror van elke benodigde image
- Lokale GitOps met Gitea
- Sync updates terwijl je in de haven ligt
De catch: updates landen in batches tijdens havenaanloop, en dat is misschien eens per paar weken. Het systeem moet er tussendoor keihard staan, want niemand pusht een hotfix midden op de Atlantische Oceaan.
Scenario 2: fabrieksvloer
Een productiefabriek die Kubernetes draait voor automatisering, waar beveiligingsbeleid internetconnectiviteit botweg verbiedt.
Architectuur:
- Volledige Kubernetes met een air-gapped installatie
- USB-gebaseerde updates, gesigned en geverifieerd
- Een apart management netwerk voor de zeldzame updates die wel gebeuren
De catch: updates hebben iemand fysiek aanwezig nodig, dus elke wijziging moet de eerste keer goed zijn. Je rolt geen slechte deploy terug vanaf je laptop thuis.
Scenario 3: mijn homelab
Ik draai mijn eigen infrastructuur island-mode capable, en ik wacht niet op een ramp om het te rechtvaardigen. De discipline is het punt. Als het ontwerp uitgaat van geen internet, komt de architectuur er vanzelf schoner uit.
Wat het me echt oplevert:
- Geen externe dependencies, dus geen externe failure modes om te erven
- Snellere image pulls vanuit de lokale registry
- Alles blijft werken tijdens ISP storingen, die hier vaker gebeuren dan me lief is
- Infrastructuur die ik echt bezit in plaats van huur
De trade-offs
Ik ga niet doen alsof dit gratis is. Dat is het niet.
| Aspect | Verbonden | Offline |
|---|---|---|
| Updates | Continu | Batched |
| Vulnerability info | Real-time | Vertraagd |
| Externe integraties | Makkelijk | Onmogelijk |
| Operationele last | Lager | Hoger |
| Soevereiniteit | Gedeeltelijk | Compleet |
Er is hier geen universeel correcte kolom. Het juiste antwoord hangt af van wat jouw setup daadwerkelijk eist, en voor een homelab betaal ik graag de hogere operationele last voor de volledige soevereiniteit.
K3s: gebouwd voor de edge
K3s verdient een eigen sectie, want het is gebouwd voor precies dit soort plek:
- Enkele binary, ~100MB
- Embedded SQLite (geen externe etcd nodig)
- Werkt op ARM (Raspberry Pi, edge devices)
- Minimale dependencies
# Air-gapped K3s installatie
curl -sfL https://get.k3s.io > install.sh
# Op air-gapped machine
INSTALL_K3S_SKIP_DOWNLOAD=true \
INSTALL_K3S_EXEC="--private-registry /etc/rancher/k3s/registries.yaml" \
./install.sh
Wijs K3s naar een lokale registry en het draait volledig offline. Voor schepen, fabrieken en Raspberry Pi clusters in een kast is die combinatie moeilijk te verslaan.
Mijn aanbeveling
Als je één ding meeneemt uit dit alles, neem dan het eerste punt. De rest volgt eruit.
Ontwerp voor offline, draai verbonden. Je koppelt waarschijnlijk niet vaak los, maar bouwen alsof het zou kunnen levert elke dag een beter cluster op.
Mirror alles. Container images, Helm charts, Git repos. Als het buiten je netwerk leeft, haal er een kopie van naar binnen.
Test disconnectie. Trek daadwerkelijk de kabel eruit en kijk wat breekt. De eerste keer dat je dit doet vind je een dependency waarvan je zwoer dat hij er niet was.
Automatiseer de sync. Terwijl je verbonden bent, ververs de mirrors op een schema. Een handmatig proces is een proces dat niet draait in de week dat je het het hardst nodig had.
Documenteer dependencies. Schrijf precies op wat je cluster van de buitenwereld nodig heeft, zodat toekomstige-jij het niet hoeft te reverse-engineeren tijdens een storing.
Island mode ging nooit over paranoia. Het gaat over je dependencies goed genoeg kennen om zonder ze te leven, en een cluster dat je offline kunt draaien is er een dat je echt begrijpt.
