六、存储

现在来了解容器是如何访问外部磁盘存储的,以及如何在它们之间共享存储空间。容器中的文件在磁盘上是临时存放的,这给容器中运行的较重要的应用程序带来一些问题:

  • 当容器崩溃时文件丢失。kubelet会重新启动容器,但容器会以干净的状态重启。当容器被重建时,我们可能希望新的容器可以在之前容器结束的位置继续运行,比如在物理机上重启进程。可能不需要(或者不想要)整个文件系统被持久化, 但又希望能保存实际数据的目录。
  • 同一Pod中运行多个容器时,我们可能希望容器之间能够共享文件,但容器本身是彼此隔离的。

Kubernetes 卷(Volume)这一抽象概念能够解决这两个问题。它们不像pod这样的顶级资源,而是被定义为pod的一部分,并和pod共享相同的生命周期(临时卷)。这意味着在pod启动时创建卷,并在删除pod时销毁卷。因此,在容器重新启动期间,卷的内容将保持不变,在重新启动容器之后,新容器可以识别前一个容器写入卷的所有文件。针对第二个问题,如果一个pod包含多个容器,那这个卷可以同时被所有的容器使用。

1 卷介绍

Kubernetes的卷是pod的一个组成部分,因此像容器一样在pod的规范中就定义了。它们不是独立的Kubernetes对象,也不能单独创建或删除。pod中的所有容器都可以使用卷,但必须先将它挂载在每个需要访问它的容器中,在每个容器中,都可以在其文件系统的任意位置挂载卷。

有多种卷类型可供选择。其中一些是通用的, 而另一些则相对于当前常用的存储技术有较大差别。

这些卷类型有各种用途,它们的使用也大同小异,因此这里主要介绍一些常用的卷。

2 常用的卷

2.1 emptyDir

最简单的卷类型是emptyDir卷。当 Pod 分派到某个 Node 上时,emptyDir 卷会被创建,并且 Pod 在该节点上运行期间,卷一直存在。 就像其名称表示的那样,卷最初是空的。 尽管 Pod 中的容器挂载 emptyDir 卷的路径可能相同也可能不同,这些容器都可以读写 emptyDir 卷中相同的文件。 当 Pod 因为某些原因被从节点上删除时,emptyDir 卷中的数据也会被永久删除。

容器崩溃并不会导致 Pod 被从节点上移除,因此容器崩溃期间 emptyDir 卷中的数据是安全的。

下面是一个pod中使用emptyDir卷的资源清单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
name: test-pd
spec:
containers:
- image: k8s.gcr.io/test-webserver
name: test-container
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}

通过volumes参数,创建一个名为cache-volume的空卷,挂载到容器的/cache目录下。

下面通过实例演示一下,我们在pod中创建两个容器,一个容器使用Nginx作为Web服务器代理HTML页面,另一个容器使用脚本来生成HTML内容。

首先创建一个名为fortune-pod.yaml的资源清单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1   
kind: Pod
metadata:
name: fortune
spec:
containers:
- name: html-generator # 第一个容器名为html-generator
image: luksa/fortune # 运行luksa/fortune镜像
volumeMounts:
- mountPath: /var/htdocs # 挂载到容器的该路径下
name: html # 名为html的卷
- name: web-server # 第二个容器名为web-server
image: nginx:alpine # 运行nginx:alpine镜像
volumeMounts:
- mountPath: /usr/share/nginx/html # 挂载到容器的该路径下
name: html # 名为html的卷
readOnly: true # 只读
ports:
- containerPort: 80
protocol: TCP
volumes:
- name: html
emptyDir: {}

pod包含两个容器和一个挂载在两个容器中的共用的卷,但在不同的路径上。当 html-generator容器启动时,它每10秒启动一次fortune命令输出到/var/htdocs/index.html文件,index.html文件被写入卷中。一旦web-server容器启动,它就开始为/usr/share/nginx/html目录中的任意HTML文件提供服务(这是Nginx服务的默认服务文件目录)。最终的效果是,一个客户端向pod上的80端口发送一个HTTP请求,将接收当前的fortune消息作为响应。

现在创建该pod

1
kubectl apply -f fortune-pod.yaml

然后查看pod的运行状态

1
2
3
kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
fortune 2/2 Running 0 6m48s 10.244.1.2 k8s-node-1 <none> <none>

可以看到运行成功,该pod运行在node-1节点上,且IP为10.244.1.2,我们尝试访问它:

1
2
3
4
curl 10.244.1.2:80
You teach best what you most need to learn.
curl 10.244.1.2:80
You can do very well in speculation where land or anything to do with dirt is concerned.

每隔几秒发送一个请求,会接收到不同的信息。说明两个容器之间通过卷共享了index.html文件。

image-20220105055804339

同样,也可以进入到nginx容器内部查看这个html文件:

1
kubectl exec -it fortune -c web-server -- /bin/sh

进入到/usr/share/nginx/html目录下:

1
2
cd /usr/share/nginx/html
cat index.html

同样可以看到该文件的变化:

image-20220105060413961

并且该文件是只读的:

image-20220105060550874

emptyDir 的一些应用场景:

  • 缓存空间,例如基于磁盘的归并排序。
  • 为耗时较长的计算任务提供检查点,以便任务能方便地从崩溃前状态恢复执行。
  • 在 Web 服务器容器服务数据时,保存内容管理器容器获取的文件。

2.2 hostPath

大多数pod应该忽略它们的主机节点,因此它们不应该访问节点文件系统上的任何文件。但是某些系统级别的pod( 这些通常由DaemonSet 管理)确实需要读取节点的文件或使用节点文件系统来访问节点设备。Kubernetes通过hostPath卷实现了这一点。

警告:

HostPath 卷存在许多安全风险,最佳做法是尽可能避免使用 HostPath。 当必须使用 HostPath 卷时,它的范围应仅限于所需的文件或目录,并以只读方式挂载。

如果通过 AdmissionPolicy 限制 HostPath 对特定目录的访问, 则必须要求 volumeMounts 使用 readOnly 挂载以使策略生效。

hostPath卷指向节点文件系统上的特定文件或目录,同一个节点上运行并在其hostPath卷中使用相同路径的pod可以看到相同的文件。

image-20220105061403174

通过hostPath,可以运行一个需要访问Docker内部的容器;使用 hostPath 挂载 /var/lib/docker 路径。

hostPath也是我们介绍的第一种类型的持久性存储,因为emptyDir卷的内容会在pod被删除时被删除,而hostPath卷的内容则不会被删除。如果删除了一个pod,并且下一个pod使用了指向主机上相同路径的hostPath卷,则新pod将会发现上一个pod留下的数据,但前提是必须将其调度到与第一个pod相同的节点上。

除了必需的 path 属性之外,用户可以选择性地为 hostPath 卷指定 type

支持的 type 值如下:

取值 行为
空字符串(默认)用于向后兼容,这意味着在安装 hostPath 卷之前不会执行任何检查。
DirectoryOrCreate 如果在给定路径上什么都不存在,那么将根据需要创建空目录,权限设置为 0755,具有与 kubelet 相同的组和属主信息。
Directory 在给定路径上必须存在的目录。
FileOrCreate 如果在给定路径上什么都不存在,那么将在那里根据需要创建空文件,权限设置为 0644,具有与 kubelet 相同的组和所有权。
File 在给定路径上必须存在的文件。
Socket 在给定路径上必须存在的 UNIX 套接字。
CharDevice 在给定路径上必须存在的字符设备。
BlockDevice 在给定路径上必须存在的块设备。

当使用这种类型的卷时要小心,因为:

  • HostPath 卷可能会暴露特权系统凭据(例如 Kubelet)或特权 API(例如容器运行时套接字), 可用于容器逃逸或攻击集群的其他部分。
  • 具有相同配置(例如基于同一 PodTemplate 创建)的多个 Pod 会由于节点上文件的不同而在不同节点上有不同的行为。
  • 下层主机上创建的文件或目录只能由 root 用户写入。你需要在 特权容器 中以 root 身份运行进程,或者修改主机上的文件权限以便容器能够写入 hostPath 卷。

下面是一个hostPath的资源清单配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Pod
metadata:
name: test-pd
spec:
containers:
- image: k8s.gcr.io/test-webserver
name: test-container
volumeMounts:
- mountPath: /test-pd
name: test-volume
volumes:
- name: test-volume
hostPath:
# 宿主上目录位置
path: /data
# 此字段为可选
type: Directory

hostPath的特点在于,只要节点可以挂载任何存储,那么节点上的pod就可以使用该存储,因此它具有很强的灵活性,可以与很多远程存储对接。

3 持久卷

当运行在一个pod中的应用程序需要将数据保存到磁盘上,并且即使该pod重新调度到另一个节点时也要求具有相同的数据可用。这就不能使用到目前为止我们提到的任何卷类型,由于这些数据需要可以从任何集群节点访问,因此必须将其存储在某种类型的网络存储(NAS) 中。

持久卷(PersistentVolume,PV)是集群中的一块存储,由管理员事先供应,或者 使用存储类(Storage Class)来动态供应。持久卷是集群资源,就像节点也是集群资源一样。

持久卷声明(PersistentVolumeClaim,PVC)表达的是用户对存储的请求。概念上与Pod类似。Pod会耗用节点资源,而PVC申领会耗用PV资源。Pod可以请求特定数量的资源(CPU和内存);同样PVC申领也可以请求特定的大小和访问模式 (例如,可以要求PV卷能够以ReadWriteOnce、ReadOnlyMany或ReadWriteMany模式之一来挂载。

举个例子:首先由集群管理员设置底层存储,然后通过Kubernetes API服务器创建持久卷并注册。在创建持久卷时,管理员可以指定其大小和所支持的访问模式。当集群用户需要在其pod中使用持久化存储时,他们首先创建持久卷声明清单,指定所需要的最低容量要求和访问模式,然后用户将持久卷声明清单提交给Kubernetes API服务器,Kubernetes将找到可匹配的待久卷并将其绑定到持久卷声明。整个流程如图所示:

image-20220105064421274

一旦绑定关系建立,则PVC绑定就是排他性的,无论该PVC是如何与PV建立的绑定关系。PVC与PV之间的绑定是一种一对一的映射。

注意,由于持久卷是集群资源,持久卷不属于任何命名空间。如下图所示:

image-20220105065949481

3.1 持久卷的类型

PV 持久卷是用插件的形式来实现的。Kubernetes 目前支持以下插件(参考官方文档):

以下的持久卷已被弃用。这意味着当前仍是支持的,但是 Kubernetes 将来的发行版会将其移除。

  • cinder - Cinder(OpenStack 块存储)(于 v1.18 弃用
  • flocker - Flocker 存储(于 v1.22 弃用
  • quobyte - Quobyte 卷 (于 v1.22 弃用
  • storageos - StorageOS 卷(于 v1.22 弃用

旧版本的 Kubernetes 仍支持这些“树内(In-Tree)”持久卷类型:

  • photonPersistentDisk - Photon 控制器持久化盘。(v1.15 之后 不可用
  • scaleIO - ScaleIO 卷(v1.21 之后 不可用

3.2 持久卷的创建

下面是一个是NFS类型的PV资源清单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: PersistentVolume # 类型为PV资源
metadata:
name: pv0003
spec:
capacity:
storage: 5Gi # 容量为5G
volumeMode: Filesystem
accessModes: # 访问模式
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle # 回收策略
storageClassName: slow # 存储类
mountOptions:
- hard
- nfsvers=4.1
nfs: # nfs服务
path: /tmp
server: 172.17.0.2

3.3 访问模式

通过accessModes参数设置访问模式。PV可以用资源提供者所支持的任何方式挂载到宿主系统上。 如下表所示,每个不同类型的卷插件所支持的模式不同。

卷插件 ReadWriteOnce ReadOnlyMany ReadWriteMany ReadWriteOncePod
AWSElasticBlockStore - - -
AzureFile -
AzureDisk - - -
CephFS -
Cinder - - -
CSI 取决于驱动 取决于驱动 取决于驱动 取决于驱动
FC - -
FlexVolume 取决于驱动 -
Flocker - - -
GCEPersistentDisk - -
Glusterfs -
HostPath - - -
iSCSI - -
Quobyte -
NFS -
RBD - -
VsphereVolume - - (Pod 运行于同一节点上时可行) -
PortworxVolume - -
StorageOS - - -

访问模式有:

  • ReadWriteOnce

    卷可以被一个节点以读写方式挂载。 ReadWriteOnce 访问模式也允许运行在同一节点上的多个 Pod 访问卷。

  • ReadOnlyMany

    卷可以被多个节点以只读方式挂载。

  • ReadWriteMany

    卷可以被多个节点以读写方式挂载。

  • ReadWriteOncePod

    卷可以被单个 Pod 以读写方式挂载。 如果你想确保整个集群中只有一个 Pod 可以读取或写入该 PVC, 请使用ReadWriteOncePod 访问模式。这只支持 CSI 卷以及需要 Kubernetes 1.22 以上版本。

3.4 回收策略

通过persistentVolumeReclaimPolicy参数设置回收策略。目前的回收策略有:

  • Retain – 手动回收
  • Recycle – 基本擦除 (rm -rf /thevolume/*)
  • Delete – 诸如 AWS EBS、GCE PD、Azure Disk 或 OpenStack Cinder 卷这类关联存储资产也被删除

目前,仅 NFS 和 HostPath 支持回收(Recycle)。 AWS EBS、GCE PD、Azure Disk 和 Cinder 卷都支持删除(Delete)。

3.5 阶段

每个卷会处于以下阶段(Phase)之一:

  • Available(可用)– 卷是一个空闲资源,尚未绑定到任何声明上去;
  • Bound(已绑定)– 该卷已经绑定到某声明;
  • Released(已释放)– 所绑定的声明已被删除,但是资源尚未被集群回收;
  • Failed(失败)– 卷的自动回收操作失败。

命令行能够显示绑定到某 PV 卷的 PVC 对象名称。

4 ConfigMap

首先来看一下容器化应用通常是如何被配置的。开发一款新应用程序的初期,除了将配置嵌入应用本身,通常会以命令行参数的形式配置应用。随着配置选项数量的逐渐增多,将配置文件化。

另一种通用的传递配置选项给容器化应用程序的方法是借助环境变量。应用程序主动查找某一特定环境变量的值,而非读取配置文件或者解析命令行参数。例如,MySQL官方镜像内部通过环境变量MYSQL_ROOT_PASSWORD设置超级用户root的密码。

为何环境变量的方案会在容器环境下如此常见?通常直接在Docker容器中采用配置文件的方式是有些许困难的,往往需要将配置文件打入容器镜像,抑或是挂载包含该文件的卷。显然,前者类似于在应用程序源代码中硬编码配置,每次修改完配置之后需要重新构建镜像。除此之外,任何拥有镜像访问权限的人可以看到配置文件中包含的敏感信息,如证书和密钥。相比之下,挂载卷的方式更好,然而在容器启动之前需确保配置文件已写入响应的卷中。

使用环境变量还有一个缺点是需要有效区分生产环境与开发环境中的配置,为了能在多个环境下复用pod的定义,需要将配置从pod定义描述中解藕出来。这就是ConfigMap资源。

ConfigMap 是一种 API 对象,用来将非机密性的数据保存到键值对中。使用时,pod可以将其用作环境变量、命令行参数或者存储卷中的配置文件。和其他 Kubernetes 对象都有一个 spec 不同的是,ConfigMap 使用 databinaryData 字段。这些字段能够接收键值对作为其取值。databinaryData 字段都是可选的。data 字段设计用来保存 UTF-8 字节序列,而 binaryData 则 被设计用来保存二进制数据作为 base64 编码的字串。

databinaryData 字段下面的每个键的名称都必须由字母数字字符或者 -_. 组成。在 data 下保存的键名不可以与在 binaryData 下 出现的键名有重叠。

从 v1.19 开始,你可以添加一个 immutable 字段到 ConfigMap 定义中,创建 不可变更的 ConfigMap

4.1 ConfigMap创建

你可以使用 kubectl create configmap 基于同一目录中的多个文件创建 ConfigMap。 当你基于目录来创建 ConfigMap 时,kubectl 识别目录下基本名可以作为合法键名的 文件,并将这些文件打包到新的 ConfigMap 中。普通文件之外的所有目录项都会被忽略(例如,子目录、符号链接、设备、管道等等)。

创建ConfigMap的命令如下:

1
2
kubectl create configmap <map-name> <data-source>
# configmap 可以简写为cm

创建ConfigMap有很多方式,下面分别介绍。

通过目录创建ConfigMap

通过一个示例进行演示。首先创建一些键值对vim /root/test/game.properties,内容如下:

1
2
3
4
5
6
7
enemies=aliens
lives=3
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAS
secret.code.allowed=true
secret.code.lives=30

通过目录创建ConfigMap:

1
2
3
kubectl create configmap game-config --from-file=/root/test
# configmap 可以简写为cm,下面的命令与上面等价
kubectl create cm game-config --from-file=/root/test

以上命令将 /root/test 目录下的所有文件,也就是 game.properties 文件打包到名为game-config的ConfigMap中。你可以使用下面的命令显示ConfigMap的详细信息:

1
kubectl describe configmaps game-config

输出如下:

image-20220105100442720

/root/test 目录中的 game.properties 文件出现在ConfigMap的data部分。

可以使用以下命令查看yaml格式的ConfigMap:

1
kubectl get configmaps game-config -o yaml

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
data:
game.properties: |
enemies=aliens
lives=3
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAS
secret.code.allowed=true
secret.code.lives=30
kind: ConfigMap
metadata:
creationTimestamp: "2021-12-25T23:58:13Z"
name: game-config
namespace: default
resourceVersion: "23326"
selfLink: /api/v1/namespaces/default/configmaps/game-config
uid: 17132b26-8858-4fee-aae5-27738a03b563
通过文件创建ConfigMap

也可以使用 kubectl create configmap 基于单个文件或多个文件创建 ConfigMap。

例如:

1
kubectl create configmap game-config-2 --from-file=/root/test/game.properties

以多次使用 --from-file 参数,从多个数据源创建 ConfigMap。

1
kubectl create configmap game-config-2 --from-file=configure-pod-container/configmap/game.properties --from-file=configure-pod-container/configmap/ui.properties
通过字面值创建 ConfigMap

你可以将 kubectl create configmap--from-literal 参数一起使用,从命令行定义键值对:

1
kubectl create configmap special-config --from-literal=special.how=very --from-literal=special.type=charm

上面的命令创建了如下两个键值对:

1
2
special.how=very
special.type=charm
通过yaml资源清单创建ConfigMap

还可以直接使用yaml文件,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: ConfigMap
metadata:
name: game-demo
data:
# 类属性键;每一个键都映射到一个简单的值
player_initial_lives: "3"
ui_properties_file_name: "user-interface.properties"

# 类文件键
game.properties: |
enemy.types=aliens,monsters
player.maximum-lives=5
user-interface.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true

4.2 使用ConfigMap

创建ConfigMap之后,接下来要在pod中使用,可以在pod的资源清单中这样定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: v1
kind: Pod
metadata:
name: dapi-test-pod
spec:
containers:
- name: test-container
image: luksa/fortune
command: [ "/bin/sh", "-c", "env" ] # 在容器内执行命令,输出当前的环境变量信息
env: # 键自己设置,值从ConfigMap中获取
- name: SPECIAL_LEVEL_KEY # 键为SPECIAL_LEVEL_KEY
valueFrom:
configMapKeyRef:
name: special-config # 使用名为special-config的ConfigMap
key: special.how # 使用special.how键的值作为SPECIAL_LEVEL_KEY的值
- name: SPECIAL_TYPE_KEY
valueFrom:
configMapKeyRef:
name: special-config
key: special.type # 使用special.type键的值作为SPECIAL_TYPE_KEY的值
envFrom: # 键值都从ConfigMap获取
- configMapRef:
name: game-config # 从名为special-config的ConfigMap导入键值对
restartPolicy: Never

通过该清单创建pod,由于重启策略为Never,因此容器创建后执行完命令就退出了。通过命令查看:

1
2
3
[root@k8s-master test]# kubectl get pod
NAME READY STATUS RESTARTS AGE
dapi-test-pod 0/1 Completed 0 5m26s

已经是Completed状态,查看日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@k8s-master test]# kubectl logs dapi-test-pod
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT=tcp://10.96.0.1:443
HOSTNAME=dapi-test-pod
HOME=/root
SPECIAL_TYPE_KEY=charm
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_PROTO=tcp
SPECIAL_LEVEL_KEY=very
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_SERVICE_HOST=10.96.0.1
PWD=/
enemies=aliens
lives=3
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAS
secret.code.allowed=true
secret.code.lives=30

可以看到我们配置的环境变量都已经写入容器了。

还可以使用 $(VAR_NAME) Kubernetes 替换语法在容器的 commandargs 部分中使用 ConfigMap 定义的环境变量。

比如对于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: Pod
metadata:
name: dapi-test-pod
spec:
containers:
- name: test-container
image: k8s.gcr.io/busybox
command: [ "/bin/sh", "-c", "echo $(SPECIAL_LEVEL_KEY) $(SPECIAL_TYPE_KEY)" ]
env:
- name: SPECIAL_LEVEL_KEY
valueFrom:
configMapKeyRef:
name: special-config
key: SPECIAL_LEVEL
- name: SPECIAL_TYPE_KEY
valueFrom:
configMapKeyRef:
name: special-config
key: SPECIAL_TYPE
restartPolicy: Never

运行Pod,可以看到输出:

1
very charm

还可以通过数据卷插件使用ConfigMap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Pod
metadata:
name: dapi-test-pod
spec:
containers:
- name: test-container
image: k8s.gcr.io/busybox
command: [ "/bin/sh", "-c", "ls /etc/config/" ]
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: special-config
restartPolicy: Never

Pod 运行时,命令 ls /etc/config/ 产生下面的输出:

1
2
SPECIAL_LEVEL
SPECIAL_TYPE

4.3 ConfigMap的热更新

通过ConfigMap配置的pod,后续只需要修改ConfigMap即可实现热更新。 kubelet 在每次定期同步时都会检查已挂载的 ConfigMap 是否是最新的。 但是,更新 ConfigMap 后,使用该 ConfigMap 挂载的 Env 不会同步更新;使用该 ConfigMap 挂载的 Volume 中的数据需要一段时间才能同步更新。

5 Secret

到目前为止传递给容器的所有信息都是比较常规的非敏感数据。然而正如前面提到的,配置通常会包含一些敏感数据,如证书和私钥, 需要确保其安全性。为了存储与分发此类信息,k8s提供了一种称为Secret的资源对象。Secret结构与ConfigMap类似, 均是键/值对的映射。Secret的使用方法也与ConfigMap相同,可以:

  • 将Secret作为环境变量传递给容器

  • 将Secret挂载到一个或多个容器上的卷中的文件

由于创建 Secret 可以独立于使用它们的 Pod, 因此在创建、查看和编辑 Pod 的工作流程中暴露 Secret(及其数据)的风险较小。 Kubernetes 和在集群中运行的应用程序也可以对 Secret 采取额外的预防措施, 例如避免将机密数据写入非易失性存储。

注意:

默认情况下,Kubernetes Secret 未加密地存储在 API 服务器的底层数据存储(etcd)中。 任何拥有 API 访问权限的人都可以检索或修改 Secret,任何有权访问 etcd 的人也可以。 此外,任何有权限在命名空间中创建 Pod 的人都可以使用该访问权限读取该命名空间中的任何 Secret;这包括间接访问,例如创建 Deployment 的能力。

为了安全地使用 Secret,请至少执行以下步骤:

  1. 为 Secret 启用静态加密
  2. 启用或配置 RBAC 规则来限制读取 Secret 的数据(包括通过间接方式)。
  3. 在适当的情况下,还可以使用 RBAC 等机制来限制允许哪些主体创建新 Secret 或替换现有 Secret。

5.1 Secret 的类型

创建 Secret 时,你可以使用 Secret 资源的 type 字段, 或者与其等价的 kubectl 命令行参数(如果有的话)为其设置类型。 Secret 的 type 有助于对不同类型机密数据的编程处理。

Kubernetes 提供若干种内置的类型,用于一些常见的使用场景:

内置类型 用法
Opaque 用户定义的任意数据
kubernetes.io/service-account-token 服务账号令牌
kubernetes.io/dockercfg ~/.dockercfg 文件的序列化形式
kubernetes.io/dockerconfigjson ~/.docker/config.json 文件的序列化形式
kubernetes.io/basic-auth 用于基本身份认证的凭据
kubernetes.io/ssh-auth 用于 SSH 身份认证的凭据
kubernetes.io/tls 用于 TLS 客户端或者服务器端的数据
bootstrap.kubernetes.io/token 启动引导令牌数据

下面介绍一些常用的Secret。

5.2 服务账号令牌

服务账号令牌Secret(service-account-token)用来存放标识某服务账号的令牌。默认会挂载至所有容器,比如在default名称空间下创建一个pod,使用kubectl describe查看,可以找到如下信息:

1
2
3
4
Volumes:
default-token-bzcfj:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-bzcfj

每个pod都会被自动挂载上一个secret卷,这个卷引用的是前面kubectl describe输出中的一个叫作default-token-bzcfj的Secret。由于Secret也是资源对象,因此可以通过kubectl get secrets命令从Secret列表中找到这个Secret。

image-20220107085823510

同样可以使用kubectl describe了解一下这个Secret的详细信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@k8s-master test]# kubectl describe secret default-token-bzcfj

Name: default-token-bzcfj
Namespace: default
Labels: <none>
Annotations: kubernetes.io/service-account.name: default
kubernetes.io/service-account.uid: faab66a4-6f6a-4a9a-855a-0ac68d237927

Type: kubernetes.io/service-account-token

Data
====
ca.crt: 1025 bytes
namespace: 7 bytes
token: eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tYnpjZmoiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImZhYWI2NmE0LTZmNmEtNGE5YS04NTVhLTBhYzY4ZDIzNzkyNyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.C8s2H4l4iW6VKEDAlHjjQ9rETZrF_vkPx6AQuZSaKoz1doq0sND9tbHvNhv_2BQ6X0-zkdHUxW_TKuRKNy43PBL3usmc_Z4b2NY6aJ6uOOJ9zaAgwTRKR7Mz2pQaY-ZqicvvlpuutENLzpHyYdpMUT4QorVTmKrpbKRLpDczBpS3IocGfpTHq4YnEKLY4fq7r2Fa_ayz9chZFOAPxdyu8Cs53TMu2dVvFYfCntxkQwAqPAC0GC1Lz13Q3fNmlVHt3NyVw6iId7RDb0xfl1ELupdtYMw_-PCsxkSit3Jpc1hYUvETLAbsRhYy0CAQfDt7RTFmOGe5dlxrhv-TJrEsCw

可以看出这个Secret包含三个条目ca.crtnamespacetoken,包含了从pod内部安全访问Kubernetes API服务器所需的全部信息。

使用kubectl describe命令时,会显示secret卷被挂载的位置:

1
2
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-bzcfj (ro)

可以进入容器查看被secret卷挂载的文件夹下包含的三个文件:

1
2
kubectl exec -it dapi-test-pod -- /bin/sh
ls /var/run/secrets/kubernetes.io/serviceaccount

image-20220107091812660

通过下图更加直观地理解默认Secret的挂载行为:

image-20220107094929532

5.3 Opaque Secret

当 Secret 配置文件中未作显式设定时,默认的 Secret 类型是 Opaque。 当你使用 kubectl 来创建一个 Secret 时,你会使用 generic 子命令来标明要创建的是一个 Opaque 类型 Secret。 例如,下面的命令会创建一个空的 Opaque 类型 Secret 对象:

1
2
kubectl create secret generic empty-secret
kubectl get secret empty-secret

输出类似于

1
2
NAME           TYPE     DATA   AGE
empty-secret Opaque 0 2m6s

DATA 列显示 Secret 中保存的数据条目个数。 在这个例子中,0 意味着我们刚刚创建了一个空的 Secret。

还可以通过配置文件创建Secret,Secret资源包含2个键值对: datastringDatadata 字段用来存储 base64 编码的任意数据。 提供 stringData 字段是为了方便,它允许 Secret 使用未编码的字符串。 datastringData 的键必须由字母、数字、-_. 组成。

例如,要使用 Secret 的 data 字段存储两个字符串,请将字符串转换为 base64 ,如下所示:

1
echo -n 'admin' | base64

输出类似于:

1
2
YWRtaW4=
echo -n '1f2d1e2e67df' | base64

输出类似于:

1
MWYyZDFlMmU2N2Rm

编写一个 Secret 配置文件,如下所示:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
data:
username: YWRtaW4=
password: MWYyZDFlMmU2N2Rm

根据这个yaml文件创建secret:

1
kubectl apply -f mysecret.yaml

对于某些场景,你可能希望使用 stringData 字段。 这字段可以将一个非 base64 编码的字符串直接放入 Secret 中, 当创建或更新该 Secret 时,此字段将被编码。上述用例的实际场景可能是这样:当你部署应用时,使用 Secret 存储配置文件, 你希望在部署过程中,填入部分内容到该配置文件。

例如,如果你的应用程序使用以下配置文件:

1
2
3
apiUrl: "https://my.api.com/api/v1"
username: "<user>"
password: "<password>"

你可以使用以下定义将其存储在 Secret 中:

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
stringData:
config.yaml: |
apiUrl: "https://my.api.com/api/v1"
username: <user>
password: <password>

stringData 字段是只写的。获取 Secret 时,此字段永远不会输出。 例如,如果你运行以下命令:

1
kubectl get secret mysecret -o yaml

输出类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
data:
password: MWYyZDFlMmU2N2Rm
username: YWRtaW4=
kind: Secret
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","data":{"password":"MWYyZDFlMmU2N2Rm","username":"YWRtaW4="},"kind":"Secret","metadata":{"annotations":{},"name":"mysecret","namespace":"default"},"type":"Opaque"}
creationTimestamp: "2018-11-15T20:40:59Z"
name: mysecret
namespace: default
resourceVersion: "47156"
selfLink: /api/v1/namespaces/default/secrets/mysecret
uid: 270be87e-06c5-4ab9-895a-2e80df522155
type: Opaque

命令 kubectl getkubectl describe 默认不显示 Secret 的内容。 这是为了防止 Secret 意外地暴露给旁观者或者保存在终端日志中。

要删除你创建的Secret很简单:

1
kubectl delete secret mysecret

5.4 Docker配置 Secret

你可以使用下面两种 type 值之一来创建 Secret,用以存放访问 Docker 仓库 来下载镜像的凭据。

  • kubernetes.io/dockercfg
  • kubernetes.io/dockerconfigjson

kubernetes.io/dockercfg 是一种保留类型,用来存放 ~/.dockercfg 文件的序列化形式。该文件是配置 Docker 命令行的一种老旧形式。 使用此 Secret 类型时,你需要确保 Secret 的 data 字段中包含名为 .dockercfg 的主键,其对应键值是用 base64 编码的某 ~/.dockercfg 文件的内容。

类型 kubernetes.io/dockerconfigjson 被设计用来保存 JSON 数据的序列化形式, 该 JSON 也遵从 ~/.docker/config.json 文件的格式规则,而后者是 ~/.dockercfg 的新版本格式。 使用此 Secret 类型时,Secret 对象的 data 字段必须包含 .dockerconfigjson 键,其键值为 base64 编码的字符串包含 ~/.docker/config.json 文件的内容。

下面是一个 kubernetes.io/dockercfg 类型 Secret 的示例:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Secret
metadata:
name: secret-dockercfg
type: kubernetes.io/dockercfg
data:
.dockercfg: |
"<base64 encoded ~/.dockercfg file>"

如果你不希望执行 base64 编码转换,可以使用 stringData 字段代替。

当你使用清单文件来创建这两类 Secret 时,API 服务器会检查 data 字段中是否 存在所期望的主键,并且验证其中所提供的键值是否是合法的 JSON 数据。 不过,API 服务器不会检查 JSON 数据本身是否是一个合法的 Docker 配置文件内容。

1
2
3
4
kubectl create secret docker-registry secret-tiger-docker \
--docker-username=tiger \
--docker-password=pass113 \
--docker-email=tiger@acme.com

上面的命令创建一个类型为 kubernetes.io/dockerconfigjson 的 Secret。 如果你对 data 字段中的 .dockerconfigjson 内容进行转储,你会得到下面的 JSON 内容,而这一内容是一个合法的 Docker 配置文件。

1
2
3
4
5
6
7
8
9
10
{
"auths": {
"https://index.docker.io/v1/": {
"username": "tiger",
"password": "pass113",
"email": "tiger@acme.com",
"auth": "dGlnZXI6cGFzczExMw=="
}
}
}

5.5 使用Secret

Secret 可以作为数据卷被挂载,或作为环境变量暴露出来以供 Pod 中的容器使用。

通过数据卷挂载

在 Pod 中使用存放在卷中的 Secret:

  1. 创建一个 Secret 或者使用已有的 Secret。多个 Pod 可以引用同一个 Secret。
  2. 修改你的 Pod 定义,在 spec.volumes[] 下增加一个卷。可以给这个卷随意命名, 它的 spec.volumes[].secret.secretName 必须是 Secret 对象的名字。
  3. spec.containers[].volumeMounts[] 加到需要用到该 Secret 的容器中。 指定 spec.containers[].volumeMounts[].readOnly = truespec.containers[].volumeMounts[].mountPath 为你想要该 Secret 出现的尚未使用的目录。
  4. 在镜像中让程序从该目录下寻找文件。 Secret 的 data 映射中的每一个键都对应 mountPath 下的一个文件名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: mypod
image: redis
volumeMounts:
- name: foo
mountPath: "/etc/foo"
readOnly: true
volumes:
- name: foo
secret:
secretName: mysecret

您想要用的每个 Secret 都需要在 spec.volumes 中引用。

如果 Pod 中有多个容器,每个容器都需要自己的 volumeMounts 配置块, 但是每个 Secret 只需要一个 spec.volumes

您可以打包多个文件到一个 Secret 中,或者使用的多个 Secret,怎样方便就怎样来。

通过环境变量

将 Secret 作为 Pod 中的环境变量使用:

  1. 创建一个 Secret 或者使用一个已存在的 Secret。多个 Pod 可以引用同一个 Secret。
  2. 修改 Pod 定义,为每个要使用 Secret 的容器添加对应 Secret 键的环境变量。 使用 Secret 键的环境变量应在 env[x].valueFrom.secretKeyRef 中指定 要包含的 Secret 名称和键名。
  3. 在镜像让程序在指定的环境变量中查找值。

这是一个使用来自环境变量中的 Secret 值的 Pod 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
name: secret-env-pod
spec:
containers:
- name: mycontainer
image: redis
env:
- name: SECRET_USERNAME
valueFrom:
secretKeyRef:
name: mysecret
key: username
- name: SECRET_PASSWORD
valueFrom:
secretKeyRef:
name: mysecret
key: password
restartPolicy: Never

5.6 Secret的更新

对于通过数据卷挂载的Secret,当已经存储于卷中被使用的 Secret 被更新时,被映射的键也将终将被更新。 组件 kubelet 在周期性同步时检查被挂载的 Secret 是不是最新的。 但是,它会使用其本地缓存的数值作为 Secret 的当前值。

对于通过环境变量的Secret,更新之后对应的环境变量不会被更新。如果某个容器已经在通过环境变量使用某 Secret,对该 Secret 的更新不会被容器马上看见,除非容器被重启。