警告:前方有用的警告

作为 Kubernetes 维护者,我们始终致力于在提升可用性的同时保持兼容性。在开发新功能、分类处理 bug 和解答支持问题的过程中,我们积累了许多对 Kubernetes 用户有用的信息。过去,分享这些信息的方式仅限于带外方法,如发布说明、通知邮件、文档和博客文章。除非有人主动寻找并设法找到这些信息,否则他们无法从中受益。

在 Kubernetes v1.19 中,我们添加了一项功能,允许 Kubernetes API 服务器向 API 客户端发送警告。警告通过一个标准的 Warning 响应头发送,因此不会以任何方式改变状态码或响应体。这使得服务器可以轻松地向任何 API 客户端发送易于阅读的警告,同时与以前的客户端版本保持兼容。

警告在 kubectl v1.19+ 的 stderr 输出中显示,在 k8s.io/client-go 客户端库 v0.19.0+ 的日志输出中显示。k8s.io/client-go 的行为可以按进程或按客户端进行覆盖

废弃警告

我们使用这项新功能的第一个场景是针对已废弃 API 的使用发送警告。

Kubernetes 是一个大型、快速发展的项目。跟上每个版本的变化可能令人畏惧,即使是全职从事该项目的人也是如此。一类重要的变化是 API 废弃。随着 Kubernetes 中的 API 晋级到 GA 版本,预发布 API 版本会被废弃并最终移除。

尽管存在一个延长的废弃期,并且废弃信息会包含在发布说明中,但它们仍然难以追踪。在废弃期内,预发布 API 仍然可用,允许用户在几个版本内过渡到稳定的 API 版本。然而,我们发现用户常常直到升级到停止提供该 API 的版本时,才意识到他们正在依赖一个已废弃的 API 版本。

从 v1.19 开始,每当向已废弃的 REST API 发出请求时,警告会与 API 响应一起返回。此警告包含 API 将不再可用的版本以及替代 API 版本的详细信息。

由于警告源自服务器并在客户端级别被拦截,因此它适用于所有 kubectl 命令,包括像 kubectl apply 这样的高级命令和像 kubectl get --raw 这样的低级命令。

kubectl applying a manifest file, then displaying a warning message 'networking.k8s.io/v1beta1 Ingress is deprecated in v1.19+, unavailable in v1.22+; use networking.k8s.io/v1 Ingress'.

这有助于受废弃影响的人了解他们正在发出的请求已废弃、他们有多少时间来解决这个问题,以及他们应该使用哪个 API 作为替代。当用户应用一个不是他们创建的 manifest 时,这尤其有用,因为他们有时间联系作者请求更新版本。

我们还意识到,使用已废弃 API 的人通常与负责升级集群的人不是同一个人,因此我们添加了两个面向管理员的工具,以帮助跟踪已废弃 API 的使用情况并确定何时升级是安全的。

指标

从 Kubernetes v1.19 开始,当向已废弃的 REST API 端点发出请求时,kube-apiserver 进程中的 apiserver_requested_deprecated_apis gauge 指标会被设置为 1。此指标带有 API 的 groupversionresourcesubresource 标签,以及一个 removed_release 标签,指示 API 将不再提供的 Kubernetes 版本。

这是一个使用 kubectl, prom2json, 和 jq 来确定从当前 API 服务器实例请求了哪些已废弃 API 的示例查询。

kubectl get --raw /metrics | prom2json | jq '
  .[] | select(.name=="apiserver_requested_deprecated_apis").metrics[].labels
'

输出

{
  "group": "extensions",
  "removed_release": "1.22",
  "resource": "ingresses",
  "subresource": "",
  "version": "v1beta1"
}
{
  "group": "rbac.authorization.k8s.io",
  "removed_release": "1.22",
  "resource": "clusterroles",
  "subresource": "",
  "version": "v1beta1"
}

这表明此服务器上已请求了已废弃的 extensions/v1beta1 Ingress 和 rbac.authorization.k8s.io/v1beta1 ClusterRole API,它们将在 v1.22 中移除。

我们可以将该信息与 apiserver_request_total 指标结合,以获取有关这些 API 请求的更多详细信息。

kubectl get --raw /metrics | prom2json | jq '
  # set $deprecated to a list of deprecated APIs
  [
    .[] | 
    select(.name=="apiserver_requested_deprecated_apis").metrics[].labels |
    {group,version,resource}
  ] as $deprecated 
  
  |
  
  # select apiserver_request_total metrics which are deprecated
  .[] | select(.name=="apiserver_request_total").metrics[] |
  select(.labels | {group,version,resource} as $key | $deprecated | index($key))
'

输出

{
  "labels": {
    "code": "0",
    "component": "apiserver",
    "contentType": "application/vnd.kubernetes.protobuf;stream=watch",
    "dry_run": "",
    "group": "extensions",
    "resource": "ingresses",
    "scope": "cluster",
    "subresource": "",
    "verb": "WATCH",
    "version": "v1beta1"
  },
  "value": "21"
}
{
  "labels": {
    "code": "200",
    "component": "apiserver",
    "contentType": "application/vnd.kubernetes.protobuf",
    "dry_run": "",
    "group": "extensions",
    "resource": "ingresses",
    "scope": "cluster",
    "subresource": "",
    "verb": "LIST",
    "version": "v1beta1"
  },
  "value": "1"
}
{
  "labels": {
    "code": "200",
    "component": "apiserver",
    "contentType": "application/json",
    "dry_run": "",
    "group": "rbac.authorization.k8s.io",
    "resource": "clusterroles",
    "scope": "cluster",
    "subresource": "",
    "verb": "LIST",
    "version": "v1beta1"
  },
  "value": "1"
}

输出显示,仅有读请求发向这些 API,且大多数请求是 watch 已废弃的 Ingress API。

你还可以通过以下 Prometheus 查询找到该信息,该查询返回有关将在 v1.22 中移除的已废弃 API 的请求信息。

apiserver_requested_deprecated_apis{removed_release="1.22"} * on(group,version,resource,subresource)
group_right() apiserver_request_total

审计注解

指标是检查是否正在使用已废弃 API 以及使用速率的快速方法,但它们不包含足够的信息来识别特定的客户端或 API 对象。从 Kubernetes v1.19 开始,对已废弃 API 的请求的审计事件会包含一个审计注解 "k8s.io/deprecated":"true"。管理员可以使用这些审计事件来识别需要更新的特定客户端或对象。

自定义资源定义

除了 API 服务器能够就已废弃 API 的使用发出警告外,从 v1.19 开始,CustomResourceDefinition 可以指示它定义的资源的特定版本已被废弃。当对已废弃版本的自定义资源发出 API 请求时,会返回一个警告消息,这与内置 API 的行为一致。

CustomResourceDefinition 的作者也可以为每个版本定制警告。如果需要,这使他们可以提供指向迁移指南或其他信息的链接。

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
  name: crontabs.example.com
spec:
  versions:
  - name: v1alpha1
    # This indicates the v1alpha1 version of the custom resource is deprecated.
    # API requests to this version receive a warning in the server response.
    deprecated: true
    # This overrides the default warning returned to clients making v1alpha1 API requests.
    deprecationWarning: "example.com/v1alpha1 CronTab is deprecated; use example.com/v1 CronTab (see http://example.com/v1alpha1-v1)"
    ...

  - name: v1beta1
    # This indicates the v1beta1 version of the custom resource is deprecated.
    # API requests to this version receive a warning in the server response.
    # A default warning message is returned for this version.
    deprecated: true
    ...

  - name: v1
    ...

准入 Webhook

准入 Webhook 是将自定义策略或验证与 Kubernetes 集成的主要方式。从 v1.19 开始,准入 Webhook 可以返回警告消息,这些消息会传递给请求的 API 客户端。警告可以与允许或拒绝的准入响应一起返回。

举例来说,为了允许一个请求但警告一个已知不能正常工作的配置,准入 Webhook 可以发送此响应:

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "<value from request.uid>",
    "allowed": true,
    "warnings": [
      ".spec.memory: requests >1GB do not work on Fridays"
    ]
  }
}

如果你正在实现一个返回警告消息的 Webhook,以下是一些建议:

  • 不要在消息中包含“Warning:”前缀(该前缀由客户端在输出时添加)。
  • 使用警告消息描述发出 API 请求的客户端应该纠正或注意的问题。
  • 保持简洁;如果可能,将警告限制在 120 个字符以内。

准入 Webhook 可以通过多种方式使用此新功能,我期待看到大家能想出什么。以下是一些可以帮助你入门的想法:

  • Webhook 实现添加“抱怨”模式,在该模式下它们返回警告而不是拒绝,从而允许在开始强制执行之前尝试策略以验证其是否按预期工作。
  • “lint”或“vet”风格的 Webhook,检查对象并在不遵循最佳实践时显示警告。

定制客户端处理

使用 k8s.io/client-go 库发出 API 请求的应用可以定制如何处理从服务器返回的警告。默认情况下,警告在收到时会记录到 stderr,但此行为可以按进程按客户端进行定制。

此示例展示了如何让你的应用行为像 kubectl,覆盖进程范围内的消息处理以对警告去重,并在支持的地方使用彩色输出高亮消息。

import (
  "os"
  "k8s.io/client-go/rest"
  "k8s.io/kubectl/pkg/util/term"
  ...
)

func main() {
  rest.SetDefaultWarningHandler(
    rest.NewWarningWriter(os.Stderr, rest.WarningWriterOptions{
        // only print a given warning the first time we receive it
        Deduplicate: true,
        // highlight the output with color when the output supports it
        Color: term.AllowsColorOutput(os.Stderr),
      },
    ),
  )

  ...

下一个示例展示了如何构建一个忽略警告的客户端。这对于操作所有资源类型元数据(使用 discovery API 在运行时动态查找)且不会从关于特定资源被废弃的警告中受益的客户端很有用。对于需要使用特定 API 的客户端,不建议抑制废弃警告。

import (
  "k8s.io/client-go/rest"
  "k8s.io/client-go/kubernetes"
)

func getClientWithoutWarnings(config *rest.Config) (kubernetes.Interface, error) {
  // copy to avoid mutating the passed-in config
  config = rest.CopyConfig(config)
  // set the warning handler for this client to ignore warnings
  config.WarningHandler = rest.NoWarnings{}
  // construct and return the client
  return kubernetes.NewForConfig(config)
}

kubectl 严格模式

如果你想确保尽快注意到废弃信息并尽早开始解决它们,kubectl 在 v1.19 中添加了 --warnings-as-errors 选项。使用此选项调用时,kubectl 会将从服务器接收到的任何警告视为错误,并以非零退出码退出。

kubectl applying a manifest file with a --warnings-as-errors flag, displaying a warning message and exiting with a non-zero exit code.

这可以在 CI 任务中使用,将 manifest 应用到当前服务器,并要求以零退出码通过,以便 CI 任务成功。

未来的可能性

现在我们有了在上下文中向用户传达有用信息的方式,我们已经在考虑可以利用此功能的其他方式来改善人们使用 Kubernetes 的体验。接下来我们关注的几个方面是警告已知的问题值(由于兼容性原因我们无法直接拒绝),以及警告使用已废弃的字段或字段值(例如使用 beta os/arch 节点标签的选择器,在 v1.14 中已废弃)。很高兴看到这方面的进展,这将继续使使用 Kubernetes 变得更容易。