Veröffentlicht am

Shared Database in Kubernetes: Schema-per-Service richtig umsetzen

Teilen:
Authors

TL;DR

  • Das Schema-per-Service Pattern bietet logische Trennung bei physisch geteilter Datenbank -- ein pragmatischer Mittelweg fuer Teams mit begrenztem DBA-Kapazitaet.
  • PostgreSQL als StatefulSet mit Persistent Volumes laeuft stabil auf Kubernetes, erfordert aber durchdachtes Backup und Failover.
  • Flyway oder Liquibase gehoeren in die CI/CD-Pipeline -- Schema-Migrationen muessen vor dem Service-Deployment laufen.
  • Strikte Zugriffskontrolle pro Schema ueber dedizierte DB-User und Network Policies ist Pflicht.
  • Ab ca. 10+ Services oder stark divergierenden Lastprofilen lohnt sich der Wechsel auf Database-per-Service.

Das Problem: Zu viele Datenbanken, zu wenig Admins

Microservices-Architektur sagt: jeder Service bekommt seine eigene Datenbank. In der Theorie sauber, in der Praxis oft ein Albtraum. Zehn Services bedeuten zehn Datenbankinstanzen mit eigenen Backups, Patches, Monitoring-Dashboards und Failover-Konfigurationen.

Fuer Teams mit ein bis zwei Ops-Leuten ist das nicht tragbar. Die Alternative: eine gemeinsame Datenbankinstanz, in der jeder Service sein eigenes Schema bekommt. Man verliert etwas Autonomie, gewinnt aber drastisch an Betreibbarkeit.

Dieses Pattern heisst "Shared Database with Schema-per-Service" und ist kein Anti-Pattern -- solange man es bewusst einsetzt und die Grenzen kennt.

Schema-per-Service vs. Database-per-Service

KriteriumSchema-per-ServiceDatabase-per-Service
IsolationLogisch (Schema-Ebene)Physisch (eigene Instanz)
BetriebsaufwandNiedrig (1 DB verwalten)Hoch (N Instanzen verwalten)
Unabhaengige SkalierungEingeschraenktVoll moeglich
Technologie-MixNein (gleiche Engine)Ja (PostgreSQL, MongoDB, etc.)
Backup-KomplexitaetEinfach (1 Backup)Hoch (N Backups koordinieren)
Cross-Service QueriesTechnisch moeglich (aber vermeiden)Nicht moeglich
KostenNiedrigHoch
Team-AutonomieEingeschraenktMaximal

Die Entscheidung ist kontextabhaengig. Fuer Teams unter 20 Entwicklern mit weniger als 10 Services ist Schema-per-Service fast immer der bessere Startpunkt.

PostgreSQL StatefulSet auf Kubernetes

Eine PostgreSQL-Instanz laeuft auf Kubernetes als StatefulSet mit Persistent Volume Claims. Hier ein produktionsnahes Setup:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: database
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:16-alpine
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-credentials
              key: password
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        resources:
          requests:
            cpu: "500m"
            memory: "1Gi"
          limits:
            cpu: "2"
            memory: "4Gi"
        volumeMounts:
        - name: postgres-data
          mountPath: /var/lib/postgresql/data
        livenessProbe:
          exec:
            command: ["pg_isready", "-U", "postgres"]
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          exec:
            command: ["pg_isready", "-U", "postgres"]
          initialDelaySeconds: 5
          periodSeconds: 5
  volumeClaimTemplates:
  - metadata:
      name: postgres-data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: fast-ssd
      resources:
        requests:
          storage: 50Gi

Dazu der Service fuer die interne DNS-Aufloesung:

apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: database
spec:
  type: ClusterIP
  ports:
  - port: 5432
    targetPort: 5432
  selector:
    app: postgres

Jeder Microservice verbindet sich ueber postgres.database.svc.cluster.local:5432 mit der Datenbank. Einfach, stabil, kein Service-Mesh noetig.

Zugriffskontrolle: Ein User pro Service

Der kritischste Punkt bei Shared Databases: kein Service darf auf fremde Schemas zugreifen. Das setzt man auf Datenbankebene durch:

#!/bin/bash
# Schema und User fuer einen neuen Service anlegen

SERVICE_NAME=$1
DB_NAME="shared_app"

# Schema erstellen
psql -h postgres.database.svc.cluster.local -U postgres -d $DB_NAME -c "
  CREATE SCHEMA IF NOT EXISTS ${SERVICE_NAME};
"

# Dedizierter User mit Zugriff nur auf sein Schema
psql -h postgres.database.svc.cluster.local -U postgres -d $DB_NAME -c "
  CREATE USER ${SERVICE_NAME}_user WITH PASSWORD '$(openssl rand -base64 24)';
  GRANT USAGE ON SCHEMA ${SERVICE_NAME} TO ${SERVICE_NAME}_user;
  GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA ${SERVICE_NAME} TO ${SERVICE_NAME}_user;
  ALTER DEFAULT PRIVILEGES IN SCHEMA ${SERVICE_NAME}
    GRANT ALL PRIVILEGES ON TABLES TO ${SERVICE_NAME}_user;
  ALTER ROLE ${SERVICE_NAME}_user SET search_path TO ${SERVICE_NAME};
"

Zusaetzlich sollte man den public Schema-Zugriff entfernen:

psql -h postgres.database.svc.cluster.local -U postgres -d $DB_NAME -c "
  REVOKE ALL ON SCHEMA public FROM PUBLIC;
"

Die Credentials kommen als Kubernetes Secret in den jeweiligen Namespace des Service:

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
  namespace: order-service
type: Opaque
stringData:
  DB_HOST: "postgres.database.svc.cluster.local"
  DB_PORT: "5432"
  DB_USER: "orders_user"
  DB_PASSWORD: "generated-password-here"
  DB_NAME: "shared_app"
  DB_SCHEMA: "orders"

Schema-Migrationen mit Flyway

Schema-Migrationen muessen automatisiert und versioniert sein. Flyway ist hier der Standard. Jeder Service verwaltet seine eigenen Migrations-Skripte:

order-service/
  src/
  db/migration/
    V1__create_orders_table.sql
    V2__add_shipping_address.sql
    V3__add_order_status_index.sql

In der CI/CD-Pipeline laeuft die Migration als Init-Container oder als separater Job vor dem Deployment:

apiVersion: batch/v1
kind: Job
metadata:
  name: order-service-migration
  namespace: order-service
spec:
  template:
    spec:
      containers:
      - name: flyway
        image: flyway/flyway:10
        args: ["migrate"]
        env:
        - name: FLYWAY_URL
          value: "jdbc:postgresql://postgres.database.svc.cluster.local:5432/shared_app"
        - name: FLYWAY_SCHEMAS
          value: "orders"
        - name: FLYWAY_USER
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: DB_USER
        - name: FLYWAY_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: DB_PASSWORD
        volumeMounts:
        - name: migrations
          mountPath: /flyway/sql
      volumes:
      - name: migrations
        configMap:
          name: order-service-migrations
      restartPolicy: Never
  backoffLimit: 3

Der entscheidende Punkt: Migrationen laufen immer vor dem Service-Deployment. Nie gleichzeitig, nie danach. ArgoCD oder Flux koennen das ueber Sync-Waves bzw. Dependencies abbilden.

Network Policies: Zugriff einschraenken

Nicht jeder Pod im Cluster sollte die Datenbank erreichen koennen. Network Policies schraenken den Zugriff auf die Services ein, die tatsaechlich berechtigt sind:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-access
  namespace: database
spec:
  podSelector:
    matchLabels:
      app: postgres
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          db-access: "true"
      podSelector:
        matchLabels:
          requires-db: "true"
    ports:
    - port: 5432
      protocol: TCP

Nur Pods mit dem Label requires-db: "true" aus Namespaces mit db-access: "true" koennen Port 5432 erreichen. Alles andere wird geblockt.

Monitoring: Was man beobachten muss

Eine Shared Database wird zum Single Point of Failure, wenn man sie nicht im Blick behaelt. Diese Metriken sind Pflicht:

MetrikWarnung beiKritisch bei
Aktive Verbindungen70% des Limits90% des Limits
Query-Laufzeit (p95)> 200ms> 1s
Disk Usage70%85%
Replication Lag> 5s> 30s
Dead Tuples Ratio> 5%> 15%
Cache Hit Ratio< 95%< 90%

Der postgres_exporter liefert diese Metriken direkt an Prometheus. Mehr zum Thema Monitoring-Setup gibt es unter Kubernetes Observability Stack.

Wann man auf Database-per-Service wechseln sollte

Das Shared-Database Pattern hat klare Grenzen. Der Wechsel ist faellig, wenn:

  • Einzelne Services extreme Last erzeugen und die anderen Services beeintraechtigen. Connection Pooling und Read Replicas helfen nur begrenzt.
  • Services unterschiedliche Datenmodelle brauchen. Ein Service will eine Graph-Datenbank, ein anderer Timeseries -- dann ist PostgreSQL fuer alle der falsche Kompromiss.
  • Team-Autonomie wichtiger wird als Betriebseffizienz. Ab 5+ Teams, die unabhaengig deployen wollen, wird die zentrale Datenbank zum Bottleneck im Deployment-Prozess.
  • Schema-Aenderungen sich gegenseitig blockieren. Wenn Migration A auf ein Lock wartet, das Migration B haelt, hat man ein echtes Problem.

Der Uebergang muss nicht auf einen Schlag passieren. Man kann einzelne Services schrittweise auf eigene Instanzen migrieren, waehrend der Rest auf der Shared Database bleibt.

Backup und Disaster Recovery

Bei einer Shared Database schuetzt ein einziges Backup alle Services -- aber ein fehlgeschlagenes Restore betrifft auch alle Services. Das erhoet die Anforderungen:

#!/bin/bash
# Taegliches Backup mit pg_dump (logisch, pro Schema moeglich)
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups/${TIMESTAMP}"
mkdir -p $BACKUP_DIR

# Vollstaendiges Backup
pg_dump -h postgres.database.svc.cluster.local \
  -U postgres \
  -d shared_app \
  -Fc \
  -f "${BACKUP_DIR}/shared_app_full.dump"

# Einzelne Schemas fuer granulares Restore
for SCHEMA in orders products customers; do
  pg_dump -h postgres.database.svc.cluster.local \
    -U postgres \
    -d shared_app \
    -n $SCHEMA \
    -Fc \
    -f "${BACKUP_DIR}/${SCHEMA}.dump"
done

Fuer Point-in-Time Recovery (PITR) ist WAL-Archivierung Pflicht. Tools wie pgBackRest oder Barman automatisieren das zuverlaessig. Mehr zu Storage-Strategien fuer persistente Daten unter Kubernetes Storage Performance.

DSGVO-Aspekte bei Shared Databases

Eine gemeinsame Datenbank mit personenbezogenen Daten erfordert besondere Sorgfalt:

  • Zugriffstrennung: Jeder Service-User darf nur sein Schema sehen. Das ist technisch durchsetzbar (siehe oben) und muss regelmaessig auditiert werden.
  • Audit Logging: PostgreSQL pgaudit Extension aktivieren, um alle Zugriffe auf personenbezogene Daten zu protokollieren.
  • Verschluesselung: TLS fuer alle Verbindungen zwischen Services und Datenbank. Encryption at Rest ueber die Storage-Ebene.
  • Loeschkonzept: Jeder Service muss seine eigenen Daten loeschen koennen, ohne andere Schemas zu beruehren. Das Schema-per-Service Pattern unterstuetzt das nativ.

Wer sich tiefer mit Compliance-Themen befassen will, findet im Artikel zu DSGVO-Compliance fuer Kubernetes weiterfuehrende Details.

Weiter gedacht

Das Shared-Database Pattern ist ein Startpunkt, kein Endzustand. Es erlaubt Teams, schnell produktiv zu werden, ohne sich in Datenbank-Ops zu verlieren. Aber es erfordert Disziplin: strikte Schema-Trennung, automatisierte Migrationen, saubere Zugriffskontrolle.

Wer mehr ueber die Gesamtarchitektur von Microservices auf Kubernetes erfahren will, findet passende Einstiegspunkte unter Kubernetes Integration Patterns und Monolith zu Microservices Migration.

Falls ihr Unterstuetzung bei der Architektur oder Umsetzung braucht -- meldet euch unter /kontakt. Wir helfen Teams dabei, ihre Kubernetes-Plattform sauber aufzubauen.

Kubernetes-Beratung gesucht?

Wir helfen deutschen Unternehmen bei der Kubernetes-Implementierung, Migration und Optimierung. DSGVO-konform und praxiserprobt.

📖 Verwandte Artikel

Weitere interessante Beiträge zu ähnlichen Themen