Skip to main content

Building Multi-Cluster Networks with Cilium

Overview​

If you want to connect the networks of multiple clusters — cross-cluster Pod-to-Pod connectivity, cross-cluster Service load balancing (global Services), and identity-based cross-cluster network policies — you can use Cilium's ClusterMesh capability to form a mesh of clusters; this article describes how to do it on TKE. All three deployment plans (Native Routing (VPC-CNI), Overlay (VPC-CNI), Overlay (GR)) are supported, verified with cilium 1.20.1 + TKE 1.32. The prerequisite is underlying network connectivity between clusters (see the next section).

Network Prerequisites​

ClusterMesh only synchronizes control plane information such as Pods, Services, and security identities between clusters; whether the data plane can communicate depends on the underlying network between the clusters themselves. All clusters must satisfy:

  1. Same routing mode: All clusters must use the same routing mode (all Native Routing or all Overlay); they cannot be mixed.
  2. Small version gap: The cilium versions of all clusters must differ by no more than one minor release (for example, 1.19.x and 1.20.x can interconnect).
  3. Non-overlapping CIDRs: Pod CIDRs and Node IPs of all clusters must not overlap (for Overlay mode, plan a different Pod CIDR per cluster, such as 10.244.0.0/16).

Pod IPs are VPC IPs, so cross-cluster Pod communication is equivalent to cross-subnet communication within the VPC — reachability depends on the VPC network itself:

  • Two clusters in the same VPC: Naturally connected, no extra configuration needed.
  • Two clusters in different VPCs: Connect them via Peering Connection or Cloud Connect Network, and make sure the routes cover the peer cluster's Pod CIDR (the VPC-CNI Pod subnet).
  • Security groups: Must allow traffic from the peer cluster's Pod CIDR to this cluster's nodes (Pod-to-Pod, any port); otherwise cross-cluster Pod connectivity fails.
Open port 2379 for clustermesh-apiserver

Regardless of the plan, the clustermesh-apiserver exposes this cluster's Cilium control plane to other clusters via an internal CLB (TCP 2379). KVStoreMesh is enabled by default in cilium 1.20: each cluster's clustermesh-apiserver actively connects to the peer CLB to pull data, while cilium-agents only connect to their own cluster's clustermesh-apiserver. Therefore, the nodes running clustermesh-apiserver in the peer cluster must be able to reach the CLB: within the same VPC this is naturally reachable; across VPCs, the peering/CCN must cover the CLB subnet, and node security groups must allow TCP 2379 from the peer cluster.

Prepare kubeconfig​

Merge the kubeconfig files of all clusters to be meshed into a single file, use contexts to distinguish clusters, name each context after the TKE cluster ID (cls-xxx), and make sure the current kubeconfig points to this file. The rest of this article refers to the two clusters via environment variables, which you can set first: export CLUSTER1=cls-xxxxxxxx CLUSTER2=cls-yyyyyyyy.

Specify Cluster Name, ID, and clustermesh-apiserver Internal CLB at Installation​

On top of Installing Cilium, append the following parameters for each cluster (different for each cluster):

helm --kube-context=$CLUSTER1 upgrade --install cilium cilium/cilium --version 1.20.1 \
--namespace kube-system \
--set cluster.name=$CLUSTER1 \
--set cluster.id=1 \
--set clustermesh.apiserver.service.annotations."service\.kubernetes\.io\/qcloud\-loadbalancer\-internal\-subnetid"="$CLUSTER1_SUBNET_ID" \
# omit other parameters

The second cluster follows the same pattern: cluster.name=$CLUSTER2, cluster.id=2, and the CLB subnet replaced with $CLUSTER2_SUBNET_ID.

Parameter notes:

  • cluster.name: Cluster name, unique across the mesh. It can be the same as the TKE cluster ID (format cls-xxx; only lowercase letters, digits, and dashes are allowed, up to 32 characters).
  • cluster.id: Numeric cluster ID (1-255), unique across the mesh.
  • clustermesh.apiserver.service.annotations: The clustermesh-apiserver exposes this cluster's Cilium control plane to other clusters via a LoadBalancer-type Service; on TKE this creates an internal CLB, and this annotation specifies the subnet ID for the CLB (replace $CLUSTERx_SUBNET_ID with a subnet ID in that cluster's VPC).

Specify the cluster name and ID at installation time: both take part in generating security identities, and changing them on a running cluster requires restarting all workloads to rebuild identities. To add these parameters to an already-installed cluster, run helm upgrade cilium cilium/cilium --version 1.20.1 -n kube-system --reuse-values with the --set flags above appended.

Enable ClusterMesh​

Enable in the first cluster​

cilium clustermesh enable --context $CLUSTER1 --service-type=LoadBalancer

This command deploys the clustermesh-apiserver via helm (Deployment + LoadBalancer Service, which creates the internal CLB) and issues mTLS certificates with a certgen Job, later rotated automatically by a CronJob every 4 months. TKE is not among the cloud providers cilium-cli can auto-detect, so --service-type=LoadBalancer must be specified explicitly.

Share the CA​

cilium clustermesh connect validates that the CA certificates of the two clusters match, and fails outright when they do not (unless you explicitly pass --allow-mismatching-ca, which adds the remote CA to the trust bundle). The standard practice is therefore to share the same CA across all clusters: copy the cilium-ca Secret generated in the first cluster to the other clusters:

kubectl --context=$CLUSTER1 -n kube-system get secret cilium-ca -o yaml > cilium-ca.yaml
kubectl --context=$CLUSTER2 -n kube-system delete secret cilium-ca --ignore-not-found
kubectl --context=$CLUSTER2 -n kube-system apply -f cilium-ca.yaml

Additionally, if you plan to use cross-cluster Hubble Relay (observing traffic of the whole mesh from any single cluster), all clusters must also use the same CA so that cross-cluster mTLS works correctly.

The CA must be copied before enabling ClusterMesh on the target cluster

When ClusterMesh is enabled, certgen runs with --ca-reuse-secret: if the cilium-ca Secret already exists it is reused to issue the clustermesh certificates, and only otherwise is a new CA created. Therefore the CA copy must happen before running cilium clustermesh enable on the target cluster — otherwise the certificates are not signed by the same CA.

If a cluster has already enabled ClusterMesh with its own CA, merely replacing cilium-ca afterwards does not re-issue the existing certificates, and running components do not reload certificates or CAs (the etcd clients of cilium-agent / cilium-operator only hot-reload the client certificate; the trusted CA is loaded when the process starts). Fix it manually:

  1. Replace cilium-ca (same three commands as above) and delete the old leaf certificate Secrets: clustermesh-apiserver-server-cert, clustermesh-apiserver-admin-cert, clustermesh-apiserver-remote-cert, clustermesh-apiserver-local-cert.
  2. Trigger a certificate issuance manually: kubectl --context=$CLUSTER2 -n kube-system create job --from=cronjob/clustermesh-apiserver-generate-certs cert-regen.
  3. Restart that cluster's clustermesh-apiserver and cilium-operator (Deployments) and cilium (DaemonSet) to reload the certificates.

Enable in the remaining clusters​

After copying the CA, run the same enable command in the remaining clusters (for example, cilium clustermesh enable --context $CLUSTER2 --service-type=LoadBalancer). When more clusters join the mesh, repeat the "Share the CA" and "Enable" steps for each cluster: keep cluster.id and cluster.name unique across the mesh, and always copy the CA from the first cluster.

Connect the Clusters​

cilium clustermesh connect --context $CLUSTER1 --destination-context $CLUSTER2

This only needs to be run in one direction (the default --connection-mode=bidirectional makes cilium-cli configure both clusters at once, taking effect in both directions). To connect more clusters, pass a comma-separated list of --destination-context values, or run the command multiple times.

Validate​

cilium clustermesh status --context $CLUSTER1 --wait
cilium clustermesh status --context $CLUSTER2 --wait

The output looks like this:

✅ Cluster access information is available:
- 10.168.0.89:2379
✅ Service "clustermesh-apiserver" of type "LoadBalancer" found
⌛ Waiting (12s) for clusters to be connected: 2 nodes are not ready
✅ All 2 nodes are connected to all clusters [min:1 / avg:1.0 / max:1]
🔌 Cluster Connections:
- cls-yyyyyyyy: 2/2 configured, 2/2 connected

Once you see ✅ All N nodes are connected to all clusters and the peer cluster shows connected under Cluster Connections, the control plane is connected properly.

Then run the multi-cluster connectivity test to verify cross-cluster Pod connectivity and Service forwarding (see Cilium Functional Tests for single-cluster usage; add --multi-cluster for multi-cluster mode):

cilium connectivity test --context $CLUSTER1 --multi-cluster $CLUSTER2

Global Service Example​

Annotate a Service with service.cilium.io/global: "true" to declare it a global Service; Cilium then automatically synchronizes the backends on both sides and load balances across clusters. Create a Service with the same name and namespace in every cluster:

rebel-base.yaml
apiVersion: v1
kind: Service
metadata:
name: rebel-base
annotations:
service.cilium.io/global: "true" # declare a global Service
spec:
type: ClusterIP
ports:
- port: 80
selector:
name: rebel-base

Deploy this Service in both clusters together with their own backends (it is also fine for only one side to have backend Pods — access from the other side is then forwarded to the remote backends). Accessing rebel-base from a Pod in either cluster shows responses from Pods of both clusters when both sides have backends.

  • By default local backends are also shared with the peer (implicitly service.cilium.io/shared: "true"); adding service.cilium.io/shared: "false" to a Service on one side stops that cluster's backends from being shared with the peer (access from that cluster still load balances to both sides).
  • Cross-cluster load balancing does not prefer local backends by default; for "local-first" affinity, see the official Service Affinity documentation.

Important Notes​

Ensure No SNAT for Cross-Cluster Pod Traffic​

If IP masquerade is enabled, make sure the Pod CIDRs of all clusters are excluded from SNAT: in Native Routing mode, add the peer clusters' Pod CIDRs to nonMasqueradeCIDRs as well — see Configuring IP Masquerade for details. Otherwise the source IP of cross-cluster Pod traffic is SNATed to a node IP, the peer cannot map the traffic back to the Pod's Cilium identity, and identity-based cross-cluster NetworkPolicies stop working. In Overlay mode, cross-cluster Pod traffic is VXLAN-encapsulated (outer layer is the node IP) and is not affected by masquerade, so nothing needs to be done.

Incompatible with Egress Gateway and CiliumEndpointSlice​

Egress Gateway is not compatible with ClusterMesh: the egress node selected by a CiliumEgressGatewayPolicy must be in the same cluster as the Pods matching the policy, an assumption broken by ClusterMesh's cross-cluster identity and endpoint synchronization. Egress Gateway is likewise incompatible with CiliumEndpointSlice (upstream known issue cilium#24833). Therefore, do not join a cluster that has Egress Gateway enabled into a ClusterMesh, and vice versa — see the "Known Issues" section of Egress Gateway Best Practices.

References​