(译)16 件关于 Kubernetes APIs 和 CRDs 你所不知道的事情

第 15 个会震惊到你!

如果你对 kubernetes 很熟悉,那么你很可能对 kubernetes API 和 controller 有所了解:调和循环查看 api 中存储的数据,并且努力让集群中的状态和声明的 API 状态相匹配。

这是一个相当强大的模式,随着时间的推移,已经证明了它的价值。但是,尽管核心思想看起来很简单,但一旦你从表面上看,有很多细节可能会令人惊讶。

1. 并不是所有的 kubernetes apis 都有对应的控制器

kubernetes 控制平面通常分为两种职责:API 和控制器。

大多数 kubernetes 发行版都在单独的 Pod 中运行核心 apis 和核心控制器:

$ k -n kube-system get pods
NAME                                         READY   STATUS    RESTARTS   AGE
kube-apiserver-kind-control-plane            1/1     Running   0          23m
kube-controller-manager-kind-control-plane   1/1     Running   0          23m

kube-apiserver 主要负责将 api 数据存储到后端存储中,通常是 etcd。

kube-controller-manager 在这些 api 的内容上运行一系列的调和循环。随着时间的推移,这些控制器确保达到(或不达到)预期的状态,并通过 api 报告状态。

而通常这就是 kubernetes API 的工作方式:通过 API 创建期望的状态,期望的状态被存储,控制器以非同步方式工作,使集群状态与期望的状态相匹配。

例如:deployment api 允许你创建一个定义 Pod 的 deployment对象。Deployment 控制器(在controller-manager中运行)根据deployment 对象创建副本集并更新状态。

如果你完全停止控制器管理器,并试图创建一个像 deployment 这样的对象,这一点就特别明显。

如果没有 controller-manager 运行,deployment 对象(仍然可以被创建!),但是没有状态,也不会创建 Pod。

但实际上,有一小部分 api 并不直接由控制器以这种方式管理。SubjectAccessReview 就是其中之一。

即使没有 controller-manager,SubjectAccessReview 也会响应一个状态。

还有一个你可能很熟悉的 api,它也可以在没有 controller-manager 的情况下使用。

Pods 在没有 controller-manager 的情况下也可以工作! 在这种情况下,kubelet 是 pod 的控制器。

到目前为止,大部分的内容可能都不太令人惊讶。但对于其他一些主题来说,这是一个很好的背景材料。

2. Group, Version 和 Resource 确定 APIs.

Group, Version 和 Kind 确定对象. kubernetes 中的大多数 API 都是处理对象的。

对象和一个 group, version, kind 和 resource 绑定:

kind 是你在 kube 对象清单顶部看到的熟悉的名字。 例如,kind: Deployment,kind: Role,等等。 每个对象都可以通过一个 API 端点来访问。每个 kind 都与一些 resource 相关联,这是用于通过 API 访问对象的名称。对于大多数对象,reresource 只是 kind 名称的一个变体(例如,Kind:Pod 可以通过 pods resource 类型访问)。有些 API 可能会在不同的 resource 名称下暴露相同的 Kind,但这是很罕见的。 每个 kind 都有一组 versions,它们可能有不同的模式。 每个 kind 被组织成一个 group。 apiVersion 是一个特定对象的 group 和 version 的组合。

看一个单个对象:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: pod-reader
rules:
- apiGroups: [""] 
  resources: ["pods"]
  verbs: ["get", "watch", "list"]

这表明 group 是 rbac.authorization.k8s.io,version 是 v1 和 kind 是 Role。

这个对象中没有任何东西告诉你 resource 是什么。kube apiserver 维护着 resource 类型 <-> kind 的映射,kubectl 和 client-go 等 kube 客户端也是如此。在这些客户端项目和相关文档中,这被称为 “REST Mapping”。

一旦你知道了 resource,就有了建立 URL 的简单规则,例如:

/apis/GROUP/VERSION/RESOURCE/NAME 用于获取集群范围的对象。 /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME 用于获取 Namespace 范围的对象。 在 api-concepts 文档中有更多细节。

3. 对象可以在所有的 api 版本中访问

任何给定了 Group 和 Kind 的对象,可以通过 apiserver 支持的全部版本访问。

我可以在 v1beta1 创建一个 ingress 对象,然后在 v1 获取同样的对象。这些版本有完全不同的 schema,但 API 会将其转换为我请求的版本。或者,我可以在 v1 创建一个 ingress 对象,然后在 v1beta1 获取它。

如果你不请求一个特定的版本,kubectl 将以 api 的首选版本请求对象,这可能与你创建的版本不同。

你可以用 kubectl get –raw /apis/GROUP 来了解首选版本是什么。例如,要找到 ingress 的支持的版本和首选版本

$ kubectl get --raw /apis/networking.k8s.io
{
  "kind": "APIGroup",
  "apiVersion": "v1",
  "name": "networking.k8s.io",
  "versions": [
    {
      "groupVersion": "networking.k8s.io/v1",
      "version": "v1"
    },
    {
      "groupVersion": "networking.k8s.io/v1beta1",
      "version": "v1beta1"
    }
  ],
  "preferredVersion": {
    "groupVersion": "networking.k8s.io/v1",
    "version": "v1"
  }
}

4. 存储在 etcd 中的对象的版本可能与提交的版本和首选版本不同。

对于每个 group 和 resource ,kube-apiserver 知道:

服务的版本:api 中可用的版本列表 可解码版本:apiserver 知道如何从存储中解码的版本列表。这可能与服务的版本不同。 存储或可编码版本:apiserver 在存储到 etcd 之前将转换为该版本。 首选版本:如果没有指定,kubectl 将使用该版本进行请求。

如果你向 apiserver 提交一个对象,而它与存储版本不匹配,apiserver 将在将其存储到 etcd 之前将其转换为存储版本。(更准确地说,它首先将其转换为内部版本,然后再将其转换回存储版本 etcd,但这是一个实现细节)

没有一种面向用户的方法可以在运行时确定一个 API 的存储版本(至少现在还没有)。 存储版本通常是首选版本,但这可以被覆盖。

例如,在 kube 1.20 中,ingress 的首选版本是 v1,但存储版本是 v1beta1。这可以通过直接查看 apiserver 存储在 etcd 中的数据来了解:

5. 改变一个对象的存储版本需要用较新的 apiserver 写入

由于存储版本在 kube-apiserver 中是硬编码的,要想让一个对象的存储版本更新,唯一的办法就是更新到一个较新的 kube-apiserver,为该对象的API配置一个不同的存储版本。

一旦 apiserver 进入新的存储版本,该对象必须被覆盖。它不会被自动转换(后面会有更多关于自动转换的内容)。

举个例子:

  • 在 kube 1.20 中创建一个 v1 的 ingress。将其存储在 v1beta1 中。
  • 将 apiserver 更新到 1.21。
  • 检查 etcd 中的数据是否仍在 v1beta1 处。
  • 用新的 apiserver 覆盖 ingress,以看到它存储在 v1 的位置。

6. API 版本可能有棘轮验证(但仍然是可圆滑的)

apiserver 可能会选择在不同的版本之间增加验证。在旧的 api 版本下有效的东西,在新的 apiversion 下可能就无效了

单独来看,这可能并不令人惊讶。可能令人惊讶的是棘轮验证与版本往返的交互方式:你能从 apiserver 获得一个对象并不意味着你能创建完全相同的对象。

例如,CustomResourceDefinition API 在 v1beta1 和 v1 之间采用了棘轮式验证,以强制执行v1下的结构模式。

这意味着:

  • 你可以用一个非结构性模式创建一个 v1beta1 的 CRD。
  • 你可以从 v1 版 CRD api 中获得该 CRD(版本是可循环的)。
  • 如果你删除了这个 CRD,然后用你刚从 API 那里得到的 v1 版 CRD 重新创建它,它将无法创建。
  • 你可以在 v1beta1版本创建 CRD 后,在 v1 版本更新它(验证是渐进式的)。

7. 存在 StorageVersion API

在 1.20+ 版本中,有一个 StorageVersion API,可以提供存储在 etcd 中的对象的信息,但它必须明确启用。

这是启用 API 类型所需的配置。

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  StorageVersionAPI: true
  APIServerIdentity: true
runtimeConfig:
  "internal.apiserver.k8s.io/v1alpha1": "true"

一旦启用,对于每个 group/resource,API 提供:

  • 当前可以解码的版本集合
  • 用于编码的版本 - 即存储版本,用于编码对象以存储在etcd中。

API 为 apiserver 的每个实例提供这一信息。

这个 API 目前不是面向用户的(因此有内部前缀)。它用于在 apiserver 升级期间围绕存储版本做出决定,其中可能有多个 apiservers 以多个版本运行。增强版有更多的细节,也请看API文档。

这里有一个 deployment API 的例子:

apiVersion: internal.apiserver.k8s.io/v1alpha1
kind: StorageVersion
metadata:
  creationTimestamp: "2021-06-04T17:16:57Z"
  name: apps.deployments
  resourceVersion: "52"
  uid: 0b80b0f3-72b0-4af6-adfd-e93eb4b4c29f
spec: {}
status:
  commonEncodingVersion: apps/v1
  conditions:
  - lastTransitionTime: "2021-06-04T17:16:57Z"
    message: Common encoding version set
    reason: CommonEncodingVersionSet
    status: "True"
    type: AllEncodingVersionsEqual
  storageVersions:
  - apiServerID: kube-apiserver-803c62b1-340f-4055-93ca-44aba8a35574
    decodableVersions:
    - apps/v1
    - apps/v1beta2
    - apps/v1beta1
    encodingVersion: apps/v1

在这种情况下,由于只有一个 apiserver,我们可以确信部署将被存储在 apps/v1 版本中。

  1. 安装kube-storage-version-migrator来自动迁移存储版本

在上一节中,我们看到一个对象需要用一个新的apiserver来重写,以便改变存储版本。

在kubernetes中,默认情况下没有任何东西可以执行这个操作。kube-storage-version-migrator是一个可选的组件,它将对集群中的所有对象自动进行获取和输入工作流程。

在 OpenShift 中,kube-storage-version-migrator 是默认启用的,但它并不自动运行(必须手动触发)。

9. CRD 定义了新的 API(不仅仅是对象)。

当你创建 CRD 来定义 api 中的一个新对象时,你所定义的东西与 apiserver 为核心 apis 所定义的东西是一样的:

  • 一个新类型的对象的组、种类和资源类型
  • 服务的版本
  • 存储的版本
  • 可解码的版本
  • 所有可解码版本的模式
  • 对象是命名空间还是集群范围的
  • API 中的新 URL 端点

生成的新 API 将遵循 kubernetes api 惯例:这意味着它们是可轮换的,只能有一个存储版本。

10. 所有的 API 都是集群范围内的,甚至有命名空间的 CRD

CRD有一个 scope 字段。

这决定了你可以创建的对象的范围,它不包括 api 本身的可用性。

如果范围设置为 Cluster,那么 API 路由看起来像:

/apis/GROUP/VERSION/RESOURCE/NAME

如果相反,范围被设置为Namespaced,API路由看起来像:

/apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME

而跨越所有命名空间的命名空间资源可以通过以下方式进行查询:

/apis/GROUP/VERSION/RESOURCETYPE

在集群和命名空间范围的API中,组和版本已经被整个集群所认可。不存在只在单一命名空间中可用的API的概念。

11. 一个 CRD 的存储版本决定了新对象的存储方式。

就像kube-apiserver在存储到etcd时选择使用的版本一样,作为CRD开发者,你也必须选择存储到etcd的版本。

CRD版本上的存储:true标志表明对象将如何被存储下去。它不影响现有的存储对象。

12. CRD 的 storageVersions 列出了每一个曾是存储版本的版本(而不是实际在etcd中的版本)。

crd 的状态块有一个 storedVersions 字段。

status:
  acceptedNames:
    kind: CronTab
    listKind: CronTabList
    plural: crontabs
    shortNames:
    - ct
    singular: crontab
  conditions:
  - lastTransitionTime: "2021-06-16T14:47:48Z"
    message: no conflicts found
    reason: NoConflicts
    status: "True"
    type: NamesAccepted
  - lastTransitionTime: "2021-06-16T14:47:48Z"
    message: the initial names have been accepted
    reason: InitialNamesAccepted
    status: "True"
    type: Established
  storedVersions:
  - v1beta1
  - v1

这个字段表示每一个作为存储版本的版本(即在规范中设置了存储:true),与etcd中存在的存储版本没有关系。

13. 你不能从 CRD 中删除一个版本,直到它被从状态中移除。

.status.storedVersions,是以前被设置为api的存储版本的记录,表明在etcd中可能仍有数据存储在这些版本下。你不想删除一个可解码的版本,直到存储中没有任何可能需要解码的东西了。

由于这个原因,如果一个版本被列为存储版本,就不可能完全从CRD中删除该版本。

请注意,对于任何版本,served都可以被设置为false–不再提供服务的存储版本仍然可以在较新的apiversions下被取走。

14. 存储版本必须从CRD的状态中手动删除

就像非CRD定义的kube apis一样,对象需要通过写操作更新到新的存储版本。kube-storage-version-migrator也可以为CRs自动完成这一工作。

然而,一旦迁移完成,从CRD的.status.storedVersion中删除未使用的存储版本是一个手动过程。

kubectl没有直接支持编辑状态。在这个例子中,我们用curl删除版本。

15. 在不同版本之间收紧一个 schema 是不安全的

一些kube apis可能有棘轮验证。但一般来说,这种验证的收紧不会发生在API模式中–收紧验证会导致客户端对数据有不正确的假设。

这是一个可能发生的情况。

一个API的v1版的字段比v1beta1版的模式更严格 该API的存储版本是v1 在v1beta1版本创建的对象不符合v1版本更严格的模式。 该对象被接受,因为它是在v1beta1创建的,而且存储版本是v1,所以它被存储为v1对象 etcd中的对象是一个 “无效的 “v1对象。 如果你在对象已经被创建之后,将单一版本的模式更新得更严格,也会出现类似的情况。

如果在版本之间加强验证,任何使用模式对api响应有期望的客户可能不会做正确的事情。

16. kube-storage-version-migrator will fail for tightened schemas

kube-storage-version-migator对每个对象进行获取/更新。如果模式已经被收紧(或者棘轮验证没有被实现,只适用于创建),那么它将失败。

附录: kind, yvim, auger, etcdctl

为了简明扼要,这些演示使用了几种工具。

kind 是本地快速创建集群的工具.

要想用同类集群检查etcd的内容,首先要配置它以暴露etcd端口。

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 2379
    hostPort: 2379

然后配置etcdctl,使其能够与 kind 的 etcd 通信:

docker cp kind-control-plane:/etc/kubernetes/pki/etcd/ca.crt ca.crt
docker cp kind-control-plane:/etc/kubernetes/pki/etcd/peer.crt peer.crt
docker cp kind-control-plane:/etc/kubernetes/pki/etcd/peer.key peer.key
export ETCDCTL_CACERT=./ca.crt 
export ETCDCTL_CERT=./peer.crt 
export ETCDCTL_KEY=./peer.key 
# confirm it works
etcdctl get /registry  --prefix=true

我偶尔会使用yvim,这只是一个别名,用来打开vim,假设文件是yaml(对kubectl的管道连接很有用)。

alias yvim='nvim -c "doautocmd Filetype yaml" -R -'

我有时也使用Auger来解码存储在etcd中的protobuf编码的对象。Auger不是开箱即用的,相反,它需要通过引用特定版本的kube来构建,以便它有正确的对象定义。

一旦它被正确构建,你就可以直接从etcdctl进行管道连接:

etcdctl get /registry/ingress/default/name-virtual-host-ingress --print-value-only | auger decode

原文: https://evancordell.com/posts/kube-apis-crds/

除特殊注明部分,本站内容采用 CC BY-NC-SA 4.0 进行许可。