容器技术使用rootfs机制和Mount Namespace,构建出一个同宿主机完全隔离开的文件系统环境
那容器里进程新建的文件,怎么样才能让宿主机获取到?宿主机上的文件和目录,怎么样才能让容器里的进程访问到?
Docker Volume就可以解决这个问题,它允许你将宿主机上指定的目录或文件挂载到容器里面进行读取和修改操作
在Docker项目里,它支持两种Volume声明方式,可以把宿主机目录挂载进容器的/test目录当中
$docker run -v /test … //docker没有显式声明宿主机目录,Docker会默认在宿主机上创建一个临时目录/var/lib/docker/volumes/[VOLUME_ID]/_data,然后把它挂载到容器的/test目录上
$docker run -v /home:/test … //直接把宿主机的/home目录挂载到容器的/test目录上
Docker如何将宿主机上的目录或文件挂载到容器里面呢?
当容器进程被创建之后,尽管开启了Monut Namespace,但是在它执行chroot之前,容器进程一直可以看到宿主机上的整个文件系统(包括了要使用的容器镜像,这个镜像的各个层,保存在/var/lib/docker/aufs/diff目录下,在容器启动后,它们会被联合挂载在var/lib/docker/aufs/mnt中)。在执行chroot之前,把Volume指定的宿主机目录(如/home目录)挂载到指定的容器目录(如/test目录)在宿主机对应的目录(var/lib/docker/aufs/mnt/[可读写层ID]/test,这个Volume的挂载工作就完成了。由于执行了这个挂载操作,容器进程(Docker初始化进程dockerinit,负责完成根目录的准备、挂载设备和目录、配置hostname等一系列需要容器内进行的初始化操作,最后它通过execv()系统调用,让应用进程取代自己,成为容器里PID=1的进程)已经创建了,也就意味着此时Monut namespace 已经开启了。所以这个挂载事件只在容器里可见,在宿主机是看不见容器内部的这个挂载点的,这就保证了容器的隔离性不会被Volume打破。
这里使用到的挂载技术,就是Linux的绑定挂载(bind mount)机制,它的主要作用是允许你将一个目录或者文件,而不是整个设备挂载到一个指定的目录上,并且这时你在该挂载点上进行的任何操作,只是发生在被挂载的目录或者文件上,而源挂载点的内容则会被隐藏起来且不受影响。
绑定挂载实际上是一个inode替换的过程,在linux系统中,inode可以理解为存放文件内容的“对象”,而dentry,也叫目录项,就是访问这个inode所使用的指针。
如上图所示,mount --bind /home/test,会将/home挂载到/test上。其实相当于将/test的dentry,重定向到了/home的inode。这样当修改/test目录时,实际修改的是/home目录的inode。这也就是为何一旦执行umount命令,/test目录原先的内容就会恢复:因为修改真正发送在/home目录里。因此在一个正确的时机进行一次绑定挂载,Docker就可以成功将一个宿主机上的目录或文件不动声色地挂载到容器中。这样进程在容器里对这个/test目录进行的所有操作都实际发生在宿主机的对应目录(如/home),而不会影响容器镜像的内容。
那test目录里的内容既然挂载在容器rootfs的可读写层,它会不会被Docker commit提交掉呢?
不会,容器的镜像操作是发生在宿主机空间的,而由于Mount Namespace的隔离作用,宿主机并不知道这个绑定挂载的存在。所以在宿主机看了,容器中可读写层的/test目录(var/lib/docker/aufs/mnt/[可读写层ID]/test),始终是空的。不过由于Docker一开始还是要创建/test这个目录作为挂载点,所以执行docker commit之后,会发现新产生的镜像里,会多出来一个空的/test目录。
如何在kubernetes集群部署时使用volume呢??
在kubernetes中,Volume属于Pod对象的一部分,所以就需要修改YAML文件里的template.spec字段
apiVersion: apps/v1 kind: Deployment metadata:
name: nginx-deployment spec: selector: matchLabels: app: nginx replicas: 2 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.8 ports: - containerPort: 80 volumeMounts: - mountPath: "/usr/share/nginx/html" name: nginx_vol volumes: - name: nginx-vol emptuDir: {}
上面在Deployment的Pod中添加了一个volumes字段,定义了这个pod声明的所有Volume。它的名字叫做nginx_vol,类型是emptyDir
那什么是emptyDir类型?即不显式声明宿主机目录的Volume,。所有kubernetes也会在宿主机上创建一个临时目录,这个目录将来就会被绑定挂载到容器所生命的volumes目录上。
而Pod中的容器,使用的volumeMounts字段来声明自己要挂载哪个Volume,并通过mountPath字段来定义容器内的Volume目录,比如:/usr/share/nginx/html
当然kubernetes也提供了显式的volume定义hostPath,如
…… volumes: - name: nginx-vol hostPath: path: /var/data
这样容器Volume挂载的宿主机目录,就变成了/var/data
接下来可以使用kubectl apply 更新这个deployment
kubectl apply -f nginx-deployment.yaml
最后还可以使用kubectl exec指令,进入到Pod当中,查看这个Vloume目录:
$kubectl exec -it nginx-deployment-5c678cfb6d-lg9lw -- /bin/bash
#ls /usr/share/nginx/html
特殊的Volume——Project Volume(投射数据卷,kubernetes v1.11之后的特性)
Project存在的意义不是为了存放容器里的数据,也不是用来进行容器和宿主机之间的数据交换,它们的作用是为容器提供预先定义好的数据。
Kubernetes支持的Projected Volume一共有四种:
-
- Secret
- ConfigMap
- Downward API
- ServcieAccountToken
1)、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是projected类型,而这个Volume的数据来源,则是名为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对象指定的名字。可以通过kubectl get secrets查看这些对象
$ 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 $ 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,一旦其对应的Etcd里的数据被更新,这里Volume里的文件内容同样也会被更新。这是kubectl组件在定时维护这些Volume,但是这个更新可能会有一定的延时,所以在写程序时,在发起数据库连接的代码处写好重试和超时的逻辑。
2)、ConfigMap
ConfigMap与Secret类型,区别在于ConfigMap保存的是不需要加密的、应用所需的配置信息。用法也与Secret完全相同:可以使用kubectl create 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 ...
3)、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 ' '; 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的Downward API Volume声明了暴露Pod的metadata.labels信息给容器。通过这样的声明方式,当前Pod的Labels字段的值,就会被kubernetes自动挂载成为容器里的/etc/podinfo/labels文件。而这个容器的启动命令,则是不断打印出/etc/podinfo/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
需要注意的是Downward API能够获取到的信息,一定是Pod里的容器进程启动之前就能够确定下来的信息。如果想要获取Pod容器运行后才会出现的信息,应该考虑在Pod里定义一个sidecar容器
Secret、ConfigMap以及Downward API这三种Projected Volume定义的信息,大多可以通过环境变量的方式出现在容器里,但是通过环境变量获取这些信息的方式,不具备自动更新的能力,因此建议使用volume文件获取这些信息
4)ServiceAccountToken
Service Account对象的作用就是kubernetes系统内置的一种服务账户,它是kubernets进程权限分配的对象。如Service Account A可以只被运行对kuberntes API进行GET操作,而Service Account B,则可以有kubernetes API所有操作的权限
Service Account的授权信息和文件,实际上保存在它所绑定的一个特殊的Secret对象里,这个Secret对象,就是ServiceAccountToken,任何运行在kubernetes集群上的应用都必须使用这个ServiceAccountToken保存的授权信息,才可以合法的访问API Server。也即ServiceAccountToken是一种特殊的Secret。
此外,kubernetes提供了一个默认的服务账户,任何一个运行在kubernetes里的pod,都可以直接使用这个默认的Service Account,而无需显示地声明挂载它。查看任意一个运行在kubernetes集群里的pod,都会发现每一个pod都已经自动声明一个类型是secret、名为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
即kubernets在每个pod创建时,都自动在它的spec.volumes部分添加上了默认ServiceAccountToken的定义,然后自动给每个容器加上了对于的volumeMounts字段,这个过程对于用户来说是完全透明的。一旦pod创建完成,容器里的应用就可以直接从这个默认的Service AccountToken的挂载目录里访问到授权信息和文件,这个容器内路径在kubernetes里是固定的
$ ls /var/run/secrets/kubernetes.io/serviceaccount ca.crt namespace token
应用程序只要直接加载这些授权文件,就可以访问并操作kubernetes API。这种把Kubernetes客户端以容器的方式运行在集群里,然后使用default Service Account自动授权的方式,被称作“IncluserConfig”