K8s in Action 阅读笔记——【7】ConfigMaps and Secrets: configuring applications
7.1 Configuring containerized applications
在我们讨论如何将配置数据传递给在Kubernetes中运行的应用程序之前,让我们先看看通常如何配置容器化应用程序。
如果忽略了你可以将配置信息嵌入到应用程序本身中的事实,那么在开发新应用程序时,通常会通过命令行参数来配置应用程序。然后,随着配置选项列表的增长,你可以将配置移动到配置文件中。
在容器化应用程序中,传递配置选项的另一种广泛流行的方法是通过环境变量。应用程序不是读取配置文件或命令行参数,而是查找某个环境变量的值。例如,官方MySQL容器镜像使用名为MYSQL_ROOT_PASSWORD
的环境变量来设置root超级用户帐户的密码。
但为什么在容器中很流行使用环境变量呢?在Docker容器内使用配置文件有点棘手,因为你必须将配置文件嵌入到容器镜像本身中或将包含文件的卷装载到容器中。很明显,将文件嵌入到镜像中类似于将配置硬编码到应用程序的源代码中,因为它要求你每次想要更改配置时都要重新构建镜像。此外,可以访问静态的所有人都可以看到配置,包括应保密的任何信息,例如凭据或加密密钥。使用卷更好,但仍需要确保在启动容器之前将文件写入卷中。
如果你阅读了前面的章节,可能会考虑使用gitRepo卷作为配置源。这并不是一个坏主意,因为它允许你保持配置的版本,并使你能够轻松回滚配置更改(如果需要)。但是,有一种更简单的方法,可以将配置数据放入顶级Kubernetes资源中,并将其及所有其他资源定义存储在同一Git存储库或任何其他基于文件的存储中。用于存储配置数据的Kubernetes资源称为ConfigMap。我们将在本章中学习如何使用它。
无论你是否使用ConfigMap存储配置数据,你都可以通过以下方式配置应用程序:
- 向容器传递命令行参数设置。
- 每个容器的自定义环境变量。
- 通过一种特殊类型的卷将配置文件装入容器中。
我们将在接下来的几个部分中介绍所有这些方式,但在开始之前,让我们从安全性角度来看待配置选项。尽管大多数配置选项不包含任何敏感信息,但有几个包含,例如凭据,私有加密密钥以及需要保密的类似数据。这种类型的信息需要特别小心处理,这就是为什么Kubernetes提供了另一种名为Secret的对象类型。我们将在本章最后一部分中详细了解它。
7.2 Passing command-line arguments to containers
到目前为止,在所有的例子中,你创建的容器都运行了容器镜像中定义的默认命令。当你想要运行不同的可执行文件,或者想要使用不同的命令行参数运行它时,Kubernetes允许在Pod的容器定义中覆盖命令。
7.2.1 Defining the command and arguments in Docker
在容器中执行的命令由两部分组成:命令和参数。
理解ENTRYPOINT 和 CMD
CMD
类似于 RUN 指令,用于运行程序,但二者运行的时间点不同:
- CMD 在docker run 时运行。
- RUN 是在 docker build。
注意:如果 Dockerfile 中如果存在多个 CMD 指令,仅最后一个生效。
格式:
CMD <shell 命令>
CMD ["<可执行文件或命令>","<param1>","<param2>",...]
CMD ["<param1>","<param2>",...] # 该写法是为 ENTRYPOINT 指令指定的程序提供默认参数
推荐使用第二种格式,执行过程比较明确。第一种格式实际上在运行的过程中也会自动转换成第二种格式运行,并且默认可执行文件是 sh。
ENTRYPOINT
类似于 CMD 指令,但其不会被 docker run 的命令行参数指定的指令所覆盖,而且这些命令行参数会被当作参数送给 ENTRYPOINT 指令指定的程序。
但是, 如果运行 docker run 时使用了 --entrypoint 选项,将覆盖 ENTRYPOINT 指令指定的程序。
优点:在执行 docker run 的时候可以指定 ENTRYPOINT 运行所需的参数。
注意:如果 Dockerfile 中如果存在多个 ENTRYPOINT 指令,仅最后一个生效。
格式:
ENTRYPOINT ["<executeable>","<param1>","<param2>",...]
可以搭配 CMD 命令使用:一般是变参才会使用 CMD ,这里的 CMD 等于是在给 ENTRYPOINT 传参,以下示例会提到。
示例:
假设已通过 Dockerfile 构建了 nginx:test 镜像:
FROM nginx
ENTRYPOINT ["nginx", "-c"] # 定参
CMD ["/etc/nginx/nginx.conf"] # 变参
1、不传参运行
$ docker run nginx:test
容器内会默认运行以下命令,启动主进程。
nginx -c /etc/nginx/nginx.conf
2、传参运行
$ docker run nginx:test -c /etc/nginx/new.conf
容器内会默认运行以下命令,启动主进程(/etc/nginx/new.conf:假设容器内已有此文件)
nginx -c /etc/nginx/new.conf
理解shell格式和exec格式
一条命令有两种格式
shell
格式:ENTRYPOINT node app.js.
exec
格式:ENTRYPOINT ["node", "app.js"]
不同之处在于指定的命令是否在shell中调用。
在之前创建的kubia镜像中,我们使用的格式是exec
,这将直接运行node进程(而不是在shell内部)。
如果你使用了shell表单(ENTRYPOINT node app.js
),那么这些将是容器的进程。
通过参数使fortune镜像的间隔可配置化
让我们修改fortune脚本和镜像,使循环中的延迟间隔可配置化。你将添加一个INTERVAL变量,并将其初始化为第一个命令行参数的值,如下面的清单所示。
#!/bin/bash
trap "exit" SIGINT
INTERVAL=$1
echo Configured to generate new fortune every $INTERVAL seconds
mkdir -p /var/htdocs
while :
do
echo $(date) Writing fortune to /var/htdocs/index.html
/usr/games/fortune > /var/htdocs/index.html
sleep $INTERVAL
done
将修改Dockerfile,以便使用ENTRYPOINT指令的exec版本,并使用CMD指令将默认间隔设置为10秒,如下面所示:
FROM ubuntu:latest
RUN apt-get update ; apt-get -y install fortune
ADD fortuneloop.sh /bin/fortuneloop.sh
ENTRYPOINT ["/bin/fortuneloop.sh"]
CMD ["10"]
然后将其推送至Dockerhub,这里我使用作者的镜像:docker.io/luksa/fortune:args
$ docker pull docker.io/luksa/fortune:args
$ docker run -it docker.io/luksa/fortune:args
Configured to generate new fortune every 10 seconds
Mon May 29 11:53:31 UTC 2023 Writing fortune to /var/htdocs/index.html
还可以通过参数传递来覆盖默认的睡眠间隔:
$ docker run -it docker.io/luksa/fortune:args 15
Configured to generate new fortune every 15 seconds
Mon May 29 11:54:58 UTC 2023 Writing fortune to /var/htdocs/index.html
7.2.2 Overriding the command and arguments in Kubernetes
在Kubernetes中,当指定容器时,可以选择覆盖ENTRYPOINT和CMD。为此,在容器规范中设置命令和args属性,如所示:
kind: Pod
sepc:
containers:
- image: some/image
command: ["/bin/command"]
args: ["arg1", "arg2"]
在大多数情况下,只需设置自定义参数,很少覆盖命令。
Docker和Kubernetes中命令和参数的对应关系如下:
Pod配置文件如下:
# fortune-pod-args.yaml
apiVersion: v1
kind: Pod
metadata:
name: fortune2s
spec:
volumes:
- name: html
emptyDir: {}
containers:
# 镜像替换
- image: luksa/fortune:args
# 传入参数,睡眠间隔修改为2秒
args: ["2"]
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
如果需要传入多个参数,可以这样传递:
args:
- foo
- bar
- "15"
其中数字必须要用引号括起来。
7.3 Setting environment variables for a container
容器化应用程序常常使用环境变量作为配置选项。 Kubernetes允许你为每个Pod的容器指定自定义环境变量列表,如图7.1所示。尽管在Pod级别定义环境变量并让容器继承它们会很有用,但目前没有这样的选项。
通过环境变量使fortune镜像的间隔可配置化
修改fortuneloop.sh,使其能通过环境变量使睡眠时间可配置化:
#!/bin/bash
trap "exit" SIGINT
echo Configured to generate new fortune every $INTERVAL seconds
mkdir -p /var/htdocs
while :
do
echo $(date) Writing fortune to /var/htdocs/index.html
/usr/games/fortune > /var/htdocs/index.html
sleep $INTERVAL
done
只需要删掉上一个脚本中的INTERVAL=$1
。
7.3.1 Specifying environment variables in a container definition
构建新镜像后(作者将其标记为luksa/fortune:env
),并将其推送到Docker Hub后,可以通过创建新的Pod来运行它,在其中通过将环境变量包含在容器定义中将其传递到脚本中,如下所示:
apiVersion: v1
kind: Pod
metadata:
name: fortune2s
spec:
volumes:
- name: html
emptyDir: {}
containers:
# 镜像替换
- image: luksa/fortune:env
# 设置环境变量,睡眠间隔修改为30秒
env:
- name: INTERVAL
value: "30"
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
如前所述,在容器定义中设置环境变量,而不是在Pod级别。
不要忘记,在每个容器中,Kubernetes还会自动为同一命名空间中的每个服务公开环境变量。这些环境变量基本上是自动注入的。
7.3.2 Referring to other environment variables in a variable’s value
在前面的示例中,你为环境变量设置了一个固定值,但你还可以使用$(VAR)
语法引用先前定义的环境变量或任何其他现有变量。如果你定义了两个环境变量,第二个可能包含第一个的值,如下所示:
env:
- name: FIRST_VAR
value: "foo"
- name: SECOND_VAR
value: "$(FIRST_VAR)bar"
7.3.3 Understanding the drawback of hardcoding environment variables
在Pod定义中添加硬编码值意味着你需要为生产Pod和开发Pod编写不同的Pod定义。要在多个环境中重用相同的Pod定义,最好将配置与Pod描述符解耦。可以使用ConfigMap资源来做到这一点,并使用valueFrom
(而不是value字段)将其用作环境变量值的来源。
7.4 Decoupling configuration with a ConfigMap
7.4.1 Introducing ConfigMaps
Kubernetes允许将配置选项分离成一个叫做ConfigMap的独立对象,它是一个包含键/值对的Map,值的范围从简单的文本到完整的配置文件。
应用程序不需要直接读取或甚至知道ConfigMap的存在。而是以环境变量或作为文件的形式传递键/值对给容器(见图7.2)。因为环境变量可以使用$(ENV_VAR)语法在命令行参数中引用,所以你也可以将ConfigMap条目作为命令行参数传递给进程。
当然,如果需要,应用程序也可以直接通过Kubernetes REST API端点读取ConfigMap的内容,但除非你真的需要这样做,否则应该尽可能使你的应用程序与Kubernetes无关。
无论应用程序如何使用ConfigMap,将配置信息放在独立的对象中能够让你为不同的环境(开发,测试,QA,生产等)维护多个具有相同名称的ConfigMaps。因为Pod通过名称引用ConfigMap,所以在所有环境中都可以使用相同的Pod规范同时在每个环境中使用不同的配置信息(见图7.3)。
7.4.2 Creating a ConfigMap
将使用特殊的kubectl create configmap
命令创建ConfigMap,而不是使用通用的kubectl create -f
命令发布YAML。
可以通过将命令直接传递给kubectl命令来定义Map中的条目,或者可以从存储在磁盘上的文件创建ConfigMap。首先使用一个简单的命令输入:
$ kubectl create configmap fortune-config --from-literal=sleep-interval=25
configmap/fortune-config created
这将创建一个名为fortune-config的ConfigMap,其中单个条目为sleep-interval = 25(见图7.4)。
ConfigMap通常包含多个条目。如果要创建具有多个字面条目的ConfigMap,则需要添加多个–from-literal参数。
$ kubectl create configmap myconfigmap --from-literal=foo=bar --from-literal=bar=baz --from-literal=one=two
让我们使用kubectl get命令查看创建的ConfigMap的YAML描述符,如下所示:
$ kubectl get configmap fortune-config -o yaml
apiVersion: v1
data:
sleep-interval: "25"
kind: ConfigMap
metadata:
creationTimestamp: "2023-05-29T13:56:08Z"
name: fortune-config
namespace: default
resourceVersion: "2881457"
uid: 090a67b4-776b-4295-ad59-37d2f5206c8c
这没什么不同的,你可以通过$ kubectl create -f fortune-config.yaml
很轻易地配置YAML文件。
从文件中创建ConfigMap
ConfigMap也可以存储粗粒度的配置数据,例如完整的配置文件。为了实现这一点,kubectl create configmap 命令还支持从磁盘读取文件,并将它们作为单个条目存储在ConfigMap中。
$ kubectl create configmap my-config --from-file=config-file.conf
当你执行前面的命令时,kubectl会在你运行kubectl的目录中查找config-file.conf文件。然后将文件的内容存储在ConfigMap中的config-file.conf
键下(文件名用作映射键),但是你还可以手动指定一个键,例如:
$ kubectl create configmap my-config --from-file=customkey=config-file.conf
这将文件的内容存在键customkey
下,同样可以使用多个--from-file
创建多个条目。
从目录中的文件创建ConfigMap
甚至可以从文件目录中导入所有文件,而无需逐个导入每个文件:
$ kubectl create configmap my-config --from-file=/path/to/dir
在这种情况下,kubectl将为指定目录中的每个文件创建单独的映射条目,但仅限于文件名是有效的ConfigMap键的文件。
组合不同的选项
创建ConfigMap时,可以结合使用本章中提到的所有选项
$ kubectl create configmap my-config --from-file=foo.json --from-file=bar=foobar.conf --from-file=config-opts/ --from-literal=some=thing
7.4.3 Passing a ConfigMap entry to a container as an environment variable
那么,如何将Map中的值传递给Pod的容器呢?有三个选项。让我们从最简单的开始 - 设置环境变量。Pod描述符应如下所示:
# fortune-pod-env-configmap.yaml
apiVersion: v1
kind: Pod
metadata:
name: fortune-env-from-configmap
spec:
volumes:
- name: html
emptyDir: {}
containers:
# 镜像替换
- image: luksa/fortune:env
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
key: sleep-interval
name: fortune-config
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
你定义了一个名为INTERVAL的环境变量,并将其值设置为存储在fortune-config ConfigMap中的sleep-interval键下的任何值。当在html-generator容器中运行的进程读取INTERVAL环境变量时,它将看到值25(如图7.6所示)。
引用Pod中不存在的ConfigMap
如果在创建Pod时引用的ConfigMap不存在会发生什么情况。Kubernetes将正常安排Pod,并尝试运行其容器。引用不存在的ConfigMap的容器将无法启动,但其他容器将正常启动。如果你随后创建缺少的ConfigMap,则无需重新创建Pod即可启动失败的容器。
还可以将对ConfigMap的引用标记为可选(通过设置配置
configMapKeyRef.optional: true
)。在这种情况下,即使ConfigMap不存在,容器也会启动。
此示例向你展示了如何将配置与Pod规格分离。这使你可以将所有配置选项保持紧密连接(即使对于多个Pods)而不是将它们散落在Pod定义中(或者在多个Pod配置文件中重复)。
7.4.4 Passing all entries of a ConfigMap as environment variables at once
当你的ConfigMap包含不止少量的条目时,从每个条目中单独创建环境变量变得繁琐且容易出错。幸运的是,Kubernetes 提供了一种将ConfigMap的所有条目都公开为环境变量的方法。
有一个名为FOO、BAR和FOO-BAR的ConfigMap。可以使用envFrom属性将它们全部公开为环境变量,而不是像之前的示例一样使用env。如下所示:
spec:
containers:
- image: some-image
envFrom: # 用envFrom代替env
- prefix: CONFIG_ # 所有环境变量都将以CONFIG_作为前缀。
configMapRef:
name: my-config-map # 引用名为my-config-map的ConfigMap
如你所见,你还可以为环境变量指定前缀(在这种情况下为CONFIG_)。这将导致容器内存在以下两个环境变量:CONFIG_FOO
和CONFIG_BAR
。
前缀是可选的,因此如果省略它,则环境变量的名称与键相同。
你是否注意到我说有两个变量,但是早些时候我说ConfigMap有三个条目(FOO,BAR和FOO-BAR)?为什么FOO-BARConfigMap 条目没有环境变量?
原因是CONFIG_FOO-BAR不是有效的环境变量名称,因为它包含连字符。 Kubernetes不会以任何方式转换键(例如,它不会将破折号转换为下划线)。 如果某个ConfigMap键格式不正确,则会跳过该条目(但是它会记录一个事件告知你已跳过它)。
7.4.5 Passing a ConfigMap entry as a command-line argument
现在,让我们来看看如何将ConfigMap中的值作为参数传递给容器中运行的主进程。你不能直接在pod.spec.containers.args
字段中引用ConfigMap条目,但是你可以首先从ConfigMap条目初始化环境变量,然后像图7.7所示的那样在参数中引用该变量。
# fortune-pod-args-configmap.yaml
apiVersion: v1
kind: Pod
metadata:
name: fortune-env-from-configmap
spec:
volumes:
- name: html
emptyDir: {}
containers:
# 镜像替换
- image: luksa/fortune:env
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
key: sleep-interval
name: fortune-config
args: ["$(INTERVAL)"]
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
像前面一样定义了环境变量,但随后使用了$(ENV_VARIABLE_NAME)
语法让Kubernetes将变量的值注入到参数中。
7.4.6 Using a configMap volume to expose ConfigMap entries as files
将配置选项作为环境变量或命令行参数传递通常用于短变量值。正如所见,ConfigMap 也可以包含整个配置文件。当想将它们暴露给容器时,可以使用在前一章中提到的一种特殊的卷类型,即 ConfigMap 卷。
ConfigMap 卷将每个 ConfigMap 的条目公开为文件。在容器中运行的进程可以通过读取文件内容来获取条目的值。
尽管该方法主要用于将大型配置文件传递到容器中,但并没有阻止以这种方式传递短单一的值。
# 启用了gzip压缩的Nginx配置:my-nginx-config.conf
server{
listen 80;
server_name www.kubia-example.com;
# 为纯文本和XML文件启用了gzip压缩。
gzip on;
gzip_types text/plain application/xml;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
现在使用 kubectl delete configmap fortune-config 命令删除现有的 fortune-config ConfigMap,以便可以用新的 ConfigMap 替换它,该新 ConfigMap 将包括 Nginx 配置文件。将从存储在本地磁盘上的文件中创建 ConfigMap。
创建一个名为 configmap-files 的新目录,并将先前文章中的 Nginx 配置文件存储到 configmap-files/my-nginx-config.conf 文件中。为了使 ConfigMap 也包含 sleep-interval 条目,请在相同目录中添加一个名为 sleep-interval 的纯文本文件,并在其中存储数字 25(参见图 7.8)。
现在从目录中的所有文件创建一个ConfigMap,如下所示:
$ kubectl create configmap fortune-config --from-file=configmap-files/
configmap/fortune-config created
查看其YAML内容:
$ kubectl get configmap fortune-config -o yaml
apiVersion: v1
data:
my-nginx-config.conf: |-
server{
listen 80;
server_name www.kubia-example.com;
# 为纯文本和XML文件启用了gzip压缩。
gzip on;
gzip_types text/plain application/xml;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
sleep-interval: "25"
kind: ConfigMap
metadata:
creationTimestamp: "2023-05-30T02:25:57Z"
name: fortune-config
namespace: default
resourceVersion: "2973738"
uid: 4411ddfa-7491-4d4b-a1a4-fb9900faba3b
在两个条目的第一行中,冒号后面的管道字符表示后面跟着一个文字多行值。
该 ConfigMap 包含两个条目,键对应于它们所创建的实际文件的名称。现在将在两个 pod 容器中使用此 ConfigMap。
在卷中使用ConfigMap条目
创建一个包含 ConfigMap 内容的卷就像创建一个通过名称引用 ConfigMap 并在容器中挂载该卷一样简单。你已经学会了如何创建卷并将其挂载,所以还要学习的就是如何使用 ConfigMap 条目创建文件来初始化该卷。
Nginx 从 /etc/nginx/nginx.conf
文件读取其配置文件。Nginx 镜像已包含了该文件的默认配置选项,你不想覆盖该文件,因此不想替换该文件。幸运的是,缺省配置文件会自动将 /etc/nginx/conf.d/
子目录中的所有 .conf 文件包含在内,因此你应在该目录中添加配置文件。图 7.9 显示了想要实现的内容。
Pod的YAML文件如下:
# fortune-pod-configmapvolume.yaml
apiVersion: v1
kind: Pod
metadata:
name: fortune-configmap-volume
spec:
containers:
- image: luksa/fortune:env
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
name: fortune-config
key: sleep-interval
name: html-generator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
- name: config
# 将在此位置挂载configMap卷
mountPath: /etc/nginx/conf.d
readOnly: true
- name: config
mountPath: /tmp/whole-fortune-config-volume
readOnly: true
ports:
- containerPort: 80
name: http
protocol: TCP
volumes:
- name: html
emptyDir: {}
- name: config
# 引用了ConfigMap
configMap:
name: fortune-config
验证
现在,Web 服务器应该已配置为压缩其发送的响应。可以通过从 localhost:8080 启用端口转发到 pod 的端口 80,并使用 curl 检查服务器的响应来验证
$ kubectl port-forward fortune-configmap-volume 8080:80 &
# [1] 9788
# Forwarding from 127.0.0.1:8080 -> 80
$ curl -H "Accept-Encoding: gzip" -I localhost:8080
# HTTP/1.1 200 OK
# Server: nginx/1.21.5
# Date: Tue, 30 May 2023 02:41:54 GMT
# Content-Type: text/html
# Last-Modified: Tue, 30 May 2023 02:41:44 GMT
# Connection: keep-alive
# ETag: W/"64756268-49"
# Content-Encoding: gzip
让我们看看/etc/nginx/conf.d
目录下的内容
$ kubectl exec fortune-configmap-volume -c web-server ls /etc/nginx/conf.d
# my-nginx-config.conf
# sleep-interval
ConfigMap 的两个条目都已作为文件添加到该目录。尽管其中包含了 sleep-interval 条目,但它并不属于该目录,因为它只适用于 fortuneloop 容器。可以创建两个不同的 ConfigMaps,并使用其中一个来配置 fortuneloop 容器,另一个用于配置 web-server 容器。但是,以某种方式使用多个 ConfigMaps 来配置同一 pod 中的容器感觉有些不对。毕竟,将容器放在同一个 pod 中意味着这些容器密切相关,可能应该作为一个单元进行配置。
显示卷中的某些ConfigMap条目
可以仅使用 ConfigMap 条目的一部分来填充 configMap 卷,例如仅使用 my-nginx-config.conf 条目。这不会影响 fortuneloop 容器,因为你将 sleep-interval 条目通过环境变量传递给它,而不是通过卷传递。
要定义应将哪些条目作为文件公开在 configMap 卷中,请使用卷的 items 属性,如所示
volumes:
- name: html
emptyDir: {}
- name: config
# 引用了ConfigMap
configMap:
name: fortune-config
items:
- key: my-nginx-config.conf
path: gzip.conf
在指定各个条目时,需要为每个条目设置文件名和条目键。如果运行上一列表中的 Pod,则 /etc/nginx/conf.d
仅包含 gzip.conf 文件而不包含其他文件。
挂载目录会隐藏该目录中的现有文件
将卷挂载为目录,这意味着隐藏了容器镜像中存储在 /etc/nginx/conf.d 目录中的任何文件。
这通常是 Linux 中在将文件系统挂载到非空目录时发生的情况。这时目录仅包含来自挂载文件系统的文件,而那个目录中的原始文件在文件系统挂载期间是不可访问的。
请想象一下将一个卷挂载到通常包含许多重要文件的 /etc 目录。这很可能会破坏整个容器,因为本应在 /etc 目录中的所有原始文件将不再存在。如果需要向像 /etc 这样的目录中添加文件,则根本不能使用这种方法。
将单个ConfigMap条目挂载为文件,而不隐藏目录中其他文件
volumeMount
上的另一个 subPath
属性允许从卷中挂载单个文件或单个目录,而不是挂载整个卷。如下图所示:
假设有一个包含myconfig.conf
文件的 configMap 卷,想将它添加到 /etc
目录中作为 someconfig.conf
。可以使用 subPath
属性将其挂载到该目录中,而不会影响该目录中的任何其他文件。下面这个 Pod 定义显示了相关部分:
spec:
containers:
- image: some/image
volumeMounts:
- name: myVolume
mountPath: /etc/someconfig.conf
# 只需挂载myconfig.conf条目,而不是挂载整个卷
subPath: myconfig.conf
默认情况下,ConfigMap 卷中所有文件的权限设置为 644(-rw-r--r--)
。您可以通过在卷规范中设置 defaultMode 属性来更改此属性,
volumes:
- name: config
configMap:
name: fortune-config
defaultMode: "6600" # 权限设置为-rw-rw------
7.4.7 Updating an app’s config without having to restart the app
当你更新 ConfigMap 时,引用它的所有卷中的文件都会被更新。然后由进程检测它们已被更改并重新加载它们。
编辑ConfigMap
现在让我们看看如何更改 ConfigMap,并让 Pod 中的进程重新加载暴露在 configMap 卷中的文件。您将修改先前示例中的 Nginx 配置文件,使其在不重新启动 Pod 的情况下使用新的配置。尝试通过使用 kubectl edit
编辑 fortune-config
ConfigMap 来关闭 gzip 压缩功能:
$ kubectl edit configmap fortune-config
configmap/fortune-config edited
将gzip on
更改为gzip off
,保存文件,然后关闭该编辑器。然后更新ConfigMap,不久之后,卷中的实际文件也会更新。可以通过使用kubectl exec打印文件的内容来确认这一点:
$ kubectl exec fortune-configmap-volume -c web-server cat /etc/nginx/conf.d/my-nginx-config.conf
server{
listen 80;
server_name www.kubia-example.com;
# 为纯文本和XML文件启用了gzip压缩。
gzip off;
gzip_types text/plain application/xml;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}r
你会看到配置文件中的更改,但你会发现这对 Nginx 没有任何影响,因为它不会自动监视文件并重新加载它们
向Nginx发送信号以重新加载配置
Nginx将继续压缩其响应,直到告诉它重新加载其配置文件,可以使用以下命令来执行此操作:
kubectl exec fortune-configmap-volume -c web-server -- nginx -s reload
$ curl -H "Accept-Encoding: gzip" -I localhost:8080
HTTP/1.1 200 OK
Server: nginx/1.21.5
Date: Tue, 30 May 2023 03:12:37 GMT
Content-Type: text/html
Content-Length: 51
Last-Modified: Tue, 30 May 2023 03:12:35 GMT
Connection: keep-alive
ETag: "647569a3-33"
Accept-Ranges: bytes
可以发现响应没有再被压缩
由于ConfigMap卷中的文件不会在所有运行的实例中同步更新,单个Pod中的文件可能会在长达一整分钟的时间内不同步。
7.5 Using Secrets to pass sensitive data to containers
7.5.1 Introducing Secrets
为了存储和分发这些信息,Kubernetes 提供了一个称为 Secret 的独立对象。Secret 与 ConfigMap 很相似——它们也是持有键值对的映射。它们的使用方式与 ConfigMap 相同,你可以:
- 将 Secret 作为环境变量传递给容器。
- 在 Volume 中将 Secret 映射为文件。
Kubernetes 通过确保每个 Secret 仅分发到需要访问 Secret 的 pod 运行的节点上,来确保你的 Secrets 安全。此外,在节点上,Secret 始终存储在内存中,永远不会写入物理存储器中。
在控制节点上(更具体地说是在 etcd 中),从 Kubernetes 版本 1.7 开始,etcd 以加密形式存储 Secrets,使系统更加安全。因此,你必须在正确的场景下选择使用 Secret 或 ConfigMap。它们之间的区别很简单:
- 使用 ConfigMap 存储非敏感的普通配置数据。
- 使用 Secret 存储具有敏感性质并需要进行加密的数据。如果配置文件包括敏感和非敏感数据,则应将文件存储在 Secret 中。
7.5.2 Introducing the default token Secret
你可以通过检查每个容器中挂载的 Secret 来开始学习 Secret。当你使用 kubectl describe 命令查看 pod 时,可能已经注意到了它。命令的输出始终包含以下内容:
Volumes:
default-token-cfee9:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-cfee9
每个 pod 都会自动附加一个 secret volume。上述kubectl describe
输出中的 volume 指的是一个名为 default-token-cfee9 的 Secret。由于 Secrets 是资源,因此可以使用 kubectl get secrets
列出它们,并在列表中找到 default-token Secret。让我们看一下:
$ kubectl get secrets
NAME TYPE DATA AGE
default-token-lqrbh kubernetes.io/service-account-token 3 16d
你也可以使用 kubectl describe 命令来了解更多信息,如下所示。
$ kubectl describe secrets
Name: default-token-lqrbh
Namespace: default
Labels: <none>
Annotations: kubernetes.io/service-account.name: default
kubernetes.io/service-account.uid: 43b73c21-1084-4115-b8e5-0b51580bb18d
Type: kubernetes.io/service-account-token
Data
====
token: eyJhbGciOiJSUzI1NiIsImtpZCI6ImhwNkxjWFIzSC1KSHNNZ0VaN09YWHdGY1ItYlh4YXNvR3Z2c1NfRmd3encifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tbHFyYmgiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjQzYjczYzIxLTEwODQtNDExNS1iOGU1LTBiNTE1ODBiYjE4ZCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.P3zG24SkN2FPmTIR_a0ZYVl4rH-CF1NTNsjw3VkOjU2J4or5bwUVTs8d1zBBl6YmQEc16v06cgpijXzRJJ6bX99dFfg4eT8Jw1O9_QKhpQp3Wv97LdXgoIRd0PVsMNVfh4IsD0yQwv4hoZsC8thG1muN7sYPSPWH0WFV42EiQ_VZKP7gv-oB3eZxWpEbdP747FvBOR4py-NcCaI6XZBT2xKJWxASguIbv_Pt7Jf-Qt2EUZkSfW1ACbkxBbV3lcqm-b_Kk2XngN5hfcmX0ukyAI2MqgftzGxYCmQzexDKqI-T3wTi_y_7B1RK6vgzWkYr5E1-vDJyHKL4kJkdp0yqrg
ca.crt: 1066 bytes
namespace: 7 bytes
你可以看到该 Secret 包含三个条目:ca.crt、namespace 和 token,它们代表你需要的一切,以便在需要时从 pod 内部安全地与 Kubernetes API server 进行通信。下图显示了 secret volume 的挂载位置:
7.5.3 Creating a Secret
现在,你将创建自己的小型 Secret。你将通过配置它,改进你的 fortune-serving Nginx 容器,使其还可以服务于 HTTPS 流量。为此,需要创建证书和私钥。私钥需要保密,所以你将把它和证书放入 Secret。
首先,在你的本地机器上生成证书和私钥文件:
$ openssl genrsa -out https.key 2048
$ openssl req -new -x509 -key https.key -out https.cert -days 3650 -subj /CN=www.kubia-example.com
为了更好地展示有关 Secrets 的一些内容,现在创建一个名为 foo 的虚拟文件,使其包含字符串 bar。
使用kubectl create secret
命令从这三个文件中创建Secret:
$ kubectl create secret generic fortune-https --from-file=https.key --from-file=https.cert --from-file=foo
secret/fortune-https created
这与创建 ConfigMaps 并没有太大的区别。在这种情况下,你正在创建一个名为 fortune-https 的通用 Secret,其中包括两个条目(https.key 包含 https.key 文件内容,https.cert 同理)。正如你之前学习的那样,你还可以使用 --from-file=fortune-https
来包含整个目录,而不是逐个指定每个文件。
7.5.4 Comparing ConfigMaps and Secrets
查看所创建的Secret的YAML描述:
$ kubectl get secret fortune-https -o yaml
apiVersion: v1
data:
foo: YmFyCg==
https.cert: LS0tLS1CRUdJTiBDRV......
https.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBL......
kind: Secret
metadata:
creationTimestamp: "2023-05-30T07:00:51Z"
name: fortune-https
namespace: default
resourceVersion: "3007580"
uid: 2154f404-8cd3-4cec-af76-f37ab51a2e36
type: Opaque
将其与先前创建的ConfigMap的YAML进行比较:
$ kubectl get configmap fortune-config -o yaml
apiVersion: v1
data:
my-nginx-config.conf: |-
server{
listen 80;
server_name www.kubia-example.com;
# 为纯文本和XML文件启用了gzip压缩。
gzip off;
gzip_types text/plain application/xml;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
sleep-interval: "25"
kind: ConfigMap
metadata:
creationTimestamp: "2023-05-30T02:25:57Z"
name: fortune-config
namespace: default
resourceVersion: "2978884"
uid: 4411ddfa-7491-4d4b-a1a4-fb9900faba3b
Secret 条目的内容以 Base64 编码字符串的形式显示,而 ConfigMap 的内容则在明文中显示。
使用 Base64 编码的原因很简单。Secret 的条目可以包含二进制值,而不仅仅是纯文本。Base64 编码允许你在 YAML 或 JSON 中包含二进制数据,它们都是纯文本格式。
你甚至可以将 Secrets 用于非敏感的二进制数据,但要注意 Secret 的最大大小限制为 1MB。
由于不是所有敏感数据都是以二进制形式存在的,Kubernetes 还允许通过 stringData 字段设置 Secret 的值:
kind: Secret
apiVersion: v1
stringData: # 可以用于非二进制的Secret数据。
foo: plain text
data:
https.cert: LS0tLS1CRUdJTiBDRV......
https.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBL......
stringData 字段是只写的(请注意:只写,不是只读)。它只能用于设置值。当你使用 kubectl get -o yaml 检索 Secret 的 YAML 时,stringData 不会显示。相反,在 data 下会显示你在 stringData 字段中指定的所有条目,并像其他条目一样进行 Base64 编码。
当你通过 secret volume 将 Secret 暴露给容器时,Secret 条目的值会被解码并以其实际形式写入文件中(无论它是纯文本还是二进制)。当你通过环境变量暴露 Secret 条目时,同样也会是这样。在这两种情况下,应用程序不需要解码,而是可以读取文件内容或查找环境变量值并直接使用它。
7.5.5 Using the Secret in a pod
修改fortune-config以启用HTTPS
$ kubectl edit configmap fortune-config
# 修改后的conf
my-nginx-config.conf: |-
server{
listen 80;
listen 443 ssl;
server_name www.kubia-example.com;
ssl_certificate certs/https.cert;
ssl_certificate_key certs/https.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
这将配置服务器从/etc/nginx/certs读取证书和密钥文件,因此你需要在那里挂载保密卷。
接下来,你将创建一个新的fortune-https pod,并将保密卷挂载到web-server容器中的正确位置,如下面的清单所示。
apiVersion: v1
kind: Pod
metadata:
name: fortune-https
spec:
containers:
- image: luksa/fortune:env
name: html-generator
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
name: fortune-config
key: sleep-interval
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
- name: config
mountPath: /etc/nginx/conf.d
readOnly: true
- name: certs # 已将Nginx配置为从/etc/nginx/certs读取证书和密钥文件,因此需要在那里挂载Secret卷
mountPath: /etc/nginx/certs/
readOnly: true
ports:
- containerPort: 80
- containerPort: 443
volumes:
- name: html
emptyDir: {}
- name: config
configMap:
name: fortune-config
items:
- key: my-nginx-config.conf
path: https.conf
- name: certs # 在此处定义Secret卷
secret:
secretName: fortune-https
一旦Pod运行起来,你可以通过打开到Pod端口443的端口转发隧道,并使用curl向服务器发送请求来查看它是否正在提供HTTPS流量
$ kubectl port-forward fortune-https 8443:443 &
[1] 32574
$ curl https://localhost:8443 -k
Write yourself a threatening letter and pen a defiant reply.
如果你正确地配置了服务器,你应该会得到一个响应。你可以检查服务器的证书,看它是否与你之前生成的证书相匹配。这也可以通过curl打开详细日志记录(使用-v选项)来完成,如下所示:
$ curl https://localhost:8443 -k -v
* Rebuilt URL to: https://localhost:8443/
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: /etc/ssl/certs/ca-certificates.crt
CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use http/1.1
* Server certificate: # 证书与你创建并存储在Secret中的证书匹配。
* subject: CN=www.kubia-example.com
* start date: May 30 06:57:31 2023 GMT
* expire date: May 27 06:57:31 2033 GMT
* issuer: CN=www.kubia-example.com
* SSL certificate verify result: self signed certificate (18), continuing anyway.
> GET / HTTP/1.1
> Host: localhost:8443
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.21.5
< Date: Tue, 30 May 2023 07:40:27 GMT
< Content-Type: text/html
< Content-Length: 79
< Last-Modified: Tue, 30 May 2023 07:40:18 GMT
< Connection: keep-alive
< ETag: "6475a862-4f"
< Accept-Ranges: bytes
<
Q: How many Martians does it take to screw in a light bulb?
A: One and a half.
* Connection #0 to host localhost left intact
通过在目录树的/etc/nginx/certs中挂载Secret卷,你已成功将证书和私钥传递给了容器。Secret卷使用内存文件系统(tmpfs)存储Secret文件。如果你列出容器的挂载点,即可看到:
$ kubectl exec fortune-https -c web-server -- mount | grep certs
tmpfs on /etc/nginx/certs type tmpfs (ro,relatime)
因为使用了tmpfs,存储在Secret中的敏感数据永远不会被写入磁盘,就不会被泄露。
除了使用卷之外,你还可以像在ConfigMap中对sleep-interval条目所做的那样,将Secret的单个条目作为环境变量暴露出来。例如,如果你想要将Secret中的foo密钥作为环境变量FOO_SECRET暴露出来,你需要将下面的代码段添加到容器定义中:
env:
- name: FOO_SECRET
valueFrom:
secretKeyRef:
name: fortune-https
key: foo
这与设置INTERVAL环境变量的方式几乎相同,只是这一次你使用secretKeyRef
而不是configMapKeyRef
来引用Secret。
在使用环境变量将你的机密数据传递给容器之前三思,因为它们可能会无意中暴露。为了安全起见,请始终使用Secret卷来暴露Secret。
7.5.6 Understanding image pull Secrets
你已经学会了如何将Secret传递给应用程序并使用其中的数据。但有时Kubernetes本身需要你向它提供凭据,例如当你想要使用来自私有容器镜像注册表的镜像时。这也是通过Secret完成的。
到目前为止,你的所有容器镜像都存储在公共镜像注册表中,这些注册表不需要任何特殊凭据才能从中拉取镜像。但大多数组织不希望它们的镜像对所有人都可用,因此使用私有镜像注册表。当部署一个容器镜像位于私有注册表中的Pod时,Kubernetes需要知道拉取镜像所需的凭据。让我们看看如何做到这一点。
除了公共镜像仓库以外,Docker Hub还允许你创建私有仓库。你可以通过在http://hub.docker.com上登录,找到仓库并选中一个复选框来将仓库标记为私有。
要运行一个使用私有仓库中的镜像的Pod,你需要做两件事:
- 创建一个Secret用于保存Docker注册表的凭据。
- 在Pod清单的
imagePullSecrets
字段中引用该Secret。
创建一个保存Docker注册表身份验证凭据的Secret与创建7.5.3节中创建的通用Secret并没有太大不同。你使用相同的kubectl create secret命令,但使用了不同的类型和选项:
$ kubectl create secret docker-registry mydockerhubsecret \ --docker-username=myusername --docker-password=mypassword \ --docker-email=my.email@provider.com
你创建的是一个名为mydockerhubsecret的docker-registry Secret,指定了你的Docker Hub用户名、密码和电子邮件。如果你使用kubectl describe查看新创建的Secret的内容,你会看到它包含一个名为dockercfg的条目。这相当于你的home目录中的.dockercfg文件,Docker在你运行docker login命令时创建它。
为了让Kubernetes在从你的私有Docker Hub存储库中拉取镜像时使用这个Secret,你需要在Pod规范中指定这个Secret的名称,如下面的代码所示:
apiVersion: v1
kind: Pod
metadata:
name: private-pod
spec:
imagePullSecrets:
- name: mydockerhubsecret
containers:
- image: username/private:tag
name: main