1. Pod 使用
Pod 的使用,其实更多是指如何使用 Pod 的字段,你还应该认真体会一下 Kubernetes “一切皆对象”的设计思想:比如应用是 Pod 对象,应用的配置是 ConfigMap 对象,应用要访问的密码则是 Secret 对象。所以,也就自然而然地有了 PodPreset 这样专门用来对 Pod 进行批量化、自动化修改的工具对象。
1.1. Pod 的基本使用
1.1.1. Containers
-
name,容器的名字。
-
image,指定使用的镜像。
-
imagePullPolicy 字段,它定义了镜像拉取的策略。ImagePullPolicy 的值默认是 Always,即每次创建 Pod 都重新拉取一次镜像。另外,当容器的镜像是类似于 nginx 或者 nginx:latest 这样的名字时,ImagePullPolicy 也会被认为 Always。而如果它的值被定义为 Never 或者 IfNotPresent,则意味着 Pod 永远不会主动拉取这个镜像,或者只在宿主机上不存在这个镜像时才拉取。
-
command,启动命令。
-
args,参数。
-
workingDir,容器的工作目录。
-
ports,容器要开放的端口。
-
env,要传入的环境变量。
-
volumeMounts,容器要挂载的 Volume。
-
Lifecycle 字段,它定义的是 Container Lifecycle Hooks。Container Lifecycle Hooks 的作用,是在容器状态发生变化时触发一系列“钩子”。比如,
postStart 指定的是在容器启动后,立刻执行一个指定的操作。需要明确的是,postStart 定义的操作,虽然是在 Docker 容器 ENTRYPOINT 执行之后,但它并不严格保证顺序。也就是说,在 postStart 启动时,ENTRYPOINT 有可能还没有结束。如果 postStart 执行超时或者错误,Kubernetes 也会在该 Pod 的 Events 中报出该容器启动失败的错误信息,导致 Pod 也处于失败的状态。
preStop 发生的时机,则是容器被杀死之前(比如,收到了 SIGKILL 信号)。而需要明确的是,preStop 操作的执行,是同步的。所以,它会阻塞当前的容器杀死流程,直到这个 Hook 定义操作完成之后,才允许容器被杀死,这跟 postStart 不一样。
我们来看这样一个例子:
apiVersion: v1
kind: Pod
metadata:
name: pod-demo
spec:
containers:
- name: string
image: string
imagePullPolicy: [Always | Never | IfNotPresent]
command: [string]
args: [string]
workingDir: string
ports:
- name: string
containerPort: int
hostPort: int
protocol: string
env:
- name: string
value: string
volumeMounts:
- name: string
mountPath: string
readOnly: boolean
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"]
可以阅读 vendor/k8s.io/api/core/v1/types.go 里 type Pod struct 的内容 ,尤其是 PodSpec 部分的内容。下次看到一个 Pod 的 YAML 文件时,可以不再需要查阅文档,而是直接查阅这个。
1.1.2. NodeName
一旦 Pod 的这个字段被赋值,Kubernetes 项目就会被认为这个 Pod 已经经过了调度,调度的结果就是赋值的节点名字。所以,这个字段一般由调度器负责设置,但用户也可以设置它来“骗过”调度器,当然这个做法一般是在测试或者调试的时候才会用到。
1.1.3. HostAliases
hostAliases 用于定义 Pod 里的 hosts 文件(比如 /etc/hosts)里的内容。
apiVersion: v1
kind: Pod
...
spec:
hostAliases:
- ip: "10.1.2.3"
hostnames:
- "foo.remote"
- "bar.remote"
...
在这个 Pod 的 YAML 文件中,我设置了一组 IP 和 hostname 的数据。这样,这个 Pod 启动后,/etc/hosts 文件的内容将,如下所示。其中,最下面两行记录,就是通过 HostAliases 字段为 Pod 设置的。
cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1 localhost
...
10.244.135.10 hostaliases-pod
10.1.2.3 foo.remote
10.1.2.3 bar.remote
需要指出的是,在 Kubernetes 项目中,如果要设置 hosts 文件里的内容,一定要通过这种方法。否则,如果直接修改了 hosts 文件的话,在 Pod 被删除重建之后,kubelet 会自动覆盖掉被修改的内容。
1.1.4. Linux Namespace 相关
Pod 的设计,就是要让它里面的容器尽可能多地共享 Linux Namespace,仅保留必要的隔离和限制能力。这样,Pod 模拟出的效果,就跟虚拟机里程序间的关系非常类似了。所以 Pod 中跟容器 Linux Namespace 相关的属性也一定是 Pod 级别。
1.1.4.1. shareProcessNamespace---Pod 内的容器共享 PID
举个例子,在下面这个 Pod 的 YAML 文件中,我定义了 shareProcessNamespace=true
,这就意味着这个 Pod 里的容器要共享 PID Namespace。
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
shareProcessNamespace: true
containers:
- name: nginx
image: nginx
- name: shell
image: busybox
stdin: true
tty: true
而在这个 YAML 文件中,我还定义了两个容器:一个是 nginx 容器,一个是开启了 tty 和 stdin 的 shell 容器。在 Pod 的 YAML 文件里声明开启它们俩,其实等同于设置了 docker run 里的 -it(-i 即 stdin,-t 即 tty)参数。于是,这个 Pod 被创建后,你就可以使用 shell 容器的 tty 跟这个容器进行交互了。我们一起实践一下:
# 创建并运行这个 Pod
$ kubectl create -f nginx.yaml
# 我们使用 kubectl attach 命令,连接到 shell 容器的 tty 上
$ kubectl attach -it nginx -c shell
可以看到,在这个容器里,我们不仅可以看到它本身的 ps ax 指令,还可以看到 nginx 容器的进程,以及 Infra 容器的 /pause 进程。这就意味着,整个 Pod 里的每个容器的进程,对于所有容器来说都是可见的,这是因为它们共享了同一个 PID Namespace。
$ kubectl attach -it nginx -c shell
/ # ps ax
PID USER TIME COMMAND
1 root 0:00 /pause
8 root 0:00 nginx: master process nginx -g daemon off;
14 101 0:00 nginx: worker process
15 root 0:00 sh
21 root 0:00 ps ax
1.1.4.2. hostNetwork/hostIPC/hostPID --- Pod 内的容器共享宿主机的 Namespace
凡是 Pod 中的容器要共享宿主机的 Namespace,也一定是 Pod 级别的定义,如下所示:在这个 Pod 中,定义了共享宿主机的 Network、IPC 和 PID Namespace。这就意味着,这个 Pod 里的所有容器,会直接使用宿主机的网络、直接与宿主机进行 IPC 通信、看到宿主机里正在运行的所有进程。
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
hostNetwork: true
hostIPC: true
hostPID: true
containers:
- name: nginx
image: nginx
- name: shell
image: busybox
stdin: true
tty: true
1.2. Pod Volume
pod volume 是 Pod 中能够被多个容器访问的共享目录:
-
Kubernetes 中的 volume 被定义在 pod 上,可以被同一个 Pod 中的多个容器共享,分别挂载到容器里具体的文件目录下;
-
Volume 与 Pod 的生命周期相同,但与容器的生命周期不相关,容器的终止和重启时,Volume 中的数据不会丢失;
-
Volume 可以被定义为各种类型,比如 emptyDir、Ceph 等。
示例 yaml 如下所示:
#chapter_3.4
apiVersion: v1
kind: Pod
metadata:
name: volume-pod
spec:
containers:
- name: tomcat
image: tomcat
ports:
- containerPort: 8080
volumeMounts:
- name: app-logs
mountPath: /usr/local/tomcat/logs #挂载Volume到tomcat容器的/usr/local/tomcat/logs目录
- name: busybox
image: busybox
command: ["sh", "-c", "tail -f /logs/catalina*.log"] #busybox容器作为logreader,执行命令tail -f /logs/catalina*.log
volumeMounts:
- name: app-logs
mountPath: /logs #挂载Volume到busybox容器的/logs目录
volumes:
- name: app-logs
emptyDir: {} #设置一个名为app-logs的emptyDir类型的Volume
1.2.1. emptyDir
emptyDir Volume 是在 Pod 被分配到 Node 时创建的,它的初始化内容为空,并且无须指定宿主机上对应的目录文件,kubernetes 会自动分配一个目录。当 Pod 从 Node 上移除时,emptyDir 中的数据也会被永久删除。
目前「kubernetes 1.14」,用户无法控制 emptyDir 使用的介质种类,如果 kubelet 的配置是使用硬盘,那么所有的 emptyDir 都会被创建在该硬盘上。Pod 在将来可以设置 emptyDir 是位于硬盘、固态硬盘还是基于内存的 tmpfs 上。
一般来说 emptyDir 的用途如下:
- 临时空间,例如用于某些应用程序运行时所需的临时目录,且无须永久保留;
- 长时间任务的中间过程 CheckPoint 的临时保存目录;
- 一个容器需要从另一个容器中获取数据的目录(多容器共享目录);
1.2.2. hostpath
hostpath 为 pod 挂载宿主机上的文件和目录。如下所示:
volumes:
- name: "hostpath"
hostpath:
path: "/data" # 宿主机上的 /data 目录
使用这种类型的 volume 需要注意:如果使用了资源配额管理,则 kubernetes 无法将 hostpath 在宿主机上使用的资源纳入管理。
一般来说 hostpath 可以用于以下几个方面:
- 容器应用程序生成的日志文件需要永久保存时,可以使用宿主机上的高速文件系统。
- 需要访问宿主机上 Docker 引擎内部数据结构时,可以通过定义 hostpath 为宿主机 /var/lib/docker 目录,从而使得容器内部应用可以直接访问 Docker 的文件系统。
1.2.3. Projected volume
Projected volume 是一种特殊的 volume,是 Kubernetes v1.11 之后的新特性。在 Kubernetes 中,这种特殊的 Volume 存在的意义不是为了存放容器里的数据,也不是用来进行容器和宿主机之间的数据交换。而是为容器提供预先定义好的数据。所以,从容器的角度来看,这些 Volume 里的信息就是仿佛是被 Kubernetes“投射”(Project)进入容器当中的。
到目前为止「2018-09 文章的参考」,Kubernetes 支持的 Projected Volume 有以下几种:Secret、ConfigMap 以及 Downward API。这三种 Projected Volume 定义的信息,大多还可以通过环境变量的方式出现在容器里。但是,通过环境变量获取这些信息的方式,不具备自动更新的能力。所以,一般情况下,我都建议你使用 Volume 文件的方式获取这些信息。
1.2.3.1. Secret
Secret 的作用,是帮你把 Pod 想要访问的加密数据,存放到 Etcd 中。然后,你就可以通过在 Pod 的容器里挂载 Volume 的方式,访问到这些 Secret 里保存的信息了。
Secret 最典型的使用场景,莫过于存放数据库的 Credential 信息。
1.2.3.1.1. 创建 Secret
首先创建数据库的用户名、密码,将它们以 Secret 对象的方式交给 Kubernetes 保存的。有两种方式:
-
命令的方式
其中,username.txt 和 password.txt 文件里,存放的就是用户名和密码;而 user 和 pass,则是我为 Secret 对象指定的名字。
$ cat ./username.txt admin $ cat ./password.txt c1oudc0w! $ kubectl create secret generic user --from-file=./username.txt $ kubectl create secret generic pass --from-file=./password.txt # 我们可以通过 kubectl get 命令来看这些 Secret 对象 $ kubectl get secrets NAME TYPE DATA AGE user Opaque 1 51s pass Opaque 1 51s
-
yaml 的方式
除了使用 kubectl create secret 指令外,我也可以直接通过编写 YAML 文件的方式来创建这个 Secret 对象,如下所示。可以看到,通过编写 YAML 文件创建出来的 Secret 对象只有一个。但它的 data 字段,却以 Key-Value 的格式保存了两份 Secret 数据。其中,“user”就是第一份数据的 Key,“pass”是第二份数据的 Key。需要注意的是,Secret 对象要求这些数据必须是经过 Base64 转码的,以免出现明文密码的安全隐患。
这里需要注意的是,像这样创建的 Secret 对象,它里面的内容仅仅是经过了转码,而并没有被加密。在真正的生产环境中,你需要在 Kubernetes 中开启 Secret 的加密插件,增强数据的安全性。
apiVersion: v1 kind: Secret metadata: name: mysecret type: Opaque data: user: YWRtaW4= pass: MWYyZDFlMmU2N2Rm
$ echo -n 'admin' | base64 YWRtaW4= $ echo -n '1f2d1e2e67df' | base64 MWYyZDFlMmU2N2Rm
1.2.3.1.2. 使用 Secret
在这个 Pod 中,定义了一个简单的容器。它声明挂载的 Volume,并不是常见的 emptyDir 或者 hostPath 类型,而是 projected 类型。而这个 Volume 的数据来源(sources),则是名为 user 和 pass 的 Secret 对象,分别对应的是数据库的用户名和密码。
apiVersion: v1
kind: Pod
metadata:
name: test-projected-volume
spec:
containers:
- name: test-secret-volume
image: busybox
args:
- sleep
- "86400"
volumeMounts:
- name: mysql-cred
mountPath: "/projected-volume"
readOnly: true
volumes:
- name: mysql-cred
projected:
sources:
- secret:
name: user
- secret:
name: pass
之后创建这个 Pod,当 Pod 变成 Running 状态之后,我们再验证一下这些 Secret 对象是不是已经在容器里了。从返回结果中,我们可以看到,保存在 Etcd 里的用户名和密码信息,已经以文件的形式出现在了容器的 Volume 目录里。而这个文件的名字,就是 kubectl create secret 指定的 Key,或者说是 Secret 对象的 data 字段指定的 Key。更重要的是,像这样通过挂载方式进入到容器里的 Secret,一旦其对应的 Etcd 里的数据被更新,这些 Volume 里的文件内容,同样也会被更新。这是 kubelet 组件在定时维护这些 Volume。
需要注意的是,这个更新可能会有一定的延时。所以在编写应用程序时,在发起数据库连接的代码处写好重试和超时的逻辑,绝对是个好习惯。
$ kubectl create -f test-projected-volume.yaml
$ kubectl exec -it test-projected-volume -- /bin/sh
$ ls /projected-volume/
user
pass
$ cat /projected-volume/user
root
$ cat /projected-volume/pass
1f2d1e2e67df
1.2.3.1.3. ServiceAccountToken
Service Account 对象的作用,就是 Kubernetes 系统内置的一种“服务账户”,它是 Kubernetes 进行权限分配的对象。比如,Service Account A,可以只被允许对 Kubernetes API 进行 GET 操作,而 Service Account B,则可以有 Kubernetes API 的所有操作权限。
像这样的 Service Account 的授权信息和文件,实际上保存在它所绑定的一个特殊的 Secret 对象里的。这个特殊的 Secret 对象,就叫作 ServiceAccountToken。任何运行在 Kubernetes 集群上的应用,都必须使用这个 ServiceAccountToken 里保存的授权信息,也就是 Token,才可以合法地访问 API Server。
另外,为了方便使用,Kubernetes 已经为你提供了一个默认“服务账户”(default Service Account)。并且,任何一个运行在 Kubernetes 里的 Pod,都可以直接使用这个默认的 Service Account,而无需显示地声明挂载它。如果你查看一下任意一个运行在 Kubernetes 集群里的 Pod,就会发现,每一个 Pod,都已经自动声明一个类型是 Secret、名为 default-token-xxxx 的 Volume,然后自动挂载在每个容器的一个固定目录上,如下所示:这个 Secret 类型的 Volume,正是默认 Service Account 对应的 ServiceAccountToken。所以说,Kubernetes 其实在每个 Pod 创建的时候,自动在它的 spec.volumes 部分添加上了默认 ServiceAccountToken 的定义,然后自动给每个容器加上了对应的 volumeMounts 字段。这个过程对于用户来说是完全透明的。
$ kubectl describe pod nginx-deployment-5c678cfb6d-lg9lw
Containers:
...
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-s8rbq (ro)
Volumes:
default-token-s8rbq:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-s8rbq
Optional: false
这样,一旦 Pod 创建完成,容器里的应用就可以直接从这个默认 ServiceAccountToken 的挂载目录里访问到授权信息和文件。
这个容器内的路径在 Kubernetes 里是固定的,即:/var/run/secrets/kubernetes.io/serviceaccount ,而这个 Secret 类型的 Volume 里面的内容如下所示。所以,你的应用程序只要直接加载这些授权文件,就可以访问并操作 Kubernetes API 了。而且,如果你使用的是 Kubernetes 官方的 Client 包(k8s.io/client-go)的话,它还可以自动加载这个目录下的文件,你不需要做任何配置或者编码操作。
$ ls /var/run/secrets/kubernetes.io/serviceaccount
ca.crt namespace token
这种把 Kubernetes 客户端以容器的方式运行在集群里,然后使用 default Service Account 自动授权的方式,被称作“InClusterConfig”,也是张磊老师最推荐的进行 Kubernetes API 编程的授权方式。
考虑到自动挂载默认 ServiceAccountToken 的潜在风险,Kubernetes 允许你设置默认不为 Pod 里的容器自动挂载这个 Volume。
除了这个默认的 Service Account 外,很多时候还可以创建一些我们自己定义的 Service Account,来对应不同的权限设置。这样,我们的 Pod 里的容器就可以通过挂载这些 Service Account 对应的 ServiceAccountToken,来使用这些自定义的授权信息。
1.2.3.2. ConfigMap
将应用所需的配置信息与程序分离是应用部署的一种最佳实践,kubernetes 1.2 之后提供了一种统一的应用配置管理方案---ConfigMap。ConfigMap 以一个或多个 key:value 的形式保存在 kubernetes 中供应用使用,既可以表示一个变量的值(app=info),也可以表示一完整配置文件的内容(server.xml=<?xml...>...)。
ConfigMap 的典型使用场景如下:
- 生成容器内的环境变量;
- 设置容器启动命令的启动参数(需设置为环境变量)
- 以 Volume 的形式挂载为容器内部的文件或目录
ConfigMap 与 Secret 的区别在于,ConfigMap 保存的是不需要加密的、应用所需的配置信息。而相比环境变量的方式,ConfigMap 更加灵活,而通过环境变量获取这些信息的方式,不具备自动更新的能力。
1.2.3.2.1. 创建
ConfigMap 的创建方法几乎与 Secret 完全相同:你可以使用 kubectl create configmap 从文件或者目录创建 ConfigMap,也可以直接编写 ConfigMap 对象的 YAML 文件。
-
使用 kubectl create configmap 从文件或者目录创建 ConfigMap。--from-file 是指从文件或者目录中创建,--from-literal 是指从文本中创建
# .properties文件的内容 $ cat example/ui.properties color.good=purple color.bad=yellow allow.textmode=true how.nice.to.look=fairlyNice # 从.properties文件创建ConfigMap $ kubectl create configmap ui-config --from-file=example/ui.properties # 查看这个ConfigMap里保存的信息(data)。kubectl get -o yaml 这样的参数,会将指定的 Pod API 对象以 YAML 的方式展示出来。 $ kubectl get configmaps ui-config -o yaml apiVersion: v1 data: ui.properties: | color.good=purple color.bad=yellow allow.textmode=true how.nice.to.look=fairlyNice kind: ConfigMap metadata: name: ui-config # 可以指定 key 的名称,并且可以包含多个 key 的 ConfigMap。没有指定 key 的话,文件名则是 key,value 则是文件的内容 $ kubectl create configmap ui-config --from-file=[key=]source --from-file=[key=]source # 从目录中进行创建,该目录下的每个配置文件名都被设置为 key,文件内容被设置为 value $ kubectl create configmap NAME --from-file=config-file-dir # 从文本中进行创建,直接将指定的 key=value 创建为 configmap 的内容 $ kubectl create configmap NAME --from-literal=key1=value1 --from-literal=key2=value2
-
YAML 文件的方式
apiVersion: v1 kind: ConfigMap metadata: name: ui-config data: // key 是 ui.properties ui.properties: | color.good=purple color.bad=yellow allow.textmode=true how.nice.to.look=fairlyNice --- apiVersion: v1 kind: ConfigMap metadata: name: ui-config data: // key 分别 color.good、color.bad color.good: purple color.bad: yellow
1.2.3.2.2. 使用
-
通过环境变量的方式使用 ConfigMap
以下面这样的 ConfigMap 为例
apiVersion: v1 kind: ConfigMap metadata: name: cm-appvars data: apploglevel: info appdatadir: /var/data
在下面的 Pod 将 cm-appvars 中的内容以环境变量的方式设置为容器内部的环境变量。
apiVersion: v1 kind: Pod metadata: name: cm-test-pod spec: containers: - name: cm-test image: busybox command: ["/bin/sh", "-c", "env | grep APP"] env: - name: APPLOGLEVEL #定义环境变量的名称 valueFrom: #key "apploglevel" 对应的值 configMapKeyRef: name: cm-appvars #环境变量的值取自cm-appvars: key: apploglevel #key为apploglevel - name: APPDATADIR #定义环境变量的名称 valueFrom: #key "appdatadir" 对应的值 configMapKeyRef: name: cm-appvars #环境变量的值取自cm-appvars: key: appdatadir #key为appdatadir #envFrom: #Kubernetes 1.6开始引入的新字段,可以自动生成环境变量 #- configMapKeyRef # name: cm-appvars #根据cm-appvars中的key=value自动生成环境变量 restartPolicy: Never
另外,从 1.6 版本开始,引入了一个新的字段 envFrom,实现了在 Pod 环境中将 ConfigMap 中所有定义的 key=value 自动生成为环境变量。比如上面注释掉的,将会在容器内部自动生成如下的环境变量。
apploglevel=info appdatadir=/var/data
- 环境变量的名称受 POSIX 命名规范([a-zA-Z_][a-zA-Z0-9_]*)的约束,不能以数字开头。如果 ConfigMap 中有不符合这个规范的,那么将会跳过该条环境变量的创建,并记录一个 Event 来提示环境变量无法生成,但是不阻止 Pod 的生成。
- envFrom 这种方式也适合于 Secret 对象。
-
通过 volumeMount 使用 ConfigMap
假如有这样的 ConfigMap 文件
apiVersion: v1 kind: ConfigMap metadata: name: cm-appconfigfiles data: key-servexml: | just test key-loggingproperties: | just test, too
那么 volumeMount 使用 ConfigMap 的方式如下所示。当登录到 /configfiles 目录下,会发现存在 server.xml 和 logging.properties 两个文件,它们的内容就是 ConfigMap cm-appconfigfiles 中定义的内容。
apiVersion: v1 kind: Pod metadata: name: cm-test-app spec: containers: - name: cm-test-app image: tomcat ports: - containerPort: 8080 volumeMounts: - name: serverxml mountPath: /configfiles volumes: - name: serverxml configMap: name: cm-appconfigfiles - items: key-serverxml path: server.xml - items: key-loggingproperties path: logging.properties
如果在引用 ConfigMap 的时候不指定 items,那么将会在容器内的目录下为每个 item 都生成一个名为 key 的文件。比如,使用下面 Pod Yaml 创建的容器中的 /configfiles 中就存在 key-servexml 和 key-loggingproperties 两个文件,文件的内容,则是 ConfigMap 中对应的 value。
apiVersion: v1 kind: Pod metadata: name: cm-test-app spec: containers: - name: cm-test-app image: tomcat ports: - containerPort: 8080 volumeMounts: - name: serverxml mountPath: /configfiles volumes: - name: serverxml configMap: name: cm-appconfigfiles
1.2.3.2.3. ConfigMap 使用的限制条件
-
ConfigMap 必须在 Pod 之前创建。
-
ConfigMap 受 Namespace 的限制,只能处于相同 Namespace 中的 Pod 才能使用它。
-
ConfigMap 的配额管理还未实现。
-
kubelet 只支持可以被 API Server 管理的 Pod 使用 ConfigMap。使用 kubelet 创建的静态 Pod 将无法引用 ConfigMap。
-
采用 volumeMount 的方式时,在容器内部只能挂载为目录,无法挂载为文件。在挂载到容器内部后,在目录下将包含 ConfigMap 定义的每个 item,如果容器中存在该目录的话,则容器内的该目录将被挂载的 ConfigMap 覆盖。
如果应用程序需要保留原来的其他文件,则需要进行额外的处理。比如将 ConfigMap 挂载到容器内部的临时目录,然后使用启动脚本将文件复制或链接到应用所用的实际配置目录下。
1.2.3.3. Downward API
Downward API 让 Pod 里的容器能够直接获取到这个 Pod API 对象本身的信息。不过,需要注意的是,Downward API 能够获取到的信息,一定是 Pod 里的容器进程启动之前就能够确定下来的信息。而如果你想要获取 Pod 容器运行后才会出现的信息,比如,容器进程的 PID,那就肯定不能使用 Downward API 了,而应该考虑在 Pod 里定义一个 sidecar 容器。
Downward API 可以通过以下两种方式将 Pod 信息注入容器内部:
- 环境变量的方式,用于单个变量,可以将 Pod 信息或者 Container 信息注入容器内部。
下面是通过 Downward API 将 Pod 的 IP、名称和所在 Namespace 注入容器的环境变量中。
apiVersion: v1
kind: Pod
metadata:
name: dapi-test-pod
spec:
containers:
- name: test-container
image: busybox
command: ["/bin/sh", "-c", "env"]
env:
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name #Pod名称
- name: MY_POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace #Pod所在的Namespace
- name: MY_POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP #Pod IP,在status中是因为IP属于状态数据而非元数据(metadata)
restartPolicy: Never
下面是通过 Downward API 将 Container 的资源请求和限制信息注入容器的环境变量中。
apiVersion: v1
kind: Pod
metadata:
name: dapi-test-pod-container-vars
spec:
containers:
- name: test-container
image: busybox
imagePullPolicy: Never
command: ["sh", "-c"]
args:
- while true; do
echo -en '\n';
printenv MY_CPU_REQUEST MY_CPU_LIMIT;
printenv MY_MEM_REQUEST MY_MEM_LIMIT;
sleep 3600;
done;
resources:
requests:
memory: "32Mi"
cpu: "125m"
limits:
memory: "64Mi"
cpu: "250m"
env:
- name: MY_CPU_REQUEST
valueFrom:
resourceFieldRef:
containerName: test-container
resource: requests.cpu #容器的CPU请求值
- name: MY_CPU_LIMIT
valueFrom:
resourceFieldRef:
containerName: test-container
resource: limits.cpu #容器的CPU限制值
- name: MY_MEM_REQUEST
valueFrom:
resourceFieldRef:
containerName: test-container
resource: requests.memory #容器的内存请求值
- name: MY_MEM_LIMIT
valueFrom:
resourceFieldRef:
containerName: test-container
resource: limits.memory #容器的内存限制值
restartPolicy: Never
- Volume 挂载,将数组类信息生成为文件并挂载到容器内部。
比如,在这个 Pod 的 YAML 文件中,我定义了一个简单的容器,声明了一个 projected 类型的 Volume。这次 Volume 的数据来源,变成了 Downward API。而这个 Downward API Volume,则声明了要暴露 Pod 的 metadata.labels 信息给容器。
通过这样的声明方式,当前 Pod 的 Labels 字段的值,就会被 Kubernetes 自动挂载成为容器里的 /etc/podinfo/labels 文件,也就是根据 path 的名称生成文件。
apiVersion: v1
kind: Pod
metadata:
name: test-downwardapi-volume
labels:
zone: us-est-coast
cluster: test-cluster1
rack: rack-22
spec:
containers:
- name: client-container
image: k8s.gcr.io/busybox
command: ["sh", "-c"]
args:
- while true; do
if [[ -e /etc/podinfo/labels ]]; then
echo -en '\n\n'; cat /etc/podinfo/labels; fi;
sleep 5;
done;
volumeMounts:
- name: podinfo
mountPath: /etc/podinfo
readOnly: false
volumes:
- name: podinfo
projected:
sources:
- downwardAPI:
items:
- path: "labels"
fieldRef:
fieldPath: metadata.labels
# volumes:
# - name: podinfo
# downwardAPI:
# items:
# - path: "labels"
# fieldRef:
# fieldPath: metadata.labels
而这个容器的启动命令,则是不断打印出 /etc/podinfo/labels 里的内容。所以,当我创建了这个 Pod 之后,就可以通过 kubectl logs 指令,查看到这些 Labels 字段被打印出来,如下所示:
$ kubectl create -f dapi-volume.yaml
$ kubectl logs test-downwardapi-volume
cluster="test-cluster1"
rack="rack-22"
zone="us-est-coast"
目前「2018-09 文章的参考」,Downward API 支持的字段已经非常丰富了,如下所示。随着 Kubernetes 项目的发展肯定还会不断增加,这里列出来的信息仅供参考,你在使用 Downward API 时,还是要记得去查阅一下官方文档。
1. 使用fieldRef可以声明使用:
spec.nodeName - 宿主机名字
status.hostIP - 宿主机IP
metadata.name - Pod的名字
metadata.namespace - Pod的Namespace
status.podIP - Pod的IP
spec.serviceAccountName - Pod的Service Account的名字
metadata.uid - Pod的UID
metadata.labels['<KEY>'] - 指定<KEY>的Label值
metadata.annotations['<KEY>'] - 指定<KEY>的Annotation值
metadata.labels - Pod的所有Label
metadata.annotations - Pod的所有Annotation
2. 使用resourceFieldRef可以声明使用:
limits.cpu:容器的CPU limit
requests.cpu:容器的CPU request
limits.memory:容器的memory limit
requests.memory:容器的memory request
1.2.3.3.1. Downward API 的作用
在某些集群中,集群中的每个节点都需要将自身的标识(ID)及进程绑定的 IP 地址等信息写入配置文件中,进程在启动时会读取这些信息,然后将这些信息发布到某个类似服务注册中心的地方,以方便集群节点的自动发现功能。
这个时候 Downward API 就可以派上用场了,就是先编写一个预启动脚本或者 Init Container,先通过环境变量或者文件方式获取 Pod 自身的名称、IP 地址等信息,然后将这些信息写入主程序的配置文件中,最后才启动主程序。
1.2.4. 其他
- gcePersistentDisk(使用谷歌云提供的永久磁盘)
- awsElasticBlockStore(使用亚马逊公有云提供的 EBS Volume)
- NFS
1.3. Pod 调度(定向/亲和/污点)
1.3.1. NodeSelector --- Node 定向调度
是一个供用户将 Pod 与 Node 进行绑定的字段
apiVersion: v1
kind: Pod
...
spec:
nodeSelector:
disktype: ssd
这样的一个配置,意味着这个 Pod 永远只能运行在携带了“disktype: ssd”标签(Label)的节点上;否则,它将调度失败。
1.4. Pod 健康检查和服务可用性检查
在 Kubernetes 中,你可以为 Pod 里的容器定义一个健康检查“探针”(Probe)。这样,kubelet 就会根据这个 Probe 的返回值决定这个容器的状态,而不是直接以容器镜像是否运行(来自 Docker 返回的信息)作为依据。这种机制,是生产环境中保证应用健康存活的重要手段。
下面是一个 Kubernetes 文档中的例子:
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: test-liveness-exec
spec:
containers:
- name: liveness
image: busybox
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 5
在这个 Pod 中,我们定义了一个有趣的容器。它在启动之后做的第一件事,就是在 /tmp 目录下创建了一个 healthy 文件,以此作为自己已经正常运行的标志。而 30 s 过后,它会把这个文件删除掉。与此同时,我们定义了一个这样的 livenessProbe(健康检查)。它的类型是 exec,这意味着,它会在容器启动后,在容器里面执行一条我们指定的命令,比如:“cat /tmp/healthy”。这时,如果这个文件存在,这条命令的返回值就是 0,Pod 就会认为这个容器不仅已经启动,而且是健康的。这个健康检查,在容器启动 5 s 后开始执行(initialDelaySeconds: 5),每 5 s 执行一次(periodSeconds: 5)。
下面来实践一下,首先是创建这个 Pod,然后是查看这个 Pod 的状态:可以看到这个 Pod 进入了 Running 状态。
$ kubectl create -f test-liveness-exec.yaml
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
test-liveness-exec 1/1 Running 0 10s
而 30s 之后再查看一下 Pod 的 Events,你会发现,这个 Pod 在 Events 报告了一个异常。
$ kubectl describe pod test-liveness-exec
FirstSeen LastSeen Count From SubobjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
2s 2s 1 {kubelet worker0} spec.containers{liveness} Warning Unhealthy Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
显然,这个健康检查探查到 /tmp/healthy 已经不存在了,所以它报告容器是不健康的。
但是接下来我们查看这个 Pod 的状态会发现这个 Pod 并没有进入 Failed 状态,而是保持了 Running 状态,同时 RESTARTS 字段从 0 变成了 1。这是因为这个异常的容器已经被 Kubernetes 重启了。在这个过程中,Pod 保持 Running 状态不变。这个功能就是 Kubernetes 里的 Pod 恢复机制,也叫 restartPolicy。
需要注意的是:Kubernetes 中并没有 Docker 的 Stop 语义。所以虽然是 Restart(重启),但实际却是重新创建了容器。
$ kubectl get pod test-liveness-exec
NAME READY STATUS RESTARTS AGE
liveness-exec 1/1 Running 1 1m
livenessProbe 除了在容器中执行命令外,livenessProbe 也可以定义为发起 HTTP 或者 TCP 请求的方式,定义格式如下:
...
livenessProbe:
httpGet:
path: /healthz
port: 8080
httpHeaders:
- name: X-Custom-Header
value: Awesome
initialDelaySeconds: 3
periodSeconds: 3
...
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
针对 HTTP 或者 TCP 的探针,你的 Pod 其实可以暴露一个健康检查 URL(比如 /healthz),或者直接让健康检查去检测应用的监听端口。这两种配置方法,在 Web 服务类的应用中非常常用。
1.4.1. ReadlinessProbe
它的用法与 livenessProbe 类似,但作用却大不一样。readinessProbe 检查结果的成功与否,决定的这个 Pod 是不是能被通过 Service 的方式访问到,而并不影响 Pod 的生命周期。
1.5. RestartPolicy---Pod 生命周期和重启策略
restartPolicy 是 Pod 的 Spec 部分的一个标准字段(pod.spec.restartPolicy),它有以下三个值可以选择:
- Always:是默认值,表示任意时候这个容器发生了异常,它一定会被重新创建;
- OnFailure: 只在容器异常时才自动重启容器;
- Never: 从来不重启容器。
在实际使用时,我们需要根据应用运行的特性,合理设置这三种恢复策略。比如,一个 Pod,它只计算 1+1=2,计算完成输出结果后退出,变成 Succeeded 状态。这时,你如果再用 restartPolicy=Always 强制重启这个 Pod 的容器,就没有任何意义了。而如果你要关心这个容器退出后的上下文环境,比如容器退出后的日志、文件和目录,就需要将 restartPolicy 设置为 Never。因为一旦容器被自动重新创建,这些内容就有可能丢失掉了(被垃圾回收了)。
需要注意的是 Pod 的恢复过程,永远都是发生在当前节点上,而不会跑到别的节点上去。事实上,一旦一个 Pod 与一个节点(Node)绑定,除非这个绑定发生了变化(pod.spec.node 字段被修改),否则它永远都不会离开这个节点。这也就意味着,如果这个宿主机宕机了,这个 Pod 也不会主动迁移到其他节点上去。而如果你想让 Pod 出现在其他的可用节点上,就必须使用 Deployment 这样的“控制器”来管理 Pod,哪怕你只需要一个 Pod 副本。这就是一个单 Pod 的 Deployment 与一个 Pod 最主要的区别。
Kubernetes 的官方文档,把 restartPolicy 和 Pod 里容器的状态,以及 Pod 状态的对应关系,总结了非常复杂的一大堆情况。实际上,根本不需要死记硬背这些对应关系,只要记住如下两个基本的设计原理即可:
-
只要 Pod 的 restartPolicy 指定的策略允许重启异常的容器(比如:Always),那么这个 Pod 就会保持 Running 状态,并进行容器重启。
-
否则,只有 Pod 里面所有的容器都进入异常状态后,Pod 才会进入 Failed 状态。在此之前,Pod 都是 Running 状态。此时,Pod 的 READY 字段会显示正常容器的个数,比如:
$ kubectl get pod test-liveness-exec NAME READY STATUS RESTARTS AGE liveness-exec 0/1 Running 1 1m
所以,假如一个 Pod 里只有一个容器,然后这个容器异常退出了。那么,只有当 restartPolicy=Never 时,这个 Pod 才会进入 Failed 状态。而其他情况下,由于 Kubernetes 都可以重启这个容器,所以 Pod 的状态保持 Running 不变。
而如果这个 Pod 有多个容器,仅有一个容器异常退出,它就始终保持 Running 状态,哪怕即使 restartPolicy=Never。只有当所有容器也异常退出之后,这个 Pod 才会进入 Failed 状态。
其他情况,都可以以此类推出来。
1.6. Satus---Pod 的生命周期,跟 spec 同一层级
Pod 生命周期的变化,主要体现在 Pod API 对象的 status 部分,这是它除了 metadata 和 spec 之外的第三个重要字段。
其中,pod.status.phase 就是 Pod 的当前状态,它有如下几种可能的情况:
- Pending。这个状态意味着,Pod 的 YAML 文件已经提交给了 Kubernetes,API 对象已经被创建并保存在 Etcd 当中。但是,这个 Pod 里有些容器因为某种原因而不能被顺利创建。比如,调度不成功。
- Running。这个状态下,Pod 已经调度成功,跟一个具体的节点绑定。它包含的容器都已经创建成功,并且至少有一个正在运行中。
- Succeeded。这个状态意味着,Pod 里的所有容器都正常运行完毕,并且已经退出了。这种情况在运行一次性任务时最为常见。
- Failed。这个状态下,Pod 里至少有一个容器以不正常的状态(非 0 的返回码)退出。这个状态的出现,意味着你得想办法 Debug 这个容器的应用,比如查看 Pod 的 Events 和日志。
- Unknown。这是一个异常状态,意味着 Pod 的状态不能持续地被 kubelet 汇报给 kube-apiserver,这很有可能是主从节点(Master 和 Kubelet)间的通信出现了问题。
更进一步地,Pod 对象的 Status 字段,还可以再细分出一组 Conditions。这些细分状态的值如下,它们主要用于描述造成当前 Status 的具体原因是什么。
- PodScheduled。
- Ready。Ready 这个细分状态非常值得我们关注:它意味着 Pod 不仅已经正常启动(Running 状态),而且已经可以对外提供服务了。这两者之间(Running 和 Ready)是有区别的,你不妨仔细思考一下。
- Initialized。
- Unschedulable。比如,Pod 当前的 phase 是 Pending,对应的 Condition 是 Unschedulable,这就意味着它的调度出现了问题。
Pod 的这些状态信息,是我们判断应用运行情况的重要标准,尤其是 Pod 进入了非“Running”状态后,你一定要能迅速做出反应,根据它所代表的异常情况开始跟踪和定位,而不是去手忙脚乱地查阅文档。
对于 Pod 状态是 Ready,实际上不能提供服务的情况能想到几个例子:
- 程序本身有 bug,本来应该返回 200,但因为代码问题,返回的是500;
- 程序因为内存问题,已经僵死,但进程还在,但无响应;
- Dockerfile 写的不规范,应用程序不是主进程,那么主进程出了什么问题都无法发现;
- 程序出现死循环。
1.7. PodPreset --- 自动填充字段
自动填充字段主要用于:比如,开发人员只需要提交一个基本的、非常简单的 Pod YAML,Kubernetes 就可以自动给对应的 Pod 对象加上其他必要的信息,比如 labels,annotations,volumes 等等。而这些信息,可以是运维人员事先定义好的。这么一来,开发人员编写 Pod YAML 的门槛,就被大大降低了。这个叫作 PodPreset(Pod 预设置)的功能已经出现在了 v1.11 版本的 Kubernetes 中。
举个例子,现在开发人员编写了如下一个 pod.yaml 文件,这个 Pod 在生产环境中根本不能用。
apiVersion: v1
kind: Pod
metadata:
name: website
labels:
app: website
role: frontend
spec:
containers:
- name: website
image: nginx
ports:
- containerPort: 80
这个时候,运维人员就可以定义一个 PodPreset 对象。在这个对象中,凡是他想在开发人员编写的 Pod 里追加的字段,都可以预先定义好。比如这个 preset.yaml:在这个 PodPreset 的定义中,首先是一个 selector。这就意味着后面这些追加的定义,只会作用于 selector 所定义的、带有“role: frontend”标签的 Pod 对象,这就可以防止“误伤”。然后,我们定义了一组 Pod 的 Spec 里的标准字段,以及对应的值。比如,env 里定义了 DB_PORT 这个环境变量,volumeMounts 定义了容器 Volume 的挂载目录,volumes 定义了一个 emptyDir 的 Volume。
apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
name: allow-database
spec:
selector:
matchLabels:
role: frontend
env:
- name: DB_PORT
value: "6379"
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}
接下来,我们假定运维人员先创建了这个 PodPreset,然后开发人员才创建 Pod。当查看这个 Pod 对象之后,如下所示:
$ kubectl create -f preset.yaml
$ kubectl create -f pod.yaml
$ kubectl get pod website -o yaml
apiVersion: v1
kind: Pod
metadata:
name: website
labels:
app: website
role: frontend
annotations:
podpreset.admission.kubernetes.io/podpreset-allow-database: "resource version"
spec:
containers:
- name: website
image: nginx
volumeMounts:
- mountPath: /cache
name: cache-volume
ports:
- containerPort: 80
env:
- name: DB_PORT
value: "6379"
volumes:
- name: cache-volume
emptyDir: {}
这个时候,我们就可以清楚地看到,这个 Pod 里多了新添加的 labels、env、volumes 和 volumeMount 的定义,它们的配置跟 PodPreset 的内容一样。此外,这个 Pod 还被自动加上了一个 annotation 表示这个 Pod 对象被 PodPreset 改动过。
需要注意的是,PodPreset 里定义的内容,只会在 Pod API 对象被创建之前追加在这个 Pod 对象本身上,而不会影响任何 Pod 控制器的定义。比如,我们现在提交的是一个 nginx-deployment,那么这个 Deployment 对象本身是永远不会被 PodPreset 改变的,被修改的只是这个 Deployment 创建出来的所有 Pod。这一点请务必区分清楚。
另外还需注意的是,如果定义了同时作用于一个 Pod 对象的多个 PodPreset 的话,Kubernetes 项目会帮你合并(Merge)这两个 PodPreset 要做的修改。而如果它们要做的修改有冲突的话,这些冲突字段就不会被修改。
1.8. InitContainers
Init Containers 的生命周期,会先于所有的 Containers,并且严格按照定义的顺序执行。在 Pod 中,所有 Init Container 定义的容器,都会比 spec.containers 定义的用户容器先启动。并且,Init Container 容器会按顺序逐一启动,而直到它们都启动并且退出了,用户容器才会启动。
1.9. 静态 Pod
静态 Pod 是由 kubelet 创建并且管理的仅存在于 kubelet 所在 Node 上的 Pod,kubelet 会对它们进行健康检查。静态 Pod 不通过 API Server 进行管理,所以无法与 ReplicationController、Deployment 或者 DaemonSet 进行关联。
1.9.1. 创建静态 Pod
创建静态 Pod 有两种方式:配置文件方式和 HTTP 方式。
1.9.1.1. 配置文件方式
kubelet 会定期扫描配置文件所在的目录,并根据该目录下 的 yaml 或者 json 文件来创建操作。删除 Pod 的操作则只能是到其所在 Node 上将 Pod 的定义文件从目录中删除。
设置 kubelet 需要监控的配置文件所在的目录,有以下两种方式:
- 设置 kubelet 的启动参数
--pod-manifest-path
(将被弃用的方式); - kubelet 配置文件中设置 staticPodPath(推荐的方式)
配置了目录之后,需要重启 kubelet。
1.9.1.2. HTTP 方式
kubectl 会定期从指定 URL 地址下载 Pod 的定义文件,并以 yaml 或者 json 文件的格式进行解析,然后创建 Pod。
通过设置 kubelet 的启动参数 --manifest-url
来指定的 URL。
巨人的肩膀
- 极客时间.张磊.《深入剖析Kubernetes》
- 《Kubernetes 权威指南》