开始学习

Stage 04 · 生产 · 第 3 / 8

生产网络:Cilium、MetalLB 与证书

安装 Cilium 的坑、裸金属 LoadBalancer、cert-manager 自动签发证书。

50 分钟|全程第 26 / 37

上一课用 Kubespray 部署完集群后,所有节点都是 NotReady,因为我们故意没让它装 CNI。这一课把一个裸金属集群的网络层补完整,分三步:用 Cilium 打通 Pod 网络,用 MetalLB 让 type: LoadBalancer 的 Service 在机房里拿到真实 IP,用 cert-manager 自动签发和续期 TLS 证书。然后是入口方案的选型:ingress-nginx 已经归档,新集群该用什么。

这几个组件在公有云上都由云厂商代劳,裸金属上要自己搞定。它们也是后面所有对外服务的基础。

组件目录约定

团队的每个集群组件都是一个独立目录,用同一套三件套管理:

text
network/
├── cilium/
│   ├── helmwave.yml        # 装哪个 Chart、哪个版本、装到哪个命名空间
│   └── values.yml          # Chart 参数,只写和默认值不同的部分
├── metallb/
│   ├── helmwave.yml
│   ├── values.yml
│   ├── kustomization.yaml  # Chart 之外的自定义资源(地址池等)
│   └── ipaddresspool.yaml
└── cert-manager/
    ├── helmwave.yml
    ├── values.yml
    ├── kustomization.yaml
    └── clusterissuer.yaml

helmwave 是一个声明式的 Helm 编排工具,作用类似 helmfile:把 helm repo addhelm upgrade --install 的参数写进文件,进 Git 管理。一个典型的 helmwave.yml

network/metallb/helmwave.yml
project: metallb

repositories:
  - name: metallb
    url: https://metallb.github.io/metallb

releases:
  - name: metallb
    namespace: metallb-system
    create_namespace: true
    chart:
      name: metallb/metallb
      version: 0.15.3
    values:
      - values.yml
    wait: true

部署流程固定为两步:

bash
cd network/metallb
helmwave up --build            # 安装/升级 Chart
kubectl apply -k .             # 应用 Chart 之外的 CR(依赖 Chart 带的 CRD,所以放第二步)

这样 Chart 版本、参数和自定义资源都能在 Git 里 review,升级就是改 version 再跑一遍。不用 helmwave 的话,换成 helm upgrade --install -f values.yml 也是同样的结构。Helm 和 Kustomize 本身见 Helm 与 Kustomize

Cilium:Pod 网络

Cilium 用 eBPF 实现 Pod 网络、Service 转发和网络策略,是目前生产集群最主流的 CNI 之一。截至本文写作时团队使用 1.19.x,请以 官方 Releases 为准。

network/cilium/values.yml
# apiserver 地址:指向 Kubespray 在每个节点上的本地 nginx 代理
k8sServiceHost: "127.0.0.1"
k8sServicePort: "6443"

# 保留 kube-proxy,Cilium 只负责 Pod 网络和策略
kubeProxyReplacement: "false"

ipam:
  mode: cluster-pool
  operator:
    clusterPoolIPv4PodCIDRList: ["172.24.0.0/13"]   # 与 kube_pods_subnet 一致
    clusterPoolIPv4MaskSize: 24                     # 与 kube_network_node_prefix 一致

cni:
  chainingMode: portmap   # 配合 kube-proxy 支持 hostPort
  exclusive: false        # 允许共存其他 CNI(如 Multus、Spiderpool 做 RDMA 网卡)

bpf:
  enableTCX: false        # 规避 TCX 程序残留的问题,见下文

operator:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: node-role.kubernetes.io/control-plane
                operator: Exists

装好后节点应在一两分钟内变成 Ready

bash
kubectl -n kube-system rollout status ds/cilium
kubectl get nodes
cilium status          # 需要单独安装 cilium CLI

几个参数背后都有坑:

k8sServiceHost: 127.0.0.1:Cilium 启动时 ClusterIP 还不能用(Service 转发还没就绪),所以必须直接告诉它 apiserver 的真实地址。填某台 master 的 IP 会引入单点;填 auto 同样会解析成单个 master 地址。Kubespray 已经在每个节点上放了一个 127.0.0.1:6443 的 nginx 代理,转发到所有 apiserver,用它最稳。

ipam.mode: cluster-pool:由 cilium-operator 从列表里给每个节点切一个 /24。Pod 网段用完时,只能在列表末尾追加新网段,然后重启 operator:

bash
kubectl -n kube-system rollout restart deployment/cilium-operator

kubeProxyReplacement:Cilium 可以完全替代 kube-proxy,性能更好,也能绕开 IPVS 废弃的问题。但替代后 Service 相关的行为全部由 Cilium 负责,排障方式也要跟着变。团队目前保守地保留 kube-proxy,新集群可以评估开启。

MetalLB:给 LoadBalancer Service 分配 IP

在云上,type: LoadBalancer 的 Service 会自动得到一个云负载均衡器。裸金属上没有这个东西,Service 会永远卡在 <pending>。MetalLB 补的就是这一块:从你预留的地址池里分配 IP,并向网络宣告这个 IP 在哪台节点上。

它有两种宣告模式:

模式原理优点局限
L2选一个节点用 ARP 回应"这个 IP 在我这"不需要网络设备配合单个 IP 的流量都进同一个节点,只有故障切换,没有负载均衡
BGP各节点和交换机建立 BGP 会话宣告路由真正的多节点 ECMP 负载均衡需要网络团队配合配置交换机

团队用 L2 模式,入口流量不大时足够用。

network/metallb/values.yml
controller:
  affinity: &cp-affinity
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: node-role.kubernetes.io/control-plane
                operator: Exists
  tolerations: &cp-tolerations
    - operator: Exists
speaker:
  affinity: *cp-affinity
  tolerations: *cp-tolerations
  ignoreExcludeLB: true   # 控制平面节点默认带"排除外部 LB"的标签,这里忽略它

speaker 只放在控制平面节点上,是为了让入口流量固定落在少数几台稳定的机器上,也方便网络团队做 ACL。

network/metallb/ipaddresspool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: default
  namespace: metallb-system
spec:
  addresses:
    - 192.168.10.200-192.168.10.210   # 向网管申请、不在 DHCP 范围内的地址
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: default
  namespace: metallb-system
spec:
  ipAddressPools: ["default"]

需要固定 IP 的服务(例如 Gateway,DNS 泛解析要指向它),用注解指定:

yaml
metadata:
  annotations:
    metallb.io/loadBalancerIPs: 192.168.10.210

排查 L2 不通

bash
# 1. 这个 IP 当前由哪个节点宣告
kubectl -n metallb-system get servicel2statuses.metallb.io

# 2. speaker 日志里有没有宣告记录
kubectl -n metallb-system logs -l app.kubernetes.io/component=speaker | grep serviceAnnounced

# 3. 在同网段的另一台机器上看 ARP 是否有回应,MAC 是否是宣告节点的
arping -I eth0 192.168.10.210

# 4. 在宣告节点上抓 ARP 包
tcpdump -ni eth0 arp and host 192.168.10.210

cert-manager:自动化证书

cert-manager 以 CRD 的方式管理证书:你声明一个 Certificate(或者在 Ingress/Gateway 上加注解),它向 Issuer/ClusterIssuer 申请证书,存进 Secret,并在过期前自动续期。

network/cert-manager/values.yml
crds:
  enabled: true          # 让 Chart 安装 CRD
replicaCount: 2
podDisruptionBudget:
  enabled: true
webhook:
  replicaCount: 2
  podDisruptionBudget:
    enabled: true
cainjector:
  replicaCount: 2
  podDisruptionBudget:
    enabled: true

webhook 默认只有 1 个副本。它挂掉时,所有 cert-manager CR 的创建和修改都会被 apiserver 拒绝,所以生产上要 2 副本加 PDB。

生产:Let's Encrypt ACME

network/cert-manager/clusterissuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ops@example.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - http01:
          gatewayHTTPRoute:    # 需在 values 中设置 config.enableGatewayAPI: true;用 Ingress 时改为 ingress.ingressClassName
            parentRefs:
              - name: main-gateway
                namespace: istio-system
                kind: Gateway

HTTP-01 验证要求 Let's Encrypt 能从公网访问 http://<域名>/.well-known/acme-challenge/...,所以域名必须解析到公网可达的入口。内网域名或者需要通配证书(*.dev1.bj1.example.com)时,要改用 DNS-01,由 cert-manager 调用 DNS 服务商的 API 写 TXT 记录。

签发卡住时顺着资源链往下查:

bash
kubectl get certificate,certificaterequest,order,challenge -A
kubectl describe challenge -n <ns> <name>    # 通常能看到具体的失败原因

内网:自建 CA

纯内网服务用不了 ACME,可以让 cert-manager 当一个私有 CA:先用 SelfSigned 签一张根证书,再用它创建 CA 类型的 Issuer。下面的练习就在 kind 里完整走一遍。

入口:从 Ingress 到 Gateway API

有了 LoadBalancer IP 和证书,还差一个七层入口把域名路由到各个服务。团队过去用 ingress-nginx,配合一个 LoadBalancer IP 和一条 *.dev1.bj1.example.com 泛解析,新服务只要写一个 Ingress 就能上线。

替代方案是 Gateway API,它是 Ingress 的官方后继者,把"基础设施负责的 Gateway"和"应用团队负责的 HTTPRoute"拆成两种资源。团队在不同集群用过两个实现:

实现特点适合
Istio(Sidecar 模式)同时提供服务网格能力:mTLS、流量治理需要网格的集群
kgateway(原 Gloo)基于 Envoy 的轻量网关,只做南北向只需要入口的集群

用 Istio 时,一个共享 Gateway 加上按命名空间授权的写法:

gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: main-gateway
  namespace: istio-system
  annotations:
    metallb.io/loadBalancerIPs: 192.168.10.210
spec:
  gatewayClassName: istio
  listeners:
    - name: https
      hostname: "*.dev1.bj1.example.com"
      port: 443
      protocol: HTTPS
      tls:
        certificateRefs:
          - name: wildcard-dev1-tls
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              gateway-access: "true"   # 只有打了这个标签的命名空间能挂路由
httproute.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: grafana
  namespace: monitoring
spec:
  parentRefs:
    - name: main-gateway
      namespace: istio-system
  hostnames: ["gf.dev1.bj1.example.com"]
  rules:
    - backendRefs:
        - name: grafana
          port: 80

Istio 的 Ambient 模式不需要 Sidecar,资源开销更小。但团队有集群用 Spiderpool 给 Pod 挂 RDMA 网卡,它和 Ambient 不兼容,所以统一选了 Sidecar 模式。选型前要先确认你的 CNI 组合是否受支持。Gateway API 的概念见 Ingress 与 Gateway API

顺带一提:按域名限制出站

Cilium 除了标准 NetworkPolicy,还支持按 FQDN 控制出站流量,这是原生 NetworkPolicy 做不到的。比如只允许某个命名空间访问 GitHub:

cnp-fqdn.yaml
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: allow-github-only
  namespace: ci
spec:
  endpointSelector: {}
  egress:
    - toFQDNs:
        - matchName: github.com
        - matchPattern: "*.githubusercontent.com"
    # 必须放行 DNS,并让 Cilium 代理 DNS 查询,才能学到域名对应的 IP
    - toEndpoints:
        - matchLabels:
            k8s:io.kubernetes.pod.namespace: kube-system
            k8s-app: kube-dns
      toPorts:
        - ports: [{ port: "53", protocol: ANY }]
          rules:
            dns: [{ matchPattern: "*" }]
    # 开了 nodelocaldns 时,DNS 实际发往本机
    - toEntities: ["host"]
      toPorts:
        - ports: [{ port: "53", protocol: ANY }]
          rules:
            dns: [{ matchPattern: "*" }]

漏掉最后一段是常见错误:开启 nodelocaldns 后 Pod 的 DNS 请求发往节点本地地址,只放行 kube-dns 会导致解析全部超时。更多策略写法见 安全

动手练习

  1. kind 里跑 MetalLB L2。创建 kind 集群后查出 kind 网络的网段,从中划一段作为地址池:

    bash
    docker network inspect kind -f '{{range .IPAM.Config}}{{.Subnet}} {{end}}'
    # 假设输出 172.18.0.0/16,就用 172.18.255.200-172.18.255.250
    helm repo add metallb https://metallb.github.io/metallb
    helm install metallb metallb/metallb -n metallb-system --create-namespace --wait

    仿照本文写 IPAddressPoolL2Advertisement 并 apply,然后 kubectl create deploy web --image=nginx && kubectl expose deploy web --port=80 --type=LoadBalancer,确认 EXTERNAL-IP 分配成功。Linux 上可以直接 curl 这个 IP;macOS 上宿主机访问不到 kind 网络,用 docker run --rm --network kind curlimages/curl <IP> 验证。

  2. 在第 1 题基础上给 Service 加 metallb.io/loadBalancerIPs 注解指定一个固定 IP,再用 kubectl -n metallb-system get servicel2statuses.metallb.io 查看是哪个节点在宣告。

  3. kind 里跑自签 CA。安装 cert-manager(helm install cert-manager oci://quay.io/jetstack/charts/cert-manager -n cert-manager --create-namespace --set crds.enabled=true),然后依次创建:SelfSigned 类型的 ClusterIssuer;一张 isCA: true 的根证书 Certificate(放在 cert-manager 命名空间);引用该根证书 Secret 的 CA 类型 ClusterIssuer;最后用这个 CA Issuer 为 demo.example.com 签一张证书。用 kubectl get secret <name> -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -issuer -dates 检查签发者和有效期。

  4. 把第 3 题中 Certificate 的 duration 设成 1hrenewBefore 设成 55m,观察几分钟后 kubectl get certificate 中的 REVISION 是否增加,理解自动续期。

  5. 思考题:你的集群需要给内网 50 个服务提供 HTTPS,它们都在 *.dev1.bj1.example.com 下。用 HTTP-01 还是 DNS-01?一张通配证书还是 50 张证书?各有什么取舍?

自测

为什么 Cilium 的 k8sServiceHost 要填 127.0.0.1?

Cilium 启动时 Service 转发还没就绪,不能通过 ClusterIP 访问 apiserver,必须给它一个真实地址。填某台 master IP 或 auto 都会形成单点;Kubespray 在每个节点上部署了监听 127.0.0.1:6443 的 nginx 代理,转发到所有 apiserver,天然高可用。

MetalLB 分配了 IP,但从外面 ping 不通,可能是什么原因?

常见原因:speaker 所在节点带有 exclude-from-external-load-balancers 标签且没设 ignoreExcludeLB;地址池不在节点所在的二层网段;externalTrafficPolicy: Local 但宣告节点上没有后端 Pod;IP 和其他设备冲突。用 servicel2statuses、speaker 日志、arpingtcpdump 逐层确认。

MetalLB L2 模式能做负载均衡吗?

不能。L2 模式下一个 IP 同一时刻只由一个节点通过 ARP 响应,所有流量都进这个节点,MetalLB 只提供故障切换。需要多节点分担流量要用 BGP 模式配合交换机 ECMP,或者在前面再放一层硬件负载均衡。

什么时候必须用 DNS-01 而不是 HTTP-01?

申请通配证书时,或者域名只在内网解析、Let's Encrypt 无法从公网访问时。DNS-01 通过在 DNS 上写 TXT 记录来证明域名所有权,不需要入口对公网开放,但需要 cert-manager 有 DNS 服务商的 API 权限。

新集群的七层入口为什么不再选 ingress-nginx?

社区版 ingress-nginx 已经归档,不再有安全修复,Kubespray 也移除了对应插件。Gateway API 是 Ingress 的官方后继者,角色划分更清晰(Gateway 归平台、HTTPRoute 归应用),Istio、kgateway、Cilium 等都有实现。

参考资料

学完了吗?

进度保存在本机浏览器中,可在学习路线页查看。