EN EnglishDE Deutsch

Kafka in Produktion: von null zum Profi (Teil 5)

Dutzende Heissluftballone schweben bei Sonnenaufgang über einem weiten Tal
Foto von Daniel Seßler / Unsplash

Kafka in Produktion – wir bauen einen skalierbaren, sicheren und Enterprise-tauglichen Message Bus auf der Google Cloud Platform. Jetzt geht es in die Cloud, genauer zum Hyperscaler Google Cloud und auf die Google Kubernetes Engine.
opensight.ch - roman hüsler

Inhaltsverzeichnis

TL;DR

Ich empfehle zwar, die Blogposts zu lesen, aber ich weiss, dass manche direkt zur «Single Source of Truth» springen: zum Code. Das ist ungefähr so, wie an einem Freitag ungetestet zu deployen.
Bitte sehr:

Einführung

Apache Kafka ist eine leistungsfähige Event-Streaming-Plattform, doch der Betrieb in Produktion ist eine ganz andere Herausforderung. Wir beginnen mit den Grundlagen, aber das hier ist kein Spielplatz: Schritt für Schritt bauen wir einen Message Bus in Enterprise-Qualität, der Hochverfügbarkeit, Sicherheit und Skalierbarkeit für echte Workloads bietet.
Ich bin selbst kein Kafka-Spezialist, ich lerne unterwegs. Da wir inzwischen bei Teil 5 angekommen sind, muss ich allerdings sagen: Über den Betrieb von Kafka in Produktion in einer Enterprise-Umgebung habe ich in der Zwischenzeit eine Menge gelernt. Wie so oft bei den opensight-Blogs können Sie mich auf meinem Weg von null zum Profi begleiten.

In dieser Blogserie gehen wir mit Kafka über die Grundlagen hinaus. Wir entwerfen ein produktionsreifes Kafka-Cluster-Deployment, das folgende Eigenschaften hat:

  • Skalierbar – bewältigt wachsende Last ohne Performance-Engpässe.
  • Sicher – setzt Authentifizierung (Plain, SCRAM, OAUTH), Autorisierung (ACL, OAUTH) und Verschlüsselung (TLS) durch.
  • Sauber strukturiert – inklusive Schema Registry für eine ordentliche Data Governance.
  • Enterprise-tauglich – mit den Funktionen, die eine ereignisgesteuerte Architektur in einer grossen Umgebung braucht.

In diesem fünften Teil unserer Kafka-Blogserie deployen wir einen produktionsreifen Apache-Kafka-Cluster auf Google Kubernetes Engine (GKE) und nutzen alle Funktionen, die es für Zuverlässigkeit, Skalierbarkeit und Sicherheit braucht.

Dafür verwende ich Strimzi, einen Open-Source-Kubernetes-Operator, der den Betrieb von Apache-Kafka-Clustern auf Kubernetes (und OpenShift) auf wirklich Cloud-native Weise vereinfacht. Strimzi folgt dem Operator-Pattern und automatisiert Deployment, Verwaltung, Skalierung, Upgrades und Konfiguration der Kafka-Komponenten.

Statt komplexe Elemente wie Broker, Metadaten-Quorum (bei neueren Kafka-Versionen über KRaft), Topics und Benutzer von Hand zu konfigurieren, deklarieren Sie alles über Kubernetes Custom Resources (CRs). Dazu gehört die zentrale Ressource Kafka, kombiniert mit KafkaNodePool für eine flexible Zuweisung der Node-Rollen im KRaft-Modus.

Strimzi Kafka ist weit verbreitet, wenn Kafka produktionsreif auf Kubernetes laufen soll.
opensight.ch - roman huesler

Inhalt dieses Teils

Es gibt einiges zu tun:

Überblick über die Kafka-Architektur in der Cloud

Architekturdiagramm: client 1 verbindet sich über einen Google Load Balancer (bootstrap sowie LB 1 bis LB 3) mit drei von Strimzi verwalteten Kafka-Brokern auf Port 9094 mit SSL-(TLS-)Verschlüsselung und SASL Oauthbearer. Die Broker kommunizieren untereinander, nutzen KRaft-Controller auf Port 9093 und holen Berechtigungen bei Keycloak ab; daneben läuft der Strimzi Operator.
Abbildung – Kafka-Cluster-Setup dieses Blogs

In diesem Setup verwenden und testen wir endlich alles, was ein produktionsreifes Deployment braucht:

  • Google Cloud Kubernetes Engine
  • Strimzi Kafka Operator 0.48
  • Kafka im KRaft-Modus
  • Verschlüsselte Kommunikation (TLS) mit den Clients
  • OAuth-Authentifizierung, Strimzi Keycloak Authorizer
  • Topic-Replikationsfaktor 3
  • Skalierbarkeit des Kafka-Clusters über Strimzi Cruise Control
  • Ein LoadBalancer-Service für den Bootstrap (für den ersten Verbindungsaufbau der Clients und das Abrufen der Metadaten) und ein separater LoadBalancer-Service pro Broker (warum? Lesen Sie weiter …)

Setup des Google-Kubernetes-Clusters

Der Aufbau eines Google-Kubernetes-Engine-Clusters (GKE) steht in diesem Artikel nicht im Mittelpunkt. Eine ausführliche Anleitung, wie Sie mit Terraform einen produktionsreifen GKE-Cluster provisionieren, finden Sie in unserem separaten Blogpost:

Terraform - Deploy GKE Cluster inkl. Workloads
Praktischer Guide für ein GKE Kubernetes Deployment mithilfe von Terraform inkl. Workloads (NGINX Ingress Controller, Certmanager, Testapp)
Eine geschnitzte Baumfigur hebt vor blauem Hintergrund winkend die Hand

Für diesen Blogpost und die Demo im Labor habe ich mit Terraform einen GKE-Cluster mit einem eigenen Node-Pool aus nur einem Node provisioniert. Das hält die Kosten für Experimente und Tests tief.

In einer echten Produktionsumgebung sollten Sie jedoch selbstverständlich einen eigenen Node-Pool mit mindestens drei Nodes verwenden (idealerweise verteilt auf mehrere Availability Zones, also ein regionaler GKE-Cluster). Konfigurieren Sie ausserdem Pod-Anti-Affinity-Regeln (und optional Topology Spread Constraints) für die Kafka-Pods, um Hochverfügbarkeit sicherzustellen: So landen nicht mehrere Broker-Replicas auf demselben Node, und das Risiko gleichzeitiger Ausfälle sinkt.

Strimzi macht das einfach: Sie können diese Regeln direkt im Abschnitt template.pod.affinity Ihrer Kafka- oder KafkaNodePool-Ressourcen (bei KRaft-Deployments) definieren.

Strimzi-Kafka-Setup

Nun zum Setup des Strimzi-Kafka-Clusters auf Kubernetes. Werfen Sie kurz einen Blick auf die Website https://strimzi.io/. Mit Strimzi betreiben Sie Kafka auf Ihrem Kubernetes in verschiedenen Deployment-Konfigurationen, und es ist in produktiven Setups weit verbreitet.

Strimzi Kubernetes Operator

Als Basis deployen wir als Nächstes den Strimzi Cluster Operator. Er übernimmt die ganze Schwerarbeit: Er automatisiert Deployment, Skalierung (nach oben und unten), Upgrades, Monitoring und Wartung der Kafka-Komponenten.

Für dieses Labor-Setup habe ich ihn unverändert über das offizielle Helm-Chart mit den Standardwerten installiert; für einen schnellen Start braucht es keine eigenen Overrides. Hier der Befehl, den ich verwendet habe (mit der damals aktuellen stabilen Version, etwa 0.48.x):

strimzi-kafka-operator/helm-charts/helm3/strimzi-kafka-operator at main · strimzi/strimzi-kafka-operator
Apache Kafka® running on Kubernetes. Contribute to strimzi/strimzi-kafka-operator development by creating an account on GitHub.
Strimzi-Logo mit dem Slogan «Apache Kafka running on Kubernetes and OpenShift»

Kafka-Cluster-Ressourcen

Für die Demonstrationen in diesem Blogpost habe ich den Kafka-Cluster (und zugehörige Ressourcen wie Topics und Benutzer) mit einem eigenen Helm-Chart deployt, das die nötigen Strimzi Custom Resources (CRs) bündelt.

Hier das YAML meines Kafka-Clusters, ein Teil des Helm-Charts:

apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
  name: kafka-cluster
  namespace: kafka
  labels:
    app: {{ include "opensight-strimzi-deployment.fullname" . }}
    chart: {{ include "opensight-strimzi-deployment.chart" . }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}
spec:
  kafka:
    version: "4.1.0"
    # OAuth-based authorization using KeycloakAuthorizer for KRaft mode
    authorization:
      type: keycloak
      authorizerClass: "io.strimzi.kafka.oauth.server.authorizer.KeycloakAuthorizer"
      tokenEndpointUri: "https://auth.opensight.ch/auth/realms/master/protocol/openid-connect/token"
      clientId: "kafka-broker"
      delegateToKafkaAcls: false
    
    listeners:
      - name: controller
        port: 9093
        type: "internal"
        tls: false
      
      - name: plain
        port: 9092
        type: "internal"
        tls: false
      
      - name: external
        port: 9094
        type: "loadbalancer"
        tls: true
        authentication:
          type: oauth
          clientId: "kafka-broker"
          clientSecret:
            secretName: "kafka-oauth-client-secret"
            key: "client-secret"
          tokenEndpointUri: "https://auth.opensight.ch/auth/realms/master/protocol/openid-connect/token"
          validIssuerUri: "https://auth.opensight.ch/auth/realms/master"
          jwksEndpointUri: "https://auth.opensight.ch/auth/realms/master/protocol/openid-connect/certs"
          userNameClaim: "preferred_username"
        configuration:
          # Bootstrap LoadBalancer service (for initial client connection and metadata discovery)
          # Per-broker LoadBalancer services (one per broker for direct connections)
          brokers:
            - broker: 0
            - broker: 1
            - broker: 2
    
    # Kafka configuration
    config:
      offsets.topic.replication.factor: 3
      transaction.state.log.replication.factor: 3
      transaction.state.log.min.isr: 2
      default.replication.factor: 3
      min.insync.replicas: 2
      inter.broker.protocol.version: "3.6"
      log.message.format.version: "3.6"
      # KeycloakAuthorizer configuration properties
      strimzi.authorization.token.endpoint.uri: "https://auth.opensight.ch/auth/realms/master/protocol/openid-connect/token"
      strimzi.authorization.client.id: "kafka-broker"
      strimzi.authorization.allow.everyone.if.no.acl.found: "false"
  
  entityOperator:
    topicOperator: {}
    userOperator: {}
  
  cruiseControl:
    config:
      anomaly.detection.interval.ms: "300000"
      max.num.cluster.movements: "1250"
      metrics.window.ms: "300000"
      num.concurrent.partition.movements.per.broker: "5"
      num.metrics.windows: "20"
      num.proposal.precompute.threads: "1"
      sample.loading.check.interval.ms: "5000"
      self.healing.broker.failure.enabled: "true"
      self.healing.disk.failure.enabled: "true"
      self.healing.enabled: "true"
      self.healing.goal.violation.enabled: "true"
      topic.config.provider.class: "com.linkedin.kafka.cruisecontrol.config.BrokerSetAwareTopicConfigProvider"
    resources:
      limits:
        cpu: 500m
        memory: 1Gi
      requests:
        cpu: 200m
        memory: 512Mi

Nachdem die Ressourcendefinitionen angewendet waren, habe ich dem Strimzi-Operator etwas Zeit gegeben, um den Cluster, die Load Balancer auf Google Cloud, die Zertifikate, die Benutzer und das test-topic einzurichten.

k9s-Ansicht der Pods im Namespace kafka: der Entity Operator und drei Kafka-Nodepool-Pods, alle im Status Running auf demselben GKE-Node.
Abbildung – drei Kafka-Broker laufen

Ja, alle Pods laufen auf demselben einzelnen Node. Wie erwähnt diente das nur dazu, im Demo-Labor Kosten zu sparen. Mir ist ausserdem aufgefallen, dass standardmässig keine Pod-Anti-Affinity-Regeln definiert sind; darum müsste man sich also selbst kümmern.

Ein eigener Load Balancer pro Broker

k9s-Ansicht der Services im Namespace kafka: zwei ClusterIP-Services sowie je ein LoadBalancer-Service für jeden der drei Nodepool-Broker und für den externen Bootstrap, jeweils mit eigener externer IP.
Abbildung – wir verwenden einen Load Balancer pro Broker

Ich habe mich gefragt, warum man pro Broker einen Load Balancer verwenden sollte, obwohl er den Verkehr nur an einen einzigen Broker-Pod weiterleitet, denn das hat Kostenfolgen. Mehr zum Thema finden Sie im Strimzi-Blog. Kurz gesagt: Wir nutzen den Load Balancer des Hyperscalers, weil der Verkehr damit direkt zum richtigen Node gelangt, ohne Umwege über andere Nodes:

When Kubernetes creates the load balancer, they usually target it to all nodes of your Kubernetes cluster and not only to the nodes where your application is actually running. That means that although the TCP connections will always end on the same node in the same broker, they might be routed through the other nodes of your cluster. When the connection is sent by the load balancer to the node which does not host the Kafka broker, the kube-proxy component of Kubernetes will forward it to the right node where the broker runs.

Konfiguration des Keycloak OAuth Authorizers

Lesen Sie Teil 4 dieser Blogserie, um mehr über die Keycloak-Integration zu erfahren. Jeder Client, der sich mit Kafka verbindet, holt sich zuerst ein Autorisierungstoken von unserem Keycloak-Server.

In Keycloak habe ich einen Client eingerichtet, mit dem die Broker ein Token abrufen und untereinander kommunizieren können.

Screenshot der Keycloak-Admin-Konsole mit den Client-Details von kafka-broker: OpenID Connect, Client-Authentifizierung und Autorisierung aktiviert, Direct Access Grants und Service-Account-Rollen gewählt.
Abbildung – Kafka-Broker-Client in Keycloak

Das Secret des Keycloak-Clients kafka-broker (unter «Credentials») habe ich in ein Kubernetes-Secret gelegt.

kubectl create secret generic kafka-oauth-client-secret \
  --from-literal=client-secret='YOUR_CLIENT_SECRET' \
  -n kafka

Es wird hier verwendet: https://github.com/butschi84/kafka-enterprise-deployment/blob/main/5_cluster_google_kubernetes_engine/helm-chart/templates/kafka-cluster.yaml#L41

Test-Applikation verbinden

Einen Kafka-Benutzer habe ich mit Strimzi hier deployt. Strimzi hat mit seiner CA ein Zertifikat erzeugt, mit dem sich der Benutzer verbindet. Exportieren wir die Zertifikate und den privaten Schlüssel und verbinden dann die Test-Applikation. Die Test-Applikation finden Sie hier.

# get kafka cluster ca-certificate
kubectl get secret -n kafka kafka-user -o jsonpath='{.data.ca\.crt}' | base64 -d > ca.crt

# get kafka-user certificate
kubectl get secret -n kafka kafka-user -o jsonpath='{.data.user\.crt}' | base64 -d > user.crt

# get kafka-user private key
kubectl get secret -n kafka kafka-user -o jsonpath='{.data.user\.key}' | base64 -d > user.key
Screenshot der Test-App, verbunden mit 34.65.5.141:9094 über OAUTHBEARER mit CA-Zertifikat, Client-Zertifikat, Client-Key und Keycloak-Token-Endpunkt; die Nachricht «hello production kafka» wurde gesendet und empfangen.
Abbildung – Verbindung zum Kafka-Cluster mit Mutual-TLS-Authentifizierung, OAuth-Autorisierung und TLS-Verschlüsselung

Der Cluster funktioniert einwandfrei. Ich kann Nachrichten produzieren und empfangen.

Cluster-Skalierung mit Strimzi Cruise Control

Zuerst versuche ich, den Cluster von 3 auf 5 Broker hochzuskalieren. Die Test-Applikation in unserem Git-Repository hat ein kleines Update erhalten und kann jetzt Replikationsstatistiken der Kafka-Topics anzeigen. Wie erwartet sind die 3 Partitionen im Moment auf 3 Broker verteilt.

Replikationsstatistik der Test-App für test-topic: 3 Partitionen mit Replikationsfaktor 3, alle Replicas in Sync und alle Partitionen auf 3 Brokern als Healthy markiert.
Abbildung – Replikationsstatus des Kafka-Clusters mit 3 Brokern vor der Skalierung

Simulieren wir ein realistisches Szenario: Der Verkehr auf unserem Kafka-Cluster hat deutlich zugenommen, und das bisherige Setup mit 3 Brokern reicht nicht mehr aus. Horizontale Skalierung ist eine der Stärken von Kafka, und Strimzi macht sie ausgesprochen einfach.

Um von 3 Brokern auf 5 zu skalieren, passen wir einfach das Feld spec.replicas in der KafkaNodePool-Ressource der Broker an (ausgehend von einem gängigen Produktions-Setup mit getrennten Controller- und Broker-Pools im KRaft-Modus).

# scale nodepool to 5 replicas
kubectl patch kafkanodepool kafka-cluster-nodepool -n kafka --type='json' -p='[{"op": "replace", "path": "/spec/replicas", "value": 5}]'

# add listeners for new brokers to route traffic to
kubectl patch kafka kafka-cluster -n kafka --type='json' -p='[{"op": "replace", "path": "/spec/kafka/listeners/2/configuration/brokers", "value": [{"broker": 0}, {"broker": 1}, {"broker": 2}, {"broker": 3}, {"broker": 4}]}]'

# we need now 5 partitions
kubectl patch kafkatopic test-topic -n kafka --type='json' -p='[{"op": "replace", "path": "/spec/partitions", "value": 5}]'

# strimzi cruise control - rebalance whole cluster
kubectl apply -f - <<EOF
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaRebalance
metadata:
  name: kafka-cluster-rebalance
  namespace: kafka
  labels:
    strimzi.io/cluster: kafka-cluster
spec:
  mode: "full"
EOF

# approve the rebalance operation
kubectl annotate kafkarebalance kafka-cluster-rebalance -n kafka \
  strimzi.io/rebalance=approve
k9s-Describe-Ansicht der Ressource KafkaRebalance kafka-cluster-rebalance im Modus full, mit der Bedingung PendingProposal.
Abbildung – Rebalancing-Vorgang im Strimzi-Cluster

Nachdem ich Strimzi Cruise Control etwas Zeit gegeben hatte, den Rebalancing-Vorschlag zu erzeugen und anzuwenden, lief alles wie am Schnürchen.

Nach ein paar kleinen Anpassungen (dem Freigeben des Optimierungsvorschlags über die Ressource KafkaRebalance) sind die Partitionen jetzt gleichmässig verteilt: Die 5 Partitionen unseres Test-Topics liegen auf den 5 Brokern, jede mit 2 In-Sync-Replicas (insgesamt also Replikationsfaktor 3). Der Cluster meldet keine unterreplizierten Partitionen, und alle Komponenten sind gesund.

Replikationsstatistik der Test-App nach dem Rebalancing: 5 Partitionen, verteilt auf die Broker 0 bis 4, alle Replicas in Sync und als Healthy markiert.
Abbildung – Partitionen und Replicas nach dem Rebalancing gleichmässig verteilt

Das war ein voller Erfolg. Jetzt skalieren wir wieder herunter, von 5 auf 3 Broker.
Zuerst führe ich ein Rebalancing durch und entferne die Broker 3 und 4.

# rebalance with strimzi cruise control from 5 to 3 brokers
kubectl apply -f - <<EOF
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaRebalance
metadata:
  name: kafka-cluster-remove-brokers
  namespace: kafka
  labels:
    strimzi.io/cluster: kafka-cluster
spec:
  mode: "remove-brokers"
  brokers: [3, 4]
EOF

# approve rebalance
kubectl annotate kafkarebalance kafka-cluster-remove-brokers -n kafka \
  strimzi.io/rebalance=approve

Nach kurzer Zeit hatte Cruise Control seine Arbeit erledigt, und unsere 5 Partitionen verteilen sich nur noch auf 3 Broker. Den langweiligen Teil, den Node-Pool wieder herunterzuskalieren, erspare ich Ihnen.

Replikationsstatistik der Test-App nach dem Herunterskalieren: 5 Partitionen nur noch auf den Brokern 0, 1 und 2, alle Replicas in Sync und als Healthy markiert.
Abbildung – Kafka wieder auf 3 Broker herunterskaliert, alles grün

Fazit

Mit diesem Teil haben wir die meisten Ziele dieser Kafka-Blogserie erreicht: Wir haben einen skalierbaren, sicheren und produktionsreifen Apache-Kafka-Cluster auf Google Kubernetes Engine (GKE) deployt, einem echten, vom Hyperscaler verwalteten Kubernetes-Dienst.

Mit Strimzi haben wir die Stärken von Kubernetes-Operatoren genutzt, um einen modernen, KRaft-basierten Cluster zu betreiben: mit automatischer Skalierung, Cruise Control für intelligentes Rebalancing und nativer Integration für Sicherheit und Hochverfügbarkeit.

Rückblickend habe ich in Teil 4 zunächst die Kafka-Images von Confluent verwendet und bin dabei auf Hürden gestossen: Bestimmte Enterprise-Funktionen (etwa die Keycloak-/OAuth-Integration) erforderten, dass ich von Strimzi bereitgestellte Java-Bibliotheken von Hand ins Image einbaute, weil sie in der Open-Source-Distribution von Confluent nicht frei verfügbar sind.

Deshalb bin ich für diese Serie ganz auf Strimzi umgestiegen: Es bietet eine sauberere, nativere Kubernetes-Erfahrung ohne solche Workarounds.

Eine aktuelle Einschränkung: Strimzi bringt keine eingebaute Schema Registry mit. In einem möglichen Teil 6 möchte ich Observability ergänzen (Prometheus-/Grafana-Metriken, Tracing), Optionen für eine Schema Registry untersuchen, etwa die von Confluent oder Apicurio Registry, und die Kompatibilität mit unserem OAuth-basierten Autorisierungs-Setup testen. Wir werden sehen, ob die Integration glatt läuft oder zusätzliche Konfiguration braucht!

Danke, dass Sie dabei waren. Teilen Sie Ihre Erfahrungen mit Strimzi auf GKE gerne in den Kommentaren, besonders rund um Schema-Management oder fortgeschrittene Sicherheitsfunktionen.