这篇文章已发表超过一年。较旧的文章可能包含过时内容。请检查页面中的信息自发布以来是否已失效。

使用准入控制器在运行时检测容器漂移

Introductory illustration

插图作者:Munire Aireti

在 Box,我们使用 Kubernetes (K8s) 来管理数百个微服务,这些微服务使 Box 能够以 PB 规模流传输数据。在部署过程中,我们运行 kube-applier 作为 GitOps 工作流的一部分,进行声明式配置和自动化部署。开发者将其 K8s 应用清单声明到一个 Git 仓库中,在任何更改合并并应用于我们的 K8s 集群之前,都需要通过代码审查和自动化检查。然而,通过 kubectl exec 和其他类似命令,开发者能够直接与正在运行的容器交互,并改变它们部署后的状态。这种交互可能会破坏我们在 CI/CD 流水线中强制执行的变更控制和代码审查流程。此外,这还可能导致受影响的容器在生产环境中长期接收流量。

为了解决这个问题,我们开发了自己的 K8s 组件,称为 kube-exec-controller,以及相应的 kubectl 插件。它们协同工作,检测和终止可能已变异的容器(由交互式 kubectl 命令引起),并将交互事件直接暴露给目标 Pod,以提高可见性。

交互式 kubectl 命令的准入控制

请求发送到 K8s 后,需要由 API server 进行身份验证和授权才能继续。此外,K8s 还有一个单独的保护层,称为准入控制器,它可以在对象持久化到 etcd 之前拦截请求。API server 二进制文件中有各种预定义的准入控制(例如,用于强制执行每个命名空间的硬性资源使用限制的 ResourceQuota)。此外,还有两种动态准入控制,分别名为MutatingAdmissionWebhookValidatingAdmissionWebhook,分别用于修改或验证 K8s 请求。我们采用了后者来检测交互式 kubectl 命令导致的运行时容器漂移。整个过程可分为以下三个步骤详细说明。

1. 准入交互式 kubectl 命令请求

首先,我们需要启用一个验证 webhook,将符合条件的请求发送到 kube-exec-controller。为了添加专门适用于交互式 kubectl 命令的新验证机制,我们将 webhook 的规则配置为资源为 [pods/exec, pods/attach],操作为 CONNECT。这些规则告诉集群的 API server,所有 execattach 请求都应受我们的准入控制 webhook 管理。在我们配置的 ValidatingAdmissionWebhook 中,我们在 clientConfig 字段下指定了 service 引用(也可以替换为给出 webhook 位置的 url)和 caBundle,以允许验证其 X.509 证书。

以下是我们的 ValidatingWebhookConfiguration 对象示例:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: example-validating-webhook-config
webhooks:
  - name: validate-pod-interaction.example.com
    sideEffects: None
    rules:
      - apiGroups: ["*"]
        apiVersions: ["*"]
        operations: ["CONNECT"]
        resources: ["pods/exec", "pods/attach"]
    failurePolicy: Fail
    clientConfig:
      service:
        # reference to kube-exec-controller service deployed inside the K8s cluster
        name: example-service
        namespace: kube-exec-controller
        path: "/admit-pod-interaction"
      caBundle: "{{VALUE}}" # PEM encoded CA bundle to validate kube-exec-controller's certificate
    admissionReviewVersions: ["v1", "v1beta1"]

2. 标记包含可能已变异容器的目标 Pod

一旦接收到 kubectl exec 请求,kube-exec-controller 会内部记录并标记关联的 Pod。添加的标签意味着我们不仅可以查询所有受影响的 Pod,还可以在控制器服务重启时,使安全机制能够检索之前识别的 Pod。

准入控制过程无法在其准入响应中直接修改目标对象。这是因为 pods/exec 请求是针对 Pod API 的子资源,并且该子资源的 API Kind 是 PodExecOptions。因此,kube-exec-controller 中有一个独立的进程异步地修补标签。准入控制始终允许 exec 请求,然后作为 K8s API 的客户端来标记目标 Pod 并记录相关事件。开发者可以使用 kubectl 或类似工具检查其 Pod 是否受影响。例如:

$ kubectl get pod --show-labels
NAME      READY  STATUS   RESTARTS  AGE  LABELS
test-pod  1/1    Running  0         2s   box.com/podInitialInteractionTimestamp=1632524400,box.com/podInteractorUsername=username-1,box.com/podTTLDuration=1h0m0s

$ kubectl describe pod test-pod
...
Events:
Type       Reason            Age     From                            Message
----       ------            ----    ----                            -------
Warning    PodInteraction    5s      admission-controller-service    Pod was interacted with 'kubectl exec' command by user 'username-1' initially at time 2021-09-24 16:00:00 -0800 PST
Warning    PodInteraction    5s      admission-controller-service    Pod will be evicted at time 2021-09-24 17:00:00 -0800 PST (in about 1h0m0s).

3. 在预定义时间后逐出目标 Pod

如您在上述事件消息中看到,受影响的 Pod 并不会立即被逐出。有时,开发者可能出于调试某些线上问题的需要,必须进入正在运行的容器。因此,我们根据 Pod 运行的集群环境定义了受影响 Pod 的存活时间 (TTL)。特别是,在我们的开发集群中,我们允许更长的时长,因为在活跃开发中运行 kubectl exec 或其他交互式命令更为常见。

对于我们的生产集群,我们设定了较低的时间限制,以避免受影响的 Pod 持续对外提供服务。kube-exec-controller 在内部为每个符合关联 TTL 的 Pod 设置并跟踪一个计时器。计时器到期后,控制器使用 K8s API 逐出该 Pod。进行逐出(而非删除)是为了确保服务可用性,因为集群会遵守任何已配置的 PodDisruptionBudget (PDB)。例如,如果用户在其 PDB 中将 x 个 Pod 定义为关键 Pod,那么当目标工作负载运行的 Pod 少于 x 个时,逐出(由 kube-exec-controller 请求)就不会继续。

以下是上述整个工作流的序列图:

Sequence Diagram

一个新的 kubectl 插件,带来更好的用户体验

我们的准入控制器组件在解决平台上遇到的容器漂移问题方面效果显著。它还能够将所有相关的 Events 提交给受影响的目标 Pod。然而,K8s 集群并不会长时间保留 Events(默认保留期为一小时)。我们需要提供其他方式让开发者获取他们的 Pod 交互活动信息。一个kubectl 插件是展示这些信息的绝佳选择。我们将插件命名为 kubectl pi (pod-interaction 的缩写),并提供了两个子命令:getextend

调用 get 子命令时,插件会检查我们的准入控制器附加的元数据,并将其转换为人类可读的信息。以下是运行 kubectl pi get 的示例输出:

$ kubectl pi get test-pod
POD-NAME  INTERACTOR  POD-TTL  EXTENSION  EXTENSION-REQUESTER  EVICTION-TIME
test-pod  username-1  1h0m0s   /          /                    2021-09-24 17:00:00 -0800 PST

该插件还可用于延长标记为未来逐出的 Pod 的 TTL。这在开发者需要额外时间调试正在出现的问题时非常有用。为此,开发者使用 kubectl pi extend 子命令,插件会修补给定 Pod 的相关 annotation。这些 annotation 包括延长的时长以及发起延长时间请求的用户名,以提高透明度(显示在 kubectl pi get 命令返回的表格中)。

相应地,kube-exec-controller 中还定义了另一个 webhook,用于准入有效的 annotation 更新。一旦被准入,这些更新会按请求重置目标 Pod 的逐出计时器。从开发者端请求延长时间的示例如下:

$ kubectl pi extend test-pod --duration=30m
Successfully extended the termination time of pod/test-pod with a duration=30m
 
$ kubectl pi get test-pod
POD-NAME  INTERACTOR  POD-TTL  EXTENSION  EXTENSION-REQUESTER  EVICTION-TIME
test-pod  username-1  1h0m0s   30m        username-2           2021-09-24 17:30:00 -0800 PST

未来改进

尽管我们的准入控制器服务在处理针对 Pod 的交互式请求方面效果显著,但它可能会在实际命令对这些请求无影响(no-op)时也逐出 Pod。例如,开发者有时只是运行 kubectl exec 来查看存储在主机上的服务日志。然而,即使容器的状态根本没有改变,目标 Pod 仍然会被重启。这里的一个改进可以是增加区分传递给交互式请求的命令的能力,以便无影响的命令不总是强制逐出 Pod。然而,当开发者获得运行中容器的 shell 并在 shell 中执行命令时,这变得具有挑战性,因为这些命令将不再对我们的准入控制器服务可见。

值得在这里指出的另一点是使用 K8s 标签(labels)注解(annotations)的选择。在我们的设计中,我们决定将所有不可变元数据附加为 标签,以便更好地在准入控制(admission control)中强制执行不可变性。然而,其中一些元数据可能更适合作为 注解。例如,我们使用一个键为 box.com/podInitialInteractionTimestamp 的标签来列出 kube-exec-controller 代码中所有受影响的 Pod,尽管其值不太可能用于查询。在 K8s 世界中,更理想的设计是,在我们的场景下,一个单独的 标签 可能更适合用于标识,而将其他元数据作为 注解 来应用。

总结

借助准入控制器的强大功能,我们能够通过在运行时检测可能被篡改的容器,并在不影响服务可用性的情况下驱逐其 Pod 来保护我们的 K8s 集群。我们还利用 kubectl 插件来提供驱逐时间的灵活性,从而为服务所有者带来更好、更自主的体验。我们很荣幸地宣布,我们已经将整个项目开源,供社区在其自己的 K8s 集群中利用。热烈欢迎并感谢任何贡献。您可以在 GitHub 上找到该项目,地址是 https://github.com/box/kube-exec-controller

特别感谢 Ayush Sobti 和 Ethan Goldblum 为本项目提供的技术指导。