Files
infra-kafka/README.md
T
Stéphane Tailland 979c9da6c0 Document the topics, the consumer account and the credentials store
The README described the cluster as it was before events existed: no declared
topics, no consumer, and no way for another namespace to reach a password
Strimzi keeps here.

The events themselves are specified by their producer, not here — this repo
owns the cluster, not the contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 08:36:48 +02:00

10 KiB
Raw Blame History

Kafka on Kubernetes (Strimzi)

Single-broker Kafka cluster for dev/test, deployed via the Strimzi operator on a single-node k3s cluster.


Architecture

kafka namespace
├── strimzi-cluster-operator   Strimzi operator — reconciles Kafka/KafkaUser/KafkaTopic CRs
├── kafka-dual-role-0          Kafka 4.3.1 broker (KRaft, controller + broker in one pod)
└── kafka-entity-operator      Topic Operator + User Operator (manages topics and users)

Key design decisions

Choice Rationale
KRaft mode No ZooKeeper dependency — simpler, fewer pods
Single node pool dual-role One pod acts as both controller and broker, suitable for dev/test
SASL_SSL + SCRAM-SHA-512 Authenticated and encrypted connections; TLS managed by Strimzi CA
Simple authorization ACLs enforced per user; kafka-admin is declared super user
local-path storage (10 Gi) Default k3s storage class; PVC deleted on cluster teardown

Listeners

Name Port Protocol Auth
tls 9093 SASL_SSL SCRAM-SHA-512

Bootstrap address (cluster-internal):

kafka-kafka-bootstrap.kafka.svc.cluster.local:9093

Users

KafkaUser Secret Role
kafka-admin kafka-admin Super user — unrestricted access
kafka-client kafka-client Application user — Read/Write all topics and groups
classic-models-api classic-models-api Producer for classic-models.* — Write and Describe only, no Read
classic-models-agent-approvisionnement same name Consumer of classic-models.catalog and group prefix approvisionnement- — Read and Describe only, no Write

Each application account is scoped to what it actually does. The producer publishes domain events and consumes nothing, so it is write-only: one that could also read every topic exfiltrates every other domain's events if it is ever compromised. The consumer is the mirror — it cannot see sales or payments, and it cannot write, so a compromised workflow can raise no event that another agent would then act on.

Duplicate one of these per application. kafka-client is the wide-open template, useful for a smoke test and for nothing that runs unattended.

Strimzi stores credentials in Kubernetes secrets. Each secret contains two keys:

  • password — the SCRAM password
  • sasl.jaas.config — the ready-to-use JAAS config string

Topics

Declared in kafka-topics.yaml, never auto-created — an application account has no Create ACL, so a producer would simply be denied, and an auto-created topic takes broker defaults nobody chose.

Topic Partitions Replicas Retention
classic-models.catalog 3 1 7 days
classic-models.sales 3 1 7 days
classic-models.payments 3 1 7 days

Partition count is the ceiling on consumer parallelism and cannot be lowered later, so it is set for the traffic expected rather than the traffic today.

The events these carry are specified by their producer, in classic-models-api/docs/DOMAIN_EVENTS.md. This repo owns the cluster, not the contract.

Getting credentials into another namespace

A Secret cannot be mounted across namespaces, and Strimzi writes a KafkaUser's password into this one. credentials-access.yaml therefore declares a ClusterSecretStore named kafka-credentials (External Secrets, kubernetes provider) backed by a ServiceAccount that can read Secrets here and nowhere else. An application namespace then pulls its own password and the cluster CA with an ExternalSecret:

secretStoreRef:
  kind: ClusterSecretStore
  name: kafka-credentials
data:
  - secretKey: password
    remoteRef: {key: <kafkauser-name>, property: password}
  - secretKey: ca.crt
    remoteRef: {key: kafka-cluster-ca-cert, property: ca.crt}

The password stays generated and rotatable by Strimzi; the copy follows. The alternative — copying it by hand — drifts the first time the user is regenerated.

The trade, stated: any namespace allowed to create an ExternalSecret against this store can pull any Kafka account's credentials. Acceptable with one operator; on a shared cluster, narrow the Role with resourceNames and run one store per application.

TLS

Strimzi generates and manages its own internal CA. The cluster CA certificate is stored in:

secret/kafka-cluster-ca-cert  (key: ca.crt)

This certificate must be trusted by any Kafka client connecting over TLS.


Prerequisites

  • kubectl configured against the target cluster
  • helm v3
  • make
  • cert-manager is not required — Strimzi manages its own CA

Makefile

All operations are available via make. Run make help for the full reference.

Cluster lifecycle:
  deploy            Install Strimzi operator + Kafka cluster + users
  smoke-test        Run produce/consume end-to-end test
  upgrade           Upgrade Kafka (KAFKA_VER=) or Strimzi operator
  teardown          Delete the Kafka cluster and namespace

Topics:
  topic-list        List all topics
  topic-create      Create TOPIC (idempotent)
  topic-delete      Delete TOPIC

Messages:
  produce           Produce MESSAGE into TOPIC
  consume           Consume MAX messages from TOPIC (from latest)
  list-messages     Consume MAX messages from TOPIC (from beginning)

Consumer groups:
  group-list        List all consumer groups
  group-offsets     Show offsets and lag for GROUP

Key variables (all overridable on the command line):

Variable Default Description
TOPIC smoke-test Topic name
MESSAGE hello Message to produce
GROUP my-group Consumer group
MAX 10 Max messages to consume
PARTITIONS 1 Partitions for topic-create
REPLICAS 1 Replication factor for topic-create
KAFKA_VER 4.1.0 Kafka version for upgrade
METADATA_VER 4.1-IV0 KRaft metadata version for upgrade

Installation

make deploy

The target performs the following steps:

  1. Creates the kafka namespace
  2. Installs the Strimzi operator via Helm (scoped to the kafka namespace)
  3. Waits for the Strimzi CRDs to be fully established
  4. Applies kafka.yaml (KafkaNodePool + Kafka CRs) and waits for Ready
  5. Applies kafka-users.yaml (KafkaUser CRs) and waits for Ready
  6. Applies kafka-topics.yaml (the declared topics) and credentials-access.yaml (the cross-namespace credentials store)

Expected duration: 4–6 minutes on a single-node k3s cluster.

Verify the installation

kubectl get kafka,kafkanodepool,kafkauser,pods -n kafka

Expected output:

NAME                           READY   KAFKA VERSION   METADATA VERSION
kafka.kafka.strimzi.io/kafka   True    4.1.0           4.1-IV0

NAME                                       DESIRED REPLICAS   ROLES                     NODEIDS
kafkanodepool.kafka.strimzi.io/dual-role   1                  ["controller","broker"]   [0]

NAME                                      CLUSTER   AUTHENTICATION   AUTHORIZATION   READY
kafkauser.kafka.strimzi.io/kafka-admin    kafka     scram-sha-512                    True
kafkauser.kafka.strimzi.io/kafka-client   kafka     scram-sha-512    simple          True

Client configuration

Get the CA certificate

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

Get user credentials

# Admin password
kubectl -n kafka get secret kafka-admin \
  -o jsonpath='{.data.password}' | base64 -d

# Client password
kubectl -n kafka get secret kafka-client \
  -o jsonpath='{.data.password}' | base64 -d

# Ready-to-use JAAS config (includes username and password)
kubectl -n kafka get secret kafka-client \
  -o jsonpath='{.data.sasl\.jaas\.config}' | base64 -d

Sample client.properties

bootstrap.servers=kafka-kafka-bootstrap.kafka.svc.cluster.local:9093
security.protocol=SASL_SSL
ssl.truststore.type=PEM
ssl.truststore.location=/path/to/kafka-ca.crt
sasl.mechanism=SCRAM-SHA-512
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required \
  username="kafka-client" password="<password>";

Testing

make smoke-test

Runs a Kubernetes Job that validates the full produce/consume cycle over SASL_SSL:

  1. Topic creation — creates topic smoke-test with --if-not-exists (idempotent)
  2. Produce — sends 5 messages (message-1 … message-5) via kafka-console-producer
  3. Consume — reads 5 messages from the beginning via kafka-console-consumer and asserts count

Expected output:

==> [1/3] Creating topic 'smoke-test' (idempotent)
    Topic ready.
==> [2/3] Producing 5 messages
    5 messages produced.
==> [3/3] Consuming (from-beginning, max 5)
message-1
message-2
message-3
message-4
message-5
==> SMOKE TEST PASSED (5/5 messages)
==> Smoke test PASSED

The job is cleaned up automatically after 1 minute (ttlSecondsAfterFinished: 60).


Adding a new user

Create a new KafkaUser manifest following the kafka-client pattern in kafka-users.yaml, then apply it:

kubectl apply -f kafka-users.yaml

Strimzi will create the corresponding secret in the kafka namespace within seconds.


Upgrade

Rule: always upgrade the Strimzi operator before upgrading the Kafka version. Check the Strimzi upgrade guide for supported upgrade paths.

Upgrade Kafka version

KAFKA_VER and METADATA_VER are independent because Strimzi recommends upgrading the broker binary first, then migrating the metadata format — this allows rollback of the binary without being blocked by an already-migrated metadata version.

# Step 1: upgrade broker binary only
make upgrade KAFKA_VER=4.1.1 METADATA_VER=4.1-IV0

# Step 2: once confirmed stable, migrate metadata format
make upgrade KAFKA_VER=4.1.1 METADATA_VER=4.1-IV1

Upgrade the Strimzi operator

make upgrade

Running upgrade without overriding KAFKA_VER upgrades only the Strimzi operator via helm upgrade and leaves the Kafka CR unchanged.


Uninstallation

make teardown

Deletes in order: KafkaUsers → Kafka cluster → Strimzi operator → namespace.

The deleteClaim: true flag in kafka.yaml ensures the 10 Gi PVC is deleted together with the KafkaNodePool, leaving no orphaned volumes.