背景#

在前两篇文章中,我分别记录了 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 的职责很明确:

  1. 身份联邦:两个集群的 Cilium Identity 互相可见,跨集群的 Network Policy 可以基于 label 而不是 IP
  2. 服务发现:标记了 io.cilium/global-service: "true" 的 Service,其 Endpoints 对两个集群都可见
  3. 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 必须使用相同的 tunnelProtocolroutingMode。我的配置:

# 两个集群共用的关键参数
kubeProxyReplacement: true
routingMode: tunnel
tunnelProtocol: vxlan

2. 集群标识唯一#

每个集群必须有唯一的 cluster.namecluster.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:完全交给 Cilium
  • disable: [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,替代 Traefik
  • clustermesh.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

工作负载恢复顺序#

重建后按依赖关系恢复所有服务:

  1. NFS Provisioner → PVC 依赖
  2. Vault → 手动 init + unseal
  3. ESO → 依赖 Vault
  4. monitoring-external NodePort → 跨集群 OTel 入口
  5. Prometheus / Loki / Tempo → 可观测基础设施
  6. ArgoCD → 之后的服务由 GitOps 接管
  7. 所有 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-serviceszitadel 的 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 配置方式(.env vs terraform.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 的核心原则是:先让它能工作,再考虑做得更好。