<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ruslan Mamakhaev</title>
    <description>The latest articles on DEV Community by Ruslan Mamakhaev (@wepaleen).</description>
    <link>https://dev.to/wepaleen</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4053869%2Fe454fd76-de28-4fdb-8352-4897fb43d4fe.jpg</url>
      <title>DEV Community: Ruslan Mamakhaev</title>
      <link>https://dev.to/wepaleen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wepaleen"/>
    <language>en</language>
    <item>
      <title>Joining two Talos Kubernetes clusters with Cilium Cluster Mesh</title>
      <dc:creator>Ruslan Mamakhaev</dc:creator>
      <pubDate>Wed, 29 Jul 2026 20:39:54 +0000</pubDate>
      <link>https://dev.to/wepaleen/joining-two-talos-kubernetes-clusters-with-cilium-cluster-mesh-2hg0</link>
      <guid>https://dev.to/wepaleen/joining-two-talos-kubernetes-clusters-with-cilium-cluster-mesh-2hg0</guid>
      <description>&lt;h2&gt;
  
  
  The setup I started with
&lt;/h2&gt;

&lt;p&gt;My lab ran on a Mac mini with Ubuntu 26.04 x86_64, 4 CPU, 16 GiB of RAM, and&lt;br&gt;
Docker 29.4.3. I wanted to find out what Cilium Cluster Mesh actually needed&lt;br&gt;
beyond two working Kubernetes clusters. I ended up with four Talos containers,&lt;br&gt;
two Kubernetes clusters, several Docker network attachments, and a node address&lt;br&gt;
problem that was more instructive than the successful installation.&lt;/p&gt;

&lt;p&gt;Both clusters used Talos Linux 1.12.5, Kubernetes 1.35.2, and Cilium 1.19.6. I&lt;br&gt;
managed them with talosctl 1.12.5, kubectl 1.35.1, and cilium CLI 0.19.7.&lt;/p&gt;

&lt;p&gt;Each cluster had one control plane and one worker. I assigned 2 GiB of RAM and&lt;br&gt;
1.5 CPU to each control-plane container. Each worker received 1.5 GiB of RAM and&lt;br&gt;
1 CPU. Both clusters ran as Docker containers on the same Ubuntu host.&lt;/p&gt;

&lt;p&gt;I had already bootstrapped Talos with no built-in CNI and with kube-proxy&lt;br&gt;
disabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cluster&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cni&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;
  &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I installed Cilium separately with &lt;code&gt;ipam.mode=kubernetes&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;kubeProxyReplacement=true&lt;/code&gt;, VXLAN tunnel routing, and &lt;code&gt;devices=eth0&lt;/code&gt;, as&lt;br&gt;
described in a separate post.&lt;/p&gt;

&lt;p&gt;The two clusters had this address plan:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cluster&lt;/th&gt;
&lt;th&gt;Cluster ID&lt;/th&gt;
&lt;th&gt;Node subnet&lt;/th&gt;
&lt;th&gt;Control plane&lt;/th&gt;
&lt;th&gt;Worker&lt;/th&gt;
&lt;th&gt;Pod CIDR&lt;/th&gt;
&lt;th&gt;Service CIDR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mesh-a&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10.51.0.0/24&lt;/td&gt;
&lt;td&gt;10.51.0.2&lt;/td&gt;
&lt;td&gt;10.51.0.3&lt;/td&gt;
&lt;td&gt;10.241.0.0/16&lt;/td&gt;
&lt;td&gt;10.101.0.0/16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mesh-b&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10.52.0.0/24&lt;/td&gt;
&lt;td&gt;10.52.0.2&lt;/td&gt;
&lt;td&gt;10.52.0.3&lt;/td&gt;
&lt;td&gt;10.242.0.0/16&lt;/td&gt;
&lt;td&gt;10.102.0.0/16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At first the missing SSH access annoyed me. I am used to opening a node and&lt;br&gt;
looking at interfaces, routes, processes and logs with ordinary Linux commands.&lt;br&gt;
In Talos I had to work out first which &lt;code&gt;talosctl&lt;/code&gt; command or resource shows the&lt;br&gt;
state I wanted. Once I got used to it I liked it more than I expected: changes&lt;br&gt;
do not dissolve into shell history, they stay as patches I can apply again.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Cluster Mesh required from the two clusters
&lt;/h2&gt;

&lt;p&gt;A working Kubernetes API in each cluster was not enough. I needed unique Cilium&lt;br&gt;
cluster IDs, non-overlapping address ranges, and direct IP reachability between&lt;br&gt;
the participating nodes.&lt;/p&gt;

&lt;p&gt;I assigned cluster ID 1 to mesh-a and cluster ID 2 to mesh-b. The ID tells&lt;br&gt;
Cilium which cluster owns a remote identity or endpoint. Reusing an ID would&lt;br&gt;
make information from two independent clusters ambiguous.&lt;/p&gt;

&lt;p&gt;The Pod CIDRs also had to remain distinct. A route to a pod address inside&lt;br&gt;
&lt;code&gt;10.241.0.0/16&lt;/code&gt; must have one unambiguous destination. If both clusters&lt;br&gt;
allocated pods from that range, an address alone would not tell the datapath&lt;br&gt;
whether to keep a packet inside the local cluster or send it to the remote one.&lt;/p&gt;

&lt;p&gt;I applied the same separation to the Service CIDRs, with &lt;code&gt;10.101.0.0/16&lt;/code&gt; in&lt;br&gt;
mesh-a and &lt;code&gt;10.102.0.0/16&lt;/code&gt; in mesh-b. Service addresses are&lt;br&gt;
virtual addresses. Reusing their ranges would add ambiguity when the clusters&lt;br&gt;
exchange service information.&lt;/p&gt;

&lt;p&gt;The last requirement was reachability. Cluster Mesh could exchange metadata,&lt;br&gt;
but cross-cluster pod traffic would still fail if the nodes had no route to each&lt;br&gt;
other. My two clusters lived on separate Docker networks, so their initial&lt;br&gt;
isolation was part of the container topology rather than a Kubernetes policy.&lt;/p&gt;

&lt;p&gt;Cilium Cluster Mesh provided cross-cluster connectivity, identity exchange,&lt;br&gt;
remote service discovery, global services, and cross-cluster load balancing in&lt;br&gt;
this lab. It was not an L7 service mesh. I had no sidecar proxies or L7 routing&lt;br&gt;
rules, and I did not install Istio or Linkerd.&lt;/p&gt;
&lt;h2&gt;
  
  
  Making the nodes reachable and hitting the node IP trap
&lt;/h2&gt;

&lt;p&gt;![Node address selection: the failure mode after attaching a second Docker network, and the Talos patch that pins the subnet]&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffwije0x5tv1sxnerb24r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffwije0x5tv1sxnerb24r.png" alt=" " width="800" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I attached the containers from each cluster to the other cluster's Docker&lt;br&gt;
network. After that change, the nodes could route directly between the&lt;br&gt;
&lt;code&gt;10.51.0.0/24&lt;/code&gt; and &lt;code&gt;10.52.0.0/24&lt;/code&gt; networks.&lt;/p&gt;

&lt;p&gt;That solved one problem and introduced another.&lt;/p&gt;

&lt;p&gt;Before the change, each Talos container had an obvious address from the&lt;br&gt;
perspective of kubelet. After I added another network interface, the container&lt;br&gt;
had more than one candidate node address. Kubelet could select an address from&lt;br&gt;
the wrong Docker network.&lt;/p&gt;

&lt;p&gt;A wrong node address does not always break anything visibly. The API can still&lt;br&gt;
list the node while Cilium tries to reach it through the wrong network, and&lt;br&gt;
local pods keep running while cross-cluster traffic fails.&lt;/p&gt;

&lt;p&gt;I fixed the selection in mesh-a by restricting kubelet to its node subnet:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;machine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kubelet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;nodeIP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;validSubnets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;10.51.0.0/24&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I applied the corresponding restriction to mesh-b:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;machine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kubelet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;nodeIP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;validSubnets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;10.52.0.0/24&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This told kubelet which interface addresses were valid without relying on&lt;br&gt;
interface discovery order. I picked &lt;code&gt;machine.kubelet.nodeIP.validSubnets&lt;/code&gt;&lt;br&gt;
deliberately. Once the containers were attached to the second Docker network,&lt;br&gt;
every node had more than one plausible address. Pinning a single IP would have&lt;br&gt;
been too rigid, while restricting the subnet kept the cluster's address plan&lt;br&gt;
intact and let me recreate containers without editing anything. This is the&lt;br&gt;
documented Talos mechanism for multihomed nodes, not a workaround I invented.&lt;/p&gt;

&lt;p&gt;The control-plane containers had a second address-sensitive component: etcd. I&lt;br&gt;
pinned its advertised subnet in mesh-a:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cluster&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;etcd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;advertisedSubnets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;10.51.0.0/24&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I used &lt;code&gt;10.52.0.0/24&lt;/code&gt; for the mesh-b control plane.&lt;/p&gt;

&lt;p&gt;A wrong kubelet address can disrupt communication with one node. A wrong etcd&lt;br&gt;
advertised address can affect the datastore used by the control plane. An etcd&lt;br&gt;
member tells clients and peers how to reach it through its advertised address.&lt;br&gt;
If it publishes an address that the intended peers cannot reach, control-plane&lt;br&gt;
communication can fail at a lower layer than ordinary pod networking.&lt;/p&gt;

&lt;p&gt;Each cluster had one control-plane node, so no redundant etcd member could&lt;br&gt;
absorb an address mistake. I had to correct the advertised subnet.&lt;/p&gt;

&lt;p&gt;The dead end was assuming that attaching more Docker networks only added&lt;br&gt;
routes. It also changed the set of addresses from which kubelet and etcd could&lt;br&gt;
choose. I did not track how long the diagnosis took.&lt;/p&gt;

&lt;p&gt;I kept Cilium configured with &lt;code&gt;devices=eth0&lt;/code&gt; and VXLAN tunnel routing. VXLAN&lt;br&gt;
added encapsulation inside a setup that already used Docker networking. I did&lt;br&gt;
not test MTU behaviour inside the nested network path, so I cannot say how much&lt;br&gt;
packet size headroom this setup retained.&lt;/p&gt;
&lt;h2&gt;
  
  
  Turning on the mesh
&lt;/h2&gt;

&lt;p&gt;I published &lt;code&gt;clustermesh-apiserver&lt;/code&gt; as a NodePort on port 32379 in both clusters.&lt;br&gt;
This gave the opposite cluster a reachable endpoint without adding a separate&lt;br&gt;
load balancer to the lab.&lt;/p&gt;

&lt;p&gt;I ran one &lt;code&gt;clustermesh-apiserver&lt;/code&gt; replica per cluster. Each cluster had one&lt;br&gt;
control-plane node, so extra replicas would only consume more memory in this&lt;br&gt;
topology. I did not measure the component's memory or CPU overhead.&lt;/p&gt;

&lt;p&gt;I disabled KVStoreMesh. For this two-cluster experiment, I did not add that&lt;br&gt;
extra layer to the design.&lt;/p&gt;

&lt;p&gt;The clusters had been created independently, so they did not share a certificate&lt;br&gt;
authority. I connected them with &lt;code&gt;--allow-mismatching-ca&lt;/code&gt;. The name can sound&lt;br&gt;
like a request to bypass certificate checks, but that is not what it did here.&lt;br&gt;
The option added the remote cluster's CA to the trust set. TLS verification&lt;br&gt;
remained enabled.&lt;/p&gt;

&lt;p&gt;After connecting the clusters, I checked the Cluster Mesh status from both&lt;br&gt;
contexts:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cilium clustermesh status &lt;span class="nt"&gt;--context&lt;/span&gt; admin@mesh-a
cilium clustermesh status &lt;span class="nt"&gt;--context&lt;/span&gt; admin@mesh-b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I did not preserve the full command logs, so the only fragment I can report is:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;abbreviated output, not a saved log

2/2 clusters configured
2/2 clusters connected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I recorded the same result from both contexts.&lt;/p&gt;

&lt;p&gt;If my goal had been to run one small application, this overhead would not be&lt;br&gt;
justified. Cluster Mesh adds CIDRs to plan, routes between networks,&lt;br&gt;
certificates, one more component to keep alive in &lt;code&gt;clustermesh-apiserver&lt;/code&gt;, and a&lt;br&gt;
separate class of problems to diagnose when something between the clusters&lt;br&gt;
breaks. As a lab it was worth it, because I wanted to watch Cilium discover&lt;br&gt;
remote backends and balance requests through a global service, not to find the&lt;br&gt;
shortest path to a running service.&lt;/p&gt;
&lt;h2&gt;
  
  
  A service that lived in two clusters
&lt;/h2&gt;

&lt;p&gt;I created the &lt;code&gt;mesh-demo&lt;/code&gt; namespace in both clusters. In mesh-a, I ran a&lt;br&gt;
deployment named &lt;code&gt;mesh-a-backend&lt;/code&gt;. In mesh-b, I ran &lt;code&gt;mesh-b-backend&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both deployments used the same test image and command:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.k8s.io/e2e-test-images/agnhost:2.53&lt;/span&gt;
&lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/agnhost&lt;/span&gt;
&lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;netexec&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--http-port=8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I then created a Service named &lt;code&gt;global-demo&lt;/code&gt; in both clusters. The relevant part&lt;br&gt;
of each Service manifest was:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;global-demo&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mesh-demo&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;service.cilium.io/global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
    &lt;span class="na"&gt;service.cilium.io/shared&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The shared name and namespace represented one global service. The annotations&lt;br&gt;
told Cilium to publish and share its backends across the mesh.&lt;/p&gt;

&lt;p&gt;I created a client pod named &lt;code&gt;mesh-client&lt;/code&gt; in mesh-a with&lt;br&gt;
&lt;code&gt;curlimages/curl:8.12.1&lt;/code&gt;. From that pod, I ran the following request 20 times:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://global-demo/hostname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I read the returned hostnames from those 20 &lt;code&gt;curl&lt;/code&gt; calls. Twelve responses came&lt;br&gt;
from &lt;code&gt;mesh-a-backend&lt;/code&gt;, and eight came from &lt;code&gt;mesh-b-backend&lt;/code&gt;. The command reached&lt;br&gt;
both the local and remote service backends through the same service name.&lt;/p&gt;

&lt;p&gt;![Twenty requests to one global service name: twelve served locally in mesh-a, eight from mesh-b over Cluster Mesh]&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fublw93xqvg4t7017askr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fublw93xqvg4t7017askr.png" alt=" " width="800" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The 12/8 split was not a failed attempt at a 10/10 split. I only sent 20&lt;br&gt;
requests, and the global service did not promise an exactly even count over&lt;br&gt;
that sample. The useful finding was that both backend groups appeared in the&lt;br&gt;
responses.&lt;/p&gt;

&lt;p&gt;I did not run the failover test where I set the local backend replica count to&lt;br&gt;
zero and check whether all requests move to the remote cluster.&lt;/p&gt;

&lt;p&gt;I also did not test &lt;code&gt;service.cilium.io/affinity=local&lt;/code&gt;, so I have no recorded&lt;br&gt;
distribution to compare with the 12/8 result.&lt;/p&gt;

&lt;p&gt;I ran &lt;code&gt;cilium connectivity test&lt;/code&gt; as a broader datapath check. The test namespace&lt;br&gt;
needed this Pod Security Admission label because some diagnostic components&lt;br&gt;
required privileged access to the datapath:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;pod-security.kubernetes.io/enforce&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;privileged&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The result was 80 of 81 tests passed, 473 of 474 actions passed, and 51 skipped.&lt;br&gt;
The single failure came from the node-to-node encryption test. The ping&lt;br&gt;
succeeded, but the test failed while stopping &lt;code&gt;tcpdump&lt;/code&gt;, which exited with code&lt;br&gt;
197.&lt;/p&gt;

&lt;p&gt;I did not interpret that as a packet delivery failure. The network action&lt;br&gt;
completed, but diagnostic cleanup failed. The suite still finished at 80 of 81.&lt;/p&gt;

&lt;p&gt;I did not measure latency or the resource cost introduced by Cluster Mesh.&lt;/p&gt;
&lt;h2&gt;
  
  
  What belongs in this lab
&lt;/h2&gt;

&lt;p&gt;The most useful thing I took from this was where the layers end. Two healthy&lt;br&gt;
Kubernetes clusters do not mean Cluster Mesh can work between them. The node&lt;br&gt;
addresses, the routing between the Docker networks, the Pod and Service CIDRs&lt;br&gt;
and the certificates all have to be right first. Cilium only connects clusters&lt;br&gt;
and serves a global service after that groundwork holds.&lt;/p&gt;

&lt;p&gt;I would not call this production, and the reason is not the single control plane&lt;br&gt;
per cluster. Both clusters sit on one physical host. If the Mac mini, Docker or&lt;br&gt;
the Ubuntu network stack goes down, both clusters go with it. The setup&lt;br&gt;
exercises cross-cluster networking, but it gives me no independent failure&lt;br&gt;
domains, and independent failure domains are usually the reason to run more&lt;br&gt;
than one cluster in the first place.&lt;/p&gt;

&lt;p&gt;If I built this again I would write down the whole address plan first, prepare&lt;br&gt;
the &lt;code&gt;nodeIP.validSubnets&lt;/code&gt; and &lt;code&gt;etcd.advertisedSubnets&lt;/code&gt; patches, and only then&lt;br&gt;
create the nodes and attach the extra Docker networks. I would also put every&lt;br&gt;
step into &lt;code&gt;scripts/up.sh&lt;/code&gt; from the beginning. The first time through I did part&lt;br&gt;
of it by hand, so I had to reconstruct the exact sequence after the lab was&lt;br&gt;
already running.&lt;/p&gt;

&lt;p&gt;My next experiment is a third cluster named mesh-c on k0s, with cluster ID 3,&lt;br&gt;
Pod CIDR &lt;code&gt;10.243.0.0/16&lt;/code&gt;, and Service CIDR &lt;code&gt;10.103.0.0/16&lt;/code&gt;. I have not installed&lt;br&gt;
k0s yet. Running it directly on the same Ubuntu host would touch iptables,&lt;br&gt;
routing, cgroups, and eBPF state shared with the Docker clusters, so I still&lt;br&gt;
need to find out whether a privileged container provides enough isolation or&lt;br&gt;
whether mesh-c requires a separate VM.&lt;/p&gt;

&lt;p&gt;The Talos patches, Cilium values and the Global Service manifests are in the repo:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/wepaleen" rel="noopener noreferrer"&gt;
        wepaleen
      &lt;/a&gt; / &lt;a href="https://github.com/wepaleen/talos-cilium-clustermesh" rel="noopener noreferrer"&gt;
        talos-cilium-clustermesh
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Lab: two Talos Linux clusters joined with Cilium Cluster Mesh. No CNI, no kube-proxy, global services across clusters.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Talos Cilium Cluster Mesh lab&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;This repository reproduces a two-cluster Cilium Cluster Mesh lab built with
Talos Linux containers on one Docker host.&lt;/p&gt;
&lt;p&gt;The lab uses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Talos Linux 1.12.5&lt;/li&gt;
&lt;li&gt;Kubernetes 1.35.2&lt;/li&gt;
&lt;li&gt;Cilium 1.19.6&lt;/li&gt;
&lt;li&gt;cilium CLI 0.19.7&lt;/li&gt;
&lt;li&gt;talosctl 1.12.5&lt;/li&gt;
&lt;li&gt;kubectl 1.35.1&lt;/li&gt;
&lt;li&gt;Docker 29.4.3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The host used for the original run was a Mac mini running Ubuntu 26.04 x86_64
with 4 CPU and 15 GiB of RAM.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Topology&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cluster&lt;/th&gt;
&lt;th&gt;Cilium ID&lt;/th&gt;
&lt;th&gt;Node subnet&lt;/th&gt;
&lt;th&gt;Control plane&lt;/th&gt;
&lt;th&gt;Worker&lt;/th&gt;
&lt;th&gt;Pod CIDR&lt;/th&gt;
&lt;th&gt;Service CIDR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mesh-a&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.51.0.0/24&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.51.0.2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.51.0.3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.241.0.0/16&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.101.0.0/16&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mesh-b&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.52.0.0/24&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.52.0.2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.52.0.3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.242.0.0/16&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.102.0.0/16&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Each cluster has one control plane and one worker. Control-plane containers use
2 GiB of RAM and 1.5 CPU. Worker containers use 1.5 GiB of RAM and 1 CPU.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Repository layout&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;talos/          Safe Talos configuration patches, never generated configs
cilium/         Cilium values for mesh-a and mesh-b
clustermesh/    Cluster Mesh enable and connect script
manifests/      Global Service demonstration
docs/&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/wepaleen/talos-cilium-clustermesh" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>networking</category>
      <category>cilium</category>
    </item>
  </channel>
</rss>
