在公有云 Kubernetes 集群中,网络插件(CNI)是承载服务通信的最底层基础设施。默认情况下,AWS EKS 采用官方的 aws-vpc-cni。虽然它与 VPC 路由开箱集成,但在业务微服务高频交互、集群规模扩大或有更严苛的安全审计要求时,会有几个受限之处:

  1. 依赖 iptables 与 kube-proxy:随 Service 与 Pod 数量增长,内核中大量的 iptables 规则和连接跟踪(conntrack)带来额外的转发延迟与 CPU 损耗;
  2. 缺乏细粒度安全管控:原生 aws-vpc-cni 早期默认不带网络策略,即便开启也以 L4 CIDR 为主,难以支持域名级(FQDN)出网控制或跨微服务探测拦截;
  3. 网络可观测性不足:AWS VPC Flow Logs 存在 10–15 分钟延迟,且无法抓取同节点 Pod 间的内部通信;
  4. 防御 SSRF 与元数据外带受限:在 Pod 出口层面较难原生拦截针对节点 IMDS(169.254.169.254)的探测或动态外部域名请求。

为了兼顾 AWS 原生私网路由互通与 eBPF 内核级转发性能,可以将 EKS 集群迁移至 Cilium(Standalone ENI IPAM 模式 + kubeProxyReplacement=true)。

以下梳理在 Nitro AL2023 架构、双 CNI 调度隔离、IAM 权限配置、节点滚动替换以及出网安全策略中的落地步骤与注意点。

目标架构与技术选型#

为什么选择 Standalone ENI 模式#

Cilium 支持多种 IPAM 模式(如 Kubernetes Host-Scope、Cluster-Scope、Overlay/VXLAN 以及云厂商专有模式)。在 AWS 上,最常见的有两种:

  • Overlay 模式(Geneve/VXLAN):Pod 使用虚拟私网网段,跨节点通信进行二次封装封包。这种模式无法直通 AWS 外部网络,且与 AWS Application Load Balancer(ALB)的 target-type: ip 直连模式不兼容;
  • Standalone ENI 模式(ipam.mode: eni):Pod 直接从当前节点所在的 AWS VPC 子网中分配真实的 Secondary Private IP。
flowchart TB subgraph VPC["AWS VPC Subnet"] direction TB subgraph NodeA["EC2 Node A"] direction TB PA["Primary ENI (ens5)"] SA["Secondary ENI (ens6)
Pod 1 (10.0.1.5) / Pod 2 (10.0.1.6)"] BPFA["eBPF Engine (TC Direct Routing)"] end subgraph NodeB["EC2 Node B"] direction TB PB["Primary ENI (ens5)"] SB["Secondary ENI (ens6)
Pod 3 (10.0.2.5) / Pod 4 (10.0.2.6)"] BPFB["eBPF Engine"] end ALB["AWS Ingress ALB (IP Mode)"] end ALB -->|"直连 Pod IP"| SA ALB -->|"直连 Pod IP"| SB BPFA <-->|"VPC 原生路由(无封包)"| BPFB

选型优势:

  1. 0 封包损耗:数据包使用 AWS 原生路由网络(Native Routing),无额外 MTU 缩水与封包/拆包 CPU 开销;
  2. 100% 兼容 AWS Ingress:AWS Load Balancer Controller 可无缝探测并直接将流量路由至 Pod IP;
  3. 保留 VPC 网络隔离:所有的子网路由表、NAT Gateway 出网链路与子网 NACL 保持完全一致,无需重构底层网络拓扑。

移除 kube-proxy#

开启 kubeProxyReplacement=true。Cilium 借助挂载在 Socket 层的 eBPF 程序(sock_ops / sock_addr),在系统调用级别(connect())直接将目标 Service IP 重写为后端真实的 Pod IP,完全绕过了内核网络栈的 iptables 规则表与 conntrack 模块,使得 Service 负载均衡性能提升数倍。

迁移风险与核心设计#

在承载业务的存量生产集群上更换 CNI 时,如果不做调度隔离直接下发 Cilium,会导致两个 CNI 插件争抢网卡与 IPAM。调度设计上需要重点考虑以下几个关键点:

双 CNI 节点级冲突与调度门禁#

  • 核心风险:绝对不能在同一个 Node 上同时启动 aws-node 和 cilium-agent。两者的 IPAM 模块会同时向 AWS EC2 请求绑定 Secondary IP,并相互覆盖 /etc/cni/net.d/ 下的 CNI 配置文件,导致节点上所有容器的网络命名空间损坏。
  • 解决方案:在部署 Cilium 前,预先对旧的 aws-node 和 kube-proxy DaemonSet 打上 nodeSelector(例如 io.cilium/aws-node-enabled: "true"),确保它们只运行在存量老节点上;而 Cilium DaemonSet 则配置反向亲和性,仅调度至新节点。

AL2023 与 Nitro 架构的网卡名称探测#

  • 核心风险:在 Amazon Linux 2023(Linux 6.x 内核)和 Nitro 架构 EC2 实例(如 m5a、c5a 等)上,系统启用了 systemd 可预测网卡命名(Predictable Network Interface Names)。主网卡名称为 ens5,Secondary ENI 为 ens6,而不是传统的 eth0、eth1。
  • 解决方案:在 Cilium Helm values 中,切勿硬编码 devices: "eth+" 或 --direct-routing-device=eth0。将 devices: "" 保持为空,Cilium 会自动通过默认网关路由(default via 10.0.x.1 dev ens5)精准探测到正确的物理设备。

Cilium Operator AWS ENI 模式的 IAM 权限#

  • 核心风险:Cilium Operator 在 AWS ENI IPAM 模式下不仅需要 ec2:CreateNetworkInterface 等标准网卡操作权限,还需要读取 VPC 的路由信息与标签。如果缺少 ec2:DescribeRouteTables 和 ec2:DescribeTags,Operator 在启动时会由于权限不足(403 Forbidden)导致 AWS IPAM 分配器崩溃,容器抛出 exit code 255。
  • 解决方案:必须在 Terraform IRSA 策略中明确补充 ec2:DescribeRouteTables 与 ec2:DescribeTags。

CoreDNS 漂移与 unmanagedPodWatcher 参数#

  • 核心风险:Cilium Operator 默认带有 unmanagedPodWatcher 机制。如果在集群尚未全部完成迁移时开启 unmanagedPodWatcher.restart: true,Operator 会认为旧节点上的 CoreDNS 是「未受 Cilium 管理的 Pod」而强行将其重启。若此时新节点尚未就绪,集群内网 DNS 会瞬间中断。
  • 解决方案:迁移全过程中保持 operator.unmanagedPodWatcher.restart: false,待全集群所有节点均换装 Cilium 后再开启。

落地流程#

迁移过程通过 Terraform 与 GitOps 分阶段落地:

  1. Terraform 准备:配置 Cilium Operator IRSA 角色与权限,将存量 EKS Addon 声明 preserve = true 脱钩。
  2. GitOps 配置:编写 Cilium Helm Values,设置节点亲和性调度门禁,通过 ArgoCD 部署 Operator。
  3. 灰度置换节点池:先置换系统管理节点,再单台滚动业务 Worker 节点。
  4. 清理与观测:确认存量节点完全下线后,清理旧 DaemonSet 并开启 Hubble 流量观测。

阶段一:Terraform 配置与 IRSA 权限#

首先为 cilium-operator 提供专用的 AWS IRSA 权限,并解耦 EKS 托管插件。

新增 IAM 角色与策略(iam.tf)#

# IRSA Role for Cilium Operator
resource "aws_iam_role" "cilium_operator" {
  name = "demo-eks-cilium-operator-irsa"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.cluster.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "${replace(aws_iam_openid_connect_provider.cluster.url, "https://", "")}:sub" = "system:serviceaccount:kube-system:cilium-operator"
            "${replace(aws_iam_openid_connect_provider.cluster.url, "https://", "")}:aud" = "sts.amazonaws.com"
          }
        }
      }
    ]
  })
}

# Cilium Operator ENI IPAM 最小策略
resource "aws_iam_policy" "cilium_operator" {
  name        = "demo-eks-cilium-operator-policy"
  description = "Permissions required by Cilium Operator in AWS ENI mode"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ec2:DescribeNetworkInterfaces",
          "ec2:DescribeSubnets",
          "ec2:DescribeVpcs",
          "ec2:DescribeSecurityGroups",
          "ec2:DescribeRouteTables",
          "ec2:DescribeTags",
          "ec2:DescribeInstances",
          "ec2:CreateNetworkInterface",
          "ec2:AttachNetworkInterface",
          "ec2:ModifyNetworkInterfaceAttribute",
          "ec2:AssignPrivateIpAddresses",
          "ec2:UnassignPrivateIpAddresses",
          "ec2:DeleteNetworkInterface"
        ]
        Resource = "*"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "cilium_operator" {
  policy_arn = aws_iam_policy.cilium_operator.arn
  role       = aws_iam_role.cilium_operator.name
}

EKS Add-on 解耦配置(addons.tf)#

将存量的 vpc-cni 与 kube-proxy EKS Addon 声明 preserve = true。这样即使后续从 Terraform 中解绑,AWS 也不会强行将旧的 DaemonSet 自动回滚或反复注入:

resource "aws_eks_addon" "vpc_cni" {
  cluster_name = module.eks.cluster_name
  addon_name   = "vpc-cni"
  preserve     = true
}

resource "aws_eks_addon" "kube_proxy" {
  cluster_name = module.eks.cluster_name
  addon_name   = "kube-proxy"
  preserve     = true
}

阶段二:GitOps 声明式配置与节点门禁#

通过 Git 仓库管理 Helm Values,使用 ArgoCD 进行部署。

存量旧节点打标与旧 DaemonSet 隔离#

在部署 Cilium 之前,先为所有存量节点打上标记,并将旧组件锁死在这些节点上:

# 1. 标记所有现有节点为旧节点
kubectl label nodes --all io.cilium/aws-node-enabled=true

# 2. 限制 aws-node 和 kube-proxy 只在被标记的旧节点上运行
kubectl -n kube-system patch daemonset aws-node -p '{"spec":{"template":{"spec":{"nodeSelector":{"io.cilium/aws-node-enabled":"true"}}}}}'
kubectl -n kube-system patch daemonset kube-proxy -p '{"spec":{"template":{"spec":{"nodeSelector":{"io.cilium/aws-node-enabled":"true"}}}}}'

Cilium Helm Values(values/cilium.yaml)#

cluster:
  name: demo-eks
  id: 1

eni:
  enabled: true
  awsReleaseExcessIPs: true
  iamRole: arn:aws:iam::123456789012:role/demo-eks-cilium-operator-irsa

ipam:
  mode: eni

routingMode: native
ipv4NativeRoutingCIDR: 10.0.0.0/16

endpointRoutes:
  enabled: true

enableIPv4Masquerade: true
bpf:
  masquerade: true

# 完全取代 kube-proxy
kubeProxyReplacement: true
k8sServiceHost: <EKS_API_ENDPOINT_FQDN>
k8sServicePort: 443

# 节点隔离门禁:绝对不调度到旧 VPC CNI 节点上
affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:
            - key: io.cilium/aws-node-enabled
              operator: NotIn
              values:
                - "true"

operator:
  replicas: 1
  unmanagedPodWatcher:
    restart: false  # 迁移期间必须设为 false,防 CoreDNS 抖动
  tolerations:
    - key: node-group
      value: system
      effect: NoSchedule
  nodeSelector:
    node-type: aws-worker
    worker-group: system

hubble:
  enabled: false  # 节点就绪后再开启

配置 ArgoCD Application(manifest/cilium.yaml)#

必须在 ArgoCD 中配置 ServerSideApply=true,以规避 Cilium CRD 超大导致的客户端应用报错:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cilium
  namespace: argocd
spec:
  project: default
  sources:
    - repoURL: https://helm.cilium.io/
      chart: cilium
      targetRevision: 1.20.4
      helm:
        valueFiles:
          - $values/values/cilium.yaml
    - repoURL: https://github.com/my-org/eks-manifests.git
      targetRevision: HEAD
      ref: values
  destination:
    server: https://kubernetes.default.svc
    namespace: kube-system
  syncPolicy:
    automated:
      prune: false
      selfHeal: true
    syncOptions:
      - ServerSideApply=true

应用该配置后,ArgoCD 自动部署 Cilium Operator。此时集群中的老节点由于反亲和性不会被 Cilium 接管,整体业务处于完全安全、零干扰状态。

阶段三:分批灰度滚动节点池#

节点滚动置换遵循 先系统管理节点、后业务计算节点 的原则。

系统节点组灰度演练#

系统节点组(System Node Group)承载着 CoreDNS、cilium-operator、aws-load-balancer-controller 等核心控制组件。

  1. 扩容系统 ASG 实例:将期望容量从 1 提升至 2。
  2. 观察新节点入网:
    • 新节点启动后不会被打上 io.cilium/aws-node-enabled=true 标签;
    • aws-node 和 kube-proxy 忽略该节点;
    • cilium-agent 和 cilium-envoy 自动在该节点拉起,并在 30 秒内达到 1/1 Running。
  3. 验证 Cilium 状态:
    kubectl -n kube-system exec -it ds/cilium -c cilium-agent -- cilium-dbg status
    # 确认 KubeProxyReplacement: True, IPAM Pool: OK, Controllers: Healthy
    
  4. 平稳排空旧节点:
    kubectl cordon <OLD_SYSTEM_NODE>
    kubectl drain <OLD_SYSTEM_NODE> --ignore-daemonsets --delete-emptydir-data --force
    
    所有核心控制器自动漂移至新的 Cilium 节点,并通过 VPC Secondary IP 成功绑定网络。
  5. 缩容并销毁旧实例:
    aws autoscaling terminate-instance-in-auto-scaling-group \
      --instance-id <OLD_SYSTEM_INSTANCE_ID> \
      --should-decrement-desired-capacity
    

业务节点组逐台平稳迁移#

对于业务 Worker 节点组,采用「扩一、排一、缩一」的节奏逐台置换,重点关注以下有状态组件:

  • 分布式协同集群(如 Vault 3 副本 Raft):逐台驱逐,每次只排空 1 个副本,Raft Quorum 始终保持法定多数,AWS KMS 自动完成 Unseal;
  • 挂载 EBS 持久化卷的服务(如 Prometheus):EBS 卷在 Pod 漂移时受可用区(Availability Zone)约束。当旧节点下线时,若目标可用区资源充足,AWS EBS CSI Driver 会自动完成卷卸载并在目标 Cilium 节点重新挂载;
  • 微服务流量交接:由于 Cilium ENI 模式为 Pod 分配相同子网 IP,AWS ALB TargetGroup 会在 Pod 就绪后平滑注册新 IP,完全避免了健康检查震荡。

逐步滚动直至存量旧 Worker 节点全部下线。至此,集群中的所有计算节点均转由 Cilium 接管。

阶段四:遗留清理与可观测性落地#

全节点就绪后,即可安全清除所有历史遗留组件并开启 Hubble。

删除旧 DaemonSet#

kubectl -n kube-system delete daemonset aws-node kube-proxy

检查确认全集群只保留 cilium、cilium-envoy 与 ebs-csi-node。

开启 Hubble 流量可观测性#

在 values/cilium.yaml 中开启 Hubble 组件,并将 TLS 证书生成方式指定为 cronJob(兼容 GitOps,避免 Helm 每次重新渲染生成非幂等证书):

operator:
  replicas: 1
  unmanagedPodWatcher:
    restart: true  # 迁移完毕,开启自动监控

hubble:
  enabled: true
  relay:
    enabled: true
  ui:
    enabled: true
  tls:
    auto:
      method: cronJob

提交代码合并至主干,ArgoCD 自动部署 hubble-relay(1/1 Running)与 hubble-ui(2/2 Running)。

本地快速打开可视化监控拓扑看板:

kubectl -n kube-system port-forward svc/hubble-ui 12000:80
# 浏览器访问 http://localhost:12000 查看实时应用层拓扑

利用 Cilium 防御网络攻击与 SSRF#

完成 Cilium 迁移不仅带来了转发性能收益,也补齐了集群的安全管控能力。针对业界频发的恶意扫描、凭据窃取与 SSRF 攻击,Cilium 提供了原生内核级防御手段:

全局阻断 AWS IMDS 云元数据访问#

攻击者在入侵 Web 服务后,常利用 SSRF 访问 http://169.254.169.254/latest/meta-data/ 窃取节点的 IAM 临时凭据。

通过下发集群级网络策略,在 eBPF 层面彻底丢弃所有业务 Pod 对元数据网段的访问:

apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: block-aws-imds-metadata
spec:
  endpointSelector:
    matchExpressions:
      - key: k8s-app
        operator: NotIn
        values: ["cilium", "cilium-operator", "ebs-csi-node"]
  egress:
    - toCIDR:
        - "169.254.169.254/32"
      toPorts:
        - ports:
            - port: "80"
              protocol: TCP

L7 FQDN 出网白名单#

如果某个容器被植入恶意脚本或反弹 Shell,攻击者通常需要向外联络 C2 服务器。通过配置域名级白名单策略,未授权的外网 IP 或恶意域名请求将直接被丢弃:

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: strict-egress-fqdn
  namespace: app
spec:
  endpointSelector:
    matchLabels:
      app: backend-service
  egress:
    # 允许连接集群 CoreDNS 解析域名
    - toEndpoints:
        - matchLabels:
            "k8s:io.kubernetes.pod.namespace": kube-system
            "k8s:k8s-app": kube-dns
      toPorts:
        - ports: [{port: "53", protocol: ANY}]
          rules: {dns: [{matchPattern: "*"}]}
    # 严格放行经过审计的外部合法域名
    - toFQDNs:
        - matchName: "api.partner.example.com"
        - matchPattern: "*.amazonaws.com"
      toPorts:
        - ports: [{port: "443", protocol: TCP}]

实测结果:

  • 合法域名 api.partner.example.com 握手成功,返回 HTTP/2 200;
  • 试图访问未报备的外部域名或直接外连公网 IP,在 TCP SYN 阶段被内核直接阻断(Connection timed out);
  • Hubble 实时抓取并记录阻断事件:
    app/backend-service:48038 <> 198.51.100.20:443 (world) Policy denied DROPPED (TCP Flags: SYN)
    

应急响应:失陷 Pod 快速断网隔离#

当安全告警捕获到异常活动时,过去通常只能执行 kubectl delete pod,但这会破坏内存取证现场。利用 Cilium,只需为 Pod 打上预设的隔离标签:

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: emergency-pod-quarantine
  namespace: app
spec:
  endpointSelector:
    matchLabels:
      security.quarantine: "true"
  ingress: []  # 阻断所有入站
  egress: []   # 阻断所有出站

执行命令:

kubectl label pod <compromised-pod> security.quarantine=true

Cilium 会实时生效网络策略并切断其进出网络包,进程继续存活供安全团队取证。

小结#

在承载业务的集群中置换网络数据面,关键在于平稳交接。核心经验汇总如下:

关键控制点 生产推荐实践 避坑提醒
IPAM 模式 Standalone ENI (ipam.mode: eni) 保证 Pod 分配原生 VPC IP,与 AWS ALB 直连模式完全兼容
网卡探针 保持 devices: "" 为空自动探测 AL2023 Nitro 实例网卡为 ens5,不要硬编码 eth0
节点门禁 双向反亲和性标签调度门禁 避免两个 CNI 在同一个节点上并行运行导致网络冲突
IAM 权限 补充 DescribeRouteTables & DescribeTags 缺少该权限会导致 Operator 启动失败(exit code 255)
滚动策略 遵循「扩一、排一、缩一」单台滚动 优先置换 System 节点,再滚动 Worker 业务节点,保障分布式 Raft Quorum
遗留清理 确认全部节点换装后再清理旧 DaemonSet 切勿过早删除 aws-node,否则旧节点未迁移 Pod 会断网
零信任加固 引入 L7 FQDN 出网策略与 IMDS 拦截 借助 eBPF 能力,阻断内网横向探测与敏感外联

在存量集群上切换至 Cilium ENI 模式,需要建立完备的调度门禁并逐台滚动节点。如果需求仅限于三四层策略和防横向移动,可以优先考虑 CNI Chaining 模式;当需要彻底卸载 kube-proxy、统一多云网络策略或开启 FQDN 出网审计时,完全替换模式的运维收益才更显著。

相关文章#

参考资料#