Konfiguration ist der Punkt, an dem Kubernetes-Tutorials oft leicht aussehen und echte Umgebungen weniger aufgeräumt werden.

Das Container-Image sollte die Anwendung und ihre Laufzeitabhängigkeiten enthalten. Es sollte nicht das Produktionspasswort der Datenbank enthalten, nicht die URL des Staging-Zahlungsdienstes und nicht einen Schalter, der jede Woche geändert wird. Kubernetes stellt dafür ConfigMaps und Secrets bereit: Werte werden aus dem Image herausgelöst und als Objekte im Cluster verwaltet.

Diese Trennung ist nützlich, aber kein Zaubertrick. Eine ConfigMap kann falsch sein. Ein Secret kann aus Versehen in Logs landen. Eine Umgebungsvariable kann von einem Prozess gelesen werden, der sie nicht sehen sollte. Ziel ist nicht, jeden Wert nach Kubernetes zu werfen und sich sicher zu fühlen. Ziel ist, Konfiguration explizit, prüfbar und austauschbar zu machen — ohne das Anwendungs-Image neu zu bauen.

Die grundlegende Trennung

Als einfache Anfangsregel nutze ich:

Eine ConfigMap ist für nicht sensible Konfiguration gedacht: Log-Level, öffentliche Basis-URLs, Feature-Schalter, Queue-Namen, Timeout-Werte oder Anwendungseinstellungen, die kein Vorfall wären, wenn jemand mit normalem Lesezugriff im Namespace sie sieht.

Ein Secret ist für sensible Werte gedacht: Passwörter, Tokens, private Schlüssel, API-Zugangsdaten, Datenbankverbindungen mit Zugangsdaten, Signierschlüssel und alles, das rotiert werden müsste, wenn es in einem Screenshot auftaucht.

Diese Regel ist nicht perfekt. Manche Werte sind durch ihren Kontext sensibel. Ein interner Hostname kann Architektur verraten. Ein Feature-Schalter kann einen kommenden Launch sichtbar machen. Trotzdem verhindert die Regel den schlimmsten Anfängerfehler: Passwörter in ConfigMaps zu legen, weil das YAML gerade bequem ist.

Eine kleine ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: checkout-config
data:
  LOG_LEVEL: info
  PAYMENT_PROVIDER_URL: "https://payments.example.internal"
  FEATURE_NEW_TAX_RULES: "false"

Ein Secret:

apiVersion: v1
kind: Secret
metadata:
  name: checkout-secret
type: Opaque
stringData:
  DATABASE_PASSWORD: "replace-me-in-real-life"
  PAYMENT_API_TOKEN: "replace-me-too"

Mit stringData kann man im Manifest normale Zeichenketten schreiben. Kubernetes speichert sie anschließend im Feld data als Base64. Für Beispiele und manche Workflows ist das bequem. Aber Vorsicht: Ein Manifest mit stringData enthält das Secret im Klartext — solange es in Git, im Terminal oder in einem Review sichtbar ist.

Base64 ist keine Verschlüsselung

Dieser Punkt verdient eine klare Formulierung. Kubernetes-Secrets werden standardmäßig Base64-kodiert. Base64 ist keine Verschlüsselung. Es ist eine Kodierung für den Transport.

Bei:

kubectl get secret checkout-secret -o yaml

kann eine Ausgabe so aussehen:

data:
  DATABASE_PASSWORD: cmVwbGFjZS1tZS1pbi1yZWFsLWxpZmU=

Wer dieses Secret lesen darf, kann den Wert dekodieren:

echo 'cmVwbGFjZS1tZS1pbi1yZWFsLWxpZmU=' | base64 --decode

Secrets sind trotzdem sinnvoll. Kubernetes behandelt sie als eigenen Objekttyp, RBAC kann Zugriff darauf einschränken, kubelet geht in manchen Pfaden anders mit ihnen um als mit ConfigMaps, und Cluster können Secret-Daten in etcd verschlüsselt speichern. Das hilft aber wenig, wenn alle Entwickler breit get secrets dürfen, wenn Secrets in Logs ausgegeben werden oder wenn rohe Secret-Manifeste in einem öffentlichen Repository liegen.

Das passende Modell ist: Ein Kubernetes-Secret ist ein sensibles Konfigurationsobjekt — aber für sich allein kein Passwortmanager.

Umgebungsvariablen: einfach und scharfkantig

Der einfachste Weg, Konfiguration in einen Container zu geben, sind Umgebungsvariablen:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
spec:
  replicas: 2
  selector:
    matchLabels:
      app: checkout
  template:
    metadata:
      labels:
        app: checkout
    spec:
      containers:
        - name: checkout
          image: ghcr.io/example/checkout:1.0.0
          env:
            - name: LOG_LEVEL
              valueFrom:
                configMapKeyRef:
                  name: checkout-config
                  key: LOG_LEVEL
            - name: DATABASE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: checkout-secret
                  key: DATABASE_PASSWORD

Das ist leicht zu verstehen. Die Anwendung liest LOG_LEVEL und DATABASE_PASSWORD aus der Prozessumgebung.

Man kann auch alle Schlüssel auf einmal importieren:

envFrom:
  - configMapRef:
      name: checkout-config
  - secretRef:
      name: checkout-secret

Für wichtige Anwendungen bevorzuge ich explizite env-Einträge. envFrom ist praktisch, aber die Umgebung wird weniger sichtbar im Deployment. Außerdem können versehentlich Namenskollisionen entstehen. Wenn ConfigMap und Secret denselben Schlüssel enthalten, muss man genau verstehen, welche Validierung greift und welche Reihenfolge zählt. Explizite Verdrahtung ist langweilig. Langweilig ist bei Produktionskonfiguration oft gut.

Ein wichtiges Betriebsdetail: Umgebungsvariablen werden beim Start des Containers gesetzt. Wenn ConfigMap oder Secret geändert werden, bekommen laufende Container diese neuen Werte nicht automatisch. Meist braucht es einen Neustart der Pods, zum Beispiel über ein neues Rollout:

kubectl rollout restart deployment/checkout
kubectl rollout status deployment/checkout

Das ist kein Kubernetes-Fehler. Die Prozessumgebung entsteht beim Start eines Prozesses. Kubernetes kann einem laufenden Prozess nicht nachträglich die Konfiguration ändern, die die Anwendung bereits gelesen hat.

Gemountete Dateien: besser für strukturierte Konfiguration

ConfigMaps und Secrets können auch als Dateien eingebunden werden. Das passt oft besser für größere Konfiguration, Zertifikate, Konfigurationsdateien oder Anwendungen, die ohnehin Dateien erwarten.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
spec:
  replicas: 2
  selector:
    matchLabels:
      app: checkout
  template:
    metadata:
      labels:
        app: checkout
    spec:
      containers:
        - name: checkout
          image: ghcr.io/example/checkout:1.0.0
          volumeMounts:
            - name: app-config
              mountPath: /etc/checkout
              readOnly: true
      volumes:
        - name: app-config
          configMap:
            name: checkout-config-files

Dazu eine ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: checkout-config-files
data:
  application.yaml: |
    logLevel: info
    paymentProviderUrl: https://payments.example.internal
    features:
      newTaxRules: false

Im Container erscheint die Datei dann hier:

/etc/checkout/application.yaml

Gemountete ConfigMaps und Secrets können nach einer Objektänderung auf der Platte aktualisiert werden — allerdings nicht sofort. kubelet aktualisiert diese Daten periodisch. Die Anwendung muss die Datei trotzdem erneut lesen oder Änderungen beobachten. Viele Anwendungen lesen Konfiguration nur beim Start. Dann bleibt ein Rollout-Neustart notwendig.

Eine häufige Falle ist subPath. Wird ein einzelner Schlüssel per subPath als Datei gemountet, werden Änderungen an ConfigMap oder Secret nicht auf dieselbe Weise sichtbar. Das überrascht viele. Wenn laufende Aktualisierungen wichtig sind, sollte man dieses Muster vermeiden und das Verhalten im eigenen Cluster testen.

Objekte mit kubectl erstellen und prüfen

Zum Lernen ist kubectl create configmap angenehm:

kubectl create configmap checkout-config \
  --from-literal=LOG_LEVEL=info \
  --from-literal=FEATURE_NEW_TAX_RULES=false

Aus einer Datei:

kubectl create configmap checkout-config-files \
  --from-file=application.yaml

Für Secrets:

kubectl create secret generic checkout-secret \
  --from-literal=DATABASE_PASSWORD='not-for-shell-history-in-real-life' \
  --from-literal=PAYMENT_API_TOKEN='also-rotate-this'

Diese Befehle sind im Labor praktisch. In echten Umgebungen muss man an Shell-History, Terminalaufzeichnungen, CI-Logs und Kopierwege denken. Ein Secret, das mit --from-literal erstellt wird, kann bereits vor Kubernetes sichtbar werden.

Zum Prüfen:

kubectl get configmap checkout-config -o yaml
kubectl describe configmap checkout-config
kubectl get secret checkout-secret
kubectl describe secret checkout-secret

describe secret zeigt Schlüssel und Metadaten, aber keine dekodierten Werte. Das ist Absicht. get secret -o yaml zeigt Base64-kodierte Daten, falls RBAC diesen Zugriff erlaubt.

Was ein laufender Pod wirklich bekommen hat, prüft man direkt:

kubectl exec -it <pod-name> -- printenv | sort
kubectl exec -it <pod-name> -- ls -la /etc/checkout
kubectl exec -it <pod-name> -- cat /etc/checkout/application.yaml

printenv sollte man in Produktions-Pods nicht sorglos verwenden, wenn dabei Secrets in Logs, Screenshots oder Chatverläufen landen können. Es ist ein Debug-Werkzeug, keine Gewohnheit.

Optionale Referenzen und fehlende Starts

Wenn eine referenzierte ConfigMap oder ein Secret fehlt, startet der Pod standardmäßig nicht. Das ist meistens gut. Ein fehlendes Datenbankpasswort soll auffallen.

Man sieht manchmal optional: true:

env:
  - name: OPTIONAL_BANNER_TEXT
    valueFrom:
      configMapKeyRef:
        name: checkout-config
        key: BANNER_TEXT
        optional: true

Das sollte sparsam eingesetzt werden. Optionale Konfiguration kann für ein unkritisches Detail sinnvoll sein. Für Werte, die die Anwendung wirklich braucht, ist es gefährlich. Anfängerfreundliche Systeme scheitern laut, wenn notwendige Konfiguration fehlt. Stille Defaults machen aus einem Deployment-Problem leicht ein fachliches Problem.

Rollouts und Konfigurationsänderungen

Eine häufige Überraschung: Eine geänderte ConfigMap erzeugt nicht automatisch ein neues Deployment-Rollout. Das Pod-Template hat sich nicht geändert — also hat der Deployment-Controller keinen Grund, Pods zu ersetzen.

Manche Teams lösen das mit einer Checksum-Annotation im Pod-Template, häufig erzeugt durch Helm oder ein anderes Deployment-Werkzeug. Wenn ConfigMap oder Secret sich ändern, ändert sich die Checksumme, dadurch ändert sich das Pod-Template, und Kubernetes rollt das Deployment neu aus.

Die Grundidee sieht so aus:

spec:
  template:
    metadata:
      annotations:
        checksum/config: "generated-hash-of-config"

Diese Prüfsumme sollte man nicht von Hand pflegen. Wenn der Workflow sie braucht, sollte Werkzeug sie erzeugen.

Zum Lernen reicht ein manueller Neustart:

kubectl rollout restart deployment/checkout
kubectl rollout status deployment/checkout

Danach prüfen:

kubectl get pods -l app=checkout
kubectl describe pod <new-pod-name>

Sicherheitsgewohnheiten, die zählen

Secrets brauchen langweilige Disziplin.

Keine echten Secret-Manifeste in Git committen. Besser ist ein geregelter Weg über externe Secret-Operatoren, Sealed Secrets, SOPS, Cloud Secret Manager, Vault oder das Werkzeug, das die eigene Organisation trägt. Das konkrete Werkzeug ist weniger wichtig als ein wiederholbarer Ablauf ohne Klartext-Secrets im Repository.

RBAC eng halten. Wer in einem Namespace get secrets darf, kann die Werte meistens lesen. Wer Pods erstellen darf, kann Secrets eventuell in einen Pod mounten und indirekt lesen. Rollen müssen beide Wege berücksichtigen.

Secret-Daten im Speicher des Control Plane Backends sollten verschlüsselt werden, wenn man den Control Plane selbst betreibt. Verwaltete Kubernetes-Plattformen bieten dafür oft Einstellungen oder Integrationen. Man sollte aber nicht blind annehmen, dass sie genau der eigenen Richtlinie entsprechen.

Secrets gehören nicht in Logs. Dazu zählen Startlogs, die vollständige Konfiguration ausgeben, Fehlermeldungen mit Verbindungszeichenketten, Debug-Endpunkte mit Umgebungsausgaben und Support-Skripte, die zu viel sammeln.

Secrets müssen rotierbar sein. Rotation ist nicht nur für Vorfälle. Wenn ein Zugang nicht ohne Panik rotiert werden kann, ist das System betrieblich noch nicht gesund.

Was wohin gehört

So sortiere ich es im Kopf:

Anwendungsprogramm und Abhängigkeiten gehören ins Image.

Umgebungsspezifische, nicht sensible Werte gehören in ConfigMaps.

Sensible Werte gehören in Secrets — oder, in reiferen Umgebungen, in ein externes Secret-System, das nach Kubernetes synchronisiert.

Große strukturierte Dateien können aus ConfigMaps oder Secrets gemountet werden.

Werte, die Anwendungsverhalten ändern, sollten im Deployment-Prozess sichtbar sein. Ein Feature-Schalter, der heimlich in einer unreviewten ConfigMap geändert wird, kann so riskant sein wie ein Code-Deploy.

Konfiguration ist Teil des Releases. Sie verdient dieselbe Ernsthaftigkeit wie Code: prüfen, testen, ausrollen — und wissen, wie man zurückrollt.

Eine kleine Checkliste

Wenn ein Pod nicht die erwartete Konfiguration hat, hilft diese Reihenfolge:

  1. Existiert ConfigMap oder Secret im selben Namespace wie der Pod?
  2. Stimmt der Schlüsselname exakt, inklusive Groß- und Kleinschreibung?
  3. Referenziert das Deployment den richtigen Objektnamen?
  4. Kommt der Wert über env, envFrom oder ein gemountetes Volume?
  5. Wurde der Pod nach einer Änderung an Umgebungsvariablen neu gestartet?
  6. Liest die Anwendung wirklich den Wert, von dem man ausgeht?
  7. Verändern RBAC, Admission Policies oder externe Secret-Controller das Verhalten?

Die meisten Konfigurationsvorfälle sind nicht dramatisch. Es sind kleine Abweichungen, die bestehen bleiben, weil niemand den tatsächlichen Pod geprüft hat. Kubernetes gibt die Werkzeuge, um diesen Vertrag zu inspizieren. Man sollte sie nutzen, bevor man rät.