Quick Start for Multi-Tenancy in Confluent Private Cloud
Multi-Tenancy in Confluent Private Cloud lets you run multiple isolated Logical Kafka Clusters (LKCs) on a single Physical Kafka Cluster (PKC). Each LKC has its own quotas, limits, and access controls.
In this quick start, you set up Multi-Tenancy on a local Kubernetes cluster by using Confluent for Kubernetes (CFK). You deploy one PKC, create two LKCs on it, and confirm that each LKC administrator can see only their own LKC’s data. For an introduction to PKCs, LKCs, and the roles that manage them, see the Multi-Tenancy Overview page.
The quick start takes 45 to 60 minutes.
When you finish, you have the following running in the confluent
namespace:
CFK, which manages the other components
OpenLDAP, the identity provider for all users
One
KRaftControllerThree
Kafkabrokers, which form the PKC (spec.multiTenant.enabled)One
KafkaRestClassnameddefaultOne
ControlCenter, with Prometheus and AlertmanagerTwo
LogicalKafkaClusterresources,paymentsandanalytics(the LKCs)Four
ConfluentRolebindingresources, which grant theOrganizationAdminandTenantAdminrolesOne
kafka-clientpod, which runs the Kafka command-line tools
Important
This quick start is a learning sandbox. It uses demo passwords and an auto-generated certificate authority (CA), and it doesn’t configure production-grade security. To plan a production deployment, see Next steps.
Prerequisites
Before you begin, make sure you have the following:
Docker with at least 16 GB of memory and six CPUs allocated to it.
kindruns each Kubernetes node as a Docker container, so the five nodes that this quick start creates (one control plane and four workers) share these resources. This quick start covers onlykind. To use another Kubernetes environment, adapt Step 1 and the load-balancer handling in Step 6.Command-line tools:
kind,kubectl,helm,git, andopenssl. Forkind, see the kind installation guide.Network access to Docker Hub for the container images, to
packages.confluent.iofor the CFK Helm chart, and to GitHub for the examples repository.A web browser and
sudoaccess on your machine. In Step 7, you add an/etc/hostsentry so that your browser can open Confluent Control Center.
The quick start deploys Confluent Platform 8.4, CFK 3.4, and Confluent Control Center 2.7, the first releases that support Multi-Tenancy. You pull these images in the steps that follow.
This quick start doesn’t require a license. Without a Confluent Private Cloud license, a PKC allows up to three LKCs, and you create two. To create more LKCs, see Apply a license to create more than three LKCs.
Sample users
The quick start uses three users that you add to the Lightweight Directory Access Protocol (LDAP) directory in Step 2:
User |
Password |
Role |
|---|---|---|
|
|
|
|
|
|
|
|
|
You grant these roles in Step 5.
Step 1: Prepare your environment
Set the following environment variables. Use the same shell for every step, or set them again in each new shell:
export EXAMPLES=~/cfk-mt/confluent-kubernetes-examples export CP_IMAGE=confluentinc/cp-server:8.4.0 export INIT_IMAGE=confluentinc/confluent-init-container:3.4.0 export C3_IMAGE=confluentinc/cp-enterprise-control-center-next-gen:2.7.0 export PROM_IMAGE=confluentinc/cp-enterprise-prometheus:2.7.0 export ALERT_IMAGE=confluentinc/cp-enterprise-alertmanager:2.7.0 export DOMAIN=mt.example.com
To use a later patch release, change the tag.
Create a local Kubernetes cluster with one control-plane node and four worker nodes:
cat <<EOF | kind create cluster --name cfk-mt --config=- kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane - role: worker - role: worker - role: worker - role: worker EOF kubectl config use-context kind-cfk-mt
Create the
confluentnamespace and set it as your default namespace:kubectl create namespace confluent kubectl config set-context --current --namespace=confluent
Note
Use the
confluentnamespace for every step. The manifests hard-code it in service endpoints, such askafka.confluent.svc.cluster.local.Clone the CFK examples repository. It contains the OpenLDAP chart that you install in the next step:
git clone --depth 1 https://github.com/confluentinc/confluent-kubernetes-examples.git "$EXAMPLES"
Step 2: Install CFK, LDAP, and the security dependencies
In this step, you set up everything the PKC depends on:
A certificate authority (CA), which CFK uses to issue the certificates for every component.
CFK itself, which deploys and manages the other components.
An LDAP directory, which Metadata Service (MDS) uses to authenticate users.
The MDS token key pair, which MDS uses to sign tokens.
The secrets that the components use to authenticate to each other.
Create the CA key pair:
openssl genrsa -out /tmp/cfk-mt-ca-key.pem 2048 openssl req -x509 -new -nodes -key /tmp/cfk-mt-ca-key.pem -days 365 -subj "/CN=cfk-mt-ca" -out /tmp/cfk-mt-ca.pem kubectl create secret tls ca-pair-sslcerts --cert=/tmp/cfk-mt-ca.pem --key=/tmp/cfk-mt-ca-key.pem rm -f /tmp/cfk-mt-ca-key.pem /tmp/cfk-mt-ca.pem
Install CFK and wait for it to roll out:
helm repo add confluentinc https://packages.confluent.io/helm helm repo update helm upgrade --install operator confluentinc/confluent-for-kubernetes --namespace confluent \ --set managedCerts.enabled=true \ --set managedCerts.caCertificate.secretRef=ca-pair-sslcerts \ --set kRaftEnabled=true kubectl rollout status deploy/confluent-operator --timeout=180s
The
managedCertssettings make CFK read themanaged-cert-add-sansannotation that theKafkamanifest in Step 3 sets.managedCertsis disabled by default, and CFK ignores the annotation until you enable it.kRaftEnabledturns on KRaft cluster ID recovery.For other installation options, see Deploy Confluent for Kubernetes.
Install OpenLDAP. The chart creates the
kafkaandmdsservice users:helm upgrade --install test-ldap "$EXAMPLES/assets/openldap" \ --namespace confluent -f "$EXAMPLES/assets/openldap/ldaps-rbac.yaml" kubectl wait --for=condition=ready pod/ldap-0 --timeout=180s
Wait about 30 seconds before you continue. The chart restarts OpenLDAP once shortly after the pod reports ready, and the restart discards any users you add before it finishes.
Add the three sample users:
kubectl exec -i ldap-0 -- ldapadd -x -c -H ldap://localhost:389 \ -D "cn=admin,dc=test,dc=com" -w confluentrox <<EOF dn: cn=platform-operator,dc=test,dc=com objectClass: simpleSecurityObject objectClass: organizationalRole cn: platform-operator userPassword: platform-operator-secret dn: cn=payments-admin,dc=test,dc=com objectClass: simpleSecurityObject objectClass: organizationalRole cn: payments-admin userPassword: payments-admin-secret dn: cn=analytics-admin,dc=test,dc=com objectClass: simpleSecurityObject objectClass: organizationalRole cn: analytics-admin userPassword: analytics-admin-secret EOF
Confirm that the three users exist:
kubectl exec ldap-0 -- ldapsearch -x -LLL -H ldap://localhost:389 \ -D "cn=admin,dc=test,dc=com" -w confluentrox \ -b "dc=test,dc=com" \ "(|(cn=platform-operator)(cn=payments-admin)(cn=analytics-admin))" dn \ | grep -c '^dn:'
The command prints
3. If it prints a lower number, run theldapaddcommand again.Create the MDS token key pair. MDS uses it to sign the tokens it issues:
openssl genrsa -out /tmp/cfk-mt-mds-priv.pem 2048 openssl rsa -in /tmp/cfk-mt-mds-priv.pem -pubout -out /tmp/cfk-mt-mds-pub.pem kubectl create secret generic mds-token \ --from-file=mdsTokenKeyPair.pem=/tmp/cfk-mt-mds-priv.pem \ --from-file=mdsPublicKey.pem=/tmp/cfk-mt-mds-pub.pem rm -f /tmp/cfk-mt-mds-priv.pem /tmp/cfk-mt-mds-pub.pem
Create the secrets that hold the credentials for the PKC, Confluent Control Center, and the sample users. The
external-userssecret contains only the three sample users. Clients that connect through the external listener authenticate against it:kubectl create secret generic credential \ --from-literal=plain-users.json='{"kafka":"kafka-secret","operator":"operator-secret","kafka_client":"kafka_client-secret"}' \ --from-literal=plain.txt=$'username=kafka\npassword=kafka-secret' \ --from-literal=plain-interbroker.txt=$'username=kafka\npassword=kafka-secret' \ --from-literal=kafka-server-listener-internal-plain-metrics.txt=$'username=kafka\npassword=kafka-secret' \ --from-literal=controlcenter-client-plain-jaas.conf='org.apache.kafka.common.security.plain.PlainLoginModule required username="kafka" password="kafka-secret";' \ --from-literal=ldap.txt=$'username=cn=mds,dc=test,dc=com\npassword=Developer!' kubectl create secret generic external-users \ --from-literal=plain-users.json='{"platform-operator":"platform-operator-secret","payments-admin":"payments-admin-secret","analytics-admin":"analytics-admin-secret"}' kubectl create secret generic mds-client-erp --from-literal=bearer.txt=$'username=kafka\npassword=kafka-secret' kubectl create secret generic rest-credential --from-literal=bearer.txt=$'username=kafka\npassword=kafka-secret' kubectl create secret generic prometheus-credentials \ --from-literal=basic.txt='prometheus-admin: prometheus-admin-password' kubectl create secret generic prometheus-client-creds \ --from-literal=basic.txt=$'username=prometheus-admin\npassword=prometheus-admin-password' kubectl create secret generic alertmanager-credentials \ --from-literal=basic.txt='alertmanager-admin: alertmanager-admin-password' kubectl create secret generic alertmanager-client-creds \ --from-literal=basic.txt=$'username=alertmanager-admin\npassword=alertmanager-admin-password'
Step 3: Deploy the PKC
A PKC is a Kafka custom resource (CR) with
spec.multiTenant.enabled set to true. When you enable
Multi-Tenancy, CFK rejects the CR unless the CR also configures the
following:
TLS on the internal and external listeners
role-based access control (RBAC) authorization
An embedded MDS
A memory request or limit for the broker pods
CFK also adds an in-cluster listener for LKCs, TOKENMT, on port
9076. LKCs use this listener for in-cluster connections.
You don’t enable Multi-Tenancy on the KRaftController or
ControlCenter CRs. The ControlCenter CR detects it from the
Kafka CR that it references, and the KRaftController CR follows the
Kafka CR that references it.
Deploy the KRaft controller:
cat <<EOF | kubectl apply -f - apiVersion: platform.confluent.io/v1beta1 kind: KRaftController metadata: name: kraftcontroller namespace: confluent spec: replicas: 1 dataVolumeCapacity: 10G configOverrides: server: - default.replication.factor=3 - min.insync.replicas=2 image: application: $CP_IMAGE init: $INIT_IMAGE authorization: type: rbac superUsers: - User:kafka - User:mds - User:operator tls: autoGeneratedCerts: true listeners: controller: authentication: type: plain jaasConfig: secretRef: credential tls: enabled: true dependencies: mdsKafkaCluster: bootstrapEndpoint: kafka.confluent.svc.cluster.local:9071 authentication: type: plain jaasConfig: secretRef: credential tls: enabled: true metricsClient: url: http://controlcenter.confluent.svc.cluster.local:9090 authentication: type: basic basic: secretRef: prometheus-client-creds EOF
Deploy the PKC:
cat <<EOF | kubectl apply -f - apiVersion: platform.confluent.io/v1beta1 kind: Kafka metadata: name: kafka namespace: confluent annotations: platform.confluent.io/managed-cert-add-sans: "*.$DOMAIN,*.b0.$DOMAIN,*.b1.$DOMAIN,*.b2.$DOMAIN" spec: replicas: 3 dataVolumeCapacity: 10G image: application: $CP_IMAGE init: $INIT_IMAGE podTemplate: resources: requests: memory: 4Gi limits: memory: 4Gi multiTenant: enabled: true createTenantNameServices: true authorization: type: rbac superUsers: - User:kafka - User:mds - User:operator tls: autoGeneratedCerts: true listeners: internal: authentication: type: plain jaasConfig: secretRef: credential tls: enabled: true external: externalAccess: type: loadBalancer loadBalancer: domain: $DOMAIN authentication: type: plain jaasConfig: secretRef: external-users tls: enabled: true replication: authentication: type: plain jaasConfig: secretRef: credential tls: enabled: true services: mds: tls: enabled: true tokenKeyPair: secretRef: mds-token provider: ldap: address: ldap://ldap.confluent.svc.cluster.local:389 authentication: type: simple simple: secretRef: credential tls: enabled: false configurations: groupNameAttribute: cn groupObjectClass: group groupMemberAttribute: member groupMemberAttributePattern: CN=(.*),DC=test,DC=com groupSearchBase: dc=test,dc=com userNameAttribute: cn userObjectClass: organizationalRole userMemberOfAttributePattern: CN=(.*),DC=test,DC=com userSearchBase: dc=test,dc=com dependencies: kRaftController: clusterRef: name: kraftcontroller controllerListener: authentication: type: plain jaasConfig: secretRef: credential tls: enabled: true kafkaRest: authentication: type: bearer bearer: secretRef: mds-client-erp metricsClient: url: http://controlcenter.confluent.svc.cluster.local:9090 authentication: type: basic basic: secretRef: prometheus-client-creds EOF
The manifest includes the following Multi-Tenancy settings:
multiTenant.createTenantNameServices: true: Makes the in-cluster LKC hostnames resolve without changes to your cluster’s DNS. Use this option only when you can’t edit your cluster’s DNS. It creates a Service for every broker of every LKC, which uses up the cluster’s Service IP range as you add LKCs and brokers. Otherwise, configure DNS rewrite rules for the LKC hostnames.managed-cert-add-sansannotation: Adds the LKC hostnames to the certificate that CFK generates. LKC hostnames take these forms, where<lkc-id>already includes thelkc-prefix:Bootstrap:
<lkc-id>-kafka.<domain>Brokers:
<lkc-id>-kafka.b<n>.<domain>
A wildcard matches only one DNS label, so the annotation lists
*.<domain>for the bootstrap hostnames and a separate*.b<n>.<domain>for each broker. CFK reads the annotation because the Helm install in Step 2 enablesmanagedCerts. For other certificate approaches, see the Configure TLS and DNS page.podTemplate.resourcesmemory of4Gi: Each LKC’s quota capacity derives from the broker heap. Lower values of memory can cause the brokers to crash-loop on startup.
CFK also creates the
kafka-bootstrap-lbandkafka-<n>-lbServiceobjects. Onkind, theseServiceobjects stay in<pending>becausekindhas no external load balancer, which is expected. In Step 6, you reach them by their cluster IP addresses.Create the
KafkaRestClassthat CFK uses to manage LKCs and role bindings through MDS:cat <<EOF | kubectl apply -f - apiVersion: platform.confluent.io/v1beta1 kind: KafkaRestClass metadata: name: default namespace: confluent spec: kafkaClusterRef: name: kafka kafkaRest: authentication: type: bearer bearer: secretRef: rest-credential EOF
Wait 5 to 10 minutes for the brokers to start, and then confirm that Multi-Tenancy is active:
kubectl wait --for=jsonpath='{.status.phase}'=RUNNING kafka/kafka --timeout=15m kubectl get kafka kafka -o jsonpath='phase={.status.phase} multiTenant={.status.multiTenant.enabled} clusterID={.status.clusterID}{"\n"}' kubectl wait --for=jsonpath='{.status.phase}'=RUNNING kraftcontroller/kraftcontroller --timeout=10m
The second command prints
multiTenant=true, which confirms that Multi-Tenancy is active.CFK restarts the KRaft controller once after the PKC becomes multi-tenant. Allow a few extra minutes for the restart to finish.
For the full list of settings that a multi-tenant PKC supports, see the Deploy a Multi-Tenant Physical Kafka Cluster page.
Step 4: Deploy Confluent Control Center
Use the environment variables that you set in Step 1. Confluent Control Center detects Multi-Tenancy from the
Kafka CR that it references, so you don’t set a Multi-Tenancy field.
The ControlCenter CR must set spec.authorization.type to rbac.
cat <<EOF | kubectl apply -f -
apiVersion: platform.confluent.io/v1beta1
kind: ControlCenter
metadata:
name: controlcenter
namespace: confluent
spec:
replicas: 1
image:
application: $C3_IMAGE
init: $INIT_IMAGE
dataVolumeCapacity: 10Gi
authorization:
type: rbac
tls:
autoGeneratedCerts: true
dependencies:
kafka:
discovery:
name: kafka
namespace: confluent
secretRef: credential
tls:
enabled: true
mds:
endpoint: https://kafka.confluent.svc.cluster.local:8090
tokenKeyPair:
secretRef: mds-token
authentication:
type: bearer
bearer:
secretRef: rest-credential
tls:
enabled: true
prometheusClient:
url: http://controlcenter.confluent.svc.cluster.local:9090
authentication:
type: basic
basic:
secretRef: prometheus-client-creds
alertManagerClient:
url: http://controlcenter.confluent.svc.cluster.local:9093
authentication:
type: basic
basic:
secretRef: alertmanager-client-creds
services:
prometheus:
image: $PROM_IMAGE
authentication:
type: basic
basic:
secretRef: prometheus-credentials
pvc:
dataVolumeCapacity: 10Gi
alertmanager:
image: $ALERT_IMAGE
authentication:
type: basic
basic:
secretRef: alertmanager-credentials
EOF
kubectl wait --for=jsonpath='{.status.phase}'=RUNNING \
controlcenter/controlcenter --timeout=10m
Step 5: Create the LKCs and grant access
Grant the
OrganizationAdminrole on the PKC to thekafkauser, the identity that CFK uses to call MDS. CFK can’t create LKCs until this identity holds a role that manages LKCs, such asOrganizationAdmin. Superuser status doesn’t grant this permission.PKC=$(kubectl get kafka kafka -o jsonpath='{.status.clusterID}') cat <<EOF | kubectl apply -f - apiVersion: platform.confluent.io/v1beta1 kind: ConfluentRolebinding metadata: name: cfk-orgadmin namespace: confluent spec: principal: type: user name: kafka role: OrganizationAdmin clustersScopeByIds: kafkaClusterId: $PKC kafkaRestClassRef: name: default EOF kubectl wait --for=jsonpath='{.status.state}'=CREATED \ confluentrolebinding/cfk-orgadmin --timeout=5m
Create the
paymentsandanalyticsLKCs:cat <<EOF | kubectl apply -f - apiVersion: platform.confluent.io/v1beta1 kind: LogicalKafkaCluster metadata: name: payments namespace: confluent spec: kafkaClusterRef: name: kafka kafkaRestClassRef: name: default quotas: ingressByteRate: 10485760 egressByteRate: 31457280 limits: maxPartitions: 1000 --- apiVersion: platform.confluent.io/v1beta1 kind: LogicalKafkaCluster metadata: name: analytics namespace: confluent spec: kafkaClusterRef: name: kafka kafkaRestClassRef: name: default quotas: ingressByteRate: 10485760 egressByteRate: 31457280 limits: maxPartitions: 1000 EOF kubectl wait --for=jsonpath='{.status.state}'=CREATED \ lkc/payments lkc/analytics --timeout=5m
Clients can produce up to 10 MiB per second to each LKC and consume up to 30 MiB per second from it, and each LKC can hold at most 1,000 partitions. The
quotasandlimitsfields are optional.Caution
When you update an LKC, include every
spec.quotasandspec.limitsfield that you want to keep. CFK overwrites both objects in MDS on every reconciliation and clears any field that’s missing from the CR. For more information, see the Monitor and Manage Per-LKC Resource Quotas page.Confirm that each LKC is ready and view its bootstrap URLs:
kubectl get lkc -o custom-columns='NAME:.metadata.name,ID:.status.logicalClusterID,STATE:.status.state,PHASE:.status.phase,EXTERNAL:.status.bootstrapEndpoint,IN-CLUSTER:.status.internalBootstrapEndpoint'
An LKC is ready when
STATEisCREATED,PHASEisRunning, and it has anlkc-<id>identifier. Each LKC has two bootstrap URLs:Path
LKC status field and example endpoint
Client authentication
In-cluster
status.internalBootstrapEndpoint, for example<lkc-id>-kafka.confluent.svc.cluster.local:9076OAUTHBEARERwith an MDS token, using the user’s LDAP username and passwordExternal
status.bootstrapEndpoint, for example<lkc-id>-kafka.mt.example.com:9092SASL/PLAIN, using the credentials in theexternal-userssecretGrant the roles to the sample users:
cat <<EOF | kubectl apply -f - apiVersion: platform.confluent.io/v1beta1 kind: ConfluentRolebinding metadata: name: platform-operator-orgadmin namespace: confluent spec: principal: type: user name: platform-operator role: OrganizationAdmin clustersScopeByIds: kafkaClusterId: $PKC kafkaRestClassRef: name: default --- apiVersion: platform.confluent.io/v1beta1 kind: ConfluentRolebinding metadata: name: payments-admin-payments namespace: confluent spec: principal: type: user name: payments-admin role: TenantAdmin kafkaClusterRef: name: payments kind: LogicalKafkaCluster kafkaRestClassRef: name: default --- apiVersion: platform.confluent.io/v1beta1 kind: ConfluentRolebinding metadata: name: analytics-admin-analytics namespace: confluent spec: principal: type: user name: analytics-admin role: TenantAdmin kafkaClusterRef: name: analytics kind: LogicalKafkaCluster kafkaRestClassRef: name: default EOF kubectl wait --for=jsonpath='{.status.state}'=CREATED \ confluentrolebinding/platform-operator-orgadmin \ confluentrolebinding/payments-admin-payments \ confluentrolebinding/analytics-admin-analytics --timeout=5m
The
TenantAdminrole covers topics, records, and consumer groups, so these users need no access control lists (ACLs). CFK looks up each LKC’s ID fromkafkaClusterRef.Caution
Set
kind: LogicalKafkaClusterand the LKC’s name on an LKC role binding. A binding that names the PKC itself, withkind: Kafkaand the PKC’s name, is scoped to the PKC and grants access to matching resources in every LKC on the PKC. For more information, see the Manage LKC Access with RBAC page.
Step 6: Create a client pod and map LKC hostnames
Use the environment variables that you set in Step 1. In this step, you create a pod to run the Kafka command-line tools from
inside the cluster. The pod includes the cluster’s CA certificate and a
client configuration for each TenantAdmin and connection path.
Create the pod only after the LKCs exist, because the pod’s host aliases include each LKC’s ID. If you create another LKC later and want to reach it through the external listener, recreate the pod.
Create a
ConfigMapthat holds the client configurations. Every configuration makes the client verify the broker’s hostname:cat <<EOF | kubectl apply -f - apiVersion: v1 kind: ConfigMap metadata: name: tenant-clients namespace: confluent data: payments-admin-tokenmt.properties: | security.protocol=SASL_SSL sasl.mechanism=OAUTHBEARER sasl.login.callback.handler.class=io.confluent.kafka.clients.plugins.auth.token.TokenUserLoginCallbackHandler sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required username="payments-admin" password="payments-admin-secret" metadataServerUrls="https://kafka.confluent.svc.cluster.local:8090"; ssl.truststore.type=PEM ssl.truststore.location=/mnt/ca/ca.pem analytics-admin-tokenmt.properties: | security.protocol=SASL_SSL sasl.mechanism=OAUTHBEARER sasl.login.callback.handler.class=io.confluent.kafka.clients.plugins.auth.token.TokenUserLoginCallbackHandler sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required username="analytics-admin" password="analytics-admin-secret" metadataServerUrls="https://kafka.confluent.svc.cluster.local:8090"; ssl.truststore.type=PEM ssl.truststore.location=/mnt/ca/ca.pem payments-admin-external.properties: | security.protocol=SASL_SSL sasl.mechanism=PLAIN sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username="payments-admin" password="payments-admin-secret"; ssl.truststore.type=PEM ssl.truststore.location=/mnt/ca/ca.pem analytics-admin-external.properties: | security.protocol=SASL_SSL sasl.mechanism=PLAIN sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username="analytics-admin" password="analytics-admin-secret"; ssl.truststore.type=PEM ssl.truststore.location=/mnt/ca/ca.pem EOF
Create the pod. The external LKC hostnames aren’t in DNS, so the pod’s
hostAliasesmap them to the cluster IP addresses of the load-balancerServiceobjects:PAY=$(kubectl get lkc payments -o jsonpath='{.status.logicalClusterID}') ANA=$(kubectl get lkc analytics -o jsonpath='{.status.logicalClusterID}') BS=$(kubectl get svc kafka-bootstrap-lb -o jsonpath='{.spec.clusterIP}') B0=$(kubectl get svc kafka-0-lb -o jsonpath='{.spec.clusterIP}') B1=$(kubectl get svc kafka-1-lb -o jsonpath='{.spec.clusterIP}') B2=$(kubectl get svc kafka-2-lb -o jsonpath='{.spec.clusterIP}') cat <<EOF | kubectl apply -f - apiVersion: v1 kind: Pod metadata: name: kafka-client namespace: confluent spec: hostAliases: - ip: "$BS" hostnames: ["$PAY-kafka.$DOMAIN", "$ANA-kafka.$DOMAIN"] - ip: "$B0" hostnames: ["$PAY-kafka.b0.$DOMAIN", "$ANA-kafka.b0.$DOMAIN"] - ip: "$B1" hostnames: ["$PAY-kafka.b1.$DOMAIN", "$ANA-kafka.b1.$DOMAIN"] - ip: "$B2" hostnames: ["$PAY-kafka.b2.$DOMAIN", "$ANA-kafka.b2.$DOMAIN"] containers: - name: cli image: $CP_IMAGE imagePullPolicy: IfNotPresent command: ["sleep", "infinity"] volumeMounts: - name: ca mountPath: /mnt/ca - name: clients mountPath: /clients volumes: - name: ca secret: secretName: ca-pair-sslcerts items: - key: tls.crt path: ca.pem - name: clients configMap: name: tenant-clients EOF kubectl wait --for=condition=ready pod/kafka-client --timeout=180s
Step 7: Verify access and LKC isolation
Wait for the KRaft controller restart from Step 3 to finish. If you create topics before it finishes, the request can fail:
kubectl rollout status statefulset/kraftcontroller --timeout=10m
Check the status of the components and the LKCs:
kubectl get kafka,kraftcontroller,controlcenter -o wide kubectl get lkc -o custom-columns='NAME:.metadata.name,ID:.status.logicalClusterID,STATE:.status.state,PHASE:.status.phase'
The output shows the following:
The
Kafka,KRaftController, andControlCenterCRs each reportRUNNING. TheKafkaandControlCenterCRs also reportMULTITENANTset totrue.Both LKCs report a state of
CREATEDand a phase ofRunning.
Connect to the
paymentsLKC aspayments-adminover the in-cluster path. Create a topic, produce 10 messages to it, and consume them:PAY_INT=$(kubectl get lkc payments -o jsonpath='{.status.internalBootstrapEndpoint}') PAY_EXT=$(kubectl get lkc payments -o jsonpath='{.status.bootstrapEndpoint}') kubectl exec kafka-client -- kafka-topics \ --bootstrap-server "$PAY_INT" \ --command-config /clients/payments-admin-tokenmt.properties \ --create --topic orders --partitions 3 --replication-factor 3 seq 1 10 | kubectl exec -i kafka-client -- kafka-console-producer \ --bootstrap-server "$PAY_INT" \ --producer.config /clients/payments-admin-tokenmt.properties \ --topic orders kubectl exec kafka-client -- kafka-console-consumer \ --bootstrap-server "$PAY_INT" \ --consumer.config /clients/payments-admin-tokenmt.properties \ --topic orders --group payments-check --from-beginning \ --max-messages 10 --timeout-ms 30000
The first command prints
Created topic orders.The consumer prints the records1through10.Connect to the
paymentsLKC aspayments-adminover the external path. List the topics and consume the records:kubectl exec kafka-client -- kafka-topics \ --bootstrap-server "$PAY_EXT" \ --command-config /clients/payments-admin-external.properties --list kubectl exec kafka-client -- kafka-console-consumer \ --bootstrap-server "$PAY_EXT" \ --consumer.config /clients/payments-admin-external.properties \ --topic orders --group payments-check-ext --from-beginning \ --max-messages 10 --timeout-ms 30000
The topic list includes
orders, and the consumer prints the same 10 records.As
analytics-admin, list the topics at thepaymentsLKC’s bootstrap URL:kubectl exec kafka-client -- kafka-topics \ --bootstrap-server "$PAY_INT" \ --command-config /clients/analytics-admin-tokenmt.properties --list
The command returns an empty list. The
analytics-adminuser has no role on thepaymentsLKC, so the user can’t see any of that LKC’s topics.Open Confluent Control Center to confirm that each
TenantAdminsees only their own LKC. Confluent Control Center accepts only its own certificate’s hostname, so map that hostname to your machine and forward the port:echo "127.0.0.1 controlcenter.confluent.svc.cluster.local" | sudo tee -a /etc/hosts kubectl port-forward svc/controlcenter 9021:9021
In a browser, go to
https://controlcenter.confluent.svc.cluster.local:9021and log in:As
platform-operator, Confluent Control Center lists the PKC and both LKCs.As
payments-admin, Confluent Control Center lists only thepaymentsLKC.
Clean up
If you want to keep the cluster and delete individual resources instead, note the following:
Deleting an LKC is permanent. CFK deletes the LKC’s topics within minutes, and its LKC ID is never reused.
Don’t delete all of the broker pods at once. If no other broker is running, a broker that uses RBAC and LDAP can wait up to 10 minutes for its authorizer.
To remove everything this quick start created, delete the cluster and the
/etc/hosts entry that you added in Step 7:
Delete the
kindcluster. This removes all the resources in it:kind delete cluster --name cfk-mt
Remove the
/etc/hostsentry for Confluent Control Center:On macOS:
sudo sed -i '' '/controlcenter.confluent.svc.cluster.local/d' /etc/hosts
On Linux:
sudo sed -i '/controlcenter.confluent.svc.cluster.local/d' /etc/hosts
Apply a license to create more than three LKCs
Without a Confluent Private Cloud license, a PKC allows up to three LKCs. MDS rejects a request to create a fourth. Deleting an LKC frees its slot.
To create more than three LKCs, apply your Confluent Private Cloud license to the PKC:
Store the license in a secret. CFK expects the secret content in the form
license=<license>:{ printf 'license='; cat <path-to-license-file>; } > /tmp/cfk-mt-license.txt kubectl create secret generic confluent-license --from-file=license.txt=/tmp/cfk-mt-license.txt rm -f /tmp/cfk-mt-license.txt
If your license file already starts with
license=, skip theprintfand pass the file to--from-fileas it is.Reference the secret from the Kafka, KRaft, and Confluent Control Center CRs. Each patch restarts that component:
kubectl patch kafka kafka --type=merge -p '{"spec":{"license":{"secretRef":"confluent-license"}}}' kubectl patch kraftcontroller kraftcontroller --type=merge -p '{"spec":{"license":{"secretRef":"confluent-license"}}}' kubectl patch controlcenter controlcenter --type=merge -p '{"spec":{"license":{"secretRef":"confluent-license"}}}'
For more information, see Manage Confluent Platform Licenses.
Troubleshooting
The following table lists common problems, organized by the step where you’re likely to see them. Find the symptom, and then apply the fix.
Symptom |
Cause and fix |
|---|---|
Step 2: Fewer than three users in LDAP |
Run the |
Step 3: The |
A Multi-Tenancy prerequisite is missing: TLS, RBAC, MDS, or broker memory. Restore it in the CR. |
Step 3: Brokers crash-loop with an error that references
|
Broker memory is too low. Keep the memory at |
Step 3: Pods stay |
Give Docker more memory. |
Step 3: Brokers log metrics-push failures |
This is expected until Confluent Control Center is running. |
Step 5: An LKC is in the |
The |
Step 5: An LKC is in the |
The |
Step 5: An LKC stays in |
MDS is still starting and returns |
Step 5: A fourth LKC is in the |
A PKC without a license allows up to three LKCs. Delete one or apply a license. See Apply a license to create more than three LKCs. |
Step 5: The operator log reports |
This is expected when you haven’t applied a license. |
Step 7: |
The |
Step 7: An external client reports an unknown host |
The LKC is missing from the client pod’s |
Step 7: An external client reports a TLS hostname error |
The certificate lacks the LKC’s Subject Alternative Names (SANs). Check the
|
Step 7: Confluent Control Center returns |
Browse to |
Step 7: |
A PKC-scoped |
To investigate other problems, run the following commands:
kubectl logs deploy/confluent-operator | grep -iE 'error|logicalkafkacluster' | tail -50
kubectl describe lkc payments
kubectl get events --sort-by=.lastTimestamp | tail -30
kubectl logs kafka-0 -c kafka | grep -i tenant | tail -30
Next steps
To learn more about Multi-Tenancy or to move toward production, see the following pages:
Multi-Tenancy Overview: Learn how LKCs, quotas, and isolation work.
Configure TLS and DNS: Choose a certificate and DNS approach for LKC endpoints.
Deploy a Multi-Tenant Physical Kafka Cluster: Set up a PKC for production.
Onboard an LKC: Provision an LKC and assign its
TenantAdmin.Onboard a Developer: Grant a developer access to an LKC.
Manage LKC Access with RBAC: Scope role bindings to an LKC and grant LKC access.
Monitor and Manage Per-LKC Resource Quotas: Track each LKC’s usage and adjust its limits.