集群部署1:理解kubeadm

todo

一、容器化Kubernetes的难点在哪?

我们知道,Kubernetes的每一个组件都是一个需要被执行的、单独的二进制文件。像Ansible这样的运维工具或者由社区维护的部署脚本,就是把这些二进制文件传输到指定的机器当中,然后编写控制脚本来启停这些组件。

在学习了容器技术之后,我们可能会萌生这样的想法:是否使用容器部署Kubernetes?这样,我们只要为每个组件做一个容器镜像,然后在每台宿主机上用docker run指令启动这些组件容器,部署不就完成了吗?

实际上,在Kubernetes项目早期的部署脚本中,确实有一个用Docker部署Kubernetes的脚本,这个脚本相比于Ansible等部署方式要简单不少。但是,这样部署的方式会带来一个比较棘手的问题,即,如何容器化kubelet?

我们知道,kubelet作为Kubernetes集群节点的agent,除了跟容器运行时打交道外,还会在管理容器数据卷、配置容器网络时直接操作宿主机。而如果将kubelet运行在一个容器里,那么直接操作宿主机就会变得很麻烦。

对于网络配置来说,kubelet容器可以通过不开启Network Namespace(即Docker的host network模式)的方式,直接共享宿主机的网络栈。但是,要让kubelet隔着容器的文件系统和Mount Namespace,操作宿主机的文件系统,那就有点儿困难了。比如,如果我们想要使用NFS做容器的持久化数据卷,那么kubelet就需要在容器进行绑定挂载前,在宿主机的指定目录上,先挂载NFS的远程目录。

这时候问题又来了。由于现在kubelet运行在容器中,这就意味着“mount -F nfs”这个挂载远程目录的命令,被隔离在了一个单独的Mount Namespace当中。也就是说,kubelet做的挂载操作,不能被“传播”到宿主机上。对于这个问题,有人说,可以使用setns()系统调用,在宿主机的Mount Namespace中执行这些操作;也有人说,可以让Docker支持一个-mnt=host的参数。但是,到目前为止,在容器里运行kubelet,依然没有很好的解决办法,因此,不推荐使用容器部署Kubernetes项目。

正因为如此,kubeadm选择了一种妥协方案:把kubelet直接运行在宿主机上,然后使用容器部署其他的Kubernetes组件。所以,我们使用kubeadm的第一步,是在机器上手动安装kubeadm、kubelet和kubectl这三个二进制文件。当然,kubeadm 已经为各个发行版的 Linux 准备好了安装包,所以我们只需要配置好软件仓库,使用包管理直接安装就可以了。

二、kubeadm init的工作流程

在安装了kubeadm、kubelet和kubectl这个三个组件之后,我们接下来要做的就是使用“kubeadm init”部署Master节点了。

2.1、Preflight Checks

当我们执行了kubeadm init命令后,kubeadm首先要做的,是一系列的检查工作,以确定这台机器可以被用来部署Kubernetes。对于这一步检查工作,我们称为“Preflight Checks”,它包括了很多方面,比如:

  • Linux内核版本是否是3.10以上?
  • Linux Cgroups 模块是否可用?
  • 机器的 hostname 是否标准? 在 Kubernetes 项目里,机器的名字以及一切存储在 Etcd 中的 API 对象,都必须使用标准的 DNS 命名(RFC 1123)。
  • 安装的 kubeadm 和 kubelet 的版本是否匹配?
  • 机器上是不是已经安装了 Kubernetes 的二进制文件?
  • Kubernetes 的工作端口 10250/10251/10252 端口是不是已经被占用?
  • ip、mount 等 Linux 指令是否存在?
  • Docker 是否已经安装?

2.2、生成证书

在通过了 Preflight Checks 之后,kubeadm 要做的是生成 Kubernetes 对外提供服务所需的各种证书和对应的目录。Kubernetes 对外提供服务时,除非专门开启“不安全模式”,否则都要通过 HTTPS 才能访问 kube-apiserver。这就需要为 Kubernetes 集群配置好证书文件。

kubeadm 为 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 就会跳过证书生成的步骤,把它完全交给用户处理。

2.3、生成访问kube-apiserver所需的配置文件

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

1
2
# ls /etc/kubernetes/
admin.conf controller-manager.conf kubelet.conf scheduler.conf

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

2.4、生成集群组件的Pod配置文件

接下来,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
...

对于上面的YAML文件,我们需要关注这样几个信息:

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

注:这些组件的参数可以在部署时指定。

在这一步完成后,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

2.5、创建Pod

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

2.6、生成token

当 Master 组件容器启动后,kubeadm 会通过检查 localhost:6443/healthz 这个 Master 组件的健康检查 URL,等待 Master 组件完全运行起来。然后,kubeadm 就会为集群生成一个 bootstrap token。之后,只要持有这个 token,任何一个安装了 kubelet 和 kubadm 的节点,都可以通过 kubeadm join 加入到这个集群当中。

注:这个 token 的值和使用方法,会在 kubeadm init 结束后被打印出来。

2.7、在Etcd中保存节点信息

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

2.8、安装默认插件

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

三、理解kubeadm join

在kubeadm init生成 bootstrap token 之后,我们就可以在任意一台安装了 kubelet 和 kubeadm 的机器上执行 kubeadm join 了。

可是,为什么执行 kubeadm join 需要这样一个 token 呢?

因为,任何一台机器想要成为 Kubernetes 集群中的一个节点,就必须在集群的 kube-apiserver 上注册。可是,要想跟 apiserver 打交道,这台机器就必须要获取到相应的证书文件(CA 文件)。可是,为了能够一键安装,我们就不能让用户去 Master 节点上手动拷贝这些文件。所以,kubeadm 至少需要向kube-apiserver发起一次“不安全模式”的访问,从而拿到保存在 ConfigMap 中的 cluster-info(它保存了 APIServer 的授权信息)。而 bootstrap token,扮演的就是这个过程中的安全验证的角色。

只要有了 cluster-info 里的 kube-apiserver 的地址、端口、证书,kubelet 就可以以“安全模式”连接到 apiserver 上,这样一个新的节点就部署完成了。接下来,我们只要在其他节点上重复这个指令就可以了。

四、定制集群参数

到这里,我们一定会有这样的疑问:kubeadm 确实简单易用,可是我们又该如何定制我们的集群组件参数呢?比如,我们要指定 kube-apiserver 的启动参数,该怎么办?

其实,在使用 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(默认的k8s.gcr.io/xxx镜像 URL 在国内访问是有困难的),指定自己的证书文件,指定特殊的容器运行时等等。

五、小结

在这篇文章中,我们主要讨论了kubeadm的工作原理,我们需要知道的是:

  • 容器化Kubernetes的难点在于如何容器化kubelet?作为kubernetes集群节点的agent,kubelet不仅会同容器运行时交互,而且还会直接操作宿主机。尤其是直接操作宿主机文件系统,这对容器来说是一个不小的挑战,因此,kubeadm选择了一种折中方案:直接在宿主机上运行kubelet,然后使用容器部署kubenetes集群的其他组件。
  • kubeadm在容器化kubenetes集群组件时,使用到了一种特殊的容器启动方式:Static Pod。它允许我们事先将要部署的Pod的YAML文件放到一个指定的目录下,这样,当kubelet启动时,就会自动检查这个目录(/etc/kubernetes/manifests),加载所有的Pod YAML文件,然后启动它们。

集群部署1:理解kubeadm
https://kuberxy.github.io/2020/12/14/集群部署1:理解kubeadm/
作者
Mr.x
发布于
2020年12月14日
许可协议