Back to posts

Kafka on GKE with KRaft — No ZooKeeper

Read the full guide on docs.beyondyou.my.id
messagingkafkagkekuberneteskraftstatefulset

Kafka on GKE with KRaft — No ZooKeeper

Table of Contents

SectionTopicDescription
01Why KRaft on GKEEliminate ZooKeeper for simpler operations.
02Architecture3-node KRaft cluster on GKE.
03server.properties as ConfigMapKRaft configuration in Kubernetes.
04StatefulSetInit container for storage format.
05ServicesHeadless + Internal LoadBalancer.
06Scaling KRaftAdding broker nodes to the quorum.
07ZooKeeper vs KRaft ComparisonSide-by-side feature matrix.
08Production ChecklistHardening for production workloads.

1. Why KRaft on GKE

KRaft (Kafka Raft) mode eliminates ZooKeeper — reducing operational complexity, resource usage, and failure surface.

AspectZooKeeper ModeKRaft Mode
Components2 systems (Kafka + ZK)1 system (Kafka)
StatefulSets2 (Kafka + ZK)1 (Kafka)
Resource overhead~50% more (ZK nodes)Lower
Metadata storageZooKeeperKafka logs
Controller electionZooKeeperRaft quorum
Minimum nodes6 (3 Kafka + 3 ZK)3 (all Kafka)
Kafka versionAll3.3+ (production ready 3.4+)

When to Use KRaft

ScenarioUse KRaft?
New Kafka deploymentYes
Existing ZooKeeper clusterMigrate gradually
Kafka < 3.3No (not supported)
Complex multi-clusterEvaluate (KRaft matures each release)

2. Architecture

graph TB
    subgraph GKE["GKE Cluster"]
        subgraph KAFKA_KRAFT["Kafka KRaft Cluster"]
            k0["kafka-0\ncontroller,broker\n:9092, :9093"]
            k1["kafka-1\nbroker\n:9092, :9093"]
            k2["kafka-2\nbroker\n:9092, :9093"]
        end
        svc["Headless Service\nkafka-headless"]
        lb["Internal LB\nkafka-svc"]
    end

    producer["Producer"] --> lb
    consumer["Consumer"] --> lb
    lb --> k0
    lb --> k1
    lb --> k2
    k0 <-->|"Raft\nconsensus"| k1
    k0 <-->|"Raft\nconsensus"| k2
    k1 <-->|"Raft\nconsensus"| k2
    svc --> k0
    svc --> k1
    svc --> k2

Node Roles

NodeRolePortPurpose
kafka-0controller + broker9092, 9093Quorum voter + message broker
kafka-1broker9092, 9093Message broker only
kafka-2broker9092, 9093Message broker only

3. server.properties as ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: kafka-config
  namespace: kafka
  labels:
    app: kafka
    app.kubernetes.io/name: kafka
    app.kubernetes.io/component: broker
    app.kubernetes.io/part-of: kafka
data:
  server.properties: |
    # KRaft mode
    process.roles=broker,controller
    node.id=0

    # Controller quorum
    controller.listener.names=CONTROLLER
    controller.quorum.voters=0@kafka-0.kafka-headless.kafka.svc.cluster.local:9093,1@kafka-1.kafka-headless.kafka.svc.cluster.local:9093,2@kafka-2.kafka-headless.kafka.svc.cluster.local:9093
    kraft.cluster.id=kraft-cluster-id-gke

    # Listeners
    listeners=PLAINTEXT://:9092,CONTROLLER://:9093
    advertised.listeners=PLAINTEXT://kafka-0.kafka-headless.kafka.svc.cluster.local:9092
    inter.broker.listener.name=PLAINTEXT
    listener.security.protocol.map=PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT

    # Log storage
    log.dirs=/bitnami/kafka/data

    # Topic configuration
    auto.create.topics.enable=false
    default.replication.factor=3
    offsets.topic.replication.factor=3
    transaction.state.log.replication.factor=3
    transaction.state.log.min.isr=2

    # Retention
    log.retention.hours=168
    log.retention.bytes=1073741824
    log.segment.bytes=268435456
    log.segment.ms=3600000

ConfigMap Breakdown

SectionParameterPurpose
KRaftprocess.roles=broker,controllerNode runs both roles
KRaftnode.idUnique node identifier
KRaftcontroller.quorum.votersAll controller nodes in quorum
KRaftkraft.cluster.idUnique cluster identifier
ListenerlistenersBind addresses
Listeneradvertised.listenersClient-facing addresses
Replicationdefault.replication.factor=3Replicate to all brokers
Replicationtransaction.state.log.min.isr=2Min ISR for transaction log

Generate Cluster ID

# Generate a new cluster ID
kafka-storage.sh random-uuid

4. StatefulSet

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: kafka
  namespace: kafka
  labels:
    app: kafka
    app.kubernetes.io/name: kafka
    app.kubernetes.io/component: broker
    app.kubernetes.io/part-of: kafka
spec:
  serviceName: kafka-headless
  replicas: 3
  podManagementPolicy: Parallel
  selector:
    matchLabels:
      app: kafka
  template:
    metadata:
      labels:
        app: kafka
    spec:
      securityContext:
        fsGroup: 1001
        runAsUser: 1001
        runAsGroup: 1001
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                app: kafka
            topologyKey: kubernetes.io/hostname
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: pool-type
                operator: In
                values:
                - messaging
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: topology.kubernetes.io/zone
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: kafka
      containers:
      - name: kafka
        image: asia-southeast2-docker.pkg.dev/example-prd/devops/tools/bitnami-kafka:3.9.0
        command:
        - /bin/bash
        - -c
        - |
          if [ ! -f "/bitnami/kafka/data/meta.properties" ]; then
            kafka-storage.sh format -t kraft-cluster-id-gke -c /opt/bitnami/kafka/config/server.properties
          fi
          exec kafka-server-start.sh /opt/bitnami/kafka/config/server.properties
        ports:
        - containerPort: 9092
          name: plaintext
        - containerPort: 9093
          name: controller
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: metadata.name
        - name: NODE_ID
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: metadata.name
        resources:
          requests:
            cpu: 500m
            memory: 1Gi
          limits:
            memory: 1Gi
        livenessProbe:
          tcpSocket:
            port: 9092
          initialDelaySeconds: 90
          periodSeconds: 30
          failureThreshold: 5
        readinessProbe:
          tcpSocket:
            port: 9092
          initialDelaySeconds: 60
          periodSeconds: 10
          failureThreshold: 3
        volumeMounts:
        - name: kafka-data
          mountPath: /bitnami/kafka/data
        - name: kafka-config
          mountPath: /opt/bitnami/kafka/config/server.properties
          subPath: server.properties
      volumes:
      - name: kafka-config
        configMap:
          name: kafka-config
  volumeClaimTemplates:
  - metadata:
      name: kafka-data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi

Key Design Decisions

DecisionRationale
podManagementPolicy: ParallelAll pods start simultaneously (KRaft needs quorum)
requiredDuringScheduling anti-affinityEach pod on a different node
Init script for kafka-storage.sh formatFormat storage only on first start
command instead of default entrypointCustom init logic for KRaft

Storage Format Init Logic

# Check if already formatted
if [ ! -f "/bitnami/kafka/data/meta.properties" ]; then
  kafka-storage.sh format -t kraft-cluster-id-gke -c /opt/bitnami/kafka/config/server.properties
fi
exec kafka-server-start.sh /opt/bitnami/kafka/config/server.properties

5. Services

Headless Service (Cluster Discovery)

apiVersion: v1
kind: Service
metadata:
  name: kafka-headless
  namespace: kafka
  labels:
    app: kafka
    app.kubernetes.io/name: kafka
    app.kubernetes.io/component: broker
    app.kubernetes.io/part-of: kafka
spec:
  clusterIP: None
  selector:
    app: kafka
  ports:
  - name: plaintext
    port: 9092
  - name: controller
    port: 9093

Internal LoadBalancer (Client Access)

apiVersion: v1
kind: Service
metadata:
  name: kafka-svc
  namespace: kafka
  labels:
    app: kafka
    app.kubernetes.io/name: kafka
    app.kubernetes.io/component: broker
    app.kubernetes.io/part-of: kafka
  annotations:
    cloud.google.com/load-balancer-type: "Internal"
spec:
  type: LoadBalancer
  selector:
    app: kafka
  ports:
  - name: plaintext
    port: 9092
    targetPort: 9092
    protocol: TCP

Service Comparison

ServiceTypePurpose
kafka-headlessNoneDNS SRV for Raft quorum and broker discovery
kafka-svcInternal LBClient connections from other services

6. Scaling KRaft

Adding a New Broker

# 1. Update ConfigMap — add new voter to quorum
# Add: 3@kafka-3.kafka-headless.kafka.svc.cluster.local:9093
kubectl edit configmap kafka-config -n kafka

# 2. Update StatefulSet replicas
kubectl scale statefulset kafka -n kafka --replicas=4

# 3. Verify new node joins quorum
kubectl logs kafka-3 -n kafka | grep "controller"

Removing a Broker

# 1. Remove from ConfigMap quorum voters
# 2. Scale down
kubectl scale statefulset kafka -n kafka --replicas=2

Scale Considerations

AspectDetail
Quorum sizeMust be odd (1, 3, 5, 7) for consensus
Minimum for HA3 nodes (tolerates 1 failure)
Maximum recommended7 nodes (tolerates 3 failures)
Scaling methodUpdate ConfigMap + scale StatefulSet

7. ZooKeeper vs KRaft Comparison

FeatureZooKeeper ModeKRaft Mode
Components2 (Kafka + ZK)1 (Kafka)
StatefulSets21
Min nodes (HA)6 (3+3)3
Metadata storageZooKeeperKafka logs
Controller electionZooKeeperRaft quorum
Startup orderZK first, then KafkaKafka only
Partition limit~200K per cluster~400K per cluster
Metadata syncZK pollingDirect log replay
Operational complexityHigherLower
Maturity10+ yearsProduction ready since 3.4
MonitoringSeparate ZK metricsUnified Kafka metrics
RecoveryZK snapshot restoreKafka log restore

Migration: ZooKeeper to KRaft

StepAction
1Upgrade to Kafka 3.4+
2Deploy KRaft cluster alongside ZK cluster
3Use kafka-metadata-migration tool
4Verify all topics/offsets migrated
5Decommission ZooKeeper cluster

8. Production Checklist

CategoryItemStatus
Replicationdefault.replication.factor >= 3Required
Min ISRmin.insync.replicas = 2Required
Quorum sizeOdd number (3, 5, 7)Required
Topic creationauto.create.topics.enable = falseRequired
PersistencePVC with ReadWriteOnceRequired
Anti-affinityrequiredDuringScheduling across nodesRequired
Zone spreadtopologySpreadConstraintsRecommended
Security contextrunAsUser, fsGroupRecommended
Resource limitsCPU and memory setRequired
Health checksLiveness + readiness probesRequired
Init scriptkafka-storage.sh format on first startRequired
Cluster IDFixed kraft.cluster.idRequired
Internal LBcloud.google.com/load-balancer-type: InternalIf needed

References