Published at 2022-06-04 | Last Update 2022-06-04
本文串联了以下几篇文章的核心部分,
论述了 K8s 的核心价值是其通用、跨厂商和平台、可灵活扩展的声明式 API 框架, 而不是容器(虽然容器是它成功的基础);然后手动创建一个 API extension(CRD), 通过测试和类比来对这一论述有一个更直观的理解。
例子及测试基于 K8s v1.21.0
,感谢原作者们的精彩文章。
时间回到 2013 年。当一条简单的 docker run postgre
命令就能运行起 postgre 这样
复杂的传统服务时,开发者在震惊之余犹如受到天启;以 docker 为代表的实用容器技术的
横空出世,也预示着一扇通往敏捷基础设施的大门即将打开。此后,一切都在往好的方向迅速发展:
但本文将要说明,容器并非 Kubernetes 最重要、最有价值的地方,Kubernetes 也并非 仅仅是一个更广泛意义上的 workload 调度器 —— 高效地调度不同类型的 workload 只是 Kubernetes 提供的一种重要价值,但并不是它成功的原因。
“等等 —— K8s 只是一堆 API?”
“不好意思,一直都是!”
K8s 的成功和价值在于,提供了一种标准的编程接口(API),可以用来编写和使用 软件定义的基础设施服务(本文所说的“基础设施”,范围要大于 IAAS):
为了进一步解释这一点,考虑下 Kubernetes 出现之前的场景。
K8s 之前,基础设施基本上是各种不同 API、格式和语义的“云”服务组成的大杂烩:
虽然 Terraform 等工具的出现,提供了一种跨厂商的通用格式,但原始的结构和语义仍然 是五花八门的,—— 针对 AWS 编写的 Terraform descriptor 是无法用到 Azure 的。
现在再来看 Kubernetes 从一开始就提供的东西:描述各种资源需求的标准 API。例如,
这些 API 是跨公有云/私有云和各家云厂商的,各云厂商会将 Kubernetes 结构和语义 对接到它们各自的原生 API。 因此我们可以说,Kubernetes 提供了一种管理软件定义基础设施(也就是云)的标准接口。 或者说,Kubernetes 是一个针对云服务(cloud services)的标准 API 框架。
提供一套跨厂商的标准结构和语义来声明核心基础设施(pod/service/volume/serviceaccount/…), 是 Kubernetes 成功的基础。在此基础上,它又通过 CRD(Custom Resource Definition), 将这个结构扩展到任何/所有基础设施资源。
CRD 在 1.7 引入,允许云厂商和开发者自己的服务复用 K8s 的 spec/impl 编程框架。
有了 CRD,用户不仅能声明 Kubernetes API 预定义的计算、存储、网络服务, 还能声明数据库、task runner、消息总线、数字证书 … 任何云厂商能想到的东西!
Operator Framework 以及 SIG API Machinery 等项目的出现,提供了方便地创建和管理这些 CRD 的工具,最小化用户工作量,最大程度实现标准化。
例如,Crossplane 之类的项目,将厂商资源 RDS 数据库、SQS queue 资源映射到 Kubernetes API,就像核心 K8s controller 一样用自己的 controller 来管理网卡、磁盘等自定义资源。 Google、RedHat 等 Kubernetes 发行商也在它们的基础 Kubernetes 发行版中包含越来越多的自定义资源类型。
我们说 Kubernetes 的核心是其 API 框架,但并不是说这套 API 框架就是完美的。 事实上,后一点并不是(非常)重要,因为 Kubernetes 模型已经成为一个事实标准: 开发者理解它、大量工具主动与它对接、主流厂商也都已经原生支持它。用户认可度、互操作性 经常比其他方面更能决定一个产品能否成功。
随着 Kubernetes 资源模型越来越广泛的传播,现在已经能够
用一组 Kubernetes 资源来描述一整个软件定义计算环境。
就像用 docker run
可以启动单个程序一样,用 kubectl apply -f
就能部署和运行一个分布式应用,
而无需关心是在私有云还是公有云以及具体哪家云厂商上,Kubernetes 的 API 框架已经屏蔽了这些细节。
因此,Kubernetes 并不是关于容器的,而是关于 API。
可以通过 GET/LIST/PUT/POST/DELETE
等 API 操作,来创建、查询、修改或删除集群中的资源。
各 controller 监听到资源变化时,就会执行相应的 reconcile 逻辑,来使 status 与 spec 描述相符。
这种类型的资源是区分 namespace,也就是可以用 namespace 来隔离。 大部分内置资源都是这种类型,包括:
API 格式:
/api/{version}/namespaces/{namespace}/{resource}
/api/v1/namespaces/default/pods
这种类型的资源是全局的,不能用 namespace 隔离,例如:
clusterxxx
一般都是,表示它是 cluster-scoped 的资源)API 格式:
/api/{version}/{resource}
/api/v1/nodes
apiextension
)API 格式:
/apis/{apiGroup}/{apiVersion}/namespaces/{namespace}/{resource}
/apis/cilium.io/v2/namespaces/kube-system/ciliumnetworkpolicies
略。
用户发现了 k8s 的强大之后,希望将越来越多的东西(数据)放到 k8s 里面, 像内置的 Pod、Service、NetworkPolicy 一样来管理,因此出现了两个东西:
CRD:用来声明用户的自定义资源,例如它是 namespace-scope 还是 cluster-scope 的资源、有哪些字段等等,K8s 会自动根据这个定义生成相应的 API;
官方文档的例子, 后文也将给出一个更简单和具体的例子。
CRD 是资源类型定义,具体的资源叫 CR。
Operator 框架:“operator” 在这里的字面意思是“承担运维任务的程序”, 它们的基本逻辑都是一样的:时刻盯着资源状态,一有变化马上作出反应(也就是 reconcile 逻辑)。
这就是扩展 API 的(最主要)声明和使用方式。
至此,我们讨论的都是一些比较抽象的东西,接下来通过一些例子和类比来更直观地理解一下。
在本节中,我们将创建一个名为 fruit
的 CRD,它有 name/sweet/weight
三个字段,
完整 CRD 如下,
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: fruits.example.org # CRD 名字
spec:
conversion:
strategy: None
group: example.org # REST API: /apis/<group>/<version>
names:
kind: Fruit
listKind: FruitList
plural: fruits
singular: fruit
scope: Namespaced # Fruit 资源是区分 namespace 的
versions:
- name: v1 # REST API: /apis/<group>/<version>
schema:
openAPIV3Schema:
properties:
spec:
properties:
comment: # 字段 1,表示备注
type: string
sweet: # 字段 2,表示甜否
type: boolean
weight: # 字段 3,表示重量
type: integer
type: object
type: object
served: true # 启用这个版本的 API(v1)
storage: true
additionalPrinterColumns: # 可选项,配置了这些 printer columns 之后,
- jsonPath: .spec.sweet # 命令行 k get <crd> <cr> 时,能够打印出下面这些字段,
name: sweet # 否则,k8s 默认只打印 CRD 的 NAME 和 AGE
type: boolean
- jsonPath: .spec.weight
name: weight
type: integer
- jsonPath: .spec.comment
name: comment
type: string
后面会解释每个 section 都是什么意思。在此之前,先来做几个(直观而粗糙的)类比。
像其他数据库技术一样,它有自己的持久存储引擎(etcd),以及构建在存储引擎之上的 一套 API 和语义。这些语义允许用户创建、读取、更新和删除(CURD)数据库中的数据。 下面是一些概念对应关系:
关系型数据库 | Kubernetes (as a database) | 说明 |
---|---|---|
DATABASE |
cluster | 一套 K8s 集群就是一个 database 【注 1】 |
TABLE |
Kind |
每种资源类型对应一个表;分为内置类型和扩展类型 【注 2】 |
COLUMN |
property | 表里面的列,可以是 string、boolean 等类型 |
rows | resources | 表中的一个具体 record |
【注 1】 如果只考虑 namespaced 资源的话,也可以说一个 namespace 对应一个 database。
【注 2】 前面已经介绍过,
- 内置
Kind
:Job、Service、Deployment、Event、NetworkPolicy、Secret、ConfigMap 等等;- 扩展
Kind
:各种 CRD,例如 CiliumNetworkPolicy。
所以,和其他数据库一样,本质上 Kubernetes 所做的不过是以 schema 规定的格式来处理 records。
另外,Kubernetes 的表都有自带文档:
$ k explain fruits
KIND: Fruit
VERSION: example.org/v1
DESCRIPTION:
<empty>
FIELDS:
apiVersion <string>
APIVersion defines the versioned schema of this representation of an
object. Servers should convert recognized schemas to the latest internal
value, and may reject unrecognized values. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
kind <string>
Kind is a string value representing the REST resource this object
represents. Servers may infer this from the endpoint the client submits
requests to. Cannot be updated. In CamelCase. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
metadata <Object>
Standard object's metadata. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
spec <Object>
另外,Kubernetes API 还有两大特色:
CRD 和内置的 Pod、Service、NetworkPolicy 一样,不过是数据库的一张表。
例如,前面给出的 fruit
CRD,有 name/sweet/weight
列,以及 “apple”, “banana” 等 entry,
用户发现了 k8s 的强大,希望将越来越多的东西(数据)放到 k8s 里面来管理。数据类 型显然多种多样的,不可能全部内置到 k8s 里。因此,一种方式就是允许用户创建自己的 “表”,设置自己的“列” —— 这正是 CRD 的由来。
CRD(及 CR)描述格式可以是 YAML 或 JSON。CRD 的内容可以简单分为三部分:
常规 k8s metadata:每种 K8s 资源都需要声明的字段,包括 apiVersion
、kind
、metadata.name
等。
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: fruits.example.org # CRD 名字
Table-level 信息:例如表的名字,最好用小写,方便以后命令行操作;
spec:
conversion:
strategy: None
group: example.org # REST API: /apis/<group>/<version>
names:
kind: Fruit
listKind: FruitList
plural: fruits
singular: fruit
scope: Namespaced # Fruit 资源是区分 namespace 的
Column-level 信息:列名及类型等等,遵循 OpenAPISpecification v3 规范。
versions:
- name: v1 # REST API: /apis/<group>/<version>
schema:
openAPIV3Schema:
properties:
spec:
properties:
comment: # 字段 1,表示备注
type: string
sweet: # 字段 2,表示甜否
type: boolean
weight: # 字段 3,表示重量
type: integer
type: object
type: object
served: true # 启用这个版本的 API(v1)
storage: true
additionalPrinterColumns: # 可选项,配置了这些 printer columns 之后,
- jsonPath: .spec.sweet # 命令行 k get <crd> <cr> 时,能够打印出下面这些字段,
name: sweet # 否则,k8s 默认只打印 CRD 的 NAME 和 AGE
type: boolean
- jsonPath: .spec.weight
name: weight
type: integer
- jsonPath: .spec.comment
name: comment
type: string
创建 CRD:这一步相当于 CREATE TABLE fruits ...;
,
$ kubectl create -f fruits-crd.yaml
customresourcedefinition.apiextensions.k8s.io/fruits.example.org created
创建 CR:相当于 INSERT INTO fruits values(...);
,
apple-cr.yaml
:
apiVersion: example.org/v1
kind: Fruit
metadata:
name: apple
spec:
sweet: false
weight: 100
comment: little bit rotten
banana-cr.yaml
:
apiVersion: example.org/v1
kind: Fruit
metadata:
name: banana
spec:
sweet: true
weight: 80
comment: just bought
创建:
$ kubectl create -f apple-cr.yaml
fruit.example.org/apple created
$ kubectl create -f banana-cr.yaml
fruit.example.org/banana created
查询 CR:相当于 SELECT * FROM fruits ... ;
或 SELECT * FROM fruits WHERE name='apple';
。
$ k get fruits.example.org # or kubectl get fruits
NAME SWEET WEIGHT COMMENT
apple false 100 little bit rotten
banana true 80 just bought
$ kubectl get fruits apple
NAME SWEET WEIGHT COMMENT
apple false 100 little bit rotten
删除 CR:相当于 DELETE FROM fruits WHERE name='apple';
,
$ kubectl delete fruit apple
可以看到,CRD/CR 的操作都能对应到常规的数据库操作。
上一节我们是通过 kubectl
命令行来执行 CR 的增删查改,它其实只是一个外壳,内部
调用的是 Kubernetes 为这个 CRD 自动生成的 API —— 所以
又回到了本文第一节论述的内容:K8s 的核心是其 API 框架。
只要在执行 kubectl
命令时指定一个足够大的 loglevel,就能看到
背后的具体 API 请求。例如,
$ kubectl create -v 10 -f apple-cr.yaml
...
Request Body: {"apiVersion":"example.org/v1","kind":"Fruit",\"spec\":{\"comment\":\"little bit rotten\",\"sweet\":false,\"weight\":100}}\n"},"name":"apple","namespace":"default"},"spec":{"comment":"little bit rotten","sweet":false,"weight":100}}
curl -k -v -XPOST 'https://127.0.0.1:6443/apis/example.org/v1/namespaces/default/fruits?fieldManager=kubectl-client-side-apply'
POST https://127.0.0.1:6443/apis/example.org/v1/namespaces/default/fruits?fieldManager=kubectl-client-side-apply 201 Created in 25 milliseconds
...
和内置资源类型一样,K8s 支持对 CR 打标签,然后根据标签做过滤:
# 查看所有 frutis
$ k get fruits
NAME SWEET WEIGHT COMMENT
apple false 100 little bit rotten
banana true 80 just bought
# 给 banana 打上一个特殊新标签
$ k label fruits banana tastes-good=true
fruit.example.org/banana labeled
# 按标签筛选 CR
$ k get fruits -l tastes-good=true
NAME SWEET WEIGHT COMMENT
banana true 80 just bought
# 删除 label
$ k label fruits banana tastes-good-
fruit.example.org/banana labeled
不管是内置 API,还是扩展 API,都能用 K8s 强大的 RBAC 来做鉴权控制。
关于如何使用 RBAC 网上已经有大量文档;但如果想了解其设计,可参考 Cracking Kubernetes RBAC Authorization Model, 它展示了如何从零开始设计出一个 RBAC 鉴权模型(假设 K8s 里还没有)。