Zum Inhalt springen
Zurück zur Übersicht
March 14, 2026|9 Min. Lesezeit

Kubernetes Multi-Tenancy richtig umgesetzt

Die technischen Entscheidungen hinter unserer Multi-Tenant-Kubernetes-Plattform - warum Kyverno statt OPA, Helm statt Custom Operators, und wie eine einzige values.yaml einen kompletten Tenant bereitstellt.

Jan LauberVon Jan Lauber

Wir betreiben Multi-Tenant-Kubernetes-Cluster für Enterprise-Kunden seit 2021. In dieser Zeit sind wir vom manuellen Anlegen von Namespaces und handgeschriebenen RBAC-YAMLs zu einem System gekommen, in dem das Onboarding eines neuen Teams aus einer einzigen values.yaml und einem Merge Request besteht.

Dieser Beitrag zeigt, welche technischen Entscheidungen wir auf dem Weg dorthin getroffen haben, was uns dabei kaputtgegangen ist und wie die Architektur heute aussieht.

Wo die meisten Setups scheitern

Fast jede Organisation, mit der wir arbeiten, beginnt gleich: Jemand legt einen Namespace an, gibt dem Team cluster-admin (oder etwas in der Art) und macht weiter. Das funktioniert für zwei Teams. Sobald es zehn werden, tauchen die Probleme überall gleichzeitig auf.

Typical Setup
Shared Cluster
team-alpha
team-beta
team-gamma
No resource quotas
No network policies
No image restrictions
Manual RBAC setup
With Tenancy Toolkit
Managed Cluster + Guardrails
team-alpha
QuotasNetPolRBACKyverno
team-beta
QuotasNetPolRBACKyverno
team-gamma
QuotasNetPolRBACKyverno
Enforced quotas & limits
Default-deny network policies
Image allow-lists per tenant
RBAC from Helm chart

Das Problem ist nicht, dass Leute nachlässig wären. Es ist, dass Kubernetes keinen eingebauten Begriff von "Tenant" kennt. Sie bekommen Namespaces, RBAC und Resource Quotas als einzelne Bausteine. Diese über Teams, Umgebungen und Cluster hinweg konsistent zu verdrahten, ist die eigentliche Engineering-Aufgabe.

Wir haben das auf die harte Tour gelernt. Früh hatte ein Staging-Workload eines Kunden keine Resource Quotas. Ein Memory Leak in einer Java-Anwendung verbrauchte 48 GB RAM auf einem geteilten Node und löste OOM-Kills in drei Production-Namespaces anderer Teams aus. Den Fehler hatten wir in 20 Minuten behoben. Das Vertrauen wieder aufzubauen, dauerte Monate.

Die Architektur: Schichten statt Monolithen

Nach einigen Iterationen sind wir bei einer Schichtenarchitektur gelandet. Jede Schicht hat eine Aufgabe und klar gezogene Grenzen.

Observability & AuditMonitoring, logging, alerting across tenants
L6
Platform ServicesSecrets, registry, certificates
L5
Smart GuardrailsKyverno admission, image policies, pod security
L4
RBAC & AuthenticationOIDC/SSO, role bindings, service accounts
L3
Tenant IsolationNamespaces, quotas, network policies
L2
Managed ClusterKubernetes API, node pools, CNI
L1

Die zentrale Erkenntnis: Der Managed Cluster ist nur das Fundament. Erst alles darüber macht den Unterschied zwischen "wir haben Kubernetes" und "wir haben eine Plattform" aus.

Schicht 1 (Managed Cluster) übernimmt die Grundarbeit, die für alle gleich aussieht: Node-Bereitstellung, API-Server, etcd, CNI. Diese Schicht betreiben wir auf eigener Infrastruktur (Natron Cloud) oder auf Kunden-Infrastruktur über Flex Stack.

In den Schichten 2 bis 5 lebt das eigentliche Tenancy-Modell. Und genau dort kommt unser Helm-basiertes Toolkit zum Einsatz.

Warum Helm und kein eigener Operator

Wir haben drei Wege geprüft, um Tenants bereitzustellen:

  1. Manuelle YAMLs in einem Git-Repo. Funktioniert für fünf Tenants. Bricht bei zwanzig zusammen. Jeder Tenant braucht 8 bis 12 Ressourcen, und Copy-Paste-Fehler sind unvermeidlich.
  2. Eigener Kubernetes-Operator. Ein CRD wie Tenant, das alle Ressourcen abgleicht. In der Theorie elegant. In der Praxis pflegen Sie eine Go-Codebase, kümmern sich um Upgrade-Pfade und debuggen Controller-Crashes um drei Uhr morgens.
  3. Helm Chart und ArgoCD. Ein Chart, eine values.yaml pro Tenant, ArgoCD übernimmt den Abgleich. Kein eigener Code, der gepflegt werden muss. Die Templating-Sprache von Helm ist nicht hübsch, aber sie ist erprobt, und jeder Platform Engineer kennt sie ohnehin.

Wir haben Variante 3 gewählt. Das Helm Chart erzeugt aus einer einzigen Values-Datei alles, was ein Tenant braucht:

values.yamlTenant definition
Tenant Helm ChartTemplate rendering
ArgoCDSync to cluster
Namespaces
RBAC & RoleBindings
Network Policies
Kyverno Policies
Vault SecretStores
Registry Pull Secrets

Die values.yaml für einen Tenant sieht so aus:

tenant:
  name: team-data
  namespaces:
    - team-data-dev
    - team-data-staging
    - team-data-prod
  quotas:
    cpu: "8"
    memory: 16Gi
    storage: 100Gi
  rbac:
    clusterRole: namespace-admin
    groups:
      - "oidc:team-data-devs"
  networkPolicy: restricted
  registry:
    project: team-data
    allowList:
      - "registry.natron.io/team-data/**"
      - "docker.io/library/**"
  vault:
    path: kv/team-data/*
  kyverno:
    disallowPrivileged: true
    requireRunAsNonRoot: true
    requireReadOnlyRoot: true

Das ist die komplette Tenant-Definition. Daraus rendert das Helm Chart 15 bis 20 Kubernetes-Ressourcen: Namespaces, Resource Quotas, Limit Ranges, Role Bindings, Network Policies, Kyverno Policies, ClusterSecretStores, Registry Pull Secrets und mehr.

Keine Tickets. Keine manuellen Schritte. Kein "Ich habe die Network Policy vergessen".

Warum Kyverno statt OPA Gatekeeper

Das ist vermutlich die Entscheidung, nach der wir am häufigsten gefragt werden. Beides sind CNCF-Projekte, beide machen Admission Control. Wir haben uns aus drei konkreten Gründen für Kyverno entschieden.

OPA Gatekeeper

Policies in Rego (custom language)

Separate ConstraintTemplates + Constraints

Validation only (no mutation, no generation)

Steep learning curve for platform teams

# Rego policy
violation[{"msg": msg}] {
input.review.object.spec
.containers[_].securityContext
.privileged == true
msg := "privileged not allowed"
}
KyvernoOur choice

Policies in YAML (native to K8s)

Single ClusterPolicy resource

Validate + Mutate + Generate

Platform teams already know YAML

# Kyverno policy
apiVersion: kyverno.io/v1
kind: ClusterPolicy
spec:
rules:
- name: disallow-privileged
match:
resources:
kinds: [Pod]
validate:
deny:
conditions:
- key: privileged
operator: Equals
value: true

Grund 1: Policies in YAML. Plattform-Teams denken bereits in YAML. Sie zusätzlich Rego (die Policy-Sprache von OPA) lernen zu lassen, schafft einen Engpass beim Wissen. In Kyverno sieht eine neue Policy aus wie jedes andere Kubernetes-Manifest, das im Team ohnehin geschrieben wird.

Grund 2: Mutation und Generierung. Kyverno validiert nicht nur. Es kann Ressourcen verändern (Labels einfügen, Defaults setzen) und neue Ressourcen erzeugen (etwa eine NetworkPolicy, sobald ein Namespace angelegt wird). OPA Gatekeeper kann nur validieren. Wir nutzen Mutation intensiv, um konsistente Labels durchzusetzen und Sidecar-Konfigurationen zu injizieren.

Grund 3: Geltungsbereich pro Tenant. Kyverno-Policies lassen sich über Label Selectors auf einzelne Namespaces einschränken. Diese Selectors bauen wir im Helm Chart als Templates auf, sodass jeder Tenant genau die Policies erhält, die in seiner values.yaml definiert sind. Ein Tenant, der PCI-Daten verarbeitet, bekommt strengere Image Policies als ein internes Tooling-Team.

Der Trade-off: Kyverno verbraucht in grossen Clustern (über 100 Policies) mehr Memory als OPA Gatekeeper. Wir entschärfen das, indem wir Kyverno im HA-Modus auf eigenen Node Pools fahren.

Wie das Onboarding tatsächlich abläuft

Wenn ein Kunde ein neues Team auf seine Plattform aufnehmen will, sieht der reale Ablauf so aus:

feat: onboard team-data with full isolation#287
feat/onboard-team-datamain
tenants/team-data/values.yaml+26
1+tenant:
2+ name: team-data
3+ namespaces:
4+ - team-data-dev
5+ - team-data-staging
6+ - team-data-prod
7+ quotas:
8+ cpu: "8"
9+ memory: 16Gi
10+ storage: 100Gi
11+ rbac:
12+ clusterRole: namespace-admin
13+ groups:
14+ - "oidc:team-data-devs"
15+ networkPolicy: restricted
16+ registry:
17+ project: team-data
18+ allowList:
19+ - "registry.natron.io/team-data/**"
20+ - "docker.io/library/**"
21+ vault:
22+ path: kv/team-data/*
23+ kyverno:
24+ disallowPrivileged: true
25+ requireRunAsNonRoot: true
26+ requireReadOnlyRoot: true
Checks
helm/template
kyverno/validate
argocd/sync
vault/secrets
Merged

Der Merge Request durchläuft drei automatisierte Prüfungen, bevor er gemerged werden darf:

  1. helm/template prüft, dass die values.yaml zu gültigen Kubernetes-Manifesten rendert.
  2. kyverno/validate wendet das Policy-Set des Clusters in der CI auf die gerenderten Manifeste an.
  3. argocd/sync läuft als Dry-Run, um Konflikte mit bestehenden Ressourcen zu finden.

Nach dem Merge erkennt ArgoCD die Änderung und gleicht innerhalb von drei Minuten ab. Das neue Team hat seine Namespaces, RBAC, Network Policies, Secrets und seinen Registry-Zugriff. Es kann sofort mit dem Deployen beginnen.

Wenn jemand eine Network Policy manuell löscht oder eine Resource Quota im Cluster anpasst, erkennt ArgoCD den Drift und stellt den in Git definierten Sollzustand wieder her. Wir haben das in der Produktion genau einmal erlebt (ein Engineer, der "nur kurz etwas testen" wollte). Das Self-Healing griff innerhalb von 90 Sekunden.

Woran wir noch arbeiten

Diese Architektur ist nicht perfekt. Ein paar Themen, an denen wir aktuell arbeiten:

Kommunikation zwischen Namespaces. Manche Teams müssen miteinander sprechen. Unsere Default-Deny-Policies blockieren das. Heute lösen wir es über explizite Ausnahmen in den Helm-Values. Das funktioniert, skaliert aber bei 30 Teams mit komplexen Abhängigkeiten nicht mehr gut. Wir prüfen die ClusterWide Network Policies von Cilium für einen deklarativeren Ansatz.

Tenant-bezogene Observability. Jeder Tenant erhält eigene Grafana-Dashboards und Alert Rules, das darunterliegende Prometheus ist aber gemeinsam genutzt. Bei wachsendem Volumen wird die Cardinality zum Problem. Wir prüfen Tenant-Level Metric Isolation über die Multi-Tenancy-Funktionen von Thanos oder Mimir.

Kostenzuordnung. Quotas zeigen, was ein Team nutzen darf, nicht was es tatsächlich nutzt. Wir bauen eine Anbindung an OpenCost, damit jeder Tenant Einblick in seinen realen Ressourcenverbrauch erhält.

Mehr erfahren

Die vollständige Architektur mit interaktiven Diagrammen finden Sie auf unserer Platform Design-Seite. Sie zeigt das verschachtelte Isolationsmodell, den Helm-Render-Flow und den GitOps-Onboarding-Workflow im Detail.

Wenn Sie eine Multi-Tenant-Kubernetes-Plattform aufbauen oder mit Ihrer bestehenden ringen, sprechen wir gerne über die Details. Termin vereinbaren und Architekturdiagramme mitbringen. Das ist ein Design-Engagement, kein Verkaufsgespräch.

Jan Lauber

Über den Autor

Jan Lauber

Cloud Engineer und Partner bei Natron Tech, baut Multi-Tenant-Kubernetes-Plattformen für Enterprise-Organisationen in der Schweiz.

Ein neues Team sollte ein Pull Request sein, kein Support-Ticket.

Nächster Artikel