# 自己的 Kubernetes 控制器（1）——工作准备

原文：[Your own Kubernetes controller - Laying out the work](https://blog.frankel.ch/your-own-kubernetes-controller/1/)

作者：[Nicolas Fränkel](https://twitter.com/nicolas_frankel)

时至今日，[Kubernetes](https://kubernetes.io/) 已经成为容器化应用部署的首选平台，是个难以忽视的存在。

> Kubernetes是一个开源系统，用于自动化部署、扩展和管理容器化应用程序。

短短几年里，Kubernetes 在 CNCF 的大旗下高歌猛进，在 DevOps 领域已经深入人心。这其中的原因众说纷纭，其中一个非常有说服力的理由是，用户能够避免被锁定在单一云提供商的 API 上。如果你对 2000 年左右微软的桌面垄断有所了解，你可能会明白我的意思。

Kubernetes 的扩展相对来说比较容易，这是它获得广泛认同的一个重要原因。很多软件供应商在 Docker 镜像之外，还会提供一或多个 Operator。

我假设读者仅对 Kubernetes 有所了解，对控制器一无所知，在这个假设的基础上，我将用三篇连载来讲述如何使用 Go 以外的语言实现自己的控制器。

## 控制器是什么

配置管理工具可以分为两种：

| 分类 | 描述 | 工具 |
| --- | --- | --- |
| 指令式 | 指定做事方法，例如启动两个节点 | Ansible、SaltStack 等 |
| 声明式 | 指定目标状态，例如总计五个节点 | Puppet、Chef 等 |

声明式的工具通常会周期性的执行以下任务：

1. 查询当前状态
    
2. 评估要从当前状态达到目标状态所需完成的步骤
    
3. 执行这些步骤
    

这个算法描述的是一个控制回路。

Kubernetes 里，已经有了这些控制回路的实现。例如 `ReplicaSet` 和 `Deployment`。这两个对象都可以针对特定镜像设置目标 Pod 数量。Kubernetes 会持续生成副本，直到达到预设的实例数量。如果副本数量发生变化，那么就会新建或删除副本，以达到目标副本数量。

现在你可能已经猜到了，控制器就是一个控制循环的实现：检查当前状态，用现有状态计算差异，弥补差异。除了 `Deployment` 和 `ReplicaSet` 的控制器之外，Kubernetes 还提供了很多开箱即用的控制器。

* Service
    
* DeamonSet
    
* PersistentVolume
    
* Job
    
* ...
    

其实大多数的 Kubernetes 资源都是由控制器管理的。

## 初识 Operator

对控制器感兴趣的读者，可能已经在搜索过程中偶然发现了 Operator 这个名词。如果你的时间非常有限，我建议你跳过这一部分，将这两个术语视为近义词即可。

前面说到 Kubernetes 的扩展性。其中一个扩展方法就是创建控制器，这也是本文的的重点内容。另一个方式就是对 Kubernetes 模型本身进行扩展：在开箱即用的 Pod、Job 等内置资源以外，还可以使用 CRD 来提供额外的资源类型。

例如下面的代码定义了一个叫做 `Hazelcast` 的资源：

> hazelcast-crd.yml

```yaml
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: hazelcasts.hazelcast.com
spec:
  group: hazelcast.com
  names:
    kind: Hazelcast
    listKind: HazelcastList
    plural: hazelcasts
    singular: hazelcast
  scope: Namespaced
  subresources:
    status: {}
versions:
    - name: v1alpha1
      served: true
      storage: true
```

把文件提交给 API Server，让 Kubernetes 注册这个新的 `Hazelcast` CRD。

```shell
kubectl apply -f hazelcast-crd.yml
```

这个动作完成之后，就可以像其他内置资源一样进行常用操作了：

```shell
kubectl get hazelcasts
```

`Operator` 就是一个用于某种 CRD 的控制器。如果知道怎么实现控制器，也就能够创建 Operator 了。

## 控制器的需求

现在我们看看 Kubernetes 控制器的需求。

### 控制器的部署位置

下图是一个简化的 Kubernetes 架构图：

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1745948422045/a2e994fb-21a2-4987-b2bb-92dff074c9b8.png align="center")

Kubernetes 的内置控制器是其控制平面的组成部分。然而自定义控制器是不会出现在这里（Controller Manager）的。控制器没什么限制，它可以在集群内部以 Pod 的形式运行，也可以作为独立的外部进程。

当然 Pod 形式会享受各种 Kubernetes 上运行容器化应用的福利，例如自愈等。

### 和 Kubernetes 的通信

在 Kubernetes 中，API Server 是一个通信组件。客户端发送 HTTP 请求，API Server 处理请求后发回响应。给 `kubectl` 加上参数就能观察到这一过程：

```shell
$ kubectl get pods --v=8
I0209 12:36:31.330067   13717 round_trippers.go:420] GET https://192.168.99.103:8443/api/v1/namespaces/default/pods?limit=500
I0209 12:36:31.330078   13717 round_trippers.go:427] Request Headers:
I0209 12:36:31.330081   13717 round_trippers.go:431]     Accept: application/json;as=Table;v=v1beta1;g=meta.k8s.io, application/json
I0209 12:36:31.330085   13717 round_trippers.go:431]     User-Agent: kubectl/v1.17.2 (darwin/amd64) kubernetes/59603c6
I0209 12:36:31.339770   13717 round_trippers.go:446] Response Status: 200 OK in 9 milliseconds
I0209 12:36:31.339780   13717 round_trippers.go:449] Response Headers:
I0209 12:36:31.339798   13717 round_trippers.go:452]     Content-Length: 2933
I0209 12:36:31.339804   13717 round_trippers.go:452]     Date: Sun, 09 Feb 2020 11:36:31 GMT
I0209 12:36:31.339822   13717 round_trippers.go:452]     Content-Type: application/json
I0209 12:36:31.340084   13717 request.go:1017] Response Body:
{ "kind":"Table",
  "apiVersion":"meta.k8s.io/v1beta1",
  "metadata":{
    "selfLink":"/api/v1/namespaces/default/pods",
    "resourceVersion":"2387836" },
  "columnDefinitions":[
    { "name":"Name",
      "type":"string",
      "format":"name",
      "description":"Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names",
      "priority":0 },
    { "name":"Ready",
      "type":"string",
      "format":"",
      "description":"The aggregate readiness state of this pod for accepting traffic.",
      "priority":0 },
    { "name":"Status",
      "type":"string",
      "format":"",
      "description":"The aggregate status of the containers in this pod.",
      "priority":0 },
    { "name":"Restarts",
      "type":"integer",
      "format":"",
      "description":"The number of times the containers in this pod have been restarted.",
      "priority":0 },
    { "name":"Age",
      "type":"stri
[truncated 1909 chars]
```

这个通信过程的需求很简单：

1. 能够处理 HTTP 的请求和响应
    
2. JSON 解析（或者说序列化和反序列化）
    

是的，有 JSON 和 HTTP 的处理能力就够了，所以要编写一个控制器，并不一定必须使用特定语言（例如 Go），理论上用单纯的 Shell 也是可以实现的。

## Go 的定位

在进入实现细节之前，首先要看看 Kubernetes 的生态。

历史上好像 Kubernetes 的祖先是用 Java 开发的，后来被移植到了 Go 上。这可能是部分代码不符合 Go 语言风格的原因。尽管 Go 具有垃圾收集功能，但它还是被称为一种低级语言，很适合运行接近于裸机的软件。这种说法是否成立，远远超出了本文的范围，也超出了我的能力。

然而 Kubernetes 生态中大量软件是使用 Go 语言编写的，我想是有其原因的。

如果你已经对 Go 相当了解，那么继续使用是个很好的选择——改弦易辙需要勇气。这并不只是一个语言的问题，除了语法之外，还有很多其他内容：

### 要多久才能用新语言写出地道的代码

我记得我在学习 Java 的时候，读过 C 语言开发者写的代码。虽然语法是 Java，但是却写出了 C 语言的风格，例如在方法结束之前释放本地变量的引用。

### 多久才能搞清楚在什么条件下使用什么库

我不了解 Go，但是我知道 Java。Java 生态的丰富是人所皆知的。例如测试的场景，就有 JUnit 4、JUnit 5 以及 TestNG 可以选择，另外需要加入断言库么？这还只是测试呢。

### 选择正确的工具链要多久

如果已经在使用 JetBrains 的产品，那么从 JetBrains IDE 之间跳转是比较容易的，例如 IDEA 和 GoLand。但是 IDE 市场非常混乱，例如微软正在推广的包含丰富插件的 VS Code。而 Java 世界中，Eclipse 仍然占据客观的市场份额。各种产品都有自己的优劣，自己的拥趸。工具的选择可能在组织内部引发圣战。

### 新工具形成生产力要多久

各种 IDE 都有各自的玩法。例如我从 Eclipse 切换到 IntelliJ 的过程中，几个星期后才停掉了频繁保存文件的习惯。除了 IDE 之外，还有除错工具等。新的语言能怎么除错？有什么先决条件么？

另外前面说的几个点只是开发，如果考虑到相关的构建、集成和投产环境，其投入可能又会有数倍的增长。

我希望上面几点能够让读者意识到，语言的切换事关重大。在很多情况下，沿用原有的语言可能是个更好的选择。

## 结论

本文的第一部分，大概了解了一下 Kubernetes 控制器的基础内容。我们详细介绍了什么是控制器，以及开发控制器的需要：即能够与 HTTP/JSON 通信。在下一篇帖子中，我们将详细介绍并实际开发自己的自定义控制器。
