存储管理6:Projected Volume

在 Kubernetes 中,有几种特殊的 Volume,它们存在的意义不是为了存放容器里的数据,也不是用来进行容器和宿主机之间的数据交换。而是为容器提供预先定义好的数据。从容器的角度来看,这些 Volume 中的信息仿佛是被 Kubernetes“投射”(Project)到容器当中的。这正是 Projected Volume 的含义。到目前为止,Kubernetes 支持的 Projected Volume 一共有四种:

  • Secret,存放容器要访问的加密数据。比如,数据库连接用户的用户名和密码
  • ConfigMap,存放容器要访问的配置信息。比如,一个 Java 应用所需的配置文件(.properties 文件)
  • Downward API,让容器能够直接获取到当前Pod对象本身的信息。
  • ServiceAccountToken,保存ServiceAccount对象的授权信息。

一、Secret

Secret的作用是,帮我们把 Pod 想要访问的加密数据,存放到 Etcd 中。然后,我们就可以在 Pod 的容器里以挂载 Volume 的方式,访问到这些 Secret 里保存的信息。

1.1、一个使用Secret的例子

Secret 最典型的使用场景,莫过于存放数据库的 Credential 信息了,比如下面这个例子:

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: 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挂载了一个projected类型的Volume。而这个Volume的数据来源,是名为user和pass的两个Secret对象,它们分别对应的是数据库的用户名和密码。

1.2、创建Secret对象

创建secret对象有两种方式,命令行和yaml文件,下面我们分别来看。

1.2.1、方式一:命令行

1
2
3
4
5
6
7
8
9
10
11
12
# 在文件中,以明文的形式写入用户名和密码
$ cat > ./username.txt <<EOF
admin
EOF

$ cat > ./password.txt <<EOF
c1oudc0w!
EOF

# 创建Secret对象
$ kubectl create secret generic user --from-file=./username.txt
$ kubectl create secret generic pass --from-file=./password.txt

在这里,username.txt 和 password.txt 文件里,存放的就是用户名和密码;而kubectl命令中的 user 和 pass,则是我们为 Secret 对象指定的名字。如果我们要查看这些 Secret 对象的话,只要执行一条 kubectl get 命令就可以了:

1
2
3
4
$ kubectl get secrets
NAME TYPE DATA AGE
user Opaque 1 51s
pass Opaque 1 51s

1.2.2、方式二:yaml文件

当然,除了使用 kubectl create secret 指令外,我们也可以直接通过编写 YAML 文件的方式来创建这个 Secret 对象,比如:

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

可以看到,通过编写 YAML 文件创建出来的 Secret 对象只有一个。但它的 data 字段,却以 Key-Value 的格式保存了两份 Secret 数据。其中,“user”就是第一份数据的 Key,“pass”则是第二份数据的 Key。

需要注意的是,Secret 对象要求这些数据必须是经过 Base64 转码的,以免出现明文密码的安全隐患。这个转码操作也很简单,比如:

1
2
3
4
$ echo -n 'admin' | base64
YWRtaW4=
$ echo -n '1f2d1e2e67df' | base64
MWYyZDFlMmU2N2Rm

这里需要注意的是,像这样创建的 Secret 对象,它里面的内容仅仅是经过了转码,而并没有被加密。在真正的生产环境中,我们需要在 Kubernetes 中开启 Secret 的加密插件,增强数据的安全性。

1.3、创建Pod对象

接下来,我们尝试一下创建这个 Pod:

1
$ kubectl create -f test-projected-volume.yaml

当 Pod 变成 Running 状态之后,我们再验证一下这些 Secret 对象是不是已经在容器里了:

1
2
3
4
5
6
7
8
$ kubectl exec -it test-projected-volume -- /bin/sh
$ ls /projected-volume/
user
pass
$ cat /projected-volume/user
admin
$ cat /projected-volume/pass
1f2d1e2e67df

从返回结果中,我们可以看到,保存在 Etcd 里的用户名和密码信息,已经以文件的形式出现在了容器的 Volume 目录里。而这个文件的名字,就是 kubectl create secret 指定的 Key,或者说是 Secret 对象的 data 字段指定的 Key。

1.4、小结

Secret对象,可以为我们保存加密数据,更重要的是,它以挂载方式进入到容器里的 Secret,一旦其对应的 Etcd 里的数据被更新,这些 Volume 里的文件内容,同样也会被更新( kubelet 组件在定时维护这些 Volume)。

需要注意的是,这个更新可能会有一定的延时。所以在编写应用程序时,在发起数据库连接的代码处写好重试和超时的逻辑,绝对是个好习惯。

二、ConfigMap

ConfigMap 保存的是不需要加密的、应用所需的配置信息。ConfigMap 的用法几乎与 Secret 完全相同,我们可以使用 kubectl create configmap 从文件或者目录创建 ConfigMap 对象,也可以直接编写 ConfigMap 对象的 YAML 文件。

比如,一个 Java 应用所需的配置文件(.properties 文件),就可以通过下面这样的方式保存在 ConfigMap 里:

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
# .properties文件的内容
$ cat > ./ui.properties <<EOF
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice
EOF

# 从.properties文件创建ConfigMap
$ kubectl create configmap ui-config --from-file=./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
...

三、Downward API

Downward API的作用是,让 Pod 里的容器能够直接获取到这个 Pod API 对象本身的信息。

3.1、示例

我们来看如下示例:

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
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: 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

在这个 Pod 的 YAML 文件中,我们定义了一个简单的容器,声明了一个 projected 类型的 Volume。只不过这次 Volume 的数据来源,变成了 Downward API。而这个 Downward API Volume,则声明要暴露 Pod 的 metadata.labels 信息给容器。

通过这样的声明方式,当前 Pod 的 Labels 字段的值,就会被 Kubernetes 自动挂载成为容器里的 /etc/podinfo/labels 文件。而这个容器的启动命令,则是不断打印出 /etc/podinfo/labels 里的内容。所以,当我们创建了这个 Pod 之后,就可以通过 kubectl logs 指令,查看到这些 Labels 字段被打印出来,如下所示:

1
2
3
4
5
$ kubectl create -f dapi-volume.yaml
$ kubectl logs test-downwardapi-volume
cluster="test-cluster1"
rack="rack-22"
zone="us-est-coast"

3.2、能获取的信息

目前,Downward API 支持的字段已经非常丰富了,比如:

  • 使用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
  • 使用resourceFieldRef可以获取:
    • 容器的CPU limit
    • 容器的CPU request
    • 容器的memory limit
    • 容器的memory request

上面这个列表的内容,随着 Kubernetes 项目的发展肯定还会不断增加。不过,需要注意的是,Downward API 能够获取到的信息,一定是 Pod 里的容器进程启动之前就能够确定下来的信息。而如果我们想要获取 Pod 容器运行后才会出现的信息,比如,容器进程的 PID,那就肯定不能使用 Downward API 了,而应该考虑在 Pod 里定义一个 sidecar 容器。

四、ServiceAccountToken

假设我们现在有一个这样的需求:

现在有了一个 Pod,我们能不能在这个 Pod 里安装一个 Kubernetes 的 Client,这样就可以从容器里直接访问并操作这个 Kubernetes 的 API ?

这当然是可以的。不过,我们首先要解决 API Server 的授权问题。

4.1、一个特殊的Secret对象

我们需要知道的是,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 项目的 Projected Volume 其实只有三种,因为第四种 ServiceAccountToken,只是一种特殊的 Secret 而已。

4.2、默认Service Account

为了方便使用,Kubernetes 已经为我们提供了一个默认“服务账户”(default Service Account)。并且,任何一个运行在 Kubernetes 里的 Pod,都可以直接使用这个默认的 Service Account,而无需显示地声明挂载它。而这又是如何做到的呢?

当然还是靠 Projected Volume 机制。如果查看一下任意一个运行在 Kubernetes 集群里的 Pod,就会发现,每一个 Pod,都已经自动声明了一个类型是 Secret、名为 default-token-xxx 的 Volume,然后 自动挂载在每个容器的一个固定目录上。比如:

1
2
3
4
5
6
7
8
9
10
$ 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

这个 Secret 类型的 Volume,正是默认“服务账户”对应的 ServiceAccountToken。所以说,Kubernetes 其实在创建每个 Pod 的时候,自动在它的 spec.volumes 部分添加上了默认 ServiceAccountToken 的定义,然后自动给每个容器加上了对应的 volumeMounts 字段。这个过程对于用户来说是完全透明的。

4.3、InClusterConfig

一旦 Pod 创建完成,容器里的应用就可以直接从这个挂载了 ServiceAccountToken 的目录里访问到授权信息和文件。这个目录的路径在容器中是固定的,即:/var/run/secrets/kubernetes.io/serviceaccount ,而这个 Secret 类型的 Volume 里面的内容如下所示:

1
2
$ ls /var/run/secrets/kubernetes.io/serviceaccount 
ca.crt namespace token

所以,我们的应用程序只要直接加载这些授权文件,就可以访问并操作 Kubernetes API 了。而且,如果我们使用的是 Kubernetes 官方的 Client 包(k8s.io/client-go)的话,它会自动加载这个目录下的文件,不需要我们做任何配置或者编码操作。

这种把 Kubernetes 客户端以容器的方式运行在集群里,然后使用 default Service Account 自动授权的方式,被称作“InClusterConfig”,它也是最推荐的进行 Kubernetes API 编程的授权方式。

当然,考虑到自动挂载默认 ServiceAccountToken 的潜在风险,Kubernetes 允许我们设置默认不为 Pod 里的容器自动挂载这个 Volume。

除了这个默认的 Service Account 外,我们很多时候还需要创建一些我们自己定义的 Service Account,来对应不同的权限设置。这样,Pod 里的容器就可以通过挂载这些 Service Account 对应的 ServiceAccountToken,来使用这些自定义的授权信息。

五、小结

在这篇文章中,我们主要讨论了如下内容:

  • Projected Volume的作用是,为容器提供预先设定好的数据;
  • Secret保存的是加密数据;
  • ConfigMap保存的是配置文件;
  • Downward API保存的是Pod本身的信息;
  • ServiceAccountToken,是一个特殊的Secret对象,它保存的是ServiceAccount对象的授权信息;

我们需要知道的是,Secret、ConfigMap,以及 Downward API 这三种 Projected Volume 定义的信息,大多是可以通过环境变量的方式来定义。但是,通过环境变量获取这些信息的方式,不具备自动更新的能力。所以,一般情况下,建议使用 Volume 文件的方式获取这些信息。


存储管理6:Projected Volume
https://kuberxy.github.io/2020/10/05/存储管理6:Projected Volume/
作者
Mr.x
发布于
2020年10月5日
许可协议