EvenChan's Ops.

如何在不重建镜像的情况下修改容器

字数统计: 1.6k阅读时长: 6 min
2020/09/29

现在我们使用容器非常频繁,偶尔有一些需求需要更改容器镜像中的一些行为,也许是一个很小的变化,一般我们能想到的就是重新构建镜像,但是这个我们就需要重新构建发布镜像了,除了构建镜像这种方式之外其实还有其他方式可以来实现这个需求。

初始化容器

Init Containers 是为了给 Pod 中定义的主容器提供附加功能的。它们在主容器之前执行,可以使用不同的容器镜像,如果出现任何故障,它们将阻止主容器的启动,所有的日志都可以很容易查看到,故障排除也相当简单,它们就像在 Pod 中定义的任何其他容器一样。这种方法在数据库等服务中比较常用,可以根据配置参数对它们进行初始化和配置。

下面的例子使用一个 emptyDir 来存储由初始化容器初始化的数据。在这个示例,它只是一个简单的 echo 命令,在实际的生产环境中,可能是一个脚本,做一些更复杂的事情。

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
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx-init
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
initContainers:
- name: prepare-webpage
image: busybox:1.28
command: ["sh", "-c"]
args: [
"set -x;
echo '<h2>Page prepared by an init container</h2>' > /web/index.html;
echo 'Init finished successfully'
",
]
volumeMounts:
- mountPath: /web
name: web
containers:
- image: nginx:1.19
name: nginx
volumeMounts:
- mountPath: /usr/share/nginx/html/
name: web
ports:
- containerPort: 80
name: http
volumes:
- name: web
emptyDir: {}

PostStart Hook

post-start hook 可用于在主容器启动后执行一些操作,它可以是在与容器相同的上下文中执行的脚本,也可以是针对定义的端点执行的 HTTP 请求,但是,不能保证回调会在容器入口点(ENTRYPOINT)之前执行。在大多数情况下,它可能是一个 shell 脚本,Pod一直保持在ContainerCreating 状态,直到这个脚本结束。由于没有可用的日志,所以调试起来可能很棘手。这个方法最大的特点是,当主容器中的服务启动时,脚本就会被执行,并且可以用来与服务进行交互,通过适当的 readinessProbe 配置,这可以提供一种很好的方式,在允许任何请求之前初始化应用程序。在下面的例子中,一个启动后的钩子会执行 echo 命令,但同样这可以是任何使用容器文件系统上可用的同一组文件来执行某种初始化的东西。

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
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx-hook
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx:1.19
name: nginx
ports:
- containerPort: 80
name: http
lifecycle:
postStart:
exec:
command:
[
"sh",
"-c",
"sleep 5;set -x; echo '<h2>Page prepared by a PostStart hook</h2>' > /usr/share/nginx/html/index.html",
]

Sidecar 容器

这种方法利用了 Pod 的概念 - 多个容器同时运行、共享 IPC 和网络命名空间。在 Kubernetes 生态系统中,它已经被 Istio、Consul Connect 等项目广泛使用。这里的假设是所有容器同时运行,这使得使用 sidecar 容器来修改主容器的行为变得有点棘手。但这是可行的,它可以用来与正在运行的应用程序或服务进行交互。我在 Jenkins Helm Chart 中使用了这个功能,其中有一个 sidecar 容器负责读取 ConfigMap 对象和 Configuration-as-Code 配置项。

在下面示例中同样只是使用 echo 这个命令,不过需要注意的是,因为 sidecar 容器必须遵循 restartPolicy 设置,所以这个容器在完成动作后还必须处于运行状态,示例中我们使用的是一个简单的 while 无限循环,在实际环境中,往往会是一个小的守护进程,像服务一样一直运行。

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
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx-sidecar
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx:1.19
name: nginx
volumeMounts:
- mountPath: /usr/share/nginx/html/
name: web
ports:
- containerPort: 80
name: http
- name: prepare-webpage
image: busybox:1.28
command: ["sh", "-c"]
args: [
"set -x;
echo '<h2>Page prepared by a sidecar container</h2>' > /web/index.html;
while :;do sleep 9999;done
",
]
volumeMounts:
- mountPath: /web
name: web
volumes:
- name: web
emptyDir: {}

EntryPoint

最后一种方法使用相同的容器镜像,与 PostStart Hook 类似,只是它在主应用程序或服务之前运行。我们在容器镜像中都定义一个ENTRYPOINT 命令,我们可以利用它来执行一些脚本,这种方式经常被很多官方镜像所使用,在这种方法中,我们只需要预置自己的脚本来修改主容器的行为。在实际生产环境中,其实我们可以提供一个修改后的原始入口点文件。

这个方法相对复杂一点,需要创建一个 ConfigMap,其中包含一个脚本内容,在主入口点之前执行。如下所示我们修改 nginx 入口点的脚本,然后嵌入到下面的 ConfigMap 中。

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: ConfigMap
metadata:
name: scripts
data:
prestart-script.sh: |-
#!/usr/bin/env bash
echo '<h2>Page prepared by a script executed before entrypoint container</h2>' > /usr/share/nginx/html/index.html
# 这是 "ENTRYPOINT CMD "从主容器镜像定义中提取出来的
exec /docker-entrypoint.sh nginx -g "daemon off;"

有一点非常重要,就是最后一行与 exec,它执行的是原始的入口点脚本,必须与 Dockerfile 中定义的脚本完全匹配,在这种情况下,它需要额外的参数,这些参数是在 CMD 中定义的。现在让我们定义一下 Deployment 资源对象。

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
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx-script
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx:1.19
name: nginx
command: ["bash", "-c", "/scripts/prestart-script.sh"]
ports:
- containerPort: 80
name: http
volumeMounts:
- mountPath: /scripts
name: scripts
volumes:
- name: scripts
configMap:
name: scripts
defaultMode: 0755 # <- 这个很重要

我们用命令覆盖入口点,我们还必须确保我们的脚本是以适当的权限挂载的(因此需要定义 defaultMode)。

总结

现在我们来总结下上面几种方式的差异。

d845d32914dad06fc48c670e7f75282d.jpg

容器讲究的是可重用性,很多时候做一些小的调整,不需要重新构建整个容器的镜像,这样发布和维护就会轻松很多。

作者:Tomasz Cholewa,原文链接:https://cloudowski.com/articles/how-to-modify-containers-wihtout-rebuilding/

CATALOG
  1. 1. 初始化容器
  2. 2. PostStart Hook
  3. 3. Sidecar 容器
  4. 4. EntryPoint
  5. 5. 总结