Cilium ClusterMesh 实战:连接两个 K3s 集群的跨云服务发现
目录
背景#
在前两篇文章中,我分别记录了 homelab 集群 和 Oracle Cloud 集群 从 Flannel 迁移到 Cilium 的过程。两个集群各自独立运行 Cilium,通过 Tailscale 子网路由实现 Pod CIDR 互通,OTel Collector 从 oracle-k3s 推送日志、指标和链路追踪到 homelab 的 LGTM 栈。
这个架构能工作,但有一个明显的缺失:两个集群之间没有原生的服务发现。oracle-k3s 的 OTel Collector 必须通过 NodePort + Tailscale IP 硬编码连接 homelab 的 Loki、Prometheus、Tempo。如果 homelab 的 Tailscale IP 变了(剧透:它真的变了),所有跨集群的连接都会断。
Cilium ClusterMesh 正是为解决这类问题设计的——它在已有的 Cilium 数据面上增加跨集群的身份联邦和服务发现,而不需要额外的 overlay 网络。
本文记录从 homelab 集群重建到 ClusterMesh 双向连接的完整过程。
架构概览#
双集群拓扑#
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ homelab (Proxmox VM) │ │ oracle-k3s (Oracle Cloud) │
│ 10.10.10.10 / TS: 100.94.186.7 │ │ 10.0.0.26 / TS: 100.107.166.37 │
│ │ │ │
│ K3s v1.34.5 + Cilium 1.19.1 │ │ K3s v1.34.5 + Cilium 1.19.1 │
│ cluster.name: homelab │ │ cluster.name: oracle-k3s │
│ cluster.id: 1 │ │ cluster.id: 2 │
│ Pod CIDR: 10.42.0.0/16 │ │ Pod CIDR: 10.52.0.0/16 │
│ Service CIDR: 10.43.0.0/16 │ │ Service CIDR: 10.53.0.0/16 │
│ │ │ │
│ ClusterMesh API: :32379 │ │ ClusterMesh API: :32379 │
│ Vault, ArgoCD, Grafana, │ │ Homepage, Miniflux, KaraKeep, │
│ Loki, Tempo, Prometheus │ │ Uptime Kuma, Timeslot │
└─────────────┬────────────────────┘ └─────────────┬────────────────────┘
│ │
└──────── Tailscale (WireGuard) ─────────┘
ClusterMesh API 互联 via :32379
ClusterMesh 的角色#
在这个架构里,ClusterMesh 的职责很明确:
- 身份联邦:两个集群的 Cilium Identity 互相可见,跨集群的 Network Policy 可以基于 label 而不是 IP
- 服务发现:标记了
io.cilium/global-service: "true"的 Service,其 Endpoints 对两个集群都可见 - KVStoreMesh:每个集群本地缓存对端的 KV 数据,减少跨网络的 etcd watch 开销
注意,我没有用 ClusterMesh 做跨集群的负载均衡或故障切换——两个集群的服务分工明确,homelab 跑核心基础设施,oracle-k3s 跑面向用户的轻量服务。ClusterMesh 目前的主要价值是为跨集群的 OTel 数据管道提供更可靠的服务发现路径。
起因:一次 Tailscale IP 变更引发的问题#
事情的起因很简单:homelab 重建后执行 tailscale up --reset,节点被分配了新的 Tailscale IP(从 100.96.84.32 变为 100.94.186.7)。
这次 IP 变更的影响范围比预期大得多:
- kubeconfig 需要更新 server 地址
- K3s TLS 证书 需要加入新 IP 的 SAN
- oracle-k3s 的 OTel Collector 三个 exporter(Loki、Prometheus、Tempo)全部断联
- oracle-k3s 的 Vault ClusterSecretStore 无法连接 homelab Vault
- 若干文档和 runbook 中的 IP 需要全量替换
花了大半天逐个修复后,我下定决心把 ClusterMesh 连起来——如果跨集群的连接能用 Service DNS 而不是硬编码 IP,这类问题的影响面会小很多。
前提条件#
在开始 ClusterMesh 之前,需要确保两个集群满足以下条件:
1. Cilium 配置一致性#
两个集群的 Cilium 必须使用相同的 tunnelProtocol 和 routingMode。我的配置:
# 两个集群共用的关键参数
kubeProxyReplacement: true
routingMode: tunnel
tunnelProtocol: vxlan
2. 集群标识唯一#
每个集群必须有唯一的 cluster.name 和 cluster.id(1-255):
# homelab
cluster:
name: homelab
id: 1
# oracle-k3s
cluster:
name: oracle-k3s
id: 2
3. Pod CIDR 不重叠#
homelab 用 10.42.0.0/16,oracle-k3s 用 10.52.0.0/16——这在两个集群初始搭建时就规划好了。
4. 网络连通性#
ClusterMesh API Server 通过 NodePort 32379 暴露。两个节点需要通过 Tailscale 互相访问对方的 32379 端口。
验证连通性:
# 从 homelab 测试到 oracle-k3s
curl -s --connect-timeout 5 https://100.107.166.37:32379 -k
# 从 oracle-k3s 测试到 homelab
curl -s --connect-timeout 5 https://100.94.186.7:32379 -k
Homelab 集群重建#
这一次重建不是普通的 Cilium 更新,而是从头开始。上一轮 Cilium 迁移保留了 K3s 的 kube-proxy,这次我决定直接启用 Cilium 的 Kube Proxy Replacement(KPR),同时切换到 Cilium Gateway API 替代 Traefik。
K3s 配置#
# /etc/rancher/k3s/config.yaml
tls-san:
- "10.10.10.10"
- "100.94.186.7"
flannel-backend: "none"
disable-network-policy: true
disable-kube-proxy: true
disable:
- traefik
- servicelb
关键变化:
disable-kube-proxy: true:完全交给 Ciliumdisable: [traefik, servicelb]:移除 K3s 内置的 Traefik 和 ServiceLB,Cilium Gateway API 接管入口
Cilium Helm Values#
kubeProxyReplacement: true
routingMode: tunnel
tunnelProtocol: vxlan
k8sServiceHost: 10.10.10.10
k8sServicePort: 6443
ipam:
operator:
clusterPoolIPv4PodCIDRList:
- "10.42.0.0/16"
cluster:
name: homelab
id: 1
gatewayAPI:
enabled: true
clustermesh:
useAPIServer: true
apiserver:
service:
type: NodePort
nodePort: 32379
kvstoremesh:
enabled: true
hubble:
enabled: true
relay:
enabled: true
几个值得说的配置:
gatewayAPI.enabled: true:启用 Cilium 作为 Gateway API 的GatewayClass,替代 Traefikclustermesh.useAPIServer: true:部署 ClusterMesh API Server,这是 ClusterMesh 的核心组件clustermesh.apiserver.kvstoremesh.enabled: true:启用 KVStoreMesh,在本地缓存远端集群的 KV 数据clustermesh.apiserver.service.type: NodePort:通过 NodePort 32379 暴露 API Server
从 Traefik 到 Cilium Gateway API#
这是这次重建中最大的架构变化。之前一直用 K3s 内置的 Traefik 作为 Gateway API 实现,现在切到 Cilium。
Gateway 和 HTTPRoute 的 YAML 结构不变(都是标准 Gateway API),只需要修改 gatewayClassName:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: homelab-gateway
namespace: kube-system
spec:
gatewayClassName: cilium # 之前是 traefik
listeners:
- name: http
port: 80
protocol: HTTP
Cilium 会自动创建一个 cilium-gateway-homelab-gateway Service(类型 LoadBalancer)。在单节点 K3s 上没有 cloud controller 分配 External IP,所以 Gateway 状态会显示 PROGRAMMED=False——但这不影响功能,Cloudflare Tunnel 直接连 Service 的 ClusterIP。
Cloudflare Tunnel 更新#
Tunnel 配置需要更新后端地址,指向 Cilium 创建的 Gateway Service:
# cloudflare/terraform/terraform.tfvars
tunnel_origin_url = "http://cilium-gateway-homelab-gateway.kube-system.svc:80"
cd cloudflare/terraform && just apply
# 重启 cloudflared pod 加载新配置
kubectl rollout restart deployment/cloudflared -n cloudflare
工作负载恢复顺序#
重建后按依赖关系恢复所有服务:
- NFS Provisioner → PVC 依赖
- Vault → 手动 init + unseal
- ESO → 依赖 Vault
- monitoring-external NodePort → 跨集群 OTel 入口
- Prometheus / Loki / Tempo → 可观测基础设施
- ArgoCD → 之后的服务由 GitOps 接管
- 所有 ArgoCD Applications →
kubectl apply -f argocd/applications/
Oracle 集群同步#
oracle-k3s 在之前的迁移中已经装好了 Cilium,但当时保留了 kube-proxy。为了和 homelab 保持一致,补上 KPR:
# /etc/rancher/k3s/config.yaml 增加
disable-kube-proxy: true
重启 K3s 后,Cilium 接管所有 kube-proxy 的职责。同时更新 oracle-k3s 上所有引用 homelab Tailscale IP 的配置:
# 批量更新旧 IP → 新 IP
sed -i 's/100.96.84.32/100.94.186.7/g' \
cloud/oracle/manifests/base/vault-store.yaml \
cloud/oracle/manifests/monitoring/otel-collector.yaml
kubectl --context oracle-k3s apply -f cloud/oracle/manifests/base/vault-store.yaml
kubectl --context oracle-k3s apply -f cloud/oracle/manifests/monitoring/otel-collector.yaml
kubectl --context oracle-k3s rollout restart daemonset/otel-collector -n monitoring
连接 ClusterMesh#
一切就绪后,连接 ClusterMesh 只需要一条命令(用 cilium-cli):
cilium clustermesh connect \
--context k3s-homelab \
--destination-context oracle-k3s \
--source-endpoint 100.94.186.7:32379 \
--destination-endpoint 100.107.166.37:32379 \
--allow-mismatching-ca
关于 --allow-mismatching-ca#
因为两个集群是独立的 Cilium 安装,它们的 CA 证书不同。正常情况下 ClusterMesh 要求相同的 CA(或者手动交换证书),但 --allow-mismatching-ca 告诉 Cilium 跳过这个校验,改用集群间的 TLS 握手来验证身份。
在通过 Tailscale WireGuard 隧道通信的场景下,传输层已经被加密,CA 不匹配的风险可以接受。
验证连接#
$ cilium clustermesh status --context k3s-homelab --wait
✅ ClusterMesh is enabled
✅ Service "clustermesh-apiserver" of type "NodePort" found
✅ Cluster access information is available:
- 100.94.186.7:32379
✅ Deployment clustermesh-apiserver is ready
ℹ️ KVStoreMesh is enabled
✅ All 1 nodes are connected to all clusters [min:1 / avg:1.0 / max:1]
✅ All 1 KVStoreMesh replicas are connected to all clusters [min:1 / avg:1.0 / max:1]
🔌 Cluster Connections:
- oracle-k3s: 1/1 configured, 1/1 connected
KVStoreMesh: 1/1 configured, 1/1 connected
跨集群节点发现#
连接建立后,两个集群可以看到对方的节点:
$ cilium-dbg node list # 在 homelab 执行
Name IPv4 Address Endpoint CIDR Source
homelab/k8s-node 10.10.10.10 10.42.0.0/24 local
oracle-k3s/oracle-k3s 10.0.0.26 10.52.0.0/24 clustermesh
反向从 oracle-k3s 也能看到 homelab 节点。ClusterMesh 的核心连接完成。
跨集群可观测性验证#
ClusterMesh 连接后,oracle-k3s 的 OTel Collector 能否正常推送数据到 homelab 是关键验证点。
问题:缺少 NodePort Service#
重建后的 homelab 集群只有 ClusterIP 类型的 monitoring 服务。oracle-k3s 的 OTel Collector 通过 Tailscale IP + NodePort 连接,需要创建对应的 NodePort Service:
# k8s/helm/manifests/monitoring-external.yaml
---
apiVersion: v1
kind: Service
metadata:
name: loki-gateway-external
namespace: monitoring
spec:
type: NodePort
selector:
app.kubernetes.io/name: loki
app.kubernetes.io/component: gateway
ports:
- port: 80
targetPort: 8080
nodePort: 31080
---
apiVersion: v1
kind: Service
metadata:
name: prometheus-otlp-external
namespace: monitoring
spec:
type: NodePort
selector:
app.kubernetes.io/name: prometheus
operator.prometheus.io/name: kube-prometheus-stack-prometheus
ports:
- port: 9090
targetPort: 9090
nodePort: 31090
---
apiVersion: v1
kind: Service
metadata:
name: tempo-otlp-external
namespace: monitoring
spec:
type: NodePort
selector:
app.kubernetes.io/name: tempo
ports:
- port: 4317
targetPort: 4317
nodePort: 31317
protocol: TCP
应用后,OTel Collector 立即恢复连接:
$ curl -s "http://100.94.186.7:31090/api/v1/query?query=up{cluster=\"oracle-k3s\"}" | jq '.data.result[0]'
{
"metric": {
"__name__": "up",
"cluster": "oracle-k3s",
"instance": "10.0.0.26:9100",
"job": "node-exporter"
},
"value": [1772951032.622, "1"]
}
oracle-k3s 的 node-exporter 指标成功出现在 homelab 的 Prometheus 里,跨集群的可观测性管道恢复正常。
最终状态#
服务健康#
所有 10 个公开服务正常响应:
| 服务 | URL | 状态 |
|---|---|---|
| Grafana | grafana.meirong.dev | 302 (登录跳转) |
| Calibre-Web | book.meirong.dev | 302 |
| Vault | vault.meirong.dev | 307 |
| Gotify | notify.meirong.dev | 200 |
| Homepage | home.meirong.dev | 200 |
| Miniflux | rss.meirong.dev | 200 |
| IT-Tools | tool.meirong.dev | 200 |
| Uptime Kuma | status.meirong.dev | 200 |
| Timeslot | slot.meirong.dev | 302 |
| KaraKeep | keep.meirong.dev | 307 |
ArgoCD 同步状态#
NAME SYNC STATUS HEALTH STATUS
argocd-image-updater Synced Healthy
cloudflare Synced Healthy
gateway Synced Degraded ← Gateway 无 External IP,预期行为
kopia Synced Healthy
monitoring-dashboards Synced Healthy
personal-services OutOfSync Healthy ← ExternalSecret CRD 默认值漂移
vault-eso Synced Healthy
zitadel OutOfSync Healthy ← 同上
gateway 的 Degraded 是 Cilium Gateway 在非云环境下的正常状态(没有 LoadBalancer controller 分配 External IP)。personal-services 和 zitadel 的 OutOfSync 是 ExternalSecret CRD webhook 注入默认值导致的漂移,不影响功能。
ClusterMesh 状态#
homelab → oracle-k3s: 1/1 connected, KVStoreMesh 1/1 connected
oracle-k3s → homelab: 1/1 connected, KVStoreMesh 1/1 connected
经验总结#
1. 重建是最好的验证#
这次 homelab 重建是被迫的(Tailscale IP 变更 + 早期 Cilium 配置不够激进),但意外地成了一次完整的灾难恢复演练。从头部署让我发现了一些以前没注意到的问题:
monitoring-external.yaml的 NodePort Service 没有被 ArgoCD 管理,重建后不会自动恢复- Cloudflare Tunnel 的 terraform API token 配置方式(
.envvsterraform.tfvars)不一致,调试花了不少时间
2. Tailscale IP 不应该是硬编码的#
这次最大的教训:跨集群的连接不应该依赖硬编码 IP。虽然 ClusterMesh 目前还没有被用来替代所有 NodePort 连接(OTel Collector 还在用 IP),但它提供了一条更可靠的路径。下一步可以考虑把 OTel 的 exporter 改为指向 ClusterMesh 发现的 Service。
3. KPR + Cilium Gateway 是值得的#
相比之前保留 kube-proxy + Traefik 的方案,完全切到 Cilium KPR + Gateway API 减少了两个组件(kube-proxy + Traefik),简化了网络栈。Gateway API 作为标准 API,HTTPRoute 定义无需修改。
4. --allow-mismatching-ca 在 Tailscale 场景下是合理的#
两个独立安装的 Cilium 集群不会共享 CA。正式环境应该手动交换或统一 CA,但在 Homelab + Tailscale WireGuard 加密的场景下,多一层 CA 校验的收益不高,--allow-mismatching-ca 是合理的权衡。
5. 先跑起来,再优化#
ClusterMesh 目前的使用方式很基础——只有节点发现和身份联邦,还没用跨集群 Service。但基础设施先到位,后续想做跨集群 failover 或 Service 发现时不需要再折腾底层。Homelab 的核心原则是:先让它能工作,再考虑做得更好。