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

332 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Kafka on Kubernetes (Strimzi)
Single-broker Kafka cluster for dev/test, deployed via the [Strimzi](https://strimzi.io/) 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`:
```yaml
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
```bash
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
```bash
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
```bash
kubectl -n kafka get secret kafka-cluster-ca-cert \
-o jsonpath='{.data.ca\.crt}' | base64 -d > kafka-ca.crt
```
### Get user credentials
```bash
# 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`
```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
```bash
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:
```bash
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](https://strimzi.io/docs/operators/latest/deploying.html#assembly-upgrade-str) 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.
```bash
# 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
```bash
make upgrade
```
Running `upgrade` without overriding `KAFKA_VER` upgrades only the Strimzi operator
via `helm upgrade` and leaves the Kafka CR unchanged.
---
## Uninstallation
```bash
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.