initContainers是一种专用的容器,在应用程序容器启动之前运行,可以包括一些应用程序镜像中不存在的实用工具和安装脚本,可以完成应用的必要数据初始化等工作。总的来说就是在正式的容器启动之前做一些准备工作的。
例如一个应用容器挂载的volume目录需要一些必不可少的初始化文件,不加init容器的时候直接挂载volume应用容器的那个目录是空的(因为docker挂载一个volume时,会用volume的内容覆盖容器内挂载目录的内容),这就需要让init容器也跟应用容器挂载同一个volume目录,将初始化文件放进去,然后在启动应用容器就能正常看到那些初始化文件了,我也正是遇到了这样的问题,才来研究和学习init容器的。
1、一个pod可以运行多个容器,同样的一个pod也可能有一个或多个先于应用容器启动的 Init 容器,每个容器启动失败后都会根据你配置的重启策略进行重启,直到运行成功。Init 容器与普通的基本一样,主要区别如下
- Init 容器总是运行到成功运行完为止。
- 前面的 Init 容器必须已经运行完成,才会开始运行下一个init容器,而应用程序容器时并行运行的。
2、因为 Init 容器具有与应用程序容器分离的单独镜像,所以使用他们可以具有如下优势:
- 它们可以包含并运行实用工具,但是出于安全考虑,是不建议在应用程序容器镜像中包含这些实用工具的。
- 它们可以包含使用工具和定制化代码来安装,但是不能出现在应用程序镜像中。例如,创建镜像没必要 FROM 另一个镜像,只需要在安装过程中使用类似 sed、 awk、 python 或 dig 这样的工具。
- 应用程序镜像可以分离出创建和部署的角色,而没有必要联合它们构建一个单独的镜像。
- Init 容器使用 Linux Namespace,所以相对应用程序容器来说具有不同的文件系统视图。因此,它们能够具有访问 Secret 的权限,而应用程序容器则不能。
- 它们必须在应用程序容器启动之前运行完成,而应用程序容器是并行运行的,所以 Init 容器能够提供了一种简单的阻塞或延迟应用容器的启动的方法,直到满足了一组先决条件。
3、下面是一些如何使用 Init 容器的思路:
- 等待一个 Service 创建完成,通过类似如下 shell 命令:
for i in {1..100}; do sleep 1; if dig myservice; then exit 0; fi; exit 1
- 将 Pod 注册到远程服务器,通过在命令中调用 API,类似如下:
curl -X POST http://$MANAGEMENT_SERVICE_HOST:$MANAGEMENT_SERVICE_PORT/register -d 'instance=$(<POD_NAME>)&ip=$(<POD_IP>)'
- 在启动应用容器之前等一段时间,使用类似 sleep 60 的命令。
- 克隆 Git 仓库到数据卷。
- 将配置值放到配置文件中,运行模板工具为主应用容器动态地生成配置文件。例如,在配置文件中存放 POD_IP 值,并使用 Jinja 生成主应用配置文件。
4、 init容器使用示例,
Kubernetes 1.6 版本后使用新语法,把 Init 容器的声明移到 spec 中,只需要添加 initContainers 即可,下面列一个具有 2 个 Init 容器的简单 Pod。 第一个等待 myservice 启动,第二个等待 mydb 启动。 一旦这两个 Service 都启动完成,Pod 将开始启动。
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
spec:
containers:
- name: myapp-container
image: busybox
command: ['sh', '-c', 'echo The app is running! && sleep 3600']
initContainers:
- name: init-myservice
image: busybox
command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 2; done;']
- name: init-mydb
image: busybox
command: ['sh', '-c', 'until nslookup mydb; do echo waiting for mydb; sleep 2; done;']
下面的 yaml 文件展示了 mydb 和 myservice 两个 Service:
kind: Service
apiVersion: v1
metadata:
name: myservice
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9376
---
kind: Service
apiVersion: v1
metadata:
name: mydb
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9377
这个 Pod 可以使用下面的命令进行启动和调试
$ kubectl create -f myapp.yaml
pod "myapp-pod" created
$ kubectl get -f myapp.yaml
NAME READY STATUS RESTARTS AGE
myapp-pod 0/1 Init:0/2 0 6m
$ kubectl describe -f myapp.yaml
Name: myapp-pod
Namespace: default
[...]
Labels: app=myapp
Status: Pending
[...]
Init Containers:
init-myservice:
[...]
State: Running
[...]
init-mydb:
[...]
State: Waiting
Reason: PodInitializing
Ready: False
[...]
Containers:
myapp-container:
[...]
State: Waiting
Reason: PodInitializing
Ready: False
[...]
Events:
FirstSeen LastSeen Count From SubObjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
16s 16s 1 {default-scheduler } Normal Scheduled Successfully assigned myapp-pod to 172.17.4.201
16s 16s 1 {kubelet 172.17.4.201} spec.initContainers{init-myservice} Normal Pulling pulling image "busybox"
13s 13s 1 {kubelet 172.17.4.201} spec.initContainers{init-myservice} Normal Pulled Successfully pulled image "busybox"
13s 13s 1 {kubelet 172.17.4.201} spec.initContainers{init-myservice} Normal Created Created container with docker id 5ced34a04634; Security:[seccomp=unconfined]
13s 13s 1 {kubelet 172.17.4.201} spec.initContainers{init-myservice} Normal Started Started container with docker id 5ced34a04634
$ kubectl logs myapp-pod -c init-myservice # Inspect the first init container
$ kubectl logs myapp-pod -c init-mydb # Inspect the second init container
一旦我们启动了 mydb 和 myservice 这两个 Service,我们能够看到 Init 容器完成,并且 myapp-pod 被创建:
$ kubectl create -f services.yaml
service "myservice" created
service "mydb" created
$ kubectl get -f myapp.yaml
NAME READY STATUS RESTARTS AGE
myapp-pod 1/1 Running 0 9m
在 Pod 上使用 activeDeadlineSeconds,在容器上使用 livenessProbe,这样能够避免 Init 容器一直失败, 这就为 Init 容器活跃设置了一个期限。在 Pod 中的每个 app 和 Init 容器的名称必须唯一,与任何其它容器共享同一个名称,会在验证时抛出错误。
在 Pod 启动过程中,Init 容器会按顺序在网络和数据卷初始化之后启动。 每个容器必须在下一个容器启动之前成功退出。 如果由于运行时或失败退出,导致容器启动失败,它会根据 Pod 的 restartPolicy 指定的策略进行重试,在所有的 Init 容器没有成功之前,Pod 将不会变成 Ready 状态,如果 Pod 重启,所有 Init 容器必须重新执行。
因为 Init 容器可能会被重启、重试或者重新执行,所以 Init 容器的代码应该是幂等的(即任意多次执行所产生的影响与一次执行的影响相同)。 特别地,被写到 EmptyDirs 中文件的代码,应该对输出文件可能已经存在做好准备。
5、Pod 能够重启,会导致 Init 容器重新执行,重启主要有如下几个原因:
- 用户更新 PodSpec 导致 Init 容器镜像发生改变。更改 Init 容器的 image 字段,等价于重启该 Pod,应用容器镜像的变更只会重启应用容器。
- Pod 基础设施容器被重启。这不多见,但某些具有 root 权限可访问 Node 的人可能会这样做。
- 当 restartPolicy 设置为 Always,Pod 中所有容器会终止,强制重启,由于垃圾收集导致 Init 容器完成的记录丢失。