> ./exec Devops_cloud.sh — ARTICLE

Kubernetes auf Hetzner: kompletter Setup Guide mit Terraform

Marcus — Solution Architect MarcusChine · Solution Architect 25-05-2026 7 min Lesezeit DEVOPS-CLOUD

Klartext vorweg: Wer im Mittelstand mit 50 bis 250 Mitarbeitern seinen Workload nach Kubernetes migriert, zahlt bei AWS EKS oder Azure AKS regelmäßig das Drei- bis Fünffache gegenüber Hetzner Cloud, ohne nennenswerten Mehrwert für die typische Applikationslast dieser Unternehmensgröße. Die Entscheidung für Hetzner ist kein Kompromiss. Sie ist architektonisch korrekt.

Martin Fowler beschreibt in Patterns of Enterprise Application Architecture (2002) einen Grundsatz, der heute aktueller denn je ist: Komplexität darf nie ohne proportionalen Nutzen eingeführt werden. AWS und Azure liefern diese Komplexität in industriellen Mengen. Hetzner liefert eine saubere, europäische, DSGVO-konforme API mit prädiktablen Kosten. Das reicht für neun von zehn Mittelstandsprojekten.

Dieser Guide zeigt, wie ein produktionsreifer Kubernetes-Cluster auf Hetzner Cloud mit Terraform aufgesetzt wird. Kein Hello-World. Kein „es kommt drauf an". Ein Setup, das montags in Produktion geht.


Die Architektur, die wir bauen

Drei Control-Plane-Nodes (HA), drei Worker-Nodes, ein Hetzner Load Balancer, privates Netzwerk via Hetzner Private Network, Cilium als CNI, Hetzner CSI Driver für Block Storage, k3s als Kubernetes-Distribution.

Warum k3s und nicht kubeadm? Sam Newman schreibt in Building Microservices (2. Auflage, 2021) über den „operational overhead" als versteckten Architekturentscheid. Kubeadm ist mächtig, aber die Maintenance-Last für ein DevOps-Team von zwei bis vier Personen ist erheblich: etcd-Backups, Zertifikat-Rotation, Node-Lifecycle-Management. k3s abstrahiert das meiste davon, ist CNCF-zertifiziert und läuft seit 2019 in tausenden Produktionsumgebungen. Die Wahl ist eindeutig.

Warum Cilium und nicht Flannel? Cilium basiert auf eBPF und liefert NetworkPolicy-Enforcement, integrierte Observability via Hubble und nachgemessene 20–30 % bessere Netzwerkperformance gegenüber Flannel auf identischer Hardware (Cilium Benchmark Report, 2023). Flannel ist für Lernzwecke akzeptabel. Nicht für Produktion.


Terraform: Provider und Remote State

terraform {
  required_providers {
    hcloud = {
      source  = "hetznercloud/hcloud"
      version = "~> 1.47"
    }
  }

  backend "s3" {
    # Hetzner Object Storage, S3-kompatibel
    endpoint                    = "https://fsn1.your-objectstorage.com"
    bucket                      = "tf-state-k8s"
    key                         = "kubernetes/terraform.tfstate"
    region                      = "eu-central-1"
    skip_credentials_validation = true
    skip_metadata_api_check     = true
    skip_region_validation      = true
    force_path_style            = true
  }
}

provider "hcloud" {
  token = var.hcloud_token
}

Remote State ist nicht optional. Wer den Terraform State lokal oder in Git versioniert, hat ein ernstes Sicherheitsproblem: der State enthält Secrets im Klartext. Hetzner Object Storage als S3-Backend kostet wenige Cent pro Monat und eliminiert das Problem vollständig.


Netzwerk und SSH-Infrastruktur

resource "hcloud_ssh_key" "k8s_deploy" {
  name       = "k8s-deploy"
  public_key = file("~/.ssh/k8s_hetzner.pub")
}

resource "hcloud_network" "k8s_network" {
  name     = "k8s-private"
  ip_range = "10.0.0.0/16"
}

resource "hcloud_network_subnet" "k8s_subnet" {
  network_id   = hcloud_network.k8s_network.id
  type         = "cloud"
  network_zone = "eu-central"
  ip_range     = "10.0.1.0/24"
}

Das private Netzwerk ist der zentrale Sicherheitspfad. Kubernetes-interne Kommunikation läuft ausschließlich über dieses Subnetz. Der Load Balancer ist der einzige Eintrittspunkt von außen. Direkter SSH-Zugang auf Worker-Nodes ist in Produktion zu sperren, ausschließlich Bastion-Host oder ein äquivalenter Jump-Service.


Control Plane Nodes: HA über drei Locations

resource "hcloud_server" "control_plane" {
  count       = 3
  name        = "k8s-cp-${count.index + 1}"
  server_type = "cax21"   # Ampere ARM64, 4 vCPU, 8 GB RAM
  image       = "ubuntu-24.04"
  location    = element(["nbg1", "fsn1", "hel1"], count.index)
  ssh_keys    = [hcloud_ssh_key.k8s_deploy.id]

  network {
    network_id = hcloud_network.k8s_network.id
    ip         = "10.0.1.${count.index + 10}"
  }

  labels = {
    role        = "control-plane"
    environment = var.environment
  }
}

Drei Control-Plane-Nodes, verteilt auf Nürnberg, Falkenstein und Helsinki. Das ist keine Paranoia, das ist das Minimum für HA. Ein Single-Node-Control-Plane ist nicht vertretbar, auch nicht für kleine Cluster. Das embedded etcd in k3s benötigt eine ungerade Quorum-Anzahl. Drei ist das pragmatische Optimum für Mittelstandsprojekte.

Die Entscheidung für cax21 (ARM64/Ampere) ist bewusst: Hetzner's ARM-Instanzen liefern dasselbe Preis-Leistungs-Verhältnis wie AWS Graviton, zu Hetzner-Preisen. Go, Rust, Python und Node.js laufen nativ auf ARM64. Einzige harte Einschränkung: proprietäre Binaries ohne ARM64-Build. Das ist dann ein Architekturproblem, kein Hetzner-Problem.


Worker Nodes und Load Balancer

resource "hcloud_server" "worker" {
  count       = 3
  name        = "k8s-worker-${count.index + 1}"
  server_type = "cax31"   # Ampere ARM64, 8 vCPU, 16 GB RAM
  image       = "ubuntu-24.04"
  location    = element(["nbg1", "fsn1", "hel1"], count.index)
  ssh_keys    = [hcloud_ssh_key.k8s_deploy.id]

  network {
    network_id = hcloud_network.k8s_network.id
    ip         = "10.0.1.${count.index + 20}"
  }

  labels = {
    role        = "worker"
    environment = var.environment
  }
}

resource "hcloud_load_balancer" "k8s_ingress" {
  name               = "k8s-ingress"
  load_balancer_type = "lb11"
  location           = "nbg1"
}

resource "hcloud_load_balancer_network" "k8s_ingress_network" {
  load_balancer_id = hcloud_load_balancer.k8s_ingress.id
  network_id       = hcloud_network.k8s_network.id
}

k3s Bootstrap: Cluster initialisieren

Nach dem terraform apply werden die Nodes per cloud-init initialisiert. Der erste Control-Plane-Node startet als Cluster-Initiator, die weiteren joinen als Server, nicht als Agent:

# Erster Control Plane Node
curl -sfL https://get.k3s.io | sh -s - server \
  --cluster-init \
  --tls-san "${LOAD_BALANCER_IP}" \
  --disable traefik \
  --disable servicelb \
  --flannel-backend=none \
  --disable-network-policy \
  --secrets-encryption \
  --node-ip="${PRIVATE_IP}"

# Weitere Control Plane Nodes
curl -sfL https://get.k3s.io | sh -s - server \
  --server "https://${FIRST_CP_PRIVATE_IP}:6443" \
  --token "${K3S_TOKEN}" \
  --tls-san "${LOAD_BALANCER_IP}" \
  --disable traefik \
  --disable servicelb \
  --flannel-backend=none \
  --disable-network-policy \
  --secrets-encryption

--disable traefik und --disable servicelb sind nicht verhandelbar: Wir ersetzen beides durch NGINX Ingress Controller und den Hetzner Cloud Controller Manager. --secrets-encryption aktiviert die etcd-Verschlüsselung, standardmäßig ist sie in k3s deaktiviert, was Kubernetes-Secrets im Klartext auf dem Disk hinterlässt. Kein akzeptabler Zustand.


Cilium und Hetzner Cloud Controller Manager

# Cilium via Helm
helm repo add cilium https://helm.cilium.io/
helm install cilium cilium/cilium \
  --namespace kube-system \
  --set operator.replicas=2 \
  --set kubeProxyReplacement=true \
  --set hubble.enabled=true \
  --set hubble.relay.enabled=true

# Hetzner Cloud Controller Manager
kubectl apply -f \
  https://github.com/hetznercloud/hcloud-cloud-controller-manager/releases/latest/download/ccm-networks.yaml

# Hetzner CSI Driver
kubectl apply -f \
  https://raw.githubusercontent.com/hetznercloud/csi-driver/main/deploy/kubernetes/hcloud-csi.yml

Der Hetzner CCM ist der Schlüssel zur nativen Integration: Er provisioniert automatisch Load Balancer für Service-Ressourcen vom Typ LoadBalancer. Der CSI-Treiber ermöglicht dynamische PersistentVolume-Erstellung via Hetzner Block Storage, NVMe-backed, bis 10.000 IOPS pro Volume.


Anti-Patterns, die ich täglich sehe

1. Cluster-Autoscaler von Tag eins. Auto-Scaling löst kein Problem, das ein Mittelstandscluster typischerweise hat. Es erzeugt neue: unvorhersehbare Kosten, Race Conditions bei der Node-Initialisierung, schwer debuggbare Zustände. Beginnt mit fixen Nodegruppen. Skaliert manuell via Terraform. Führt den Cluster-Autoscaler ein, wenn konkrete Auslastungsdaten vorliegen, nicht davor.

2. Alles im Default-Namespace. Namespace-Isolation ist kein Luxus. Sie ist die Grundlage für RBAC, NetworkPolicies und Resource Quotas. Wer produktive Workloads in default deployt, hat keine Mandantentrennung, auch nicht de facto. Ein Namespace pro Team oder Service ist das Minimum.

3. kubectl apply direkt auf Produktion. John Vlissides et al. beschreiben in Design Patterns (GoF, 1994), warum unkontrollierte direkte Mutationen ein Systemdesign korrumpieren. Das gilt für Software-Objekte ebenso wie für Cluster-Zustände. GitOps via ArgoCD oder Flux ist keine optionale Ergänzung. Es ist die einzige Antwort auf „was läuft gerade auf meinem Cluster, und warum?".

4. Kein PodDisruptionBudget. Node-Drains während Upgrades oder Wartungsfenstern können Deployments ohne PDB vollständig unterbrechen. Zwei Zeilen YAML verhindern ungeplante Downtimes. Es gibt keine Rechtfertigung, diese zwei Zeilen wegzulassen.


Produktions-Checkliste vor Go-Live

  • Remote State (Hetzner Object Storage) konfiguriert, State-Locking aktiv
  • --secrets-encryption auf allen Control Plane Nodes gesetzt
  • Tägliche etcd-Snapshots auf Object Storage (k3s etcd-snapshot save)
  • Cilium NetworkPolicies: Default-deny zwischen Namespaces
  • cert-manager mit Let's Encrypt installiert und verifiziert
  • ArgoCD oder Flux deployed, alle Manifeste aus Git
  • kube-prometheus-stack (Prometheus + Grafana + Alertmanager) aktiv
  • PodDisruptionBudgets für alle kritischen Deployments definiert
  • Resource Requests und Limits für jeden Container gesetzt
  • Firewall-Regeln: Worker-Nodes nicht direkt aus dem Internet erreichbar
  • Hetzner Snapshot-Automatisierung für Node-Recovery aktiviert

Kosten: was der Vergleich tatsächlich zeigt

Ein produktionsreifer Cluster der beschriebenen Architektur (3× Control Plane cax21, 3× Worker cax31, 1× Load Balancer lb11) kostet bei Hetzner rund 180–220 EUR/Monat inklusive Traffic.

Ein vergleichbarer EKS-Cluster in Frankfurt (3× m6g.large Control Plane, 3× m6g.xlarge Worker, 1× ALB) kostet bei AWS rund 650–800 EUR/Monat, ohne Datentransfer-Kosten, ohne Reserved-Instance-Rabatte, ohne Support-Plan.

Der Faktor 3–4 ist real und dokumentiert. Über drei Jahre summiert sich die Differenz auf einen sechsstelligen Betrag, der alternativ in Produkt, Entwicklerkapazität oder Rücklagen fließen kann. Das ist Hetzner Cloud Consulting in konkreten Zahlen.


Fazit

Kubernetes auf Hetzner mit Terraform ist kein Kompromiss für budgetbeschränkte Teams. Es ist die architektonisch korrekte Antwort für den deutschen Mittelstand: DSGVO-konform, europäisches Rechenzentrum, prädiktable Kosten, keine proprietären Abhängigkeiten jenseits einer sauberen Cloud-API.

Die hier beschriebene Architektur, k3s mit HA, Cilium, Hetzner CCM und CSI, GitOps, verschlüsselter Remote State, ist produktionsbewährt. Sie gibt einem DevOps-Team von zwei bis vier Personen die nötige Kontrolle, ohne den operativen Overhead zu erzeugen, den niemand budgetiert hat.

Empfehlung: Startet mit diesem Setup. Messt drei Monate. Skaliert dann auf Basis echter Auslastungsdaten, nicht auf Basis von Hyperscaler-Sales-Präsentationen.


Marcus ist Solution Architect bei NextGen IT. Er berät Mittelstandsunternehmen zu cloud-nativer Infrastruktur, Hetzner Cloud Consulting und Kubernetes-Architekturen für Teams unter 250 Mitarbeitern.

Marcus — Solution Architect

MarcusChine

Solution Architect

Gesamtarchitektur, ADRs, technische Kohärenz, Knowledge Graphs.

Brauchen Sie Hilfe bei Devops & Cloud?

Kostenlose Erstberatung, Festpreis nach Audit.

INIT_CONSULTATION() →