先说明一下,Kubernetes 1.30在本文写作时尚未正式发布(预计2024年4月发布),目前只有alpha/beta版本。本文基于K8s 1.30的已公布新特性和KEP来写,一些API可能会在正式版中调整。生产环境建议等正式版发布后再升级。

Kubernetes每个版本都会带来一些新特性和改进,1.30也不例外。虽然1.30不是一个颠覆性的版本,但有一些新特性和废弃的API,给了我们重构老旧代码的好机会。

很多项目里的K8s相关代码,因为历史原因,写得比较啰嗦、复杂、难以维护,用的还是旧版本的API和模式。K8s 1.30的新特性,加上一些最佳实践,可以让我们把这些烂代码重构成优雅代码。

这篇文章就来聊聊,如何基于K8s 1.30+的新特性和最佳实践,重构K8s相关的代码。

K8s 1.30的重要新特性

在讲重构之前,先快速了解一下K8s 1.30的重要新特性和变化。

结构性特性(Structured Authorization Configuration)GA

结构性授权配置在1.29引入了beta,1.30正式GA了。这个特性允许你用结构化的配置文件来定义授权策略,支持CEL(Common Expression Language)表达式,比传统的ABAC更灵活、更强大。

传统的K8s授权方式是RBAC,虽然功能强大,但对于复杂的授权策略,写起来很麻烦,而且不够灵活。结构性授权配置可以用CEL表达式来定义复杂的授权规则,比如"只有在工作时间内,且来自公司IP的请求,才能修改生产环境的资源"。

CEL for Admission Control GA

CEL准入控制在1.30也正式GA了。ValidatingAdmissionPolicy(VAP)允许你用CEL表达式写准入控制规则,不需要写Webhook。

传统的准入控制需要写Webhook,部署一个服务,配置证书,很麻烦。用VAP,直接写YAML定义规则,K8s自动执行,简单高效。比如"所有Pod必须设置resources.limits"、"禁止使用latest标签"、"Service的type不能是LoadBalancer除非有特定注解",这些规则都可以用VAP轻松实现。

Pod调度就绪(Pod Scheduling Readiness)GA

Pod调度就绪特性在1.30 GA了。它允许你标记一个Pod为"未就绪调度",K8s调度器会跳过它,直到你把它标记为就绪。

这个特性对于批量任务、作业调度很有用。比如你有一批Job要提交,但想等所有Job都提交完了再一起调度,就可以先把它们标记为未就绪,全部提交完之后再统一标记为就绪。

侧车容器(Sidecar Containers)beta

侧车容器在1.29引入了alpha,1.30升级到beta。它允许你在Pod的init containers中定义一个sidecar容器,这个容器会在整个Pod生命周期中运行,和主容器一起启动,一起停止。

传统的sidecar容器是放在普通containers里的,但这样会有一些问题,比如Job完成后sidecar容器还在运行导致Job不结束。用init containers里的sidecar容器,K8s会正确管理它的生命周期,解决了这些问题。

镜像拉取策略改进

K8s 1.30改进了镜像拉取策略,新增了IfNotPresent的一些优化,还有镜像拉取的可观测性提升。kubelet的镜像拉取日志更详细了,方便排查镜像拉取问题。

一些API的废弃和移除

K8s 1.30移除了一些长期废弃的API,比如flowcontrol.apiserver.k8s.io/v1beta2的FlowSchema和PriorityLevelConfiguration,还有一些旧版本的CRD相关API。升级到1.30之前,要检查代码里有没有用这些废弃的API,及时迁移。

kubectl的改进

kubectl在1.30也有一些改进,比如kubectl apply的性能优化,kubectl debug的增强,kubectl events的改进等。这些改进能提升日常操作的效率。

重构案例一:用ValidatingAdmissionPolicy替代Webhook

第一个重构案例,用VAP替代传统的准入控制Webhook。

这是一个常见的准入控制Webhook代码,用Go写的,用来校验Pod必须设置resources.limits:

// webhook.go
func (v *Validator) Handle(ctx context.Context, req admission.Request) admission.Response {
    pod := &corev1.Pod{}
    if err := json.Unmarshal(req.Object.Raw, pod); err != nil {
        return admission.Errored(http.StatusBadRequest, err)
    }

    for _, container := range pod.Spec.Containers {
        if container.Resources.Limits == nil || container.Resources.Limits.Cpu().IsZero() {
            return admission.Denied(fmt.Sprintf("容器 %s 必须设置 CPU limits", container.Name))
        }
        if container.Resources.Limits.Memory().IsZero() {
            return admission.Denied(fmt.Sprintf("容器 %s 必须设置内存 limits", container.Name))
        }
    }

    for _, container := range pod.Spec.InitContainers {
        if container.Resources.Limits == nil || container.Resources.Limits.Cpu().IsZero() {
            return admission.Denied(fmt.Sprintf("init容器 %s 必须设置 CPU limits", container.Name))
        }
    }

    return admission.Allowed("校验通过")
}

除了这段代码,还需要:

  • 写HTTP服务器,处理TLS
  • 部署Webhook服务
  • 配置ValidatingWebhookConfiguration
  • 管理证书(cert-manager或者手动签发)
  • 维护Webhook服务的可用性和扩展性

整个方案很复杂,代码量大,运维成本高。

用K8s 1.30的ValidatingAdmissionPolicy重构:

# policy.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: require-resource-limits
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups: [""]
      apiVersions: ["v1"]
      resources: ["pods"]
      operations: ["CREATE", "UPDATE"]
  validations:
    - expression: "object.spec.containers.all(c, has(c.resources.limits) && has(c.resources.limits.cpu) && has(c.resources.limits.memory))"
      message: "所有容器必须设置 CPU 和内存 limits"
    - expression: "object.spec.initContainers.all(c, !has(c.resources.limits) || (has(c.resources.limits.cpu) && has(c.resources.limits.memory)))"
      message: "init容器如果设置了resources,必须包含 CPU 和内存 limits"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: require-resource-limits-binding
spec:
  policyName: require-resource-limits
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchExpressions:
        - key: kubernetes.io/metadata.name
          operator: NotIn
          values: ["kube-system"]

重构后的改进:

  1. 不需要写Go代码,不需要部署Webhook服务,不需要管理证书,大大降低了复杂度和运维成本。
  2. 用CEL表达式定义规则,简洁明了,可读性强。
  3. 性能更好,VAP在API Server内部执行,没有网络开销,比Webhook快。
  4. 更容易维护和版本控制,规则就是YAML文件,可以用Git管理。

当然,VAP也有局限性,比如不能做复杂的外部调用(查数据库、调外部API),对于这些场景还是需要Webhook。但对于大多数简单的校验规则,VAP完全够用了。

重构案例二:用结构性授权配置替代复杂的RBAC

第二个重构案例,用结构性授权配置替代复杂的RBAC规则。

很多项目里,RBAC规则写得很复杂,大量的Role和RoleBinding,维护起来很麻烦。比如有一个需求:开发人员只能在工作时间(9:00-18:00)修改dev命名空间的Deployment,其他时间只能读。

用RBAC实现这个需求几乎不可能,因为RBAC不支持时间条件。只能用Webhook来做,很复杂。

用K8s 1.30的结构性授权配置实现:

# authorization-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AuthorizationConfiguration
authorizers:
  - type: RBAC
  - type: Webhook
    webhook:
      subjectAccessReviewVersion: v1
      # 传统的Webhook授权
  - type: CEL
    cel:
      - expression: |
          // 开发人员在工作时间可以修改dev命名空间的Deployment
          user.groups.exists(g, g == 'developers') &&
          request.namespace == 'dev' &&
          request.resource.resource == 'deployments' &&
          request.verb in ['create', 'update', 'patch'] &&
          time.now().getHours('Asia/Shanghai') >= 9 &&
          time.now().getHours('Asia/Shanghai') < 18
        state:
          # 可以定义状态变量
      - expression: |
          // 其他情况拒绝
          false

不过需要说明的是,结构性授权配置目前主要是和Webhook配合使用,CEL授权的能力还在发展中。对于大多数场景,RBAC还是主力,但对于需要复杂条件(时间、来源IP、请求属性等)的授权场景,结构性授权配置是一个很好的补充。

重构的思路是:把简单的、静态的权限用RBAC管理,把复杂的、动态的权限用结构性授权配置管理,两者结合,既保持了RBAC的简单高效,又获得了复杂条件的灵活性。

重构案例三:用Sidecar容器重构Job的sidecar

第三个重构案例,用K8s 1.30的Sidecar Containers特性,重构Job中的sidecar容器。

这是一个常见的Job配置,主容器跑数据处理,sidecar容器跑日志收集(比如fluent-bit):

apiVersion: batch/v1
kind: Job
metadata:
  name: data-processor
spec:
  template:
    spec:
      containers:
        - name: processor
          image: data-processor:latest
          # 主容器处理完数据就退出
        - name: log-collector
          image: fluent-bit:latest
          # sidecar收集日志,不会主动退出
      restartPolicy: Never

这个配置的问题:主容器处理完数据退出了,但sidecar容器还在运行,导致Job不会完成,一直处于Running状态。需要写额外的逻辑,让主容器完成后通知sidecar退出,很麻烦。

用K8s 1.30的Sidecar Containers重构:

apiVersion: batch/v1
kind: Job
metadata:
  name: data-processor
spec:
  template:
    spec:
      initContainers:
        - name: log-collector
          image: fluent-bit:latest
          # 标记为sidecar容器,会在整个Pod生命周期运行
          # 主容器完成后,sidecar会被自动终止
          restartPolicy: Always
      containers:
        - name: processor
          image: data-processor:latest
      restartPolicy: Never

注意:在K8s 1.30中,sidecar容器是定义在initContainers里的,设置restartPolicy: Always。K8s会把它当作sidecar来管理,在主容器启动之前启动,在所有主容器完成之后终止。

重构后的改进:

  1. 不需要写额外的逻辑来终止sidecar,K8s自动管理生命周期。
  2. Job能正确完成,主容器退出后sidecar也会被终止。
  3. sidecar容器的启动顺序有保证,在主容器之前启动,确保日志收集从一开始就生效。
  4. 语义更清晰,一看就知道这个容器是sidecar。

重构案例四:用Pod Scheduling Readiness重构批量调度

第四个重构案例,用Pod Scheduling Readiness重构批量任务的调度。

场景:有100个训练任务要提交,想等所有任务都提交完了再一起调度,避免部分任务先占用资源,导致后面的任务资源不够。

传统的做法:用一个调度器扩展,或者用优先级队列,很复杂。或者先提交到一个暂停的命名空间,等全部提交完再取消暂停,也很麻烦。

用K8s 1.30的Pod Scheduling Readiness重构:

# 提交任务时,标记为未就绪调度
apiVersion: v1
kind: Pod
metadata:
  name: training-task-1
  labels:
    scheduling-ready: "false"
spec:
  schedulingGates:
    - name: scheduling-ready
  containers:
    - name: trainer
      image: training:latest

所有100个任务都提交之后,用一个命令统一移除schedulingGates,让它们都可以被调度:

# 移除所有任务的调度门控
kubectl label pods -l app=training-task scheduling-ready- --all
# 或者用patch移除schedulingGates
kubectl get pods -l app=training-task -o json | jq '.items[].spec.schedulingGates = null' | kubectl apply -f -

重构后的改进:

  1. 不需要额外的调度器或复杂的逻辑,K8s原生支持。
  2. 所有任务同时进入调度队列,调度器能做全局最优的资源分配。
  3. 可以灵活控制什么时候开始调度,比如等资源准备好、等数据加载完、等所有任务都提交完。

重构案例五:清理废弃API

第五个重构案例,清理K8s 1.30中废弃和移除的API。

K8s 1.30移除了一些长期废弃的API,如果你的代码里还在用,升级到1.30之后会出问题。常见的需要清理的API:

  1. flowcontrol.apiserver.k8s.io/v1beta2的FlowSchema和PriorityLevelConfiguration,要迁移到v1或v1beta3。

旧代码:

apiVersion: flowcontrol.apiserver.k8s.io/v1beta2
kind: FlowSchema
metadata:
  name: my-flow-schema
spec:
  # ...

新代码:

apiVersion: flowcontrol.apiserver.k8s.io/v1
kind: FlowSchema
metadata:
  name: my-flow-schema
spec:
  # ...
  1. apiextensions.k8s.io/v1beta1的CRD,要迁移到v1。v1beta1在很久以前就废弃了,1.30可能会有更严格的要求。
  1. 一些旧版本的RBAC API,比如rbac.authorization.k8s.io/v1beta1,要迁移到v1。
  1. client-go的旧版本,如果你用Go写K8s相关代码,要升级client-go到支持1.30的版本,清理掉使用废弃API的代码。

清理废弃API的步骤:

  1. 用kubectl api-resources检查集群里有没有用废弃API的资源。
  2. 用kubectl find检查代码和YAML文件里有没有废弃API。
  3. 逐个迁移到新的API版本,测试验证。
  4. 升级集群到1.30。

重构案例六:用kubectl新特性优化运维脚本

第六个重构案例,用K8s 1.30的kubectl新特性,优化运维脚本。

很多项目里有大量的运维脚本,用kubectl做各种操作,代码写得很啰嗦,效率也不高。K8s 1.30的kubectl改进,能让这些脚本更简洁高效。

比如,用kubectl events替代传统的kubectl get events:

旧脚本:

# 查看Pod的事件,需要排序和过滤
kubectl get events --field-selector involvedObject.name=my-pod --sort-by=.lastTimestamp

新脚本(kubectl events是1.30增强的):

# kubectl events专门用来查看事件,更友好
kubectl events --for pod/my-pod

比如,用kubectl debug的增强功能调试Pod:

旧方式:需要手动创建调试容器,配置网络和PID命名空间,很麻烦。

新方式(kubectl debug在1.30增强了):

# 直接调试运行中的Pod,自动创建调试容器
kubectl debug my-pod -it --image=busybox --target=app-container

# 调试节点
kubectl debug node/my-node -it --image=busybox

# 复制Pod进行调试,不影响原Pod
kubectl debug my-pod -it --copy-to=my-pod-debug --container=debug --image=busybox

比如,用kubectl apply的性能优化,批量应用大量YAML文件:

旧方式:一个一个apply,慢。 新方式:kubectl apply在1.30优化了批量处理性能,大量YAML文件apply更快。

重构的最佳实践

基于K8s 1.30重构代码,有一些最佳实践。

第一,先升级测试环境,再升级生产。不要直接在生产环境升级到1.30,先在测试环境验证所有功能正常,特别是用了新特性和废弃API的部分。

第二,渐进式重构,不要一次性全改。K8s的新特性和旧代码是兼容的,可以一个模块一个模块地重构,先重构最复杂、最需要改进的部分。

第三,充分测试。重构可能会引入bug,特别是准入控制、授权、调度这些核心功能,重构之后要充分测试,确保功能正常,没有安全漏洞。

第四,关注废弃API。升级之前,一定要检查所有代码和YAML文件里有没有用废弃API,及时迁移。K8s每个版本都会废弃一些API,不及时迁移,升级后会出问题。

第五,文档和培训。新特性需要团队成员学习和适应。重构之后,要更新文档,组织培训,确保团队成员理解新的代码和工作方式。

第六,保留回滚方案。重构之后,如果出问题,要能快速回滚。用Git管理代码,用蓝绿部署或金丝雀发布,确保重构的风险可控。

K8s代码质量的通用建议

除了K8s 1.30的新特性,还有一些通用的K8s代码质量建议,适用于所有版本。

第一,用Helm或Kustomize管理YAML。不要手写大量重复的YAML,用模板工具管理,减少重复,提高可维护性。

第二,用命名空间和标签做隔离和组织。合理使用命名空间隔离环境和团队,用标签做资源分类和选择,不要用名称来区分。

第三,设置合理的resources和limits。所有Pod都要设置requests和limits,避免资源争抢和OOM。

第四,用探针做健康检查。所有服务都要配置livenessProbe和readinessProbe,确保K8s能正确管理Pod的生命周期。

第五,用ConfigMap和Secret管理配置。不要把配置硬编码在镜像里,用ConfigMap管理非敏感配置,用Secret管理敏感配置。

第六,做好可观测性。给所有服务配置日志、指标、链路追踪,方便排查问题。

第七,用GitOps管理集群。用ArgoCD或Flux等GitOps工具,把集群状态存在Git里,通过Git提交来管理集群,提高可追溯性和可恢复性。

写在最后

Kubernetes 1.30虽然不是一个颠覆性的版本,但它带来的新特性和改进,给了我们重构老旧代码的好机会。ValidatingAdmissionPolicy、Sidecar Containers、Pod Scheduling Readiness、结构性授权配置等新特性,能让我们的代码更简洁、更优雅、更易维护。

但重构不是目的,提升代码质量和可维护性才是目的。不要为了用新特性而用新特性,要根据实际情况,选择合适的特性来重构。有些旧代码虽然老,但如果运行稳定、易于维护,也不一定需要重构。

Kubernetes在快速发展,每个版本都有新特性和改进。作为开发者,要保持学习,跟上技术的发展,用最新的、最好的方式来写代码。但也不要盲目追新,要根据项目的实际情况,选择合适的技术和方案。

希望这篇文章能帮你更好地理解K8s 1.30的新特性,并用它们来提升你的代码质量。有什么问题欢迎交流。