在k8s中,有几种特殊的Volume,他们存在的意义不是为了存放容器里的数据,也不是用来进行容器和宿主机之间的数据交换。这些特殊volume的作用,是为容器提供预先定义好的数据。被称为projected volume(投射)
到目前为止,k8s支持的projected volume一共有四种:
    1.Secret;
    2.ConfigMap;
    3.Downward API;
    4.ServiceAccountToken;

Secret:帮你把pod想要访问的加密数据,存放到Etcd中。然后,你就可以通过pod的容器里挂载volume的方式,访问到这些Secret里保存的信息。
Secret最经典的使用场景,莫过于存放数据库的Credential(凭证)信息:

    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中,我定义了一个简单的容器。他声明挂载volume,并不是常见的emptyDir或者hostPath类型,而是projected类型。而这个volume的数据来源(sources),则是名为user和pass的Secret对象,分别对应的是数据库的用户名和密码。
这里用到的数据库的用户名、密码,正是以secret对象的方式交给kubernetes保存的。完成这个操作的指令。

    $ cat ./username.txt
    admin
    $ cat ./password.txt
    c1oudc0w!

    $ kubectl create secret generic user --from-file=./username.txt
    $ kubectl create secret generic pass --from-file=./password.txt


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

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


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

    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转码,以免出现明文密码的安全隐患。

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


这里需要注意的是,像这样创建的secret对象,它里面的内容仅仅经过了转码,而并没有被加密。在真正地生产环境中,你需要在k8s中开启secret的加密插件,增强数据的安全性。
接下来,创建这个pod:

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


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

    $ kubectl exec -it test-projected-volume -- /bin/sh
    $ ls /projected-volume/
    user
    pass
    $ cat /projected-volume/user
    root
    $ cat /projected-volume/pass
    1f2d1e2e67df


从返回结果中,我们可以看到,保存在Etcd里的用户名和密码信息,已经以文件的形式出现在了容器的volume目录里。而这个文件的名字,就是kubectl create secret指定的key,或者说是secret对象的data字段指定的key。
更重要的是,像这样通过挂载方式进入到容器的secret,一旦其对应的Etcd里的数据被更新,这些volume里的文件内容,同样也会被更新。其实,这是kubelet组件在定时维护这些volume。

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

例子:一个java应用所需的配置文件(.properties文件),就可以通过下面这样的方式保存在ConfigMap里:
    # .properties 文件的内容

    $ cat example/ui.properties
    color.good=purple
    color.bad=yellow
    allow.textmode=true
    how.nice.to.look=fairlyNice

    # 从.properties 文件创建 ConfigMap

    $ kubectl create configmap ui-config --from-file=example/ui.properties

    # 查看这个 ConfigMap 里保存的信息 (data)

    $ 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


      ...

kubectl get -o yaml 这样的参数,会将指定的pod api对象以YAML的方式展示出来。

Downward API:让pod里的容器能够直接获取到这个pod API对象本身的信息。
  

  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: k8s.gcr.io/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字段的值,就会被k8s自动挂载成为容器里的/etc/podinfo/labels文件。
而这个容器的启动命令,则是不断打印出/etc/podinfo/labels里的内容。所以,当我创建了这个pod之后,就可以通过kubectl logs指令,查看到这些labels字段被打印出来:
  

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


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

1. 使用 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

    2. 使用 resourceFieldRef 可以声明使用:
        容器的 CPU limit
        容器的 CPU request
        容器的 memory limit
        容器的 memory request


上面这个列表的内容,随着k8s项目的发展肯定还会不断增加。所以这个列出来的信息仅供参考,你在使用Downward API时,还是要记得去查阅一下官方文档。
不过,需要注意的是,Downward API能够获取到的信息,一定是pod里的容器进程启动之前就能够确定下来的信息。而如果你想要获取pod容器运行后才会出现的信息,比如,容器进程的PID,那就肯定不能使用Downward API了,而应该考虑在pod里定义的一个sidecar容器。
其实,secret、ConfigMap、Downward API这三种projected volume定义的信息,大多还可以通过环境变量的方式出现在容器里。但是,通过环境变量获取这些信息的方式,不具备自动更新的能力。所以,一般情况下,我都建议你使用volume文件的方式获取这些信息。

明白secret之后,我们了解下与它密切相关的概念:Service Account。
背景:我现在有了一个Pod,能不能在这个Pod里安装一个k8s的Client,这样就可以从容器里直接访问并且操作这个k8s的API了?
答案是可以,不过我们首先要解决API Server的授权问题。
Service Account 对象的作用,就是k8s系统内置的一种“服务账户”,它是k8s进行分配的对象。如:Service Account A,可以只被允许对k8s API进行GET操作,而Server Account B,则可以有k8s API的所有操作的权限。
像这样的Service Account的授权信息和文件,实际上保存在它所绑定的一个特殊的Secret对象里的,称作ServiceAccountToken。任何运行在k8s集群上的应用,都必须使用这个ServiceAccountToken里保存的授权信息,也就是Token,才可以合法的使用 API Server。

所以准确的说:k8s项目只有三种projected volume对象,ServiceAccountToken知识一种特殊的Secret。

另外,k8s为我们提供了一个默认“服务账户”(default Service Account)。并且,任何一个运行在k8s里的Pod,都可以直接使用这个Service Account,而无需显示地声明挂载它。

ServiceAccountToken原理:如果你查看一下任意一个运行在k8s集群里的Pod,就会发现,每一个Pod,都已经自动声明一个类型是default-token-xxxx的volume,然后自动挂载在每个容器的一个固定目录上:
  

 $ 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正是默认Service Account对应的ServiceAccountToken。所以,k8s其实在每个Pod创建的时候,自动在它的spec.volumes部分添加了默认ServiceAccountToken的定义,然后自动给每个容器加上了对应的volumeMount字段。这个过程对于用户来说是完全同名了。

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

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

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

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

当然,考录到自动挂载默认ServiceAccountToken的潜在风险,k8s允许你设置默认不为Pod里的容器自动挂载这个Volume。
除了这个默认的Service Account外,我们很多时候还需要创建一些我们自己定义的Service Account,来对应不同的权限设置。这样,我们的pod里的容器就可以通过挂载这些Service Account对应的ServiceAccountToken,来使用这些自定义的授权信息。

接下来,一起看看pod的另一个重要的配置:容器健康检查和恢复机制。

在k8s中,你可以为pod里的容器定义一个健康检查“探针”probe。这样,kubelet就会根据这个probe的返回值决定这个容器的状态,而不是直接以容器进行是否运行作为依据。这种机制,是生产环境中保证应用健康存活的重要手段。

例子:

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: test-liveness-exec
spec:
  containers:
  - name: liveness
    image: busybox
    args:
    - /bin/sh
    - -c
    - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
    livenessProbe:
      exec:
        command:
        - cat
        - /tmp/healthy
      initialDelaySeconds: 5
      periodSeconds: 5

在这个pod中,我们定义了一个有趣的容器。在它启动之后的第一件事,就是在/tmp下创建healthy文件,以此作为自己已经经常运行的标志。而30s之后,它会把这个文件删除。

与此同时,我们定义了一个这样的livenessProbe(健康检查)。它的类型是exec,这意味着,它会在容器启动后,在容器里面执行一句我们指定的命令。这时,如果这个文件存在,这条命令的返回值就是0,pod就会认为这个容器不仅已经启动,而且是健康的。这个健康检查,在容器启动5s后开始执行(initialDelaySeconds: 5),每5s执行一次(periodSeconds: 5)。

首先,创建这个pod:

$ kubectl create -f test-liveness-exec.yaml

然后,查看这个pod的状态:

$ kubectl get pod
NAME                READY     STATUS    RESTARTS   AGE
test-liveness-exec   1/1       Running   0          10s

可以看到,由于已经通过了健康检查,这个pod就进入running状态。
而30s之后,我们再查看pod的EVENTS:

$ kubectl describe pod test-liveness-exec


这个pod在Events里报告了一个异常:

FirstSeen LastSeen    Count   From            SubobjectPath           Type        Reason      Message
--------- --------    -----   ----            -------------           --------    ------      -------
2s        2s      1   {kubelet worker0}   spec.containers{liveness}   Warning     Unhealthy   Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory

显然,健康检查发现/tmp/healthy文件不存在了,所以它报告容器是不健康的,然后再次查看pod的状态:

$ kubectl get pod test-liveness-exec
NAME           READY     STATUS    RESTARTS   AGE
liveness-exec   1/1       Running   1          1m

发现pod并没有进入Failed状态,而是保持了running状态。但是RESTART字段从0变成了1,说明这个异常的容器已经被k8s重启了。在这个过程中,pod保持running的状态不变。

注意:k8s中没有docker的stop语义。所以虽然restart,但是却是重新创建了容器。

这个功能就是k8s里的pod恢复机制,也叫restartPolicy。它是pod的spec部分的一个标准字段(pod.spec.restartPolicy),默认值是Always,即:任何时候这个容器发生了异常,它一定会被重新启动。

强调:pod的恢复过程,永远都是发生在当前节点上,而不会跑到别的节点上去。事实上,一旦一个pod与一个节点node绑定,除非这个绑定发生了变化(pod.spec.node字段被修改),否则它永远都不会离开这个节点。这也意味着,如果这个宿主机宕机了,这个pod也不会主动迁移到其他节点上去。

而如果你想让pod出现在其他的可用节点上,就必须使用DePloyment这样的“控制器”来管理pod,哪怕你只需要一个pod副本。

除此之外,我们还可以通过设置restartPolicy,改变pod的恢复策略。
    Always: 在任何情况下,只要容器不在运行状态,就自动重启容器;
    OnFailure:只在容器异常时才自动重启容器;
    Never:从来不重启容器。


在实际使用时,我们需要根据应用运行的特性,合理设置这三种恢复策略。比如:一个pod,只计算1+1=2,计算完成输出结果后退出,变成successed状态。这时,你如果再用restartPolicy=Always强制重启这个容器就没有意义了。

而如果你要关心这个容器退出后的上下文环境,如日志,文件,目录,就需要将值设为Never。因为一旦容器被自动重新创建,这些内容就有可能丢失。

这里有两个基本的设计原理:
    1.只要pod的restartPolicy指定的策略允许重启异常的容器,那么这个pod就会保持running状态,并进行容器重启。否则pod就会进入failed状态。
    2.对于包含多个容器的pod,只有它里面所有的容器都进入异常状态后,pod才会进入failed状态。在此之前,pod都是running状态。此时,pod的READY字段会显示正常容器的个数:

    

$ kubectl get pod test-liveness-exec
    NAME           READY     STATUS    RESTARTS   AGE
    liveness-exec   0/1       Running   1          1m

所以,假如一个pod里只有一个容器,然后这个容器异常退出了。那么,只有当restartPolicy=Never时,这个pod才会进入failed状态。而其他情况下,由于k8s都可以重启这个容器,所以它会保持running不变。

而如果这个pod有多个容器,仅有一个容器异常退出,它就始终保持running状态,哪怕restartPolicy=Never。只有当所有容器也异常退出之后,这个pod才会进入failed状态。

除了在容器中执行命令,livenessProbe还可以定义为发起HTTP或者TCP请求的方式:

...
livenessProbe:
     httpGet:
       path: /healthz
       port: 8080
       httpHeaders:
       - name: X-Custom-Header
         value: Awesome
       initialDelaySeconds: 3
       periodSeconds: 3
...
livenessProbe:
    tcpSocket:
      port: 8080
    initialDelaySeconds: 15
    periodSeconds: 20

所以,你的pod其实可以暴露一个健康检查URL(比如/healthz),或者直接让健康检查去检测应用的监听端口。这两种配置方法,在web服务类的应用中经常用到。

在 Kubernetes 的 Pod 中,还有一个叫readinessProbe 的字段。虽然它的用法与 lienessProbe 类似,但作用却大不一样。readinessProbe 检查结果的成功与否,决定的这个 Pod 是不是能被通过 Service 的方式访问到,而并不影响pod的生命周期。

到此,我们已经对pod对象的语义和描述能力有了初步的感觉,那么k8s能不能自动给pod填充某些字段呢?这个需求实际上非常实用。比如,开发人员只需要提交一个基本的、非常简单的 Pod YAML, k8s就可以自动给对应的pod对象加上其他必要的信息,如labels、annotations、volumes等。而这些信息,可以是运维人员事先定义好的。这叫作PodPreset(Pod预设置)功能,k8s v1.11版本后出现。
例如,现在开发人员编写了如下一个pod.yaml文件:

apiVersion: v1
kind: Pod
metadata:
  name: website
  labels:
    app: website
    role: frontend
spec:
  containers:
    - name: website
      image: nginx
      ports:
        - containerPort: 80

这时,运维人员可以定义一个PodPreset对象。在这个对象中,凡是他想在开发人员编写的pod里追加的字段,都可以预先定义好,比如preset.yaml:

apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
  name: allow-database
spec:
  selector:
    matchLabels:
      role: frontend
  env:
    - name: DB_PORT
      value: "6379"
  volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
    - name: cache-volume
      emptyDir: {}

在这个PodPreset中,首先是selector。这就意味着后面追加的定义,只会作用于selecter锁定义的,带有"role:frontend"标签的pod对象。
然后,我们定义了一组pod的spec里的标准字段,以及对应的值。比如,env定义了DB_PORT这个环境变量,volumeMount定义了容器Volume的挂载目录,volumes定义了一个emptyDir的volume。
接下来,我们先创建PodPreset,然后运行开发人员创建的pod:

$ kubectl create -f preset.yaml
$ kubectl create -f pod.yaml

这时,pod运行起来之后,我们查看一下这个pod的API对象:

$ kubectl get pod website -o yaml
apiVersion: v1
kind: Pod
metadata:
  name: website
  labels:
    app: website
    role: frontend
  annotations:
    podpreset.admission.kubernetes.io/podpreset-allow-database: "resource version"
spec:
  containers:
    - name: website
      image: nginx
      volumeMounts:
        - mountPath: /cache
          name: cache-volume
      ports:
        - containerPort: 80
      env:
        - name: DB_PORT
          value: "6379"
  volumes:
    - name: cache-volume
      emptyDir: {}

这个时候,我们就可以清楚地看到,这个pod里多了新添加的labels、env、volumes和volumeMount的定义,他们的配置跟PodPreset内容一样。此外,这个Pod还被自动加上了一个annotation表示这个pod对象被PodPreset改动过。

PodPreset里定义的内容,只会在Pod API对象被创建之前追加在这个对象本身,而不会影响任何pod的控制器的定义。

比如,我们现在提交的是一个nginx-deployment,那么这个deployment对象本身是永远不会被PodPreset改变的,被修改的只是这个deployment创建出来的所有pod。

如果定义了同时作用于一个pod对象的多个PodPreset,k8s会帮我们合并这些PodPreset要做的修改。而如果它们要做的修改有冲突的话,这些冲突字段就不会被修改。

总结


今天我们了解了pod对象更高阶的使用方法,希望通过对这些实例的讲解,你可以更深入地理解pod api对象的各个字段。k8s“一切皆对象”的设计思想:比如应用时pod对象,应用的配置是ConfigMap对象,应用要访问的密码则是secret对象。k8s就是围绕着这些各种各样的对象进行容器编排的。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐