Kubernetes一件部署利器:kubeadm

在 Kubernetes 项目初期,它的部署完全要依靠一堆由社区维护的脚本。除了将各个组件编译成二进制文件外,用户还要负责为这些文件编写对应的配置文件、配置自启动脚本,以及为 kube-apiserver 配置授权文件等等诸多运维工作。

目前,各大云厂商最常用的部署方法,是采用 SaltStack、Ansible 等运维工具自动化地执行这些步骤,但是这个部署过程仍然非常繁琐,因为这类专业运维工具本身的学习成本就可能比 Kubernetes 还要高。直到 2017 年社区才终于发起了一个独立的部署工具,名叫 kubeadm
这个项目的目的,就是要让用户能够通过这样两条指令完成一个 Kubernetes 集群的部署:

1
2
3
4
5
# 创建一个Master节点
$ kubeadm init

# 将一个Node节点加入到当前集群中
$ kubeadm join <Master节点的IP和端口>

kubeadm 的工作原理

在部署 Kubernetes 时,它的每一个组件都是一个需要被执行的、单独的二进制文件。所以不难想象,SaltStack 这样的运维工具或者由社区维护的脚本的功能,就是要把这些二进制文件传输到指定的机器当中,然后编写控制脚本来启停这些组件。
不过,在了解了容器技术后,你可能已经萌生出了这样一个想法,为什么不用容器来部署 Kubernetes 呢?
因为这样做会带来一个很麻烦的问题,即:如何容器化 kubelet。

在上一篇文章中,已经提到 kubelet 是 Kubernetes 原来操作 Docker 等容器运行时的核心组件。可是除了与容器运行时打交道外,kubelet 在配置容器网络、管理容器数据卷时,都需要直接操作宿主机。
而如果现在 kubelet 本身就运行在一个容器里,那么直接操作宿主机就会变得很麻烦。对于网络配置来说还好,kubelet 容器可以通过不开启 Network Namespace(即 Docker 的 host network 模式)的方式,直接共享宿主机的网络栈。可是要让 kubelet 隔着容器的 Mount Namespace 和文件系统,操作宿主机的文件系统,那就有点儿困难了。
举个例子,如果用户想要使用 NFS 做容器的持久化数据卷,那么 kubelet 就要在容器进行绑定挂载前,在宿主机的指定目录上,先挂载 NFS 的远程目录。这个时候问题就来了,由于 kubelet 现在是运行在容器里的,这就意味着它要做的这个 mount -F nfs 指令被隔离在了一个单独的 Mount Namespace 当中,也就是说 kubelet 做的挂载操作不能被“传播”到宿主机上。

因此,kubeadm 采用了一种折衷的方案:

把 kubelet 直接运行在宿主机上,然后使用容器部署其他的 Kubernetes 组件。

所以,使用 kubeadm 的第一步,是在机器上手动安装 kubeadm、kubelet 和 kubectl 这三个二进制文件。在 Linux 系统上只需要执行:

1
$ apt-get install kubeadm

即可。

接下来,你就可以使用 kubeadm init 来部署 Master 节点了。

kubeadm init 的工作流程

当你执行 kubeadm init 后,kubeadm 首先要做的,是一系列的检查工作,以确定这台机器可以用来部署 Kubernetes。这一步检查我们称为 “Preflight Checks”。

其实,Preflight Checks 包括了很多方面,比如:

  • Linux 内核版本是否符合要求?
  • Cgroups 模块是否可用?
  • kubeadm 和 kubelet 的版本是否匹配?
  • 机器上是否已经安装了 Kubernetes 的二进制文件?

在通过了 Preflight Checks 之后,kubeadm 要为你做的,是生成 Kubernetes 对外提供服务所需的各种证书和对应的目录
Kubernetes 在对外提供服务时,除非专门开启“不安全模式”,,否则都要通过 HTTPS 才能访问 kube-apiserver。这就需要为 Kubernetes 集群配置好证书文件。这些证书文件都放在 Master 节点的 /etc/kubernetes/pki 目录下。在这个目录下最主要的证书文件是 ca.crt 和对应的私钥 ca.key。
此外,用户使用 kubectl 获取容器日志等 streaming 操作时,需要通过 kube-apiserver 向 kubelet 发起请求,这个连接也必须是安全的。kubeadm 为这一步生成的是 apiserver-kubelet-client.crt 文件,对应的私钥是 apiserver-kubelet-client.key。
除此之外,Kubernetes 集群中还有 Aggregate APIServer 等特性,也需要用到专门的证书。需要指出的是,你可以选择不让 kubeadm 来生成这些证书,而是拷贝现有的证书到如下证书的目录里:

1
/etc/kubernetes/pki/ca.{crt,key}

这时,kubeadm 就会跳过生成证书的步骤,把它完全交给用户处理。

证书生成后,kubeadm 接下来会为其他组件生成访问 kube-apiserver 所需的配置文件。这些文件的路径是:/etc/kubernetes/xxx.conf:

1
2
$ ls /etc/kubernetes/
admin.conf controller-manager.conf kubelet.conf manifests pki scheduler.conf super-admin.conf

这些文件里面记录的是当前这个 Master 节点的服务器地址、监听端口、证书目录等信息。这样,对应的客户端(比如 scheduler,kublet 等),可以直接加载相应的文件,使用里面的信息与 kube-apiserver 建立安全连接。

接下来,kubeadm 会为 Master 组件生成 Pod 配置文件。Kubernetes 中有三个 Master 组件 kube-apiserver、kube-controller-manager 和 kube-scheduler,而它们都会被以 Pod 的方式部署起来。

你可能会有些疑问:此时 Kubernetes 集群尚不存在,难道 kubeadm 会直接执行 docker run 来启动这些容器吗?
当然不是。
在 Kubernetes 中,有一种特殊的容器启动方法叫做 “Static Pod”。它允许你把要部署的 Pod 的 YAML 文件放在一个指定的目录里。这样,当这台机器上的 kubelet 启动时,它会自动检查这个目录,加载所有的 Pod YAML 文件,然后在这台机器上启动它们。

从这点也可以看出,kubelet 在 Kubernetes 项目中的地位非常高,在设计上它就是一个完全独立的组件,而其他的 Master 组件,则更像是辅助性的系统容器。

在 kubeadm 中,Master 组件的 YAML 文件会被生成在 /etc/kubernetes/manifests 路径下。比如,kube-apiserver.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
apiVersion: v1
kind: Pod
metadata:
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ""
creationTimestamp: null
labels:
component: kube-apiserver
tier: control-plane
name: kube-apiserver
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
- --authorization-mode=Node,RBAC
- --runtime-config=api/all=true
- --advertise-address=10.168.0.2
...
- --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
- --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
image: k8s.gcr.io/kube-apiserver-amd64:v1.11.1
imagePullPolicy: IfNotPresent
livenessProbe:
...
name: kube-apiserver
resources:
requests:
cpu: 250m
volumeMounts:
- mountPath: /usr/share/ca-certificates
name: usr-share-ca-certificates
readOnly: true
...
hostNetwork: true
priorityClassName: system-cluster-critical
volumes:
- hostPath:
path: /etc/ca-certificates
type: DirectoryOrCreate
name: etc-ca-certificates
...

关于一个 Pod 的 YAML 文件怎么写、里面的字段如何解读,会在后续的文章中详细分析。在这里,只需要关注这样几个信息:

  1. 这个 Pod 中只定义了一个容器,它使用的镜像是:k8s.gcr.io/kube-apiserver-amd64:v1.11.1。这个镜像是 Kubernetes 官方维护的一个组件镜像。
  2. 这个容器的启动命令(commands)是 kube-apiserver –authorization-mode=Node,RBAC …,这样一句非常长的命令。其实,它就是容器里 kube-apiserver 这个二进制文件加上这些配置参数而已。
  3. 如果你要修改一个已有集群的 kube-apiserver 的配置,需要修改这个 YAML 文件。
  4. 这些组件的参数也可以在部署时指定。

在这步完成后,kubeadm 还会再生成一个 Etcd 的 Pod YAML 文件,用来通过同样的 Static Pod 的方式启动 Etcd。所以,最后 Master 组件的 Pod YAML 文件如下所示:

1
2
$ ls /etc/kubernetes/manifests
etcd.yaml kube-apiserver.yaml kube-controller-manager.yaml kube-scheduler.yaml

而一旦这些 YAML 文件出现在被 kubelet 监视的 /etc/kubernetes/manifests 目录下,kubelet 就会自动创建这些 YAML 文件中定义的 Pod,即 Master 组件的容器。

Master 容器启动后,kubeadm 会通过检查 localhost:6443/healthz 这个 Master 组件的健康检查 URL,等待 Master 组件完全运行起来。

然后,kubeadm 就会为集群生成一个 bootstrap token。在后面,只要有这个 token,任何一个安装了 kubelet 和 kubeadm 的节点,都可以通过 kubeadm join 加入到这个集群中。
这个 token 的值和使用方法,也会在 kubeadm init 结束后被打印出来。

在 token 生成后,kubeadm 也会将 ca.crt 等 Master 节点的重要信息,通过 ConfigMap 的方式保存在 Etcd 当中,供后续部署 Node 节点使用。这个 ConfigMap 的名字是 cluster-info。

kubeadm init 的最后一步,就是安装默认插件。Kubernetes 默认 kube-proxy 和 DNS 这两个插件是必须安装的,它们分别用来提供整个集群的服务发现和 DNS 功能。其实,这两个插件也只是两个容器镜像而已,所以 kubeadm 只要用 Kubernetes 客户端创建两个 Pod 就可以了。

kubeadm join 的工作流程

这个流程其实非常简单,kubeadm init 生成 bootstrap token 之后,你就可以在任意一台安装了 kubectl 和 kubeadm 的机器上执行 kubeadm join 了。
可是,为什么执行 kubeadm join 需要这样的一个 token 呢?
因为任何一台机器想要成为 Kubernetes 集群中的一个节点,就必须在集群的 kube-apiserver 上进行注册。但是,要想和 apiserver 打交道,这台机器就必须获取到相应的证书文件。可是,为了能够一键安装,我们就不能让用户去 Master 节点上手动拷贝这些文件。
所以,kubeadm 至少需要发起一次“不安全模式”的访问到 kube-apiserver,从而拿到保存在 ConfigMap 中的 cluster-info(它保存了 APIServer 的授权信息)。而 bootstrap token 扮演的就是这个过程中安全验证的角色。
只要有了 cluster-info 中的 kube-apiserver 的地址、端口、证书,kubelet 就可以以“安全模式”连接到 apiserver 上,这样一个新的节点就部署完成了。
接下来,只要在其他节点上重复这个指令就可以了。

配置 kubeadm 的部署参数

在前面讲了利用 kubeadm 部署 Kubernetes 集群的最关键的两个步骤,kubeadm initkubeadm join。相信你一定有这样的疑问:kubeadm 确实简单易用,但是我该如何定制我的集群组件参数呢?
在这里,强烈推荐在使用 kubeadm init 部署 Master 节点时,使用下面的这条命令:

1
$ kubeadm init --config kubeadm.yaml

这时,你就可以给 kubeadm 提供一个 YAML 文件(例如 kubeadm.yaml),它的内容大致如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: kubeadm.k8s.io/v1alpha2
kind: MasterConfiguration
kubernetesVersion: v1.11.0
api:
advertiseAddress: 192.168.0.102
bindPort: 6443
...
etcd:
local:
dataDir: /var/lib/etcd
image: ""
imageRepository: k8s.gcr.io
kubeProxy:
config:
bindAddress: 0.0.0.0
...
kubeletConfiguration:
baseConfig:
address: 0.0.0.0
...
networking:
dnsDomain: cluster.local
podSubnet: ""
serviceSubnet: 10.96.0.0/12
nodeRegistration:
criSocket: /var/run/dockershim.sock
...

通过制定这样的配置文件,就可以很方便地填写各种自定义的部署参数了。例如我现在要指定 kube-apiserver 的参数,就只要在这个文件里加上这样的一段信息:

1
2
3
4
5
6
...
apiServerExtraArgs:
advertise-address: 192.168.0.103
anonymous-auth: false
enable-admission-plugins: AlwaysPullImages,DefaultStorageClass
audit-log-path: /home/johndoe/audit.log

然后,kubeadm 就会使用这些信息来替换 /etc/kubernetes/manifests/kube-apiserver.yaml 中 command 字段里的参数了。
而这个 YAML 文件提供的可配置项远不止这些。比如,你还可以修改 kubelet 和 kube-proxy 的配置,修改 Kubernetes 使用的基础镜像的 URL,指定自己的证书文件,指定特殊的容器运行时等等。这些配置项,就留给你在后续实践中探索了。

总结

你可以看到,kubeadm 的设计非常简洁。并且,它在实现每一步部署功能时,都在最大程度地重用 Kubernetes 已有的功能,这也就使得我们在使用 kubeadm 部署 Kubernetes 项目时,非常有“原生”的感觉,一点都不会感到突兀。
而 kubeadm 的源代码,直接就在 kubernetes/cmd/kubeadm 目录下,是 Kubernetes 项目的一部分。其中,app/phases 文件夹下的代码,对应的就是我在这篇文章中详细介绍的每一个具体步骤。

最后,我再来回答一下我在今天这次分享开始提到的问题:kubeadm 能够用于生产环境吗?
到目前为止,这个问题的答案是:不能。
因为 kubeadm 目前最欠缺的是,一键部署一个高可用的 Kubernetes 集群,即:Etcd、Master 组件都应该是多节点集群,而不是现在这样的单点。这,当然也正是 kubeadm 接下来发展的主要方向。

2025年1月4日,此时 Kubernetes 社区中的 kubeadm 相关文档中已经出现了部署高可用集群和部署高可用Etcd集群的章节。看上去在这么多年以后,kubeadm 已经大幅完善了这部分的功能。

当然,如果你有部署规模化生产环境的需求,我推荐使用kops或者 SaltStack 这样更复杂的部署工具。

Comments