AWS EKS 从 VPC CNI 迁移到 Cilium eBPF 独立 ENI 模式
目录
在公有云 Kubernetes 集群中,网络插件(CNI)是承载服务通信的最底层基础设施。默认情况下,AWS EKS 采用官方的 aws-vpc-cni。虽然它与 VPC 路由开箱集成,但在业务微服务高频交互、集群规模扩大或有更严苛的安全审计要求时,会有几个受限之处:
- 依赖 iptables 与 kube-proxy:随 Service 与 Pod 数量增长,内核中大量的 iptables 规则和连接跟踪(conntrack)带来额外的转发延迟与 CPU 损耗;
- 缺乏细粒度安全管控:原生
aws-vpc-cni早期默认不带网络策略,即便开启也以 L4 CIDR 为主,难以支持域名级(FQDN)出网控制或跨微服务探测拦截; - 网络可观测性不足:AWS VPC Flow Logs 存在 10–15 分钟延迟,且无法抓取同节点 Pod 间的内部通信;
- 防御 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。
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
选型优势:
- 0 封包损耗:数据包使用 AWS 原生路由网络(Native Routing),无额外 MTU 缩水与封包/拆包 CPU 开销;
- 100% 兼容 AWS Ingress:AWS Load Balancer Controller 可无缝探测并直接将流量路由至 Pod IP;
- 保留 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-proxyDaemonSet 打上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 分阶段落地:
- Terraform 准备:配置 Cilium Operator IRSA 角色与权限,将存量 EKS Addon 声明
preserve = true脱钩。 - GitOps 配置:编写 Cilium Helm Values,设置节点亲和性调度门禁,通过 ArgoCD 部署 Operator。
- 灰度置换节点池:先置换系统管理节点,再单台滚动业务 Worker 节点。
- 清理与观测:确认存量节点完全下线后,清理旧 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 等核心控制组件。
- 扩容系统 ASG 实例:将期望容量从 1 提升至 2。
- 观察新节点入网:
- 新节点启动后不会被打上
io.cilium/aws-node-enabled=true标签; aws-node和kube-proxy忽略该节点;cilium-agent和cilium-envoy自动在该节点拉起,并在 30 秒内达到1/1 Running。
- 新节点启动后不会被打上
- 验证 Cilium 状态:
kubectl -n kube-system exec -it ds/cilium -c cilium-agent -- cilium-dbg status # 确认 KubeProxyReplacement: True, IPAM Pool: OK, Controllers: Healthy - 平稳排空旧节点:
所有核心控制器自动漂移至新的 Cilium 节点,并通过 VPC Secondary IP 成功绑定网络。
kubectl cordon <OLD_SYSTEM_NODE> kubectl drain <OLD_SYSTEM_NODE> --ignore-daemonsets --delete-emptydir-data --force - 缩容并销毁旧实例:
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 出网审计时,完全替换模式的运维收益才更显著。
相关文章#
- K8s 网络策略与出站控制:云原生 CNI、Calico、出网代理和 Cilium 怎么选
- K3s 从 Flannel 换到 Cilium 之后冒出来的三个问题
- Oracle Cloud K3s 迁移到 Cilium:一次把网络、密钥和状态数据都翻出来的升级
- Cilium ClusterMesh 实战:连接两个 K3s 集群的跨云服务发现
参考资料#
- Cilium Documentation: AWS ENI IPAM:Cilium AWS ENI 模式配置与参数说明
- Cilium Documentation: Kubernetes Without kube-proxy:基于 eBPF 完全替换 kube-proxy 的配置与校验
- Cilium Documentation: DNS-Based Policies (
toFQDNs):域名级策略与规则 - Isovalent Blog: EKS BYOCNI and Cilium:在 Amazon EKS 上部署原生 Cilium 的最佳实践
- AWS Documentation: Amazon VPC CNI plugin:EKS VPC CNI 与 IAM 角色配置指南