Kafscale Production Operations Guide
This guide explains how to install the Kafscale control plane via Helm, configure S3 and etcd dependencies, and expose the operations console. It assumes you already have a Kubernetes cluster and the prerequisites listed below.
Prerequisites
- Kubernetes 1.26+
- Helm 3.12+
- A reachable etcd cluster (the operator uses it for metadata + leader election)
- An S3 bucket per environment plus IAM credentials with permission to list/create objects
- Optional: an ingress controller for the console UI
Installing with Helm
helm upgrade --install kafscale deploy/helm/kafscale \
--namespace kafscale --create-namespace \
--set operator.etcdEndpoints[0]=http://etcd.kafscale.svc:2379 \
--set operator.image.tag=v0.1.0 \
--set console.image.tag=v0.1.0
The chart ships the KafscaleCluster and KafscaleTopic CRDs so the operator can immediately reconcile resources. Create a Kubernetes secret that contains your S3 access/secret keys, reference it inside a KafscaleCluster resource (see config/samples/), and the operator will launch broker pods with the right IAM credentials.
By default, image tags follow the chart appVersion. Override operator.image.tag and console.image.tag to pin a different release (for example, v1.1.0). For a dev/latest install, set operator.image.useLatest=true, console.image.useLatest=true, and operator.brokerImage.useLatest=true (this also forces imagePullPolicy=Always for those images).
Per-cluster broker image
spec.brokers.image overrides the operator’s BROKER_IMAGE default for that cluster:
spec:
brokers:
image:
repository: ghcr.io/kafscale/kafscale-broker
tag: v1.6.3
Omitting the image keeps the operator default. A repository without a tag is used
as supplied, including a complete repository@sha256:... digest reference. A tag
without a repository replaces the tag or digest in the operator default. When both
are set, the tag replaces any tag or digest in the supplied repository reference.
The operator’s image pull policy still applies. Updating the image changes the
broker StatefulSet pod template, triggering its normal rolling update; removing
the override returns that cluster to the operator default.
Values to pay attention to
| Value | Purpose |
|---|---|
operator.replicaCount |
Number of operator replicas (default 2). Operators use etcd to elect a leader and stay HA. |
operator.leaderKey |
Kubernetes leader election lock name. Use a DNS-1123 compatible name. |
console.service.* |
Type/port used to expose the UI. Combine with .console.ingress to publish via an ingress controller. |
console.auth.* |
Console login credentials. Set both console.auth.username and console.auth.password to enable the UI. |
imagePullSecrets |
Provide if your container registry (e.g., GHCR) is private. |
Post-install Steps
- Apply a
KafscaleClustercustom resource describing the S3 bucket, etcd endpoints, cache sizes, and credentials secret. - Apply any required
KafscaleTopicresources. The operator writes the desired metadata to etcd and the brokers begin serving Kafka clients right away. - Expose the console UI (optional) by enabling ingress in
values.yamlor by creating a LoadBalancer service.
Broker partition restore
On first access after a broker restart, a partition rebuilds its in-memory segment
catalog from S3 before serving requests for that partition. Restore uses up to 16
workers per partition to read segment footers and indexes. These requests also
share the broker-wide KAFSCALE_S3_CONCURRENCY limit with other S3 operations.
Broker logs report restore start, the listed segment count, progress every five seconds during downloads, and completion with restored/skipped counts, last offset, and duration. Failures include processed counts and the error. A failed restore cancels the remaining workers and does not install a partial catalog.
Restore still lists all objects and reads a footer and index per eligible segment; large partitions can still take time. This change does not merge small segments or add a persistent manifest. Requests for the restoring partition wait for completion.
Security & Hardening
- RBAC – The Helm chart creates a scoped service account and RBAC role so the operator only touches its CRDs, Secrets, and Deployments inside the release namespace.
- S3 credentials – Credentials live in user-managed Kubernetes secrets. The operator never writes them to etcd. Snapshot jobs map
KAFSCALE_S3_ACCESS_KEY/KAFSCALE_S3_SECRET_KEYinto the AWS CLI env vars (AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY) automatically. - Console auth – The UI requires
KAFSCALE_UI_USERNAMEandKAFSCALE_UI_PASSWORD. There are no defaults; if unset, the login screen shows a warning and the API blocks access. In Helm, setconsole.auth.usernameandconsole.auth.password, for example:
helm upgrade --install kafscale deploy/helm/kafscale \
--set console.auth.username=kafscaleadmin \
--set console.auth.password='use-a-secret'
- TLS and mTLS – The proxy can terminate TLS 1.3 and authenticate Kafka clients with mTLS. See mTLS authentication. Brokers and the console have no TLS listener of their own; terminate TLS for the console at your ingress or service mesh.
- Admin APIs – Create/Delete Topics are enabled by default. Set
KAFSCALE_ALLOW_ADMIN_APIS=falseon broker pods to disable them, and gate external access via mTLS, ingress auth, or network policies. - Network policies – If your cluster enforces policies, allow the operator + brokers to reach etcd and S3 endpoints and lock everything else down.
- Pod Security (restricted) – Chart-templated workloads (operator, proxy, console, MCP) ship PSA
restricted-compatiblesecurityContextdefaults (runAsNonRoot, dropped capabilities,readOnlyRootFilesystem,seccompProfile: RuntimeDefault, UID/GID10001). Override per component via<component>.podSecurityContextand<component>.containerSecurityContext. The proxy mounts a writable/tmpemptyDirfor LFS verify temp files. - Pod placement – Operator-managed broker and etcd StatefulSets, and the chart proxy Deployment (when
proxy.affinityis empty), use soft (preferredDuringSchedulingIgnoredDuringExecution) pod anti-affinity onkubernetes.io/hostnameso replicas spread across nodes on multi-node clusters while still scheduling on single-node dev clusters. Setproxy.affinityexplicitly to replace the chart default. For operator-managed brokers and etcd, usespec.brokers.affinityandspec.etcd.affinityas described below. - ACLs (v1.5) – Optional, basic ACL enforcement is available at the broker. With proxy mTLS enabled, identity comes from the client certificate mapping. Without it, identity comes from Kafka
client.idor network identity, which is not authentication. Configure viaKAFSCALE_ACL_ENABLEDplus eitherKAFSCALE_ACL_JSONorKAFSCALE_ACL_FILE. UseKAFSCALE_ACL_FAIL_OPEN=trueto allow traffic when the ACL config is missing/invalid (default is fail-closed).- Principal source – Set
KAFSCALE_PRINCIPAL_SOURCEtoclient_id(default),remote_addr, orproxy_addr. Useproxy_addrwithKAFSCALE_PROXY_PROTOCOL=trueto derive principals from a trusted TCP proxy (PROXY protocol v1/v2). - Auth denials – Broker logs emit a rate-limited
authorization deniedentry with principal/action/resource context. - Trust boundary – Only enable
proxy_addr/PROXY protocol when brokers are reachable only through a trusted LB or sidecar that injects the header. Do not expose brokers directly, or clients can spoof identity. - Fail-closed – When
KAFSCALE_PROXY_PROTOCOL=true, brokers reject connections that do not include a valid PROXY header. - Header limits – PROXY v1 headers are capped at 256 bytes; oversized headers are rejected.
- Health checks – PROXY v2
LOCALconnections are accepted (no identity); ensure LB health checks don’t rely on ACL-protected operations.
- Principal source – Set
Example ACL values (Helm operator):
operator:
acl:
enabled: true
configJson: |
{"default_policy":"deny","principals":[
{"name":"analytics","allow":[{"action":"fetch","resource":"topic","name":"orders-*"}]}
]}
auth:
principalSource: "proxy_addr"
proxyProtocol: true
- Health / metrics – Prometheus can scrape
/metricson the brokers and operator for early detection of S3 pressure or degraded nodes. The operator exposes metrics on port8080and the Helm chart can create a metrics Service, ServiceMonitor, and PrometheusRule. - Startup gating – Broker pods exit immediately if they cannot read metadata or write a probe object to S3 during startup, so Kubernetes restarts them rather than leaving a stuck listener in place.
- Leader IDs – Each broker advertises a numeric
NodeIDin etcd. In the single-node demo you’ll always seeLeader=0in the Console’s topic detail because the only broker has ID0. In real clusters those IDs align with the broker addresses the operator published; if you seeLeader=3, look for the broker withNodeID 3in the metadata payload.
Proxy TLS via LoadBalancer (Recommended)
The proxy is the external Kafka entrypoint. One option is to terminate TLS at the cloud LoadBalancer by supplying Service annotations in the Helm values. For per-client authentication and broker ACLs, use the proxy’s own mTLS listener with TLS passthrough at the LoadBalancer instead. See mTLS authentication. This keeps broker traffic plaintext inside the cluster while enabling HTTPS/TLS for clients.
Example (provider-specific annotations omitted here):
proxy:
enabled: true
service:
type: LoadBalancer
port: 9092
annotations:
# Add your cloud provider TLS annotations here (ACM / GCP / Azure, etc.)
loadBalancerSourceRanges:
- 203.0.113.0/24
If you need in-cluster ACME/Let’s Encrypt support, document it as an optional stack (Traefik or another TCP-capable gateway + cert-manager). Keep it off by default to avoid extra operational dependencies.
Provider Examples (Verify with your cloud)
These snippets show common patterns; annotation keys vary by provider and feature. Always validate against your cloud provider docs.
AWS NLB TLS (example):
proxy:
service:
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "arn:aws:acm:REGION:ACCOUNT:certificate/ID"
service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "9092"
service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "tcp"
GCP / Azure:
- L4 LoadBalancers typically do TCP pass-through; TLS termination often requires a provider gateway/ingress (or a TCP-capable ingress controller).
- If you terminate TLS outside the Service, keep the proxy Service as plain TCP.
Ops API Examples
Kafscale exposes Kafka admin APIs for operator workflows (consumer group visibility,
config inspection, cleanup). The canonical plan and scope live in docs/ops-api.md.
# List consumer groups
kafka-consumer-groups.sh --bootstrap-server <broker> --list
# Describe a consumer group
kafka-consumer-groups.sh --bootstrap-server <broker> --describe --group <group-id>
# Delete a consumer group
kafka-consumer-groups.sh --bootstrap-server <broker> --delete --group <group-id>
# Read topic configs
kafka-configs.sh --bootstrap-server <broker> --describe --entity-type topics --entity-name <topic>
# Increase partition count for a topic (additive only)
kafka-topics.sh --bootstrap-server <broker> --alter --topic <topic> --partitions <count>
# Update topic retention (whitelist only)
kafka-configs.sh --bootstrap-server <broker> --alter --entity-type topics --entity-name <topic> \
--add-config retention.ms=604800000
etcd Availability & Storage
Kafscale depends on etcd for metadata + offsets. Treat etcd as a production datastore:
- Run a dedicated etcd cluster (do not share the Kubernetes control-plane etcd).
- Use SSD-backed disks for data and WAL volumes; avoid networked storage when possible.
- Deploy an odd number of members (3 for most clusters, 5 for higher fault tolerance).
- Spread members across zones/racks to survive single-AZ failures.
- Enable compaction/defragmentation and monitor fsync/proposal latency.
- HA requires a stable client access layer: use a Kafka-aware proxy or per-broker endpoints so bootstrap addresses and leader routing survive broker churn.
Operator-managed etcd (default path)
If no etcd endpoints are supplied, the operator will provision a 3-node etcd StatefulSet for you. Recommended settings:
- Use an SSD-capable StorageClass for the etcd PVCs (
storageClassName), with enough IOPS headroom. - Set a PodDisruptionBudget so only one etcd pod can be evicted at a time.
- Pin etcd pods across zones with topology spread or anti-affinity.
- Enable snapshot backups to a dedicated S3 bucket and retain at least 7 days of snapshots.
- Monitor leader changes, fsync latency, and disk usage; alert on slow or flapping members.
The operator tunes managed etcd automatically:
- In-process periodic compaction (5m retention) and a 4 GiB backend quota on the etcd StatefulSet.
{cluster}-etcd-maintenanceCronJob (every 6h): per-member defrag and NOSPACE alarm disarm.{cluster}-etcd-maintenance-checkCronJob (every 15m): runs maintenance only when backend size crosses 75% of quota or a NOSPACE alarm is active.
Override schedules and thresholds via the operator etcd env vars listed below.
Etcd Endpoint Resolution
The operator resolves etcd endpoints in this order:
KafscaleCluster.spec.etcd.endpointsKAFSCALE_OPERATOR_ETCD_ENDPOINTS- Managed etcd (operator creates a 3-node StatefulSet)
Etcd Schema Direction
Kafscale uses a snapshot-based metadata schema today: the operator publishes a full metadata snapshot to etcd and brokers consume it. We avoid per-key writes for broker registrations and assignments until the ops surface requires it.
Etcd Availability Signals (clients)
When etcd is unavailable, the broker rejects producer/admin/consumer-group operations with REQUEST_TIMED_OUT. Producers see per-partition errors in the Produce response; admin and group APIs return the same code in their response payloads.
Snapshot job defaults and operator env overrides:
KAFSCALE_OPERATOR_ETCD_SNAPSHOT_BUCKET(default:kafscale-etcd-<namespace>-<cluster>, separate from broker segment storage)KAFSCALE_OPERATOR_ETCD_SNAPSHOT_PREFIX(default:etcd-snapshots)KAFSCALE_OPERATOR_ETCD_SNAPSHOT_SCHEDULE(default:0 * * * *)KAFSCALE_OPERATOR_ETCD_SNAPSHOT_ETCDCTL_IMAGE(default:kubesphere/etcd:3.6.4-0)KAFSCALE_OPERATOR_ETCD_SNAPSHOT_IMAGE(default:amazon/aws-cli:2.15.0)KAFSCALE_OPERATOR_ETCD_SNAPSHOT_S3_ENDPOINT(optional, for MinIO or custom S3)KAFSCALE_OPERATOR_ETCD_SNAPSHOT_STALE_AFTER_SEC(default: 7200)KAFSCALE_OPERATOR_ETCD_SNAPSHOT_CREATE_BUCKET(optional, set to1to auto-create the backup bucket)KAFSCALE_OPERATOR_ETCD_SNAPSHOT_PROTECT_BUCKET(optional, set to1to enable versioning + block public access)KAFSCALE_OPERATOR_ETCD_SNAPSHOT_SKIP_PREFLIGHT(optional, set to1to skip the operator S3 write check)
The operator performs an S3 write preflight before enabling snapshots. If the check fails, the EtcdSnapshotAccess condition is set to False and reconciliation returns an error until access is restored. Snapshots are uploaded as timestamped files plus a .sha256 checksum for recovery validation.
Snapshot Restore (KafScale managed etcd)
Snapshot restore refers to etcd operational data (cluster metadata/offsets). It is not the broker topic snapshot flow.
When the KafScale operator manages etcd, each cluster pod runs restore init containers before etcd starts:
- The snapshot download container pulls the latest
.dbsnapshot from the configured bucket/prefix. - The restore container runs
etcdctl snapshot restoreif the data directory is empty and a snapshot file is present. - If no snapshot is available, etcd starts with a clean data directory.
The restore image must include /bin/sh and etcdctl. Override with KAFSCALE_OPERATOR_ETCD_SNAPSHOT_ETCDCTL_IMAGE if you use a custom image.
Topic Recovery On The DR Spine
For broker topic data, KafScale now supports recovery into a new topic on the DR side. This is intentionally not an in-place rollback on the primary cluster.
Use kafscale-cli restore to create a fresh target topic, copy .kfs segment/index pairs up to a cutoff timestamp, and set the recovered topic’s next offsets:
kafscale-cli restore \
--topic orders \
--target-topic orders-restore-20260513 \
--to 2026-05-13T14:23:00Z
Operational semantics:
- Recovery runs against the existing KafScale S3 + etcd control plane, including
KAFSCALE_S3_BUCKET,KAFSCALE_S3_REGION,KAFSCALE_S3_ENDPOINT,KAFSCALE_S3_PATH_STYLE, andKAFSCALE_ETCD_ENDPOINTS. - The target topic must be new. KafScale refuses to restore over an existing persisted topic.
- Recovery copies every fully eligible segment/index pair, then inspects the first intersecting segment and truncates it at the first record whose timestamp exceeds the cutoff.
- If the intersecting batch is compressed, exact truncation is not safe, so the restore fails and the operator should choose an earlier cutoff or a segment boundary instead.
- Offsets are preserved inside the recovered topic so replay, validation, and downstream cutover can happen without rewriting the source topic.
- The safer pattern is restore, validate, then cut consumers or downstream jobs over deliberately.
Consumer Offsets After Restore
Etcd restores recover committed consumer offsets. If a consumer has no committed offsets, it may start at the end and see zero records even though data exists in S3. In production:
- Ensure consumers commit offsets (default for most Kafka clients).
- Set
auto.offset.reset=earliestas a safety net for new or uncommitted consumers.
Minimal env + spec checklist for a smooth run:
- Operator env:
KAFSCALE_OPERATOR_ETCD_SNAPSHOT_BUCKET(optional override),KAFSCALE_OPERATOR_ETCD_SNAPSHOT_S3_ENDPOINT(if non-AWS), optionallyKAFSCALE_OPERATOR_ETCD_SNAPSHOT_CREATE_BUCKET=1. - Cluster spec:
spec.s3.bucket,spec.s3.region,spec.s3.credentialsSecretRef,spec.s3.endpoint(if non-AWS). Optional read replica:spec.s3.readBucket,spec.s3.readRegion,spec.s3.readEndpoint. - Secret keys:
KAFSCALE_S3_ACCESS_KEY,KAFSCALE_S3_SECRET_KEY. - Console auth env:
KAFSCALE_UI_USERNAME,KAFSCALE_UI_PASSWORD.
Recommended operator alerting (when using Prometheus Operator):
KafscaleSnapshotAccessFailed– S3 snapshot writes failing.KafscaleSnapshotStale– last successful snapshot older than the staleness threshold.KafscaleSnapshotNeverSucceeded– no successful snapshots recorded.
Proxy readiness
The proxy exposes /readyz and /livez on KAFSCALE_PROXY_HEALTH_ADDR
(default :9094).
/readyz answers two questions, not one:
- Is a backend known? Cached backend state when fresh, otherwise a live metadata fetch from etcd.
- Does that backend actually serve Fetch? A Fetch round-trip on a fresh connection that must decode cleanly.
The second question exists because the proxy is a full Fetch codec: it decodes,
merges and re-encodes broker Fetch responses. A proxy-to-broker Fetch
serialization or connection-state mismatch therefore breaks consume while
Metadata and ListOffsets keep answering normally. Readiness based on the first
question alone reports ready while every consumer silently receives zero
records, and Kubernetes keeps the pod in the Service.
The probe asks for one partition of a deliberately unknown topic ID, so it needs
no real topic and reads no data; a correct broker answers UNKNOWN_TOPIC_ID.
The fresh connection is deliberate too: it detects a stale or half-open pooled
connection, which a cached-state check cannot.
Set KAFSCALE_PROXY_READYZ_FETCH_PROBE=false to fall back to the shallow
“a backend exists” check. That is an escape hatch for a probe that reports a
false negative in the field, not a recommended steady state: it removes the only
readiness signal that distinguishes “serving” from “running”.
Startup ordering. With the probe enabled the proxy stays NotReady until a broker serves Fetch. An installer that waits for proxy readiness before creating the cluster resource that produces the broker will therefore deadlock. Create the broker first, or do not gate the install on proxy readiness.
Environment Variable Index
Operator
KAFSCALE_OPERATOR_ETCD_ENDPOINTS– Comma-separated etcd endpoints to use instead of managed etcd.KAFSCALE_OPERATOR_ETCD_IMAGE– Managed etcd image (defaultkubesphere/etcd:3.6.4-0).KAFSCALE_OPERATOR_ETCD_REPLICAS– Managed etcd replica count (default3).KAFSCALE_OPERATOR_ETCD_STORAGE_SIZE– PVC size for managed etcd (default10Gi).KAFSCALE_OPERATOR_ETCD_STORAGE_CLASS– StorageClass for managed etcd PVCs.KAFSCALE_OPERATOR_ETCD_STORAGE_MEMORY– Use in-memoryemptyDirfor etcd data (1to enable, test/dev only).KAFSCALE_OPERATOR_ETCD_AUTO_COMPACTION_MODE– Managed etcd compaction mode (periodicorrevision; defaultperiodic).KAFSCALE_OPERATOR_ETCD_AUTO_COMPACTION_RETENTION– Compaction retention (5mfor periodic mode; default5m).KAFSCALE_OPERATOR_ETCD_QUOTA_BACKEND_BYTES– Managed etcd backend quota in bytes (default4294967296, 4 GiB).KAFSCALE_OPERATOR_ETCD_MAINTENANCE_SCHEDULE– Baseline maintenance CronJob schedule (default0 */6 * * *).KAFSCALE_OPERATOR_ETCD_MAINTENANCE_CHECK_SCHEDULE– Reactive size-check CronJob schedule (default*/15 * * * *).KAFSCALE_OPERATOR_ETCD_MAINTENANCE_ENABLED– Enable maintenance CronJobs (0/falseto disable; enabled by default).KAFSCALE_OPERATOR_ETCD_MAINTENANCE_SIZE_THRESHOLD_PCT– Backend size threshold percent of quota before reactive maintenance runs (default75).KAFSCALE_OPERATOR_ETCD_SNAPSHOT_BUCKET– Override snapshot bucket (default:kafscale-etcd-<namespace>-<cluster>).KAFSCALE_OPERATOR_ETCD_SNAPSHOT_PREFIX– Snapshot prefix (defaultetcd-snapshots).KAFSCALE_OPERATOR_ETCD_SNAPSHOT_SCHEDULE– Cron schedule for snapshots (default0 * * * *).KAFSCALE_OPERATOR_ETCD_SNAPSHOT_ETCDCTL_IMAGE– Etcdctl image for snapshots.KAFSCALE_OPERATOR_ETCD_SNAPSHOT_IMAGE– AWS CLI image for uploads.KAFSCALE_OPERATOR_ETCD_SNAPSHOT_S3_ENDPOINT– S3 endpoint override (MinIO/custom).KAFSCALE_OPERATOR_ETCD_SNAPSHOT_STALE_AFTER_SEC– Staleness threshold seconds (default7200).KAFSCALE_OPERATOR_ETCD_SNAPSHOT_CREATE_BUCKET– Auto-create the snapshot bucket (1to enable).KAFSCALE_OPERATOR_ETCD_SNAPSHOT_PROTECT_BUCKET– Enable versioning + public access block (1to enable).KAFSCALE_OPERATOR_ETCD_SNAPSHOT_SKIP_PREFLIGHT– Skip the S3 write preflight (1to enable).KAFSCALE_OPERATOR_LEADER_KEY– Override the operator leader election ID (defaultkafscale-operator).KAFSCALE_S3_NAMESPACE– Prefix used for broker S3 object keys (defaults to the cluster namespace).KAFSCALE_SEGMENT_BYTES– Broker segment flush threshold in bytes (default4194304).KAFSCALE_FLUSH_INTERVAL_MS– Broker flush interval in milliseconds (default500).
Broker
KAFSCALE_BROKER_ADDR– Kafka listener address (host:port).KAFSCALE_BROKER_HOST– Advertised host (used withKAFSCALE_BROKER_PORT).KAFSCALE_BROKER_PORT– Advertised port (default9092).KAFSCALE_BROKER_ID– Broker node ID.KAFSCALE_METRICS_ADDR– Metrics listen address.KAFSCALE_CONTROL_ADDR– Control-plane listen address.KAFSCALE_ETCD_ENDPOINTS– Etcd endpoints for metadata/offsets.KAFSCALE_ETCD_USERNAME,KAFSCALE_ETCD_PASSWORD– Etcd basic auth.KAFSCALE_S3_BUCKET– S3 bucket for segments/snapshots.KAFSCALE_S3_REGION– S3 region.KAFSCALE_S3_ENDPOINT– S3 endpoint override.KAFSCALE_S3_READ_BUCKET– Optional read replica bucket (CRR/MRAP).KAFSCALE_S3_READ_REGION– Optional read replica region.KAFSCALE_S3_READ_ENDPOINT– Optional read replica endpoint override.KAFSCALE_S3_PATH_STYLE– Force path-style addressing (true/false).KAFSCALE_S3_KMS_ARN– KMS key ARN for SSE-KMS.KAFSCALE_S3_ACCESS_KEY,KAFSCALE_S3_SECRET_KEY,KAFSCALE_S3_SESSION_TOKEN– S3 credentials.KAFSCALE_S3_CONCURRENCY– Broker-wide cap on concurrent S3 operations (default64,0to disable). Lower for slower S3-compatible backends.
Read replica example (multi-region reads):
export KAFSCALE_S3_BUCKET=prod-segments
export KAFSCALE_S3_REGION=us-east-1
export KAFSCALE_S3_READ_BUCKET=prod-segments-replica
export KAFSCALE_S3_READ_REGION=eu-west-1
KAFSCALE_CACHE_BYTES– Broker cache size in bytes.KAFSCALE_READAHEAD_SEGMENTS– Segment readahead count.KAFSCALE_AUTO_CREATE_TOPICS– Auto-create topics (true/false).KAFSCALE_AUTO_CREATE_PARTITIONS– Partition count for auto-created topics.KAFSCALE_USE_MEMORY_S3– Use in-memory S3 client (dev only).KAFSCALE_PRODUCE_SYNC_FLUSH– Flush to S3 on Produce whenacks != 0(defaulttrue).KAFSCALE_LOG_LEVEL– Log level (debug,info,warn,error).KAFSCALE_TRACE_KAFKA– Enable protocol tracing (true/false).KAFSCALE_THROUGHPUT_WINDOW_SEC– Throughput window size seconds.KAFSCALE_S3_HEALTH_WINDOW_SEC– S3 health sampling window seconds.KAFSCALE_S3_LATENCY_WARN_MS,KAFSCALE_S3_LATENCY_CRIT_MS– Latency thresholds.KAFSCALE_S3_ERROR_RATE_WARN,KAFSCALE_S3_ERROR_RATE_CRIT– Error-rate thresholds.KAFSCALE_STARTUP_TIMEOUT_SEC– Broker startup timeout.
Produce Flush Policy (cost vs durability)
By default, the broker flushes to S3 for every Produce request with acks != 0
to minimize the loss window after a broker crash. You can relax this for lower
S3 write costs by disabling sync flush and relying on the background flush
interval/segment size.
Durability-optimized (default):
KAFSCALE_PRODUCE_SYNC_FLUSH=trueKAFSCALE_FLUSH_INTERVAL_MS=500KAFSCALE_SEGMENT_BYTES=4194304
Cost-optimized (accepts a larger loss window after crash):
KAFSCALE_PRODUCE_SYNC_FLUSH=falseKAFSCALE_FLUSH_INTERVAL_MS=2000(or higher)KAFSCALE_SEGMENT_BYTES=33554432(or higher)
Proxy
KAFSCALE_PROXY_ADDR– Proxy listen address (host:port).KAFSCALE_PROXY_HEALTH_ADDR– Health/readiness listen address (default:9094; exposes/readyzand/livez).KAFSCALE_PROXY_ADVERTISED_HOST– Address Kafka clients should connect to.KAFSCALE_PROXY_ADVERTISED_PORT– Advertised port (default9092).KAFSCALE_PROXY_ETCD_ENDPOINTS– Etcd endpoints for metadata snapshots.KAFSCALE_PROXY_ETCD_USERNAME,KAFSCALE_PROXY_ETCD_PASSWORD– Etcd auth for proxy.KAFSCALE_PROXY_BACKENDS– Optional comma-separated broker list (host:port) for backend routing.KAFSCALE_PROXY_BACKEND_CACHE_TTL_SEC– Seconds to cache backend metadata before refreshing from etcd (default60).KAFSCALE_PROXY_BACKEND_BACKOFF_MS– Backoff between backend connection retries (default500).KAFSCALE_PROXY_BACKEND_RETRIES– Backend connection retry count (default6).KAFSCALE_PROXY_READYZ_FETCH_PROBE– Gate/readyzon a real Fetch round-trip (defaulttrue). See Proxy readiness.KAFSCALE_PROXY_LFS_ENABLED– Enable the LFS HTTP API on the unified proxy (defaultfalse).
Console
KAFSCALE_CONSOLE_HTTP_ADDR– Console listen address.KAFSCALE_CONSOLE_ETCD_ENDPOINTS– Etcd endpoints for metadata read-only access.KAFSCALE_CONSOLE_ETCD_USERNAME,KAFSCALE_CONSOLE_ETCD_PASSWORD– Etcd auth for console.KAFSCALE_CONSOLE_BROKER_METRICS_URL– Broker Prometheus endpoint.KAFSCALE_UI_USERNAME,KAFSCALE_UI_PASSWORD– Console login credentials.
External Kafka Access
By default, brokers advertise the in-cluster service DNS name. That works for clients running inside Kubernetes. External clients need a reachable address in Metadata responses.
Recommended: deploy the Kafka-aware proxy (proxy.enabled=true). When the
proxy is enabled, clients connect to the proxy Service, not individual broker
pods. The proxy answers Metadata/FindCoordinator with a single stable endpoint,
then forwards all other Kafka requests to brokers. This keeps clients connected as
broker pods scale or rotate.
Optional: expose brokers directly via KafscaleCluster spec.brokers Service
settings (LoadBalancer or NodePort). Use this only when you intentionally bypass
the proxy (traffic isolation, pinned producers, or legacy integrations). Broker
NodePort does not replace the proxy entrypoint in scaled deployments.
The Helm chart does not ship a Kafka Ingress resource. External TLS uses LoadBalancer annotations or an external TCP gateway (see Proxy TLS via LoadBalancer).
Kafka Proxy (recommended)
Recommended settings:
- Run 2+ proxy replicas behind a LoadBalancer service.
- Point the proxy at etcd via
KAFSCALE_PROXY_ETCD_ENDPOINTSso it can read the cluster snapshot. - Set
KAFSCALE_PROXY_ADVERTISED_HOST/KAFSCALE_PROXY_ADVERTISED_PORTto the public DNS + port clients should use.
Helm values to enable:
proxy.enabled=trueproxy.etcdEndpoints[0]=http://kafscale-etcd-client.<namespace>.svc.cluster.local:2379proxy.advertisedHost=<public DNS>
Example (HA proxy + external access):
helm upgrade --install kafscale deploy/helm/kafscale \
--namespace kafscale --create-namespace \
--set proxy.enabled=true \
--set proxy.replicaCount=2 \
--set proxy.service.type=LoadBalancer \
--set proxy.service.port=9092 \
--set proxy.advertisedHost=kafka.example.com \
--set proxy.advertisedPort=9092 \
--set proxy.etcdEndpoints[0]=http://kafscale-etcd-client.kafscale.svc.cluster.local:2379
For local clusters (kind, minikube), pin the proxy NodePort so host port mappings
stay stable. Set proxy.service.type=NodePort and proxy.service.nodePort to a
value in 30000–32767, then set proxy.advertisedHost to the node IP (or the
mapped host address). See deploy/helm/kafscale/README.md for details.
Example (kind / local dev):
helm upgrade --install kafscale deploy/helm/kafscale \
--namespace kafscale --create-namespace \
--set proxy.enabled=true \
--set proxy.service.type=NodePort \
--set proxy.service.nodePort=30092 \
--set proxy.advertisedHost=127.0.0.1 \
--set proxy.advertisedPort=30092 \
--set proxy.etcdEndpoints[0]=http://kafscale-etcd-client.kafscale.svc.cluster.local:2379
Helm chart docs: deploy/helm/kafscale/README.md.
Direct broker exposure (optional)
Broker exposure settings (KafscaleCluster spec.brokers):
advertisedHost/advertisedPort– Address Kafka clients should connect to.service.type–ClusterIP,LoadBalancer, orNodePort.service.annotations– Cloud provider LB annotations.service.loadBalancerIP/service.loadBalancerSourceRanges– Static IP + CIDR allowlist.service.externalTrafficPolicy–ClusterorLocal.service.kafkaNodePort/service.metricsNodePort– Optional NodePort overrides.
Example (GKE/AWS/Azure load balancer):
apiVersion: kafscale.io/v1alpha1
kind: KafscaleCluster
metadata:
name: kafscale
namespace: kafscale
spec:
brokers:
advertisedHost: kafka.example.com
advertisedPort: 9092
service:
type: LoadBalancer
annotations:
networking.gke.io/load-balancer-type: "External"
loadBalancerSourceRanges:
- 203.0.113.0/24
s3:
bucket: kafscale
region: us-east-1
credentialsSecretRef: kafscale-s3-credentials
etcd:
endpoints: []
TLS note: brokers speak plaintext. For TLS on Kafka traffic, enable TLS or mTLS
on the proxy (mTLS authentication), or terminate TLS at your load balancer, ingress TCP proxy, or service mesh and
advertise that endpoint in advertisedHost/advertisedPort. See docs/security.md
for the current transport security posture.
Example certificate (cert-manager) for a TCP proxy or load balancer that uses a Kubernetes TLS secret:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: kafscale-kafka-cert
namespace: kafscale
spec:
secretName: kafscale-kafka-tls
dnsNames:
- kafka.example.com
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
Upgrades & Rollbacks
- Use
helm upgrade --installwith the desired image tags. The operator drains brokers through the gRPC control plane before restarting pods. - CRD schema changes follow Kubernetes best practices; run
helm upgradeto pick them up. - Rollbacks can be performed with
helm rollback kafscale <REVISION>which restores the previous deployment and service versions. Brokers are stateless so the recovery window is short.
S3 Cost Estimation (Example)
Assumptions:
- 100 GB/day ingestion
- 7-day retention
- 4 MB average segment size
- 3 broker pods
Monthly costs (US-East-1):
| Item | Calculation | Cost |
|---|---|---|
| Storage | 700 GB x $0.023/GB | $16.10 |
| PUT requests | 25,000/day x 30 x $0.005/1000 | $3.75 |
| GET requests | 100,000/day x 30 x $0.0004/1000 | $1.20 |
| Data transfer (in-region) | Free | $0 |
| Total S3 | ~$21/month |
Multi-Region S3 (CRR) for Global Reads
Kafscale writes to a primary bucket and can optionally read from a replica bucket in the broker region. With S3 Cross-Region Replication (CRR), objects written to the primary are asynchronously copied to replica buckets in other regions. Brokers attempt reads from their local replica and fall back to the primary if the replica is missing an object (for example, due to CRR lag).
CRR Setup (AWS)
- Create buckets in each region:
- Primary:
kafscale-prod-us-east-1 - Replica:
kafscale-prod-eu-west-1 - Replica:
kafscale-prod-ap-southeast-1
- Primary:
-
Enable versioning on all buckets (required for CRR).
- Create a replication rule on the primary bucket:
- Replicate all objects (or the Kafscale prefix) to both replica buckets.
- Use an IAM role that can write to the replica buckets.
- Optional: Enable encryption (SSE-KMS) on all buckets with compatible keys.
Kafscale Configuration (Per Region)
Primary region (writers and primary readers):
spec:
s3:
bucket: kafscale-prod-us-east-1
region: us-east-1
credentialsSecretRef: kafscale-s3-creds
EU read replica:
spec:
s3:
bucket: kafscale-prod-us-east-1
region: us-east-1
readBucket: kafscale-prod-eu-west-1
readRegion: eu-west-1
credentialsSecretRef: kafscale-s3-creds
Asia read replica:
spec:
s3:
bucket: kafscale-prod-us-east-1
region: us-east-1
readBucket: kafscale-prod-ap-southeast-1
readRegion: ap-southeast-1
credentialsSecretRef: kafscale-s3-creds
How Reads and Writes Work
- Writes always go to the primary bucket.
- Reads go to the local
readBucketfirst; on miss or error, the broker retries against the primary bucket. - List/restore operations use the primary bucket to avoid missing newly written segments.
Latency and Consistency Impact
- Lower read latency for consumers in EU/Asia when objects are already replicated.
- CRR lag means the newest segments may not appear immediately in the replica; the broker will fall back to the primary for those reads.
- Extra cross-region traffic occurs only on replica misses; steady-state reads stay in-region once replication catches up.
Broker and managed etcd affinity overrides
spec.brokers.affinity and spec.etcd.affinity accept Kubernetes Affinity
objects independently. Omit a field (or set it to null) to retain the default
soft pod anti-affinity on kubernetes.io/hostname. An explicit affinity: {}
disables that default. A non-empty object replaces the entire affinity; the
operator does not merge the default into it. Removing an override restores the
default on the next reconciliation. Changes update the StatefulSet pod template
and can trigger a rolling update. Etcd placement applies only to managed etcd;
it does not affect an externally configured etcd cluster.
For example, require brokers on a labeled node pool and disable etcd affinity in a single-node development cluster (merge these fields into your existing CR):
spec:
brokers:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kafscale.io/node-pool
operator: In
values: [brokers]
etcd:
affinity: {}
To extend the default, include its soft anti-affinity term alongside your custom
rules. For a cluster named example, the broker selector is
{app: kafscale-broker, cluster: example}; etcd uses
{app: kafscale-etcd, cluster: example}. The default term has weight 100 and
topology key kubernetes.io/hostname. Required affinity/anti-affinity rules can
leave pods Pending when matching nodes or topology domains are unavailable.
The affinity CRD fields use a generated structural schema. After changing the
API types or upgrading Kubernetes API dependencies, regenerate those fields
with python3 hack/update_affinity_schema.py (requires Go and PyYAML).
python3 hack/update_affinity_schema.py --check checks for drift while preserving
the other manually maintained CRD fields.