Karpenter v1 API 设计解读:EC2NodeClass 稳定化、AMI 别名与标签体系演进
Karpenter v1 API 设计解读:EC2NodeClass 稳定化、AMI 别名与标签体系演进
导读
本文以 karpenter-provider-aws 仓库中的 v1 API 设计文档 为骨架,系统讲解 Karpenter 从 v1beta1 走向 v1 稳定版的过程中,EC2NodeClass API 的关键变更设计:Printer Columns 与状态条件、强制要求 AMISelectorTerms 并引入 alias 别名机制、默认禁用容器对 IMDS 的访问,以及 karpenter.sh/managed-by 标签的退役与 eks:eks-cluster-name/eks:eks-cluster-arn 标签的引入。读完本文,你将理解这些变更背后的稳定性与安全动机,并能在当前仓库的源码与 CRD 中逐一找到它们的落地实现,为生产环境的 AMI 固定、IMDS 加固与基于标签的 IAM 权限收敛提供可操作的设计参考。
背景:为什么 Karpenter 需要一次 "v1" 级的 API 收敛
Karpenter 于 2023 年 10 月发布了其 API 与功能的 beta 版本,beta 的目的在于:在把 Karpenter 视为"功能完整"之前,先确定最终需要加入或修改的变更集合。设计文档将 v1 之前的必做变更划分为三类(见 designs/v1-api.md):
- Breaking(破坏性):需要在 v1 之后就无法再进行的 API 变更或移除;
- Stability(稳定性):修复 beta 阶段中"漏水"或存在竞态条件的行为,保障运维质量;
- Planned Deprecations(计划内弃用):清理项目此前计划好的弃用项。
围绕这三类标准,v1 的设计聚焦在四个主题上:EC2NodeClass 的可用性呈现(Printer Columns 与状态条件)、AMI 选择机制的安全化(强制 amiSelectorTerms 与 alias 别名)、容器对 IMDS 访问的默认收敛,以及标签体系的规范化。下文逐一展开。
说明:该设计文档定位为 RFC(Request for Comments),描述的是 v1 的目标形态;本文在阐述设计意图的同时,会以当前仓库 v1 代码(pkg/apis/v1)与 CRD 清单(pkg/apis/crds/karpenter.k8s.aws_ec2nodeclasses.yaml)作为落地证据,帮助读者把"设计"与"实现"对应起来。
EC2NodeClass API:从 v1beta1 到 v1 的完整形态
设计文档给出了一份接近完整规格的 v1 EC2NodeClass 示例,覆盖了 kubelet 配置、选择器、AMI、身份、存储、元数据与块设备映射等全部核心字段。下面的示例与文档保持一致(仅修正了原文中 detailedMonitoring 的排版笔误),作为后续讨论的公共上下文:
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
kubelet:
podsPerCore: 2
maxPods: 20
systemReserved:
cpu: 100m
memory: 100Mi
ephemeral-storage: 1Gi
kubeReserved:
cpu: 200m
memory: 100Mi
ephemeral-storage: 3Gi
evictionHard:
memory.available: 5%
nodefs.available: 10%
nodefs.inodesFree: 10%
evictionSoft:
memory.available: 500Mi
nodefs.available: 15%
nodefs.inodesFree: 15%
evictionSoftGracePeriod:
memory.available: 1m
nodefs.available: 1m30s
nodefs.inodesFree: 2m
evictionMaxPodGracePeriod: 60
imageGCHighThresholdPercent: 85
imageGCLowThresholdPercent: 80
cpuCFSQuota: true
clusterDNS: ["10.0.1.100"]
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: "${CLUSTER_NAME}"
- id: subnet-09fa4a0a8f233a921
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: "${CLUSTER_NAME}"
- name: my-security-group
- id: sg-063d7acfb4b06c82c
amiFamily: AL2023
amiSelectorTerms:
- alias: al2023@v20240625
- tags:
karpenter.sh/discovery: "${CLUSTER_NAME}"
- name: my-ami
- id: ami-123
role: "KarpenterNodeRole-${CLUSTER_NAME}"
instanceProfile: "KarpenterNodeInstanceProfile-${CLUSTER_NAME}"
userData: |
echo "Hello world"
tags:
team: team-a
app: team-a-app
instanceStorePolicy: RAID0
metadataOptions:
httpEndpoint: enabled
httpProtocolIPv6: disabled
httpPutResponseHopLimit: 1 # 收紧:禁止非 hostNetwork 容器访问 IMDS
httpTokens: required
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 100Gi
volumeType: gp3
iops: 10000
encrypted: true
kmsKeyID: "1234abcd-12ab-34cd-56ef-1234567890ab"
deleteOnTermination: true
throughput: 125
snapshotID: snap-0123456789
detailedMonitoring: true
对应的 status 由 Karpenter 回填解析结果,包括按可用区列出的 subnets、解析出的 securityGroups、带架构 requirements 的 amis、由 role 自动生成的 instanceProfile,以及一组状态条件(InstanceProfileReady、SubnetsReady、SecurityGroupsReady、AMIsReady、Ready):
status:
subnets:
- id: subnet-0a462d98193ff9fac
zone: us-east-2b
- id: subnet-0322dfafd76a609b6
zone: us-east-2c
# ... 其余子网按 zone 列出
securityGroups:
- id: sg-041513b454818610b
name: ClusterSharedNodeSecurityGroup
- id: sg-0286715698b894bca
name: ControlPlaneSecurityGroup-1AQ073TSAAPW
amis:
- id: ami-01234567890123456
name: custom-ami-amd64
requirements:
- key: kubernetes.io/arch
operator: In
values:
- amd64
- id: ami-01234567890123456
name: custom-ami-arm64
requirements:
- key: kubernetes.io/arch
operator: In
values:
- arm64
instanceProfile: "${CLUSTER_NAME}-0123456778901234567789"
conditions:
- lastTransitionTime: "2024-02-02T19:54:34Z"
status: "True"
type: InstanceProfileReady
- lastTransitionTime: "2024-02-02T19:54:34Z"
status: "True"
type: SubnetsReady
- lastTransitionTime: "2024-02-02T19:54:34Z"
status: "True"
type: SecurityGroupsReady
- lastTransitionTime: "2024-02-02T19:54:34Z"
status: "True"
type: AMIsReady
- lastTransitionTime: "2024-02-02T19:54:34Z"
status: "True"
type: Ready
从当前仓库的 pkg/apis/v1/ec2nodeclass.go 可以看到 v1 规格的完整字段定义(EC2NodeClassSpec),其中大量使用 +kubebuilder:validation:XValidation 注解以 CEL 规则在 API Server 侧做校验。几个值得注意的约束:
subnetSelectorTerms、securityGroupSelectorTerms、amiSelectorTerms均为必填(+required),且每个 term 至少包含tags/id/name/alias/ssmParameter中的一种匹配方式;id与tags互斥(CEL 规则!self.all(x, has(x.id) && has(x.tags)))。role与instanceProfile必须二选一(CEL 规则(has(self.role) && !has(self.instanceProfile)) || (!has(self.role) && has(self.instanceProfile))),设计上推荐使用role并让 Karpenter 代为创建托管实例配置文件。tags字段中禁止使用eks:eks-cluster-name、kubernetes.io/cluster/、karpenter.sh/nodepool、karpenter.sh/nodeclaim、karpenter.k8s.aws/ec2nodeclass等受限标签键(见 pkg/apis/v1/labels.go 中的RestrictedTagPatterns与 pkg/apis/v1/ec2nodeclass.go 中的 CEL 注解)。
关于 kubelet 配置,v1 中 spec.kubelet 是一个保留未知字段的开放式映射(KubeletConfiguration map<a href="https://link.gitcode.com/i/3dd57d6cf9ab06320ef994f2cdb0d3f7" target="_blank">string]apiextensionsv1.JSON,见 [pkg/apis/v1/kubeletconfiguration.go),CRD schema 无法对其实施结构性校验,而是由控制器的 ValidateKubeletConfig 完成。Karpenter 会从中提取与调度、启动引导相关的字段(maxPods、podsPerCore、kubeReserved、systemReserved、evictionHard 等,见 ParsedKubeletConfig),其余字段原样透传到 UserData。
Printer Columns 与状态条件:让 EC2NodeClass 的"就绪度"一目了然
类别:Stability(稳定性)、Breaking(破坏性)
当前形态与目标形态
beta 阶段 kubectl get ec2nodeclasses 只能看到名称与年龄,无法判断该 EC2NodeClass 是否已解析完成、能否被 NodePool 使用:
➜ karpenter git:(main) ✗ k get ec2nodeclasses -o wide
NAME AGE
default 2d8h
v1 的目标形态是增加 READY 与 ROLE 两列:
➜ karpenter git:(main) ✗ k get ec2nodeclasses -o wide
NAME READY AGE ROLE
default True 2d8h KarpenterNodeRole-test-cluster
列的定义
- 标准列:
NAME、READY、AGE。READY来自 EC2NodeClass 的状态条件,向用户直观呈现"该 EC2NodeClass 是否已解析全部数据、可被 NodePool 使用"。 - Wide 列(
-o wide):ROLE。作为最佳实践,官方推荐用户使用 Node role 并让 Karpenter 代为创建托管实例配置文件,因此需要把 role 直接暴露出来。
这一设计已在当前仓库的 CRD 与类型定义中落地:kubebuilder:printcolumn 注解定义了 Ready、Age(标准列)与 Role(priority=1,即仅 -o wide 时显示)三列(见 pkg/apis/v1/ec2nodeclass.go),生成的 CRD 清单中对应的 additionalPrinterColumns 也在 pkg/apis/crds/karpenter.k8s.aws_ec2nodeclasses.yaml 中可见。
状态条件体系
设计文档明确:v1 发布时完整的状态条件类型集合不在本文档范围内(将由 Karpenter 的 Observability 设计另行细化),v1 最小集合至少包含:
- 一个
Ready条件,用于调度时判断 EC2NodeClass 是否可被 NodePool 使用; - 一组按"实例启动所需的每个概念"拆分的条件:InstanceProfile 是否解析、Subnet 是否解析、Security Group 是否解析、AMI 是否解析等。
当前仓库的实现比文档的最小集合更进一步:EC2NodeClassStatusConditions 定义了 AMIsReady、SubnetsReady、SecurityGroupsReady、InstanceProfileReady、ValidationSucceeded、PlacementGroupReady(以及容量预留特性开启时的 CapacityReservationsReady),并通过 status.NewReadyConditions(...) 聚合出聚合后的 Ready 条件(见 pkg/apis/v1/ec2nodeclass_status.go 与 pkg/apis/v1/ec2nodeclass_status.go)。状态中回填的 subnets、securityGroups、amis、instanceProfile 结构也定义在同一文件中(Subnet、SecurityGroup、AMI 等类型)。
强制 AMISelectorTerms:用 alias 替代"隐式自动升级"
类别:Stability(稳定性)、Breaking(破坏性)
问题:隐式 AMI 自动升级在生产环境的隐患
当用户指定 amiFamily 却不提供任何 amiSelectorTerms 时,Karpenter 会"隐式地"在对应 EKS 优化镜像家族发布新版本时自动升级 AMI,已有节点会因漂移(drift)被替换到新版本。设计文档指出:
- 这在预生产环境尚可接受(方便用最新版做测试),但在生产环境风险极高;
- 这与 eksctl、MNG 的行为不同——它们在首次创建节点组时取最新 AMI,但之后会固定住;
- Karpenter 官方建议在生产环境固定(pin)AMI、先测试再推广,但"未配置选择器即自动升级"的默认行为仍容易让用户措手不及。
v1 的解法:amiSelectorTerms 必填 + 新增 alias term
v1 的变更包含两点:
amiSelectorTerms从可选变为必填;- 新增一种 term 类型
alias,用于选择 EKS 优化 AMI。每个 alias 由"AMI 家族 + 版本"组成(格式family@version):- 版本设为
latest:继续获得自动升级(对应漂移行为); - 版本固定为某个具体版本(如
al2023@v20240625):将 AMI 钉死在指定版本,获得可预测的发布行为。
- 版本设为
这一点在文档的 EC2NodeClass 示例中直接体现:amiSelectorTerms 同时给出了 alias、tags、name、id 四种匹配方式(alias: al2023@v20240625 表明该文档撰写时 AL2023 家族的某个具体版本)。
源码级印证:alias 的解析与校验
当前仓库对 alias 的支撑非常完整,可以从三处确认:
- 类型定义:
AMISelectorTerm包含Alias、Tags、ID、Name、Owner、SSMParameter六个可选匹配字段(见 pkg/apis/v1/ec2nodeclass.go)。注释明确:alias 格式为family@version,合法家族为al2、al2023、bottlerocket、windows2019、windows2022、windows2025;版本可固定(如al2023@v20240625、bottlerocket@v1.10.0)或设为latest;Windows 家族不支持版本固定,只能使用latest。 - CEL 校验:CRD 对 alias 施加了多条规则——格式必须匹配
^<a href="https://link.gitcode.com/i/d820d5e5eb0cf4083bb0ec8f396c05b3" target="_blank">a-zA-Z0-9]+@.+$、家族必须属于上述列表、Windows 家族版本只能是latest、alias 与id/tags/name/owner互斥、且 alias term 只能单独出现(!(self.exists(x, has(x.alias)) && self.size() != 1),见 [pkg/apis/crds/karpenter.k8s.aws_ec2nodeclasses.yaml)。 - 运行时解析:
EC2NodeClass.AMIFamily()按优先级解析家族——先取spec.amiFamily,若未设置则从 alias 中推导家族(Alias()方法把 alias 拆分为Family与Version,见 pkg/apis/v1/ec2nodeclass.go)。设计文档中的示例正是"不写amiFamily、只写 alias"或"alias 与对应家族一致"两种合法形态。此外,EC2NodeClass 哈希计算会使用动态解析后的AMIFamily()而不是字面值,从而避免"把amiFamily从 nil 改成AL2023配合al2023@latest"这类场景误触发漂移(见 pkg/apis/v1/ec2nodeclass.go)。
实践建议
- 生产环境:使用固定版本的 alias(如
alias: al2023@v20240625),配合 Karpenter 的 AMI 测试/灰度发布流程,先验证再推广;需要持续自动跟进时再显式使用alias: al2023@latest——这是文档中明确"不推荐用于生产"的用法。 - 自定义 AMI:使用
tags、name、id、owner、ssmParameter等其他 term 类型(ssmParameter支持从 SSM 参数动态读取镜像 ID)。
默认禁用容器对 IMDS 的访问:默认 HopLimit = 1
类别:Stability(稳定性)、Breaking(破坏性)
安全背景
httpPutResponseHopLimit 是节点启动时配置的实例元数据选项之一,决定一个 PUT 请求在被 IMDS 拒绝前可以经过多少跳。对于 Kubernetes Pod 而言:
- 处于独立网络命名空间的 Pod(即未使用
hostNetwork: true)若要访问 IMDS,通常需要 HopLimit 为 2; - 放开 Pod 访问 IMDS 本身就是安全风险:一旦 Pod 拿到 IMDS 的 token,就可以构造请求获得与实例配置文件同等的权限——而这个实例配置文件正是驱动 kubelet 在集群中执行调用的身份。
v1 的变更
v1 将默认的 httpPutResponseHopLimit 收紧为 1,即默认禁止容器访问 IMDS,从而降低用户暴露在凭据窃取风险中的面。设计文档特别强调:该新默认值不影响已经部署的 EC2NodeClass,只作用于新建的 EC2NodeClass(因为这是默认值层面的变更,旧对象已有显式/既有取值)。
示例中的注释给出了明确意图:
metadataOptions:
httpEndpoint: enabled
httpProtocolIPv6: disabled
httpPutResponseHopLimit: 1 # 收紧:禁止非 hostNetwork 容器访问 IMDS
httpTokens: required
源码级印证
MetadataOptions 的默认值在 pkg/apis/v1/ec2nodeclass.go 中有完整定义,并通过 +kubebuilder:default 注入 CRD:
httpEndpoint默认enabled;httpProtocolIPv6默认disabled;httpPutResponseHopLimit默认1(取值范围 1~64);httpTokens默认required(required时所有元数据请求必须携带签名 token,IAM 角色凭据始终返回 2.0 版本;optional时允许无 token 获取 1.0 版凭据)。
整体默认值在字段注释中总结为:httpEndpoint enabled + httpProtocolIPv6 disabled + httpPutResponseHopLimit 1 + httpTokens required。这正是文档所设计的"默认最小暴露面"。若你的工作负载确实需要容器访问 IMDS(例如某些需要查询实例元数据的中间件),可以显式将 httpPutResponseHopLimit 设为 2 或更高——但这属于主动放宽,而非默认行为。
标签体系演进:karpenter.sh/managed-by 退役,改用 eks: 标签
类别:Planned Deprecations(计划内弃用)、Breaking(破坏性)
为什么弃用 karpenter.sh/managed-by
karpenter.sh/managed-by 标签在 v0.28.0 引入,用于 Karpenter 迁移到 NodeClaim(当时叫 Machine)的过程中,在 EC2 实例上标记"该实例由哪个集群管理",值为集群名。NodeClaim 迁移完成后,这个标签不再有存在价值,因此 v1 将移除它。
设计文档还解释了它的历史用途与局限:该标签曾被用于 ABAC 场景下收敛 Pod Identity 策略——因为它把集群名放在标签值里,而 kubernetes.io/cluster/<cluster-name> 是把集群名放在标签键里;Session tags 无法作用于标签键,所以需要一个可以推荐给用户用于 ABAC Pod Identity 策略的标签。
v1 的替代方案
v1 起,Karpenter 改用两条 eks: 前缀标签:
eks:eks-cluster-name: <cluster-name>:用于给实例、卷、主 ENI 等资源打标与做权限范围界定;eks:eks-cluster-arn: <cluster-arn>:用于给 Karpenter 创建的实例配置文件打标与做权限范围界定。
源码级印证
当前仓库已全面切换到 eks:eks-cluster-name 体系:
- 常量
EKSClusterNameTagKey = "eks:eks-cluster-name"定义于 pkg/apis/v1/labels.go; - 实例打标控制器
nodeclaim/tagging在实例上写入三枚标签:Name(节点名)、karpenter.sh/nodeclaim(NodeClaim 名)、eks:eks-cluster-name(集群名),见 pkg/controllers/nodeclaim/tagging/controller.go; - 实例配置文件打标逻辑
InstanceProfileTags会合并kubernetes.io/cluster/<cluster-name>: owned、eks:eks-cluster-name、karpenter.k8s.aws/ec2nodeclass、topology.k8s.aws/region等标签(见 pkg/apis/v1/ec2nodeclass.go)——注意这里同时出现了eks:eks-cluster-name与kubernetes.io/cluster/,后者由 Karpenter 内部统一管理(EC2NodeClass 的spec.tags被 CEL 规则禁止使用这些受限键)。 - 作为配套,CRD 的
spec.tags校验规则禁止用户自定义这些受限标签键(见 pkg/apis/v1/ec2nodeclass.go),防止用户标签与 Karpenter 内部标签冲突。
实践建议
升级到 v1 后,若你此前依赖 karpenter.sh/managed-by 做 ABAC 策略,应迁移为基于 eks:eks-cluster-name(实例、卷、ENI)与 eks:eks-cluster-arn(实例配置文件)的标签条件;集群内由 Karpenter 自动打上的 karpenter.sh/nodeclaim 等标签也可用于细粒度权限界定。
与其他设计文档的关系
v1 API 设计是 Karpenter 从 v1beta1 走向稳定版的收官设计,与仓库内其他设计文档形成完整演进脉络:
- designs/v1beta1-api.md 定义了 v1beta1 的三种 Kind 边界(
NodePool管调度兼容性、EC2NodeClass管云厂商节点配置、NodeClaim管单节点生命周期),v1 文档正是在此基础上对EC2NodeClass做最后收敛; - designs/v1beta1-full-changelist.md 逐项列出了 v1beta1 的完整变更清单;
- designs/v1-roadmap.md 记录了 v1 的整体路线图;
- 若需了解从 v1alpha5 的
Provisioner/AWSNodeTemplate迁移到 v1beta1 的三种路径(Periodic Rolling with Drift、Forced Deletion、Manual Rolling),可参阅 designs/v1beta1-api.md 的 Migration 章节——这些迁移思路同样适用于理解 v1 的 API 演进哲学。
总结
Karpenter v1 API 的这份设计文档,本质上是把 beta 阶段积累的经验沉淀为四项"稳定化"承诺:
- 可观测:通过 Printer Columns(
READY、ROLE)与按概念拆分的状态条件,让 EC2NodeClass 的就绪度对用户透明可见; - 可预测:
amiSelectorTerms必填 +alias别名机制,让"自动跟随最新 AMI"从隐式默认变为显式选择,生产环境可用固定版本获得确定性; - 更安全:默认
httpPutResponseHopLimit: 1关闭容器对 IMDS 的访问,配合httpTokens: required收窄凭据暴露面; - 更规范:退役
karpenter.sh/managed-by,全面切换到eks:eks-cluster-name与eks:eks-cluster-arn标签体系,为基于标签的 ABAC 权限收敛提供稳定锚点。
这些设计在当前仓库的 pkg/apis/v1/ec2nodeclass.go、pkg/apis/v1/ec2nodeclass_status.go、pkg/apis/v1/labels.go、pkg/apis/v1/kubeletconfiguration.go 及生成的 CRD pkg/apis/crds/karpenter.k8s.aws_ec2nodeclasses.yaml 中均有完整落地,读者可以直接对照阅读,也可以在 pkg/apis/v1/ec2nodeclass_validation_cel_test.go 中查看相关校验的测试用例。