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

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

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

1. 允许交互式 kubectl 命令请求

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

以下是我们的 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 类型是 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 的生存时间 (TTL)。特别是,我们在开发集群中允许更长的时间,因为运行 kubectl exec 或其他交互式命令进行主动开发更为常见。

对于我们的生产集群,我们指定较低的时间限制,以避免受影响的 Pod 持续提供流量。kube-exec-controller 在内部为每个与关联的 TTL 匹配的 Pod 设置并跟踪计时器。一旦计时器结束,控制器就会使用 K8s API 驱逐该 Pod。驱逐(而不是删除)是为了确保服务可用性,因为集群尊重任何配置的 Pod 中断预算 (PDB)。假设用户在其 PDB 中定义了 x 个 Pod 为关键 Pod,那么当目标工作负载运行的 Pod 少于 x 个时,驱逐(根据 kube-exec-controller 的请求)不会继续。

以下是上述整个工作流程的时序图

Sequence Diagram

用于更好用户体验的新 kubectl 插件

我们的准入控制器组件在解决我们在平台上遇到的容器漂移问题方面效果很好。它还能够将所有相关事件提交给受影响的目标 Pod。然而,K8s 集群不会长时间保留事件(默认保留时间为一小时)。我们需要为开发人员提供其他方法来获取他们的 Pod 交互活动。kubectl 插件 是我们公开此信息的完美选择。我们将插件命名为 kubectl pipod-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 的相关 _注释_。这些 _注释_ 包括持续时间和发出扩展请求以提高透明度的用户名(在 kubectl pi get 命令返回的表中显示)。

相应地,kube-exec-controller 中定义了另一个 webhook,该 webhook 允许有效的注释更新。一旦被允许,这些更新就会重置目标 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 的交互式请求方面效果很好,但它也可能在这些请求中的实际命令为空操作时驱逐 Pod。例如,开发人员有时仅运行 kubectl exec 来检查存储在主机上的服务日志。然而,尽管容器的状态根本没有改变,目标 Pod 仍然会被弹回。这里的一个改进可能是添加区分传递给交互式请求的命令的能力,以便空操作命令不应总是强制驱逐 Pod。然而,当开发人员获取正在运行的容器的 shell 并在 shell 中执行命令时,这将变得具有挑战性,因为我们的准入控制器服务将不再看到它们。

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

总结

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

特别感谢 Ayush Sobti 和 Ethan Goldblum 在这个项目上的技术指导。