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 provisioniert

Jan LauberVon Jan Lauber

Wir betreiben Multi-Tenant-Kubernetes-Cluster für Enterprise-Kunden seit 2021. In dieser Zeit sind wir von manueller Namespace-Provisionierung und handgeschriebenen RBAC-YAMLs zu einem System gekommen, bei dem das Onboarding eines neuen Teams eine einzige values.yaml-Datei und ein Merge Request ist.

Dieser Beitrag beschreibt die technischen Entscheidungen, die wir dabei getroffen haben, was dabei kaputtgegangen ist und wie die Architektur heute aussieht.

Wo die meisten Setups scheitern

Fast jede Organisation, mit der wir arbeiten, startet gleich: Jemand erstellt einen Namespace, gibt dem Team cluster-admin (oder etwas Ähnliches) und geht weiter. Das funktioniert für zwei Teams. Sobald es zehn sind, tauchen überall Probleme 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 sind. Es ist, dass Kubernetes kein eingebautes Konzept eines "Tenants" hat. Man bekommt Namespaces, RBAC und Resource Quotas als separate Primitiven. Diese konsistent über Teams, Umgebungen und Cluster hinweg zu verdrahten, ist die eigentliche Engineering-Herausforderung.

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-Applikation verbrauchte 48 GB RAM auf einem Shared Node, was OOM Kills in drei Production-Namespaces anderer Teams auslöste. Der Fix dauerte 20 Minuten. Das Vertrauen wiederherzustellen dauerte Monate.

Die Architektur: Schichten statt Monolithen

Nach einigen Iterationen haben wir uns auf eine geschichtete Architektur festgelegt. Jede Schicht hat eine Aufgabe und klare 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. Alles darüber ist das, was den Unterschied macht zwischen "wir haben Kubernetes" und "wir haben eine Plattform".

Schicht 1 (Managed Cluster) übernimmt die undifferenzierte Schwerstarbeit - Node-Provisionierung, API Server, etcd, CNI. Das betreiben wir auf unserer eigenen Infrastruktur (Natron Cloud) oder auf Kunden-Infrastruktur via Flex Stack.

Schichten 2-5 sind dort, wo das Tenancy-Modell lebt. Und hier kommt unser Helm-basiertes Toolkit ins Spiel.

Warum Helm und nicht ein Custom Operator

Wir haben drei Ansätze für Tenant-Provisionierung evaluiert:

  1. Manuelle YAMLs in einem Git-Repo. Funktioniert für fünf Tenants. Scheitert bei zwanzig. Jeder Tenant braucht 8-12 Ressourcen, und Copy-Paste-Fehler sind unvermeidlich.
  2. Custom Kubernetes Operator. Ein CRD wie Tenant, das alle Ressourcen reconciled. Elegant in der Theorie. In der Praxis wartet man eine Go-Codebase, kümmert sich um Upgrade-Pfade und debuggt Controller-Crashes um 3 Uhr morgens.
  3. Helm Chart + ArgoCD. Ein Chart, eine values.yaml pro Tenant, ArgoCD übernimmt die Reconciliation. Kein Custom Code zu pflegen. Die Helm-Templating-Sprache ist hässlich, aber sie ist kampferprobt und jeder Platform Engineer kennt sie bereits.

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

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 gesamte Tenant-Definition. Das Helm Chart rendert daraus 15-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. Beide 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: YAML-native Policies. Platform-Teams denken bereits in YAML. Sie aufzufordern, Rego (OPAs Policy-Sprache) zu lernen, schafft einen Wissens-Engpass. Mit Kyverno sieht eine neue Policy aus wie jedes andere Kubernetes-Manifest, das sie bereits schreiben.

Grund 2: Mutation und Generation. Kyverno validiert nicht nur. Es kann Ressourcen mutieren (Labels injizieren, Defaults setzen) und neue Ressourcen generieren (eine NetworkPolicy erstellen, wenn ein Namespace angelegt wird). OPA Gatekeeper kann nur validieren. Wir nutzen Mutation intensiv, um konsistentes Labeling durchzusetzen und Sidecar-Konfigurationen zu injizieren.

Grund 3: Per-Tenant-Scoping. Kyverno Policies können über Label Selectors auf spezifische Namespaces beschränkt werden. Wir templaten diese Selectors im Helm Chart, sodass jeder Tenant genau die Policies bekommt, 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 mehr Memory als OPA Gatekeeper in grossen Clustern (100+ Policies). Wir mitigieren das durch Kyverno im HA-Modus mit dedizierten Node Pools.

Wie Onboarding tatsächlich funktioniert

Wenn ein Kunde ein neues Team zu seiner Plattform hinzufügen möchte, ist das der tatsächliche Workflow:

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 Checks, bevor er gemerged werden kann:

  1. helm/template validiert, dass die values.yaml gültige Kubernetes-Manifeste rendert
  2. kyverno/validate führt das Policy-Set des Clusters gegen die gerenderten Manifeste in der CI aus
  3. argocd/sync führt einen Dry-Run-Sync durch, um Konflikte mit bestehenden Ressourcen zu erkennen

Nach dem Merge erkennt ArgoCD die Änderung und synchronisiert innerhalb von 3 Minuten. Das neue Team hat seine Namespaces, RBAC, Network Policies, Secrets und Registry-Zugriff. Sie können sofort mit dem Deployen beginnen.

Falls jemand manuell eine Network Policy löscht oder eine Resource Quota im Cluster verändert, erkennt ArgoCD den Drift und synchronisiert zurück auf den gewünschten Zustand in Git. Wir haben das genau einmal in Production 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. Einige Dinge, an denen wir aktiv arbeiten:

Cross-Namespace-Kommunikation. Manche Teams müssen miteinander kommunizieren. Unsere Default-Deny Network Policies blockieren das. Heute lösen wir das mit expliziten Ausnahmen in den Helm-Chart-Values. Es funktioniert, skaliert aber nicht gut bei 30 Teams mit komplexen Dependency-Graphen. Wir evaluieren Ciliums ClusterWide Network Policies für einen deklarativeren Ansatz.

Tenant-spezifische Observability. Jeder Tenant bekommt eigene Grafana-Dashboards und Alert Rules, aber das darunterliegende Prometheus ist shared. Bei Skalierung wird Cardinality zum Problem. Wir prüfen Tenant-Level Metric Isolation via Thanos oder Mimir Multi-Tenancy Features.

Kostenattribution. Quotas sagen, was ein Team nutzen darf, nicht was es tatsächlich nutzt. Wir bauen Integration mit OpenCost, um jedem Tenant Sichtbarkeit in seinen tatsächlichen Ressourcenverbrauch zu geben.

Mehr erfahren

Wir haben die vollständige Architektur mit interaktiven Diagrammen auf unserer Platform Design-Seite dokumentiert. Sie zeigt das geschachtelte Isolationsmodell, den Helm-Render-Flow und den GitOps-Onboarding-Workflow im Detail.

Wenn Sie eine Multi-Tenant-Kubernetes-Plattform aufbauen, oder mit Ihrer bestehenden kämpfen, sprechen wir gerne über die Details. Termin vereinbaren und bringen Sie Ihre Architekturdiagramme mit. 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