Wat gebeurt er als je Kubernetes cluster geen internet kan bereiken? Niet “trage verbinding” — helemaal geen verbinding. Schepen op zee. Afgelegen mijnbouwlocaties. Fabrieksvloeren met air-gapped netwerken. Militaire deployments.
Dit is geen edge case. Het is een ontwerp-eis voor iedereen die soevereiniteit serieus neemt.
Waarom Dit Ertoe Doet: Voorbij het Technische
Kubernetes offline draaien dwingt je om een vraag onder ogen te zien die de meeste cloud-native guides negeren: 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
Dit zijn geen optionele extra’s. Het zijn dragende aannames ingebakken in hoe de meeste clusters werken. Je cluster “werkt” totdat een van deze verdwijnt.
Dit is belangrijk voor soevereiniteit. Als je je infrastructuur niet kunt draaien zonder externe afhankelijkheden, bezit je het niet echt. Je huurt capaciteit van systemen die je niet controleert.
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. Zelfs als ik nooit echt offline draai, dwingt deze constraint betere architectuur af:
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: als je de netwerkkabel eruit trekt, blijft alles draaien. Updates stoppen, maar operaties gaan door.
Component 1: Lokale Container Registry
Je cluster heeft images nodig. In een verbonden omgeving pullt het on-demand van Docker Hub of gcr.io. Offline falen die requests.
Oplossing: Mirror alles lokaal.
Harbor is mijn keuze voor self-hosted registries. Het ondersteunt:
- 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
Wanneer verbonden, synct Harbor images. Wanneer losgekoppeld, serveert het vanuit cache.
Kritiek: Pre-pull je dependencies.
Voordat je offline gaat, zorg dat alle images die je workloads nodig hebben in de lokale registry staan:
# 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
Kubernetes gebruikt DNS voor service discovery. CoreDNS handelt cluster-interne namen af, maar wat met externe namen?
Probleem: Pods die api.github.com of registry-1.docker.io proberen te resolven zullen falen zonder upstream DNS.
Oplossing: Configureer CoreDNS om bekende externe namen intern af te handelen:
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
Tijdsynchronisatie is belangrijker dan je denkt. TLS certificaten valideren tegen tijd. Logs hebben accurate timestamps nodig. Distributed systems hebben clock agreement nodig.
Zonder internet NTP servers heb je een lokale 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 sleutel: local stratum 10 laat de server als tijdsbron fungeren zelfs zonder upstream. Nodes syncen met elkaar, behouden relatieve consistentie.
Component 4: Lokale Certificate Authority
TLS overal betekent certificaten overal. In verbonden omgevingen praat cert-manager met Let’s Encrypt. Offline is dat onmogelijk.
Oplossing: 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
Distribueer de root CA naar alle nodes en clients. Nu geeft cert-manager certificaten uit vanuit je interne CA zonder externe calls.
Component 5: GitOps Zonder Internet
ArgoCD synct normaal van GitHub of GitLab. Offline heb je een lokale Git server nodig.
Optie 1: Embedded GitLab
Draai GitLab in-cluster. Zwaar, maar full-featured.
Optie 2: Gitea
Lichtgewicht Git server, perfect voor edge:
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"
ArgoCD wijst naar git.internal in plaats van github.com. Wanneer verbonden, push je wijzigingen. ArgoCD synct lokaal of je nu verbonden bent of niet.
Component 6: Helm Charts Offline
Helm charts pullen normaal van remote repositories. Offline heb je lokale chart storage nodig.
ChartMuseum biedt een 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
Voordat je looskoppelt, verifieer:
#!/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 ==="
Real-World Scenario’s
Scenario 1: Schip op Zee
Een containerschip draait Kubernetes voor cargo management. Satellietconnectiviteit is duur en onbetrouwbaar.
Architectuur:
- K3s voor lage resource footprint
- Harbor met volledige mirror van benodigde images
- Lokale GitOps met Gitea
- Sync updates wanneer in de haven
De catch: Updates gebeuren in batches tijdens havenaanloop. Het systeem moet weken stabiel draaien tussen syncs.
Scenario 2: Fabrieksvloer
Een productiefabriek draait Kubernetes voor automatisering. Beveiligingsbeleid verbiedt internetconnectiviteit.
Architectuur:
- Volledige Kubernetes met air-gapped installatie
- USB-gebaseerde updates (gesigned en geverifieerd)
- Apart management netwerk voor zeldzame updates
De catch: Updates vereisen fysieke aanwezigheid. Elke wijziging moet bulletproof zijn.
Scenario 3: Mijn Homelab
Ik draai mijn infrastructuur om island-mode capable te zijn. Niet omdat ik disconnectie verwacht, maar omdat het goed ontwerp afdwingt.
Voordelen:
- Geen externe dependencies = geen externe failure modes
- Snellere pulls vanuit lokale registry
- Werkt tijdens ISP storingen
- Werkelijk soevereine infrastructuur
De Trade-offs
Offline Kubernetes is niet gratis:
| Aspect | Verbonden | Offline |
|---|---|---|
| Updates | Continu | Batched |
| Vulnerability info | Real-time | Vertraagd |
| Externe integraties | Makkelijk | Onmogelijk |
| Operationele last | Lager | Hoger |
| Soevereiniteit | Gedeeltelijk | Compleet |
De vraag is niet “wat is beter” — het is “wat heb je nodig?”
K3s: Gebouwd voor de Edge
K3s verdient speciale vermelding. Het is ontworpen voor precies deze use case:
- 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
Met een lokale registry geconfigureerd, draait K3s volledig offline.
Mijn Aanbeveling
Ontwerp voor offline, draai verbonden. Zelfs als je nooit loskoppelt, is de architectuur beter.
Mirror alles. Container images, Helm charts, Git repos. Als het extern is, mirror het.
Test disconnectie. Trek daadwerkelijk de netwerkkabel eruit en kijk wat breekt. Je zult verrast zijn.
Automatiseer de sync. Wanneer verbonden, update mirrors automatisch. Vertrouw niet op handmatige processen.
Documenteer dependencies. Weet precies wat je cluster van de buitenwereld nodig heeft.
Island mode gaat niet over paranoia. Het gaat over het begrijpen van je dependencies. Een systeem dat je offline kunt draaien is een systeem dat je werkelijk begrijpt.
