mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 06:03:45 +08:00
mod
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
<audio id="audio" title="10 | Kubernetes一键部署利器:kubeadm" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/53/99/53e5baf328edf104e60797b9f7d9bb99.mp3"></audio>
|
||||
|
||||
你好,我是张磊。今天我和你分享的主题是:Kubernetes一键部署利器之kubeadm。
|
||||
|
||||
通过前面几篇文章的内容,我其实阐述了这样一个思想:**要真正发挥容器技术的实力,你就不能仅仅局限于对Linux容器本身的钻研和使用。**
|
||||
|
||||
这些知识更适合作为你的技术储备,以便在需要的时候可以帮你更快地定位问题,并解决问题。
|
||||
|
||||
而更深入地学习容器技术的关键在于,**如何使用这些技术来“容器化”你的应用。**
|
||||
|
||||
比如,我们的应用既可能是Java Web和MySQL这样的组合,也可能是Cassandra这样的分布式系统。而要使用容器把后者运行起来,你单单通过Docker把一个Cassandra镜像跑起来是没用的。
|
||||
|
||||
要把Cassandra应用容器化的关键,在于如何处理好这些Cassandra容器之间的编排关系。比如,哪些Cassandra容器是主,哪些是从?主从容器如何区分?它们之间又如何进行自动发现和通信?Cassandra容器的持久化数据又如何保持,等等。
|
||||
|
||||
这也是为什么我们要反复强调Kubernetes项目的主要原因:这个项目体现出来的容器化“表达能力”,具有独有的先进性和完备性。这就使得它不仅能运行Java Web与MySQL这样的常规组合,还能够处理Cassandra容器集群等复杂编排问题。所以,对这种编排能力的剖析、解读和最佳实践,将是本专栏最重要的一部分内容。
|
||||
|
||||
不过,万事开头难。
|
||||
|
||||
作为一个典型的分布式项目,Kubernetes的部署一直以来都是挡在初学者前面的一只“拦路虎”。尤其是在Kubernetes项目发布初期,它的部署完全要依靠一堆由社区维护的脚本。
|
||||
|
||||
其实,Kubernetes作为一个Golang项目,已经免去了很多类似于Python项目要安装语言级别依赖的麻烦。但是,除了将各个组件编译成二进制文件外,用户还要负责为这些二进制文件编写对应的配置文件、配置自启动脚本,以及为kube-apiserver配置授权文件等等诸多运维工作。
|
||||
|
||||
目前,各大云厂商最常用的部署的方法,是使用SaltStack、Ansible等运维工具自动化地执行这些步骤。
|
||||
|
||||
但即使这样,这个部署过程依然非常繁琐。因为,SaltStack这类专业运维工具本身的学习成本,就可能比Kubernetes项目还要高。
|
||||
|
||||
**难道Kubernetes项目就没有简单的部署方法了吗?**
|
||||
|
||||
这个问题,在Kubernetes社区里一直没有得到足够重视。直到2017年,在志愿者的推动下,社区才终于发起了一个独立的部署工具,名叫:[kubeadm](https://github.com/kubernetes/kubeadm)。
|
||||
|
||||
这个项目的目的,就是要让用户能够通过这样两条指令完成一个Kubernetes集群的部署:
|
||||
|
||||
```
|
||||
# 创建一个Master节点
|
||||
$ kubeadm init
|
||||
|
||||
# 将一个Node节点加入到当前集群中
|
||||
$ kubeadm join <Master节点的IP和端口>
|
||||
|
||||
```
|
||||
|
||||
是不是非常方便呢?
|
||||
|
||||
不过,你可能也会有所顾虑:**Kubernetes的功能那么多,这样一键部署出来的集群,能用于生产环境吗?**
|
||||
|
||||
为了回答这个问题,在今天这篇文章,我就先和你介绍一下kubeadm的工作原理吧。
|
||||
|
||||
## kubeadm的工作原理
|
||||
|
||||
在上一篇文章《从容器到容器云:谈谈Kubernetes的本质》中,我已经详细介绍了Kubernetes的架构和它的组件。在部署时,它的每一个组件都是一个需要被执行的、单独的二进制文件。所以不难想象,SaltStack这样的运维工具或者由社区维护的脚本的功能,就是要把这些二进制文件传输到指定的机器当中,然后编写控制脚本来启停这些组件。
|
||||
|
||||
不过,在理解了容器技术之后,你可能已经萌生出了这样一个想法,**为什么不用容器部署Kubernetes呢?**
|
||||
|
||||
这样,我只要给每个Kubernetes组件做一个容器镜像,然后在每台宿主机上用docker run指令启动这些组件容器,部署不就完成了吗?
|
||||
|
||||
事实上,在Kubernetes早期的部署脚本里,确实有一个脚本就是用Docker部署Kubernetes项目的,这个脚本相比于SaltStack等的部署方式,也的确简单了不少。
|
||||
|
||||
但是,**这样做会带来一个很麻烦的问题,即:如何容器化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做的挂载操作,不能被“传播”到宿主机上。
|
||||
|
||||
对于这个问题,有人说,可以使用setns()系统调用,在宿主机的Mount Namespace中执行这些挂载操作;也有人说,应该让Docker支持一个–mnt=host的参数。
|
||||
|
||||
但是,到目前为止,在容器里运行kubelet,依然没有很好的解决办法,我也不推荐你用容器去部署Kubernetes项目。
|
||||
|
||||
正因为如此,kubeadm选择了一种妥协方案:
|
||||
|
||||
>
|
||||
把kubelet直接运行在宿主机上,然后使用容器部署其他的Kubernetes组件。
|
||||
|
||||
|
||||
所以,你使用kubeadm的第一步,是在机器上手动安装kubeadm、kubelet和kubectl这三个二进制文件。当然,kubeadm的作者已经为各个发行版的Linux准备好了安装包,所以你只需要执行:
|
||||
|
||||
```
|
||||
$ apt-get install kubeadm
|
||||
|
||||
```
|
||||
|
||||
就可以了。
|
||||
|
||||
接下来,你就可以使用“kubeadm init”部署Master节点了。
|
||||
|
||||
## kubeadm init的工作流程
|
||||
|
||||
当你执行kubeadm init指令后,**kubeadm首先要做的,是一系列的检查工作,以确定这台机器可以用来部署Kubernetes**。这一步检查,我们称为“Preflight Checks”,它可以为你省掉很多后续的麻烦。
|
||||
|
||||
其实,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是否已经安装?
|
||||
- ……
|
||||
|
||||
**在通过了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为你生成这些证书,而是拷贝现有的证书到如下证书的目录里:
|
||||
|
||||
```
|
||||
/etc/kubernetes/pki/ca.{crt,key}
|
||||
|
||||
```
|
||||
|
||||
这时,kubeadm就会跳过证书生成的步骤,把它完全交给用户处理。
|
||||
|
||||
**证书生成后,kubeadm接下来会为其他组件生成访问kube-apiserver所需的配置文件**。这些文件的路径是:/etc/kubernetes/xxx.conf:
|
||||
|
||||
```
|
||||
ls /etc/kubernetes/
|
||||
admin.conf controller-manager.conf kubelet.conf scheduler.conf
|
||||
|
||||
```
|
||||
|
||||
这些文件里面记录的是,当前这个Master节点的服务器地址、监听端口、证书目录等信息。这样,对应的客户端(比如scheduler,kubelet等),可以直接加载相应的文件,使用里面的信息与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:
|
||||
|
||||
```
|
||||
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文件怎么写、里面的字段如何解读,我会在后续专门的文章中为你详细分析。在这里,你只需要关注这样几个信息:
|
||||
|
||||
<li>
|
||||
这个Pod里只定义了一个容器,它使用的镜像是:`k8s.gcr.io/kube-apiserver-amd64:v1.11.1` 。这个镜像是Kubernetes官方维护的一个组件镜像。
|
||||
</li>
|
||||
<li>
|
||||
这个容器的启动命令(commands)是kube-apiserver --authorization-mode=Node,RBAC …,这样一句非常长的命令。其实,它就是容器里kube-apiserver这个二进制文件再加上指定的配置参数而已。
|
||||
</li>
|
||||
<li>
|
||||
如果你要修改一个已有集群的kube-apiserver的配置,需要修改这个YAML文件。
|
||||
</li>
|
||||
<li>
|
||||
这些组件的参数也可以在部署时指定,我很快就会讲到。
|
||||
</li>
|
||||
|
||||
在这一步完成后,kubeadm还会再生成一个Etcd的Pod YAML文件,用来通过同样的Static Pod的方式启动Etcd。所以,最后Master组件的Pod YAML文件如下所示:
|
||||
|
||||
```
|
||||
$ 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和kubadm的节点,都可以通过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之后,你就可以在任意一台安装了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的部署参数
|
||||
|
||||
我在前面讲了kubeadm部署Kubernetes集群最关键的两个步骤,kubeadm init和kubeadm join。相信你一定会有这样的疑问:kubeadm确实简单易用,可是我又该如何定制我的集群组件参数呢?
|
||||
|
||||
比如,我要指定kube-apiserver的启动参数,该怎么办?
|
||||
|
||||
在这里,我强烈推荐你在使用kubeadm init部署Master节点时,使用下面这条指令:
|
||||
|
||||
```
|
||||
$ kubeadm init --config kubeadm.yaml
|
||||
|
||||
```
|
||||
|
||||
这时,你就可以给kubeadm提供一个YAML文件(比如,kubeadm.yaml),它的内容如下所示(我仅列举了主要部分):
|
||||
|
||||
```
|
||||
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的参数,那么我只要在这个文件里加上这样一段信息:
|
||||
|
||||
```
|
||||
...
|
||||
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集群。
|
||||
|
||||
从今天的分享中,你可以看到,kubeadm的设计非常简洁。并且,它在实现每一步部署功能时,都在最大程度地重用Kubernetes已有的功能,这也就使得我们在使用kubeadm部署Kubernetes项目时,非常有“原生”的感觉,一点都不会感到突兀。
|
||||
|
||||
而kubeadm的源代码,直接就在kubernetes/cmd/kubeadm目录下,是Kubernetes项目的一部分。其中,app/phases文件夹下的代码,对应的就是我在这篇文章中详细介绍的每一个具体步骤。
|
||||
|
||||
看到这里,你可能会猜想,kubeadm的作者一定是Google公司的某个“大神”吧。
|
||||
|
||||
实际上,kubeadm几乎完全是一位高中生的作品。他叫Lucas Käldström,芬兰人,今年只有18岁。kubeadm,是他17岁时用业余时间完成的一个社区项目。
|
||||
|
||||
所以说,开源社区的魅力也在于此:一个成功的开源项目,总能够吸引到全世界最厉害的贡献者参与其中。尽管参与者的总体水平参差不齐,而且频繁的开源活动又显得杂乱无章难以管控,但一个有足够热度的社区最终的收敛方向,却一定是代码越来越完善、Bug越来越少、功能越来越强大。
|
||||
|
||||
最后,我再来回答一下我在今天这次分享开始提到的问题:kubeadm能够用于生产环境吗?
|
||||
|
||||
到目前为止(2018年9月),这个问题的答案是:不能。
|
||||
|
||||
因为kubeadm目前最欠缺的是,一键部署一个高可用的Kubernetes集群,即:Etcd、Master组件都应该是多节点集群,而不是现在这样的单点。这,当然也正是kubeadm接下来发展的主要方向。
|
||||
|
||||
另一方面,Lucas也正在积极地把kubeadm phases开放给用户,即:用户可以更加自由地定制kubeadm的每一个部署步骤。这些举措,都可以让这个项目更加完善,我对它的发展走向也充满了信心。
|
||||
|
||||
当然,如果你有部署规模化生产环境的需求,我推荐使用[kops](https://github.com/kubernetes/kops)或者SaltStack这样更复杂的部署工具。但,在本专栏接下来的讲解中,我都会以kubeadm为依据进行讲述。
|
||||
|
||||
- 一方面,作为Kubernetes项目的原生部署工具,kubeadm对Kubernetes项目特性的使用和集成,确实要比其他项目“技高一筹”,非常值得我们学习和借鉴;
|
||||
- 另一方面,kubeadm的部署方法,不会涉及到太多的运维工作,也不需要我们额外学习复杂的部署工具。而它部署的Kubernetes集群,跟一个完全使用二进制文件搭建起来的集群几乎没有任何区别。
|
||||
|
||||
因此,使用kubeadm去部署一个Kubernetes集群,对于你理解Kubernetes组件的工作方式和架构,最好不过了。
|
||||
|
||||
## 思考题
|
||||
|
||||
<li>
|
||||
在Linux上为一个类似kube-apiserver的Web Server制作证书,你知道可以用哪些工具实现吗?
|
||||
</li>
|
||||
<li>
|
||||
回忆一下我在前面文章中分享的Kubernetes架构,你能够说出Kubernetes各个功能组件之间(包含Etcd),都有哪些建立连接或者调用的方式吗?(比如:HTTP/HTTPS,远程调用等等)
|
||||
</li>
|
||||
|
||||
感谢你的收听,欢迎你给我留言,也欢迎分享给更多的朋友一起阅读。
|
||||
@@ -0,0 +1,455 @@
|
||||
<audio id="audio" title="11 | 从0到1:搭建一个完整的Kubernetes集群" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/88/5d/88f0e653ae6279883eada707758f2a5d.mp3"></audio>
|
||||
|
||||
你好,我是张磊。今天我和你分享的主题是:从0到1搭建一个完整的Kubernetes集群。
|
||||
|
||||
不过,首先需要指出的是,本篇搭建指南是完全的手工操作,细节比较多,并且有些外部链接可能还会遇到特殊的“网络问题”。所以,对于只关心学习 Kubernetes 本身知识点、不太关注如何手工部署 Kubernetes 集群的同学,可以略过本节,直接使用 [MiniKube](https://github.com/kubernetes/minikube) 或者 [Kind](https://github.com/kubernetes-sigs/kind),来在本地启动简单的 Kubernetes 集群进行后面的学习即可。如果是使用 MiniKube 的话,阿里云还维护了一个[国内版的 MiniKube](https://github.com/AliyunContainerService/minikube),这对于在国内的同学来说会比较友好。
|
||||
|
||||
在上一篇文章中,我介绍了kubeadm这个Kubernetes半官方管理工具的工作原理。既然kubeadm的初衷是让Kubernetes集群的部署不再让人头疼,那么这篇文章,我们就来使用它部署一个完整的Kubernetes集群吧。
|
||||
|
||||
>
|
||||
<p>备注:这里所说的“完整”,指的是这个集群具备Kubernetes项目在GitHub上已经发布的所有功能,并能够模拟生产环境的所有使用需求。但并不代表这个集群是生产级别可用的:类似于高可用、授权、多租户、灾难备份等生产级别集群的功能暂时不在本篇文章的讨论范围。<br>
|
||||
目前,kubeadm的高可用部署[已经有了第一个发布](https://kubernetes.io/docs/setup/independent/high-availability/)。但是,这个特性还没有GA(生产可用),所以包括了大量的手动工作,跟我们所预期的一键部署还有一定距离。GA的日期预计是2018年底到2019年初。届时,如果有机会我会再和你分享这部分内容。</p>
|
||||
|
||||
|
||||
这次部署,我不会依赖于任何公有云或私有云的能力,而是完全在Bare-metal环境中完成。这样的部署经验会更有普适性。而在后续的讲解中,如非特殊强调,我也都会以本次搭建的这个集群为基础。
|
||||
|
||||
## 准备工作
|
||||
|
||||
首先,准备机器。最直接的办法,自然是到公有云上申请几个虚拟机。当然,如果条件允许的话,拿几台本地的物理服务器来组集群是最好不过了。这些机器只要满足如下几个条件即可:
|
||||
|
||||
<li>
|
||||
满足安装Docker项目所需的要求,比如64位的Linux操作系统、3.10及以上的内核版本;
|
||||
</li>
|
||||
<li>
|
||||
x86或者ARM架构均可;
|
||||
</li>
|
||||
<li>
|
||||
机器之间网络互通,这是将来容器之间网络互通的前提;
|
||||
</li>
|
||||
<li>
|
||||
有外网访问权限,因为需要拉取镜像;
|
||||
</li>
|
||||
<li>
|
||||
能够访问到`gcr.io、quay.io`这两个docker registry,因为有小部分镜像需要在这里拉取;
|
||||
</li>
|
||||
<li>
|
||||
单机可用资源建议2核CPU、8 GB内存或以上,再小的话问题也不大,但是能调度的Pod数量就比较有限了;
|
||||
</li>
|
||||
<li>
|
||||
30 GB或以上的可用磁盘空间,这主要是留给Docker镜像和日志文件用的。
|
||||
</li>
|
||||
|
||||
在本次部署中,我准备的机器配置如下:
|
||||
|
||||
<li>
|
||||
2核CPU、 7.5 GB内存;
|
||||
</li>
|
||||
<li>
|
||||
30 GB磁盘;
|
||||
</li>
|
||||
<li>
|
||||
Ubuntu 16.04;
|
||||
</li>
|
||||
<li>
|
||||
内网互通;
|
||||
</li>
|
||||
<li>
|
||||
外网访问权限不受限制。
|
||||
</li>
|
||||
|
||||
>
|
||||
备注:在开始部署前,我推荐你先花几分钟时间,回忆一下Kubernetes的架构。
|
||||
|
||||
|
||||
然后,我再和你介绍一下今天实践的目标:
|
||||
|
||||
<li>
|
||||
在所有节点上安装Docker和kubeadm;
|
||||
</li>
|
||||
<li>
|
||||
部署Kubernetes Master;
|
||||
</li>
|
||||
<li>
|
||||
部署容器网络插件;
|
||||
</li>
|
||||
<li>
|
||||
部署Kubernetes Worker;
|
||||
</li>
|
||||
<li>
|
||||
部署Dashboard可视化插件;
|
||||
</li>
|
||||
<li>
|
||||
部署容器存储插件。
|
||||
</li>
|
||||
|
||||
好了,现在,就来开始这次集群部署之旅吧!
|
||||
|
||||
## 安装kubeadm和Docker
|
||||
|
||||
我在上一篇文章《 Kubernetes一键部署利器:kubeadm》中,已经介绍过kubeadm的基础用法,它的一键安装非常方便,我们只需要添加kubeadm的源,然后直接使用apt-get安装即可,具体流程如下所示:
|
||||
|
||||
>
|
||||
备注:为了方便讲解,我后续都会直接在root用户下进行操作。
|
||||
|
||||
|
||||
```
|
||||
$ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
$ cat <<EOF > /etc/apt/sources.list.d/kubernetes.list
|
||||
deb http://apt.kubernetes.io/ kubernetes-xenial main
|
||||
EOF
|
||||
$ apt-get update
|
||||
$ apt-get install -y docker.io kubeadm
|
||||
|
||||
```
|
||||
|
||||
>
|
||||
提示:如果 apt.kubernetes.io 因为网络问题访问不到,可以换成中科大的 Ubuntu 镜像源deb [http://mirrors.ustc.edu.cn/kubernetes/apt](http://mirrors.ustc.edu.cn/kubernetes/apt) kubernetes-xenial main。
|
||||
|
||||
|
||||
在上述安装kubeadm的过程中,kubeadm和kubelet、kubectl、kubernetes-cni这几个二进制文件都会被自动安装好。
|
||||
|
||||
另外,这里我直接使用Ubuntu的docker.io的安装源,原因是Docker公司每次发布的最新的Docker CE(社区版)产品往往还没有经过Kubernetes项目的验证,可能会有兼容性方面的问题。
|
||||
|
||||
## 部署Kubernetes的Master节点
|
||||
|
||||
在上一篇文章中,我已经介绍过kubeadm可以一键部署Master节点。不过,在本篇文章中既然要部署一个“完整”的Kubernetes集群,那我们不妨稍微提高一下难度:通过配置文件来开启一些实验性功能。
|
||||
|
||||
所以,这里我编写了一个给kubeadm用的YAML文件(名叫:kubeadm.yaml):
|
||||
|
||||
```
|
||||
apiVersion: kubeadm.k8s.io/v1alpha1
|
||||
kind: MasterConfiguration
|
||||
controllerManagerExtraArgs:
|
||||
horizontal-pod-autoscaler-use-rest-clients: "true"
|
||||
horizontal-pod-autoscaler-sync-period: "10s"
|
||||
node-monitor-grace-period: "10s"
|
||||
apiServerExtraArgs:
|
||||
runtime-config: "api/all=true"
|
||||
kubernetesVersion: "stable-1.11"
|
||||
|
||||
```
|
||||
|
||||
这个配置中,我给kube-controller-manager设置了:
|
||||
|
||||
```
|
||||
horizontal-pod-autoscaler-use-rest-clients: "true"
|
||||
|
||||
```
|
||||
|
||||
这意味着,将来部署的kube-controller-manager能够使用自定义资源(Custom Metrics)进行自动水平扩展。这是我后面文章中会重点介绍的一个内容。
|
||||
|
||||
其中,“stable-1.11”就是kubeadm帮我们部署的Kubernetes版本号,即:Kubernetes release 1.11最新的稳定版,在我的环境下,它是v1.11.1。你也可以直接指定这个版本,比如:kubernetesVersion: “v1.11.1”。
|
||||
|
||||
然后,我们只需要执行一句指令:
|
||||
|
||||
```
|
||||
$ kubeadm init --config kubeadm.yaml
|
||||
|
||||
```
|
||||
|
||||
就可以完成Kubernetes Master的部署了,这个过程只需要几分钟。部署完成后,kubeadm会生成一行指令:
|
||||
|
||||
```
|
||||
kubeadm join 10.168.0.2:6443 --token 00bwbx.uvnaa2ewjflwu1ry --discovery-token-ca-cert-hash sha256:00eb62a2a6020f94132e3fe1ab721349bbcd3e9b94da9654cfe15f2985ebd711
|
||||
|
||||
```
|
||||
|
||||
这个kubeadm join命令,就是用来给这个Master节点添加更多工作节点(Worker)的命令。我们在后面部署Worker节点的时候马上会用到它,所以找一个地方把这条命令记录下来。
|
||||
|
||||
此外,kubeadm还会提示我们第一次使用Kubernetes集群所需要的配置命令:
|
||||
|
||||
```
|
||||
mkdir -p $HOME/.kube
|
||||
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
|
||||
sudo chown $(id -u):$(id -g) $HOME/.kube/config
|
||||
|
||||
```
|
||||
|
||||
而需要这些配置命令的原因是:Kubernetes集群默认需要加密方式访问。所以,这几条命令,就是将刚刚部署生成的Kubernetes集群的安全配置文件,保存到当前用户的.kube目录下,kubectl默认会使用这个目录下的授权信息访问Kubernetes集群。
|
||||
|
||||
如果不这么做的话,我们每次都需要通过export KUBECONFIG环境变量告诉kubectl这个安全配置文件的位置。
|
||||
|
||||
现在,我们就可以使用kubectl get命令来查看当前唯一一个节点的状态了:
|
||||
|
||||
```
|
||||
$ kubectl get nodes
|
||||
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
master NotReady master 1d v1.11.1
|
||||
|
||||
```
|
||||
|
||||
可以看到,这个get指令输出的结果里,Master节点的状态是NotReady,这是为什么呢?
|
||||
|
||||
在调试Kubernetes集群时,最重要的手段就是用kubectl describe来查看这个节点(Node)对象的详细信息、状态和事件(Event),我们来试一下:
|
||||
|
||||
```
|
||||
$ kubectl describe node master
|
||||
|
||||
...
|
||||
Conditions:
|
||||
...
|
||||
|
||||
Ready False ... KubeletNotReady runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized
|
||||
|
||||
```
|
||||
|
||||
通过kubectl describe指令的输出,我们可以看到NodeNotReady的原因在于,我们尚未部署任何网络插件。
|
||||
|
||||
另外,我们还可以通过kubectl检查这个节点上各个系统Pod的状态,其中,kube-system是Kubernetes项目预留的系统Pod的工作空间(Namepsace,注意它并不是Linux Namespace,它只是Kubernetes划分不同工作空间的单位):
|
||||
|
||||
```
|
||||
$ kubectl get pods -n kube-system
|
||||
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
coredns-78fcdf6894-j9s52 0/1 Pending 0 1h
|
||||
coredns-78fcdf6894-jm4wf 0/1 Pending 0 1h
|
||||
etcd-master 1/1 Running 0 2s
|
||||
kube-apiserver-master 1/1 Running 0 1s
|
||||
kube-controller-manager-master 0/1 Pending 0 1s
|
||||
kube-proxy-xbd47 1/1 NodeLost 0 1h
|
||||
kube-scheduler-master 1/1 Running 0 1s
|
||||
|
||||
```
|
||||
|
||||
可以看到,CoreDNS、kube-controller-manager等依赖于网络的Pod都处于Pending状态,即调度失败。这当然是符合预期的:因为这个Master节点的网络尚未就绪。
|
||||
|
||||
## 部署网络插件
|
||||
|
||||
在Kubernetes项目“一切皆容器”的设计理念指导下,部署网络插件非常简单,只需要执行一句kubectl apply指令,以Weave为例:
|
||||
|
||||
```
|
||||
$ kubectl apply -f https://git.io/weave-kube-1.6
|
||||
|
||||
```
|
||||
|
||||
部署完成后,我们可以通过kubectl get重新检查Pod的状态:
|
||||
|
||||
```
|
||||
$ kubectl get pods -n kube-system
|
||||
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
coredns-78fcdf6894-j9s52 1/1 Running 0 1d
|
||||
coredns-78fcdf6894-jm4wf 1/1 Running 0 1d
|
||||
etcd-master 1/1 Running 0 9s
|
||||
kube-apiserver-master 1/1 Running 0 9s
|
||||
kube-controller-manager-master 1/1 Running 0 9s
|
||||
kube-proxy-xbd47 1/1 Running 0 1d
|
||||
kube-scheduler-master 1/1 Running 0 9s
|
||||
weave-net-cmk27 2/2 Running 0 19s
|
||||
|
||||
```
|
||||
|
||||
可以看到,所有的系统Pod都成功启动了,而刚刚部署的Weave网络插件则在kube-system下面新建了一个名叫weave-net-cmk27的Pod,一般来说,这些Pod就是容器网络插件在每个节点上的控制组件。
|
||||
|
||||
Kubernetes支持容器网络插件,使用的是一个名叫CNI的通用接口,它也是当前容器网络的事实标准,市面上的所有容器网络开源项目都可以通过CNI接入Kubernetes,比如Flannel、Calico、Canal、Romana等等,它们的部署方式也都是类似的“一键部署”。关于这些开源项目的实现细节和差异,我会在后续的网络部分详细介绍。
|
||||
|
||||
至此,Kubernetes的Master节点就部署完成了。如果你只需要一个单节点的Kubernetes,现在你就可以使用了。不过,在默认情况下,Kubernetes的Master节点是不能运行用户Pod的,所以还需要额外做一个小操作。在本篇的最后部分,我会介绍到它。
|
||||
|
||||
## 部署Kubernetes的Worker节点
|
||||
|
||||
Kubernetes的Worker节点跟Master节点几乎是相同的,它们运行着的都是一个kubelet组件。唯一的区别在于,在kubeadm init的过程中,kubelet启动后,Master节点上还会自动运行kube-apiserver、kube-scheduler、kube-controller-manger这三个系统Pod。
|
||||
|
||||
所以,相比之下,部署Worker节点反而是最简单的,只需要两步即可完成。
|
||||
|
||||
第一步,在所有Worker节点上执行“安装kubeadm和Docker”一节的所有步骤。
|
||||
|
||||
第二步,执行部署Master节点时生成的kubeadm join指令:
|
||||
|
||||
```
|
||||
$ kubeadm join 10.168.0.2:6443 --token 00bwbx.uvnaa2ewjflwu1ry --discovery-token-ca-cert-hash sha256:00eb62a2a6020f94132e3fe1ab721349bbcd3e9b94da9654cfe15f2985ebd711
|
||||
|
||||
```
|
||||
|
||||
## 通过Taint/Toleration调整Master执行Pod的策略
|
||||
|
||||
我在前面提到过,默认情况下Master节点是不允许运行用户Pod的。而Kubernetes做到这一点,依靠的是Kubernetes的Taint/Toleration机制。
|
||||
|
||||
它的原理非常简单:一旦某个节点被加上了一个Taint,即被“打上了污点”,那么所有Pod就都不能在这个节点上运行,因为Kubernetes的Pod都有“洁癖”。
|
||||
|
||||
除非,有个别的Pod声明自己能“容忍”这个“污点”,即声明了Toleration,它才可以在这个节点上运行。
|
||||
|
||||
其中,为节点打上“污点”(Taint)的命令是:
|
||||
|
||||
```
|
||||
$ kubectl taint nodes node1 foo=bar:NoSchedule
|
||||
|
||||
```
|
||||
|
||||
这时,该node1节点上就会增加一个键值对格式的Taint,即:foo=bar:NoSchedule。其中值里面的NoSchedule,意味着这个Taint只会在调度新Pod时产生作用,而不会影响已经在node1上运行的Pod,哪怕它们没有Toleration。
|
||||
|
||||
那么Pod又如何声明Toleration呢?
|
||||
|
||||
我们只要在Pod的.yaml文件中的spec部分,加入tolerations字段即可:
|
||||
|
||||
```
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
...
|
||||
spec:
|
||||
tolerations:
|
||||
- key: "foo"
|
||||
operator: "Equal"
|
||||
value: "bar"
|
||||
effect: "NoSchedule"
|
||||
|
||||
```
|
||||
|
||||
这个Toleration的含义是,这个Pod能“容忍”所有键值对为foo=bar的Taint( operator: “Equal”,“等于”操作)。
|
||||
|
||||
现在回到我们已经搭建的集群上来。这时,如果你通过kubectl describe检查一下Master节点的Taint字段,就会有所发现了:
|
||||
|
||||
```
|
||||
$ kubectl describe node master
|
||||
|
||||
Name: master
|
||||
Roles: master
|
||||
Taints: node-role.kubernetes.io/master:NoSchedule
|
||||
|
||||
```
|
||||
|
||||
可以看到,Master节点默认被加上了`node-role.kubernetes.io/master:NoSchedule`这样一个“污点”,其中“键”是`node-role.kubernetes.io/master`,而没有提供“值”。
|
||||
|
||||
此时,你就需要像下面这样用“Exists”操作符(operator: “Exists”,“存在”即可)来说明,该Pod能够容忍所有以foo为键的Taint,才能让这个Pod运行在该Master节点上:
|
||||
|
||||
```
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
...
|
||||
spec:
|
||||
tolerations:
|
||||
- key: "foo"
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
|
||||
```
|
||||
|
||||
当然,如果你就是想要一个单节点的Kubernetes,删除这个Taint才是正确的选择:
|
||||
|
||||
```
|
||||
$ kubectl taint nodes --all node-role.kubernetes.io/master-
|
||||
|
||||
```
|
||||
|
||||
如上所示,我们在“`node-role.kubernetes.io/master`”这个键后面加上了一个短横线“-”,这个格式就意味着移除所有以“`node-role.kubernetes.io/master`”为键的Taint。
|
||||
|
||||
到了这一步,一个基本完整的Kubernetes集群就部署完毕了。是不是很简单呢?
|
||||
|
||||
有了kubeadm这样的原生管理工具,Kubernetes的部署已经被大大简化。更重要的是,像证书、授权、各个组件的配置等部署中最麻烦的操作,kubeadm都已经帮你完成了。
|
||||
|
||||
接下来,我们再在这个Kubernetes集群上安装一些其他的辅助插件,比如Dashboard和存储插件。
|
||||
|
||||
## 部署Dashboard可视化插件
|
||||
|
||||
在Kubernetes社区中,有一个很受欢迎的Dashboard项目,它可以给用户提供一个可视化的Web界面来查看当前集群的各种信息。毫不意外,它的部署也相当简单:
|
||||
|
||||
```
|
||||
$ kubectl apply -f
|
||||
$ $ kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc6/aio/deploy/recommended.yaml
|
||||
|
||||
```
|
||||
|
||||
部署完成之后,我们就可以查看Dashboard对应的Pod的状态了:
|
||||
|
||||
```
|
||||
$ kubectl get pods -n kube-system
|
||||
|
||||
kubernetes-dashboard-6948bdb78-f67xk 1/1 Running 0 1m
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,由于Dashboard是一个Web Server,很多人经常会在自己的公有云上无意地暴露Dashboard的端口,从而造成安全隐患。所以,1.7版本之后的Dashboard项目部署完成后,默认只能通过Proxy的方式在本地访问。具体的操作,你可以查看Dashboard项目的[官方文档](https://github.com/kubernetes/dashboard)。
|
||||
|
||||
而如果你想从集群外访问这个Dashboard的话,就需要用到Ingress,我会在后面的文章中专门介绍这部分内容。
|
||||
|
||||
## 部署容器存储插件
|
||||
|
||||
接下来,让我们完成这个Kubernetes集群的最后一块拼图:容器持久化存储。
|
||||
|
||||
我在前面介绍容器原理时已经提到过,很多时候我们需要用数据卷(Volume)把外面宿主机上的目录或者文件挂载进容器的Mount Namespace中,从而达到容器和宿主机共享这些目录或者文件的目的。容器里的应用,也就可以在这些数据卷中新建和写入文件。
|
||||
|
||||
可是,如果你在某一台机器上启动的一个容器,显然无法看到其他机器上的容器在它们的数据卷里写入的文件。**这是容器最典型的特征之一:无状态。**
|
||||
|
||||
而容器的持久化存储,就是用来保存容器存储状态的重要手段:存储插件会在容器里挂载一个基于网络或者其他机制的远程数据卷,使得在容器里创建的文件,实际上是保存在远程存储服务器上,或者以分布式的方式保存在多个节点上,而与当前宿主机没有任何绑定关系。这样,无论你在其他哪个宿主机上启动新的容器,都可以请求挂载指定的持久化存储卷,从而访问到数据卷里保存的内容。**这就是“持久化”的含义。**
|
||||
|
||||
由于Kubernetes本身的松耦合设计,绝大多数存储项目,比如Ceph、GlusterFS、NFS等,都可以为Kubernetes提供持久化存储能力。在这次的部署实战中,我会选择部署一个很重要的Kubernetes存储插件项目:Rook。
|
||||
|
||||
Rook项目是一个基于Ceph的Kubernetes存储插件(它后期也在加入对更多存储实现的支持)。不过,不同于对Ceph的简单封装,Rook在自己的实现中加入了水平扩展、迁移、灾难备份、监控等大量的企业级功能,使得这个项目变成了一个完整的、生产级别可用的容器存储插件。
|
||||
|
||||
得益于容器化技术,用几条指令,Rook就可以把复杂的Ceph存储后端部署起来:
|
||||
|
||||
```
|
||||
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/master/cluster/examples/kubernetes/ceph/common.yaml
|
||||
|
||||
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/master/cluster/examples/kubernetes/ceph/operator.yaml
|
||||
|
||||
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/master/cluster/examples/kubernetes/ceph/cluster.yaml
|
||||
|
||||
```
|
||||
|
||||
在部署完成后,你就可以看到Rook项目会将自己的Pod放置在由它自己管理的两个Namespace当中:
|
||||
|
||||
```
|
||||
$ kubectl get pods -n rook-ceph-system
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
rook-ceph-agent-7cv62 1/1 Running 0 15s
|
||||
rook-ceph-operator-78d498c68c-7fj72 1/1 Running 0 44s
|
||||
rook-discover-2ctcv 1/1 Running 0 15s
|
||||
|
||||
$ kubectl get pods -n rook-ceph
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
rook-ceph-mon0-kxnzh 1/1 Running 0 13s
|
||||
rook-ceph-mon1-7dn2t 1/1 Running 0 2s
|
||||
|
||||
```
|
||||
|
||||
这样,一个基于Rook持久化存储集群就以容器的方式运行起来了,而接下来在Kubernetes项目上创建的所有Pod就能够通过Persistent Volume(PV)和Persistent Volume Claim(PVC)的方式,在容器里挂载由Ceph提供的数据卷了。
|
||||
|
||||
而Rook项目,则会负责这些数据卷的生命周期管理、灾难备份等运维工作。关于这些容器持久化存储的知识,我会在后续章节中专门讲解。
|
||||
|
||||
这时候,你可能会有个疑问:为什么我要选择Rook项目呢?
|
||||
|
||||
其实,是因为这个项目很有前途。
|
||||
|
||||
如果你去研究一下Rook项目的实现,就会发现它巧妙地依赖了Kubernetes提供的编排能力,合理的使用了很多诸如Operator、CRD等重要的扩展特性(这些特性我都会在后面的文章中逐一讲解到)。这使得Rook项目,成为了目前社区中基于Kubernetes API构建的最完善也最成熟的容器存储插件。我相信,这样的发展路线,很快就会得到整个社区的推崇。
|
||||
|
||||
>
|
||||
备注:其实,在很多时候,大家说的所谓“云原生”,就是“Kubernetes原生”的意思。而像Rook、Istio这样的项目,正是贯彻这个思路的典范。在我们后面讲解了声明式API之后,相信你对这些项目的设计思想会有更深刻的体会。
|
||||
|
||||
|
||||
## 总结
|
||||
|
||||
在本篇文章中,我们完全从0开始,在Bare-metal环境下使用kubeadm工具部署了一个完整的Kubernetes集群:这个集群有一个Master节点和多个Worker节点;使用Weave作为容器网络插件;使用Rook作为容器持久化存储插件;使用Dashboard插件提供了可视化的Web界面。
|
||||
|
||||
这个集群,也将会是我进行后续讲解所依赖的集群环境,并且在后面的讲解中,我还会给它安装更多的插件,添加更多的新能力。
|
||||
|
||||
另外,这个集群的部署过程并不像传说中那么繁琐,这主要得益于:
|
||||
|
||||
<li>
|
||||
kubeadm项目大大简化了部署Kubernetes的准备工作,尤其是配置文件、证书、二进制文件的准备和制作,以及集群版本管理等操作,都被kubeadm接管了。
|
||||
</li>
|
||||
<li>
|
||||
Kubernetes本身“一切皆容器”的设计思想,加上良好的可扩展机制,使得插件的部署非常简便。
|
||||
</li>
|
||||
|
||||
上述思想,也是开发和使用Kubernetes的重要指导思想,即:基于Kubernetes开展工作时,你一定要优先考虑这两个问题:
|
||||
|
||||
<li>
|
||||
我的工作是不是可以容器化?
|
||||
</li>
|
||||
<li>
|
||||
我的工作是不是可以借助Kubernetes API和可扩展机制来完成?
|
||||
</li>
|
||||
|
||||
而一旦这项工作能够基于Kubernetes实现容器化,就很有可能像上面的部署过程一样,大幅简化原本复杂的运维工作。对于时间宝贵的技术人员来说,这个变化的重要性是不言而喻的。
|
||||
|
||||
## 思考题
|
||||
|
||||
<li>
|
||||
你是否使用其他工具部署过Kubernetes项目?经历如何?
|
||||
</li>
|
||||
<li>
|
||||
你是否知道Kubernetes项目当前(v1.11)能够有效管理的集群规模是多少个节点?你在生产环境中希望部署或者正在部署的集群规模又是多少个节点呢?
|
||||
</li>
|
||||
|
||||
感谢你的收听,欢迎你给我留言,也欢迎分享给更多的朋友一起阅读。
|
||||
350
极客时间专栏/深入剖析Kubernetes/Kubernetes集群搭建与实践/12 | 牛刀小试:我的第一个容器化应用.md
Normal file
350
极客时间专栏/深入剖析Kubernetes/Kubernetes集群搭建与实践/12 | 牛刀小试:我的第一个容器化应用.md
Normal file
@@ -0,0 +1,350 @@
|
||||
<audio id="audio" title="12 | 牛刀小试:我的第一个容器化应用" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/06/5a/068473ddafac8ef5798b843cddccf65a.mp3"></audio>
|
||||
|
||||
你好,我是张磊。今天我和你分享的主题是:牛刀小试之我的第一个容器化应用。
|
||||
|
||||
在上一篇文章《从0到1:搭建一个完整的Kubernetes集群》中,我和你一起部署了一套完整的Kubernetes集群。这个集群虽然离生产环境的要求还有一定差距(比如,没有一键高可用部署),但也可以当作是一个准生产级别的Kubernetes集群了。
|
||||
|
||||
而在这篇文章中,我们就来扮演一个应用开发者的角色,使用这个Kubernetes集群发布第一个容器化应用。
|
||||
|
||||
在开始实践之前,我先给你讲解一下Kubernetes里面与开发者关系最密切的几个概念。
|
||||
|
||||
作为一个应用开发者,你首先要做的,是制作容器的镜像。这一部分内容,我已经在容器基础部分[《白话容器基础(三):深入理解容器镜像》](https://time.geekbang.org/column/article/17921)重点讲解过了。
|
||||
|
||||
而有了容器镜像之后,你需要按照Kubernetes项目的规范和要求,将你的镜像组织为它能够“认识”的方式,然后提交上去。
|
||||
|
||||
那么,什么才是Kubernetes项目能“认识”的方式呢?
|
||||
|
||||
这就是使用Kubernetes的必备技能:编写配置文件。
|
||||
|
||||
>
|
||||
备注:这些配置文件可以是YAML或者JSON格式的。为方便阅读与理解,在后面的讲解中,我会统一使用YAML文件来指代它们。
|
||||
|
||||
|
||||
Kubernetes跟Docker等很多项目最大的不同,就在于它不推荐你使用命令行的方式直接运行容器(虽然Kubernetes项目也支持这种方式,比如:kubectl run),而是希望你用YAML文件的方式,即:把容器的定义、参数、配置,统统记录在一个YAML文件中,然后用这样一句指令把它运行起来:
|
||||
|
||||
```
|
||||
$ kubectl create -f 我的配置文件
|
||||
|
||||
```
|
||||
|
||||
这么做最直接的好处是,你会有一个文件能记录下Kubernetes到底“run”了什么。比如下面这个例子:
|
||||
|
||||
```
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
replicas: 2
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.7.9
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
||||
```
|
||||
|
||||
像这样的一个YAML文件,对应到Kubernetes中,就是一个API Object(API对象)。当你为这个对象的各个字段填好值并提交给Kubernetes之后,Kubernetes就会负责创建出这些对象所定义的容器或者其他类型的API资源。
|
||||
|
||||
可以看到,这个YAML文件中的Kind字段,指定了这个API对象的类型(Type),是一个Deployment。
|
||||
|
||||
所谓Deployment,是一个定义多副本应用(即多个副本Pod)的对象,我在前面的文章中(也是第9篇文章《从容器到容器云:谈谈Kubernetes的本质》)曾经简单提到过它的用法。此外,Deployment还负责在Pod定义发生变化时,对每个副本进行滚动更新(Rolling Update)。
|
||||
|
||||
在上面这个YAML文件中,我给它定义的Pod副本个数(spec.replicas)是:2。
|
||||
|
||||
而这些Pod具体的又长什么样子呢?
|
||||
|
||||
为此,我定义了一个Pod模版(spec.template),这个模版描述了我想要创建的Pod的细节。在上面的例子里,这个Pod里只有一个容器,这个容器的镜像(spec.containers.image)是nginx:1.7.9,这个容器监听端口(containerPort)是80。
|
||||
|
||||
关于Pod的设计和用法我已经在第9篇文章[《从容器到容器云:谈谈Kubernetes的本质》](https://time.geekbang.org/column/article/23132)中简单的介绍过。而在这里,你需要记住这样一句话:
|
||||
|
||||
>
|
||||
Pod就是Kubernetes世界里的“应用”;而一个应用,可以由多个容器组成。
|
||||
|
||||
|
||||
需要注意的是,像这样使用一种API对象(Deployment)管理另一种API对象(Pod)的方法,在Kubernetes中,叫作“控制器”模式(controller pattern)。在我们的例子中,Deployment扮演的正是Pod的控制器的角色。关于Pod和控制器模式的更多细节,我会在后续编排部分做进一步讲解。
|
||||
|
||||
你可能还注意到,这样的每一个API对象都有一个叫作Metadata的字段,这个字段就是API对象的“标识”,即元数据,它也是我们从Kubernetes里找到这个对象的主要依据。这其中最主要使用到的字段是Labels。
|
||||
|
||||
顾名思义,Labels就是一组key-value格式的标签。而像Deployment这样的控制器对象,就可以通过这个Labels字段从Kubernetes中过滤出它所关心的被控制对象。
|
||||
|
||||
比如,在上面这个YAML文件中,Deployment会把所有正在运行的、携带“app: nginx”标签的Pod识别为被管理的对象,并确保这些Pod的总数严格等于两个。
|
||||
|
||||
而这个过滤规则的定义,是在Deployment的“spec.selector.matchLabels”字段。我们一般称之为:Label Selector。
|
||||
|
||||
另外,在Metadata中,还有一个与Labels格式、层级完全相同的字段叫Annotations,它专门用来携带key-value格式的内部信息。所谓内部信息,指的是对这些信息感兴趣的,是Kubernetes组件本身,而不是用户。所以大多数Annotations,都是在Kubernetes运行过程中,被自动加在这个API对象上。
|
||||
|
||||
一个Kubernetes的API对象的定义,大多可以分为Metadata和Spec两个部分。前者存放的是这个对象的元数据,对所有API对象来说,这一部分的字段和格式基本上是一样的;而后者存放的,则是属于这个对象独有的定义,用来描述它所要表达的功能。
|
||||
|
||||
在了解了上述Kubernetes配置文件的基本知识之后,我们现在就可以把这个YAML文件“运行”起来。正如前所述,你可以使用kubectl create指令完成这个操作:
|
||||
|
||||
```
|
||||
$ kubectl create -f nginx-deployment.yaml
|
||||
|
||||
```
|
||||
|
||||
然后,通过kubectl get命令检查这个YAML运行起来的状态是不是与我们预期的一致:
|
||||
|
||||
```
|
||||
$ kubectl get pods -l app=nginx
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
nginx-deployment-67594d6bf6-9gdvr 1/1 Running 0 10m
|
||||
nginx-deployment-67594d6bf6-v6j7w 1/1 Running 0 10m
|
||||
|
||||
```
|
||||
|
||||
kubectl get指令的作用,就是从Kubernetes里面获取(GET)指定的API对象。可以看到,在这里我还加上了一个-l参数,即获取所有匹配app: nginx标签的Pod。需要注意的是,**在命令行中,所有key-value格式的参数,都使用“=”而非“:”表示。**
|
||||
|
||||
从这条指令返回的结果中,我们可以看到现在有两个Pod处于Running状态,也就意味着我们这个Deployment所管理的Pod都处于预期的状态。
|
||||
|
||||
此外, 你还可以使用kubectl describe命令,查看一个API对象的细节,比如:
|
||||
|
||||
```
|
||||
$ kubectl describe pod nginx-deployment-67594d6bf6-9gdvr
|
||||
Name: nginx-deployment-67594d6bf6-9gdvr
|
||||
Namespace: default
|
||||
Priority: 0
|
||||
PriorityClassName: <none>
|
||||
Node: node-1/10.168.0.3
|
||||
Start Time: Thu, 16 Aug 2018 08:48:42 +0000
|
||||
Labels: app=nginx
|
||||
pod-template-hash=2315082692
|
||||
Annotations: <none>
|
||||
Status: Running
|
||||
IP: 10.32.0.23
|
||||
Controlled By: ReplicaSet/nginx-deployment-67594d6bf6
|
||||
...
|
||||
Events:
|
||||
|
||||
Type Reason Age From Message
|
||||
|
||||
---- ------ ---- ---- -------
|
||||
|
||||
Normal Scheduled 1m default-scheduler Successfully assigned default/nginx-deployment-67594d6bf6-9gdvr to node-1
|
||||
Normal Pulling 25s kubelet, node-1 pulling image "nginx:1.7.9"
|
||||
Normal Pulled 17s kubelet, node-1 Successfully pulled image "nginx:1.7.9"
|
||||
Normal Created 17s kubelet, node-1 Created container
|
||||
Normal Started 17s kubelet, node-1 Started container
|
||||
|
||||
```
|
||||
|
||||
在kubectl describe命令返回的结果中,你可以清楚地看到这个Pod的详细信息,比如它的IP地址等等。其中,有一个部分值得你特别关注,它就是**Events(事件)。**
|
||||
|
||||
在Kubernetes执行的过程中,对API对象的所有重要操作,都会被记录在这个对象的Events里,并且显示在kubectl describe指令返回的结果中。
|
||||
|
||||
比如,对于这个Pod,我们可以看到它被创建之后,被调度器调度(Successfully assigned)到了node-1,拉取了指定的镜像(pulling image),然后启动了Pod里定义的容器(Started container)。
|
||||
|
||||
所以,这个部分正是我们将来进行Debug的重要依据。**如果有异常发生,你一定要第一时间查看这些Events**,往往可以看到非常详细的错误信息。
|
||||
|
||||
接下来,如果我们要对这个Nginx服务进行升级,把它的镜像版本从1.7.9升级为1.8,要怎么做呢?
|
||||
|
||||
很简单,我们只要修改这个YAML文件即可。
|
||||
|
||||
```
|
||||
...
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.8 #这里被从1.7.9修改为1.8
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
||||
```
|
||||
|
||||
可是,这个修改目前只发生在本地,如何让这个更新在Kubernetes里也生效呢?
|
||||
|
||||
我们可以使用kubectl replace指令来完成这个更新:
|
||||
|
||||
```
|
||||
$ kubectl replace -f nginx-deployment.yaml
|
||||
|
||||
```
|
||||
|
||||
不过,在本专栏里,我推荐你使用kubectl apply命令,来统一进行Kubernetes对象的创建和更新操作,具体做法如下所示:
|
||||
|
||||
```
|
||||
$ kubectl apply -f nginx-deployment.yaml
|
||||
|
||||
# 修改nginx-deployment.yaml的内容
|
||||
|
||||
$ kubectl apply -f nginx-deployment.yaml
|
||||
|
||||
```
|
||||
|
||||
这样的操作方法,是Kubernetes“声明式API”所推荐的使用方法。也就是说,作为用户,你不必关心当前的操作是创建,还是更新,你执行的命令始终是kubectl apply,而Kubernetes则会根据YAML文件的内容变化,自动进行具体的处理。
|
||||
|
||||
而这个流程的好处是,它有助于帮助开发和运维人员,围绕着可以版本化管理的YAML文件,而不是“行踪不定”的命令行进行协作,从而大大降低开发人员和运维人员之间的沟通成本。
|
||||
|
||||
举个例子,一位开发人员开发好一个应用,制作好了容器镜像。那么他就可以在应用的发布目录里附带上一个Deployment的YAML文件。
|
||||
|
||||
而运维人员,拿到这个应用的发布目录后,就可以直接用这个YAML文件执行kubectl apply操作把它运行起来。
|
||||
|
||||
这时候,如果开发人员修改了应用,生成了新的发布内容,那么这个YAML文件,也就需要被修改,并且成为这次变更的一部分。
|
||||
|
||||
而接下来,运维人员可以使用git diff命令查看到这个YAML文件本身的变化,然后继续用kubectl apply命令更新这个应用。
|
||||
|
||||
所以说,如果通过容器镜像,我们能够保证应用本身在开发与部署环境里的一致性的话,那么现在,Kubernetes项目通过这些YAML文件,就保证了应用的“部署参数”在开发与部署环境中的一致性。
|
||||
|
||||
**而当应用本身发生变化时,开发人员和运维人员可以依靠容器镜像来进行同步;当应用部署参数发生变化时,这些YAML文件就是他们相互沟通和信任的媒介。**
|
||||
|
||||
以上,就是Kubernetes发布应用的最基本操作了。
|
||||
|
||||
接下来,我们再在这个Deployment中尝试声明一个Volume。
|
||||
|
||||
在Kubernetes中,Volume是属于Pod对象的一部分。所以,我们就需要修改这个YAML文件里的template.spec字段,如下所示:
|
||||
|
||||
```
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
replicas: 2
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.8
|
||||
ports:
|
||||
- containerPort: 80
|
||||
volumeMounts:
|
||||
- mountPath: "/usr/share/nginx/html"
|
||||
name: nginx-vol
|
||||
volumes:
|
||||
- name: nginx-vol
|
||||
emptyDir: {}
|
||||
|
||||
```
|
||||
|
||||
可以看到,我们在Deployment的Pod模板部分添加了一个volumes字段,定义了这个Pod声明的所有Volume。它的名字叫作nginx-vol,类型是emptyDir。
|
||||
|
||||
那什么是emptyDir类型呢?
|
||||
|
||||
它其实就等同于我们之前讲过的Docker的隐式Volume参数,即:不显式声明宿主机目录的Volume。所以,Kubernetes也会在宿主机上创建一个临时目录,这个目录将来就会被绑定挂载到容器所声明的Volume目录上。
|
||||
|
||||
>
|
||||
备注:不难看到,Kubernetes的emptyDir类型,只是把Kubernetes创建的临时目录作为Volume的宿主机目录,交给了Docker。这么做的原因,是Kubernetes不想依赖Docker自己创建的那个_data目录。
|
||||
|
||||
|
||||
而Pod中的容器,使用的是volumeMounts字段来声明自己要挂载哪个Volume,并通过mountPath字段来定义容器内的Volume目录,比如:/usr/share/nginx/html。
|
||||
|
||||
当然,Kubernetes也提供了显式的Volume定义,它叫作hostPath。比如下面的这个YAML文件:
|
||||
|
||||
```
|
||||
...
|
||||
volumes:
|
||||
- name: nginx-vol
|
||||
hostPath:
|
||||
path: " /var/data"
|
||||
|
||||
```
|
||||
|
||||
这样,容器Volume挂载的宿主机目录,就变成了/var/data。
|
||||
|
||||
在上述修改完成后,我们还是使用kubectl apply指令,更新这个Deployment:
|
||||
|
||||
```
|
||||
$ kubectl apply -f nginx-deployment.yaml
|
||||
|
||||
```
|
||||
|
||||
接下来,你可以通过kubectl get指令,查看两个Pod被逐一更新的过程:
|
||||
|
||||
```
|
||||
$ kubectl get pods
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
nginx-deployment-5c678cfb6d-v5dlh 0/1 ContainerCreating 0 4s
|
||||
nginx-deployment-67594d6bf6-9gdvr 1/1 Running 0 10m
|
||||
nginx-deployment-67594d6bf6-v6j7w 1/1 Running 0 10m
|
||||
$ kubectl get pods
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
nginx-deployment-5c678cfb6d-lg9lw 1/1 Running 0 8s
|
||||
nginx-deployment-5c678cfb6d-v5dlh 1/1 Running 0 19s
|
||||
|
||||
```
|
||||
|
||||
从返回结果中,我们可以看到,新旧两个Pod,被交替创建、删除,最后剩下的就是新版本的Pod。这个滚动更新的过程,我也会在后续进行详细的讲解。
|
||||
|
||||
然后,你可以使用kubectl describe查看一下最新的Pod,就会发现Volume的信息已经出现在了Container描述部分:
|
||||
|
||||
```
|
||||
...
|
||||
Containers:
|
||||
nginx:
|
||||
Container ID: docker://07b4f89248791c2aa47787e3da3cc94b48576cd173018356a6ec8db2b6041343
|
||||
Image: nginx:1.8
|
||||
...
|
||||
Environment: <none>
|
||||
Mounts:
|
||||
/usr/share/nginx/html from nginx-vol (rw)
|
||||
...
|
||||
Volumes:
|
||||
nginx-vol:
|
||||
Type: EmptyDir (a temporary directory that shares a pod's lifetime)
|
||||
|
||||
```
|
||||
|
||||
>
|
||||
备注:作为一个完整的容器化平台项目,Kubernetes为我们提供的Volume类型远远不止这些,在容器存储章节里,我将会为你详细介绍这部分内容。
|
||||
|
||||
|
||||
最后,你还可以使用kubectl exec指令,进入到这个Pod当中(即容器的Namespace中)查看这个Volume目录:
|
||||
|
||||
```
|
||||
$ kubectl exec -it nginx-deployment-5c678cfb6d-lg9lw -- /bin/bash
|
||||
# ls /usr/share/nginx/html
|
||||
|
||||
```
|
||||
|
||||
此外,你想要从Kubernetes集群中删除这个Nginx Deployment的话,直接执行:
|
||||
|
||||
```
|
||||
$ kubectl delete -f nginx-deployment.yaml
|
||||
|
||||
```
|
||||
|
||||
就可以了。
|
||||
|
||||
## 总结
|
||||
|
||||
在今天的分享中,我通过一个小案例,和你近距离体验了Kubernetes的使用方法。
|
||||
|
||||
可以看到,Kubernetes推荐的使用方式,是用一个YAML文件来描述你所要部署的API对象。然后,统一使用kubectl apply命令完成对这个对象的创建和更新操作。
|
||||
|
||||
而Kubernetes里“最小”的API对象是Pod。Pod可以等价为一个应用,所以,Pod可以由多个紧密协作的容器组成。
|
||||
|
||||
在Kubernetes中,我们经常会看到它通过一种API对象来管理另一种API对象,比如Deployment和Pod之间的关系;而由于Pod是“最小”的对象,所以它往往都是被其他对象控制的。这种组合方式,正是Kubernetes进行容器编排的重要模式。
|
||||
|
||||
而像这样的Kubernetes API对象,往往由Metadata和Spec两部分组成,其中Metadata里的Labels字段是Kubernetes过滤对象的主要手段。
|
||||
|
||||
在这些字段里面,容器想要使用的数据卷,也就是Volume,正是Pod的Spec字段的一部分。而Pod里的每个容器,则需要显式的声明自己要挂载哪个Volume。
|
||||
|
||||
上面这些基于YAML文件的容器管理方式,跟Docker、Mesos的使用习惯都是不一样的,而从docker run这样的命令行操作,向kubectl apply YAML文件这样的声明式API的转变,是每一个容器技术学习者,必须要跨过的第一道门槛。
|
||||
|
||||
所以,如果你想要快速熟悉Kubernetes,请按照下面的流程进行练习:
|
||||
|
||||
- 首先,在本地通过Docker测试代码,制作镜像;
|
||||
- 然后,选择合适的Kubernetes API对象,编写对应YAML文件(比如,Pod,Deployment);
|
||||
- 最后,在Kubernetes上部署这个YAML文件。
|
||||
|
||||
更重要的是,在部署到Kubernetes之后,接下来的所有操作,要么通过kubectl来执行,要么通过修改YAML文件来实现,**就尽量不要再碰Docker的命令行了**。
|
||||
|
||||
## 思考题
|
||||
|
||||
在实际使用Kubernetes的过程中,相比于编写一个单独的Pod的YAML文件,我一定会推荐你使用一个replicas=1的Deployment。请问,这两者有什么区别呢?
|
||||
|
||||
感谢你的收听,欢迎你给我留言,也欢迎分享给更多的朋友一起阅读。
|
||||
Reference in New Issue
Block a user