Karpenter v1 API 设计解读:EC2NodeClass 稳定化、AMI 别名与标签体系演进

原创2026-09-15 22:43:491,626 阅读

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):

  1. Breaking(破坏性):需要在 v1 之后就无法再进行的 API 变更或移除;
  2. Stability(稳定性):修复 beta 阶段中"漏水"或存在竞态条件的行为,保障运维质量;
  3. 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 的变更包含两点:

  1. amiSelectorTerms 从可选变为必填;
  2. 新增一种 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 阶段积累的经验沉淀为四项"稳定化"承诺:

  1. 可观测:通过 Printer Columns(READY、ROLE)与按概念拆分的状态条件,让 EC2NodeClass 的就绪度对用户透明可见;
  2. 可预测:amiSelectorTerms 必填 + alias 别名机制,让"自动跟随最新 AMI"从隐式默认变为显式选择,生产环境可用固定版本获得确定性;
  3. 更安全:默认 httpPutResponseHopLimit: 1 关闭容器对 IMDS 的访问,配合 httpTokens: required 收窄凭据暴露面;
  4. 更规范:退役 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 中查看相关校验的测试用例。

登录后查看全文
karpenter-provider-aws