The Setup
When you deploy Confluent Schema Registry on EKS against an MSK Serverless cluster, you're combining three things that each have their own opinions about authentication:
- Confluent's Docker image — assumes it controls its own environment
- Kubernetes — helpfully injects environment variables from service discovery
- MSK Serverless — requires SASL/OAUTHBEARER (IAM) with a specific JAR on the JVM classpath
In isolation, each is fine. Together, they'll fight you in exactly three layers. Here's what I found and how I fixed each one.
The Environment
- EKS 1.31 (Fargate managed node group)
- MSK Serverless (us-east-1)
-
confluentinc/cp-schema-registry:7.5.0as the base image - MSK IAM auth via IRSA (IAM Roles for Service Accounts)
-
aws-msk-iam-auth-2.3.2-all.jarfor the SASL callback handler
Layer 1: Kubernetes Poisons the Schema Registry Config
Symptom: Schema registry pod crashes immediately on startup. Logs show something like:
Schema Registry environment variables are not properly configured
SCHEMA_REGISTRY_PORT must be set to a valid port number
Root cause: Kubernetes injects environment variables for every Service in the same namespace. If there's a service named schema-registry, Kubernetes adds:
SCHEMA_REGISTRY_PORT=tcp://10.100.200.50:8081
SCHEMA_REGISTRY_PORT_8081_TCP=tcp://10.100.200.50:8081
SCHEMA_REGISTRY_PORT_8081_TCP_ADDR=10.100.200.50
Confluent's startup scripts map any env var prefixed with SCHEMA_REGISTRY_ into the config file. So SCHEMA_REGISTRY_PORT=tcp://10.100.200.50:8081 gets translated to port=tcp://10.100.200.50:8081 — which is not a valid port number, causing the crash.
Fix: Add enableServiceLinks: false to the pod spec:
apiVersion: apps/v1
kind: Deployment
metadata:
name: schema-registry
spec:
template:
spec:
enableServiceLinks: false # prevents K8s service env var injection
containers:
- name: schema-registry
image: your-ecr/schema-registry:7.5.0-msk
# ...
This tells Kubernetes to skip environment variable injection from service discovery for this pod. Your own explicitly-defined env vars (like SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS) are unaffected.
Same issue hits Kafka broker pods. Any Confluent image that uses the
KAFKA_orSCHEMA_REGISTRY_env var convention will be crashed by K8s service link injection. Always setenableServiceLinks: falseon these deployments.
Layer 2: The MSK IAM Auth JAR Doesn't Reach the JVM
Symptom: Schema registry starts but immediately fails to connect to MSK Serverless:
ERROR org.apache.kafka.clients.NetworkClient - [Producer clientId=...]
Connection to node -1 (b-1.xxx.kafka.us-east-1.amazonaws.com/...) failed
authentication due to: SSL handshake failed
Or worse — it silently retries PLAINTEXT/SSL when what MSK Serverless requires is SASL/OAUTHBEARER.
Root cause: MSK Serverless only supports IAM (SASL/OAUTHBEARER). The Confluent 7.5 base image doesn't include the aws-msk-iam-auth JAR. You need to add it to the JVM classpath. But there's a catch: Confluent's entrypoint scripts call java directly and ignore the CLASSPATH environment variable.
Setting CLASSPATH=/path/to/aws-msk-iam-auth.jar in your deployment manifest does nothing.
Fix: Build a custom image that bakes the JAR directly into the directory Confluent's scripts scan:
FROM confluentinc/cp-schema-registry:7.5.0
# Confluent's kafka-run-class hardcodes /usr/share/java/schema-registry/*
# and does NOT honour the CLASSPATH env var — the JAR must be here.
USER root
ADD https://repo1.maven.org/maven2/software/amazon/msk/aws-msk-iam-auth/2.3.2/aws-msk-iam-auth-2.3.2-all.jar \
/usr/share/java/schema-registry/aws-msk-iam-auth.jar
RUN chmod 644 /usr/share/java/schema-registry/aws-msk-iam-auth.jar
USER appuser
Using ADD with the Maven Central URL means you don't need to manage the JAR locally — Docker pulls it at build time. The USER root / USER appuser sandwich is required because Confluent's base image runs as appuser (uid 1000) by default, but writing to /usr/share/java/schema-registry/ requires root.
Then configure the SASL settings in your env vars:
env:
- name: SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS
value: "b-1.xxx.kafka.us-east-1.amazonaws.com:9098"
- name: SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL
value: "SASL_SSL"
- name: SCHEMA_REGISTRY_KAFKASTORE_SASL_MECHANISM
value: "AWS_MSK_IAM"
- name: SCHEMA_REGISTRY_KAFKASTORE_SASL_JAAS_CONFIG
value: "software.amazon.msk.auth.iam.IAMLoginModule required;"
- name: SCHEMA_REGISTRY_KAFKASTORE_SASL_CLIENT_CALLBACK_HANDLER_CLASS
value: "software.amazon.msk.auth.iam.IAMClientCallbackHandler"
The key is that /usr/share/java/schema-registry/ is the directory Confluent's launch scripts automatically add to the JVM classpath via wildcard (-cp /usr/share/java/schema-registry/*). Putting your JAR there means it's picked up without any script modification.
Why not CLASSPATH? The Confluent Docker image uses launch scripts that construct the
javacommand internally. TheCLASSPATHenv var is only used ifjavais invoked without explicit-cp/-classpathflags. Confluent sets its own-cp, so yourCLASSPATHoverride is ignored.
Layer 3: The Ensure Script Fails Pre-Flight Checks
Symptom: Pod starts, gets past the JAR loading, but then hangs for minutes before failing with a connectivity error — even though MSK is accessible. Or it fails immediately with an obscure error from the Confluent ensure script.
Root cause: The Confluent Docker image runs /etc/confluent/docker/ensure as a pre-flight check before launching Schema Registry. This script attempts to connect to Kafka to verify bootstrap connectivity. In the EKS environment, it may run before your IRSA credentials are fully available, or it uses a different code path that doesn't pick up the IAM auth callback handler.
Fix: Override the ensure script with a no-op via a ConfigMap subPath mount:
# ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: schema-registry-scripts
data:
ensure: |
#!/bin/bash
# No-op: pre-flight connectivity check skipped in EKS.
# IRSA credentials are injected at runtime; the ensure script
# runs before the IAM token exchange is complete.
exit 0
# In the Deployment spec
volumeMounts:
- name: schema-registry-scripts
mountPath: /etc/confluent/docker/ensure
subPath: ensure
volumes:
- name: schema-registry-scripts
configMap:
name: schema-registry-scripts
defaultMode: 0755
The subPath: ensure mount replaces only that single file in the container's filesystem — the rest of /etc/confluent/docker/ is untouched. The defaultMode: 0755 ensures the script is executable.
Is this safe? The
ensurescript's job is to verify connectivity before startup. Since Schema Registry itself will retry connectivity on startup with proper retry/backoff logic, skipping the pre-flight check doesn't lose safety — it just removes a redundant check that doesn't understand your auth setup anyway.
The Complete Custom Image
FROM confluentinc/cp-schema-registry:7.5.0
# Confluent's kafka-run-class hardcodes /usr/share/java/schema-registry/*
# and does NOT honour the CLASSPATH env var — the JAR must be here.
USER root
ADD https://repo1.maven.org/maven2/software/amazon/msk/aws-msk-iam-auth/2.3.2/aws-msk-iam-auth-2.3.2-all.jar \
/usr/share/java/schema-registry/aws-msk-iam-auth.jar
RUN chmod 644 /usr/share/java/schema-registry/aws-msk-iam-auth.jar
USER appuser
Build and push to ECR:
docker build -t <account-id>.dkr.ecr.us-east-1.amazonaws.com/ai-platform/schema-registry:7.5.0-msk \
-f docker/schema-registry/Dockerfile docker/schema-registry/
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.com
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/ai-platform/schema-registry:7.5.0-msk
The Full Deployment Manifest
apiVersion: apps/v1
kind: Deployment
metadata:
name: schema-registry
namespace: microservices
spec:
replicas: 1
strategy:
type: Recreate # rolling update with 2 concurrent pods is toxic on MSK consumer groups
selector:
matchLabels:
app: schema-registry
template:
metadata:
labels:
app: schema-registry
spec:
enableServiceLinks: false # Layer 1: prevents K8s service env var injection
serviceAccountName: schema-registry-sa # IRSA service account
containers:
- name: schema-registry
image: <account-id>.dkr.ecr.us-east-1.amazonaws.com/ai-platform/schema-registry:7.5.0-msk
ports:
- containerPort: 8081
env:
- name: SCHEMA_REGISTRY_HOST_NAME
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: SCHEMA_REGISTRY_LISTENERS
value: "http://0.0.0.0:8081"
- name: SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS
value: "b-1.xxx.kafka.us-east-1.amazonaws.com:9098"
- name: SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL
value: "SASL_SSL"
- name: SCHEMA_REGISTRY_KAFKASTORE_SASL_MECHANISM
value: "AWS_MSK_IAM"
- name: SCHEMA_REGISTRY_KAFKASTORE_SASL_JAAS_CONFIG
value: "software.amazon.msk.auth.iam.IAMLoginModule required;"
- name: SCHEMA_REGISTRY_KAFKASTORE_SASL_CLIENT_CALLBACK_HANDLER_CLASS
value: "software.amazon.msk.auth.iam.IAMClientCallbackHandler"
startupProbe:
httpGet:
path: /subjects
port: 8081
failureThreshold: 30
periodSeconds: 10 # 300s total — _schemas topic sync can take a while
livenessProbe:
httpGet:
path: /subjects
port: 8081
initialDelaySeconds: 60
periodSeconds: 30
volumeMounts:
- name: schema-registry-scripts
mountPath: /etc/confluent/docker/ensure
subPath: ensure # Layer 3: override ensure script
volumes:
- name: schema-registry-scripts
configMap:
name: schema-registry-scripts
defaultMode: 0755
Why Each Layer Was Non-Obvious
| Layer | What made it hard to spot |
|---|---|
enableServiceLinks: false |
The error message blames SCHEMA_REGISTRY_PORT being invalid — doesn't mention Kubernetes service injection at all |
| Custom Dockerfile with JAR | The CLASSPATH env var looks like it should work; the fact that Confluent ignores it is undocumented |
No-op ensure script |
The ensure failure is a pre-flight check exit, which looks like a connection error — masks that IRSA credentials weren't ready |
The real lesson: when you combine a third-party container image with a managed cloud service and a container orchestrator, each one adds an invisible layer of convention. The debug process is peeling them back one at a time — which is exactly why the error messages never tell you the right thing.
The Smoke Test
After all three layers are resolved, Schema Registry starts clean and the MSK Serverless connection goes through IAM:
# From inside the EKS cluster
kubectl exec -n microservices deploy/user-service -- \
curl -s http://schema-registry:8081/subjects
# -> [] (empty on first boot, connection confirmed)
And the full path: POST /api/v1/users through the ALB → user-service → MSK Serverless (via Schema Registry for Avro serialization) → notification-service consumer. HTTP 201.
Have you run into similar layering conflicts when combining third-party container images with managed cloud services on Kubernetes? Which of these three layers caught you most off-guard?
Top comments (0)