容器技術使用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 '\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的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”