今天來介紹服務 (Service) 這個物件。在第一天的操作中,使用了 Service 物件來揭露 Pod 對外提供服務、可讓外界存取的 port。在 Kubernetes 的生態系中,會盡量去拆解系統的各個服務,將這些服務以 Pod 的型式包裝,以達到去耦化的效果。在這種狀況下,每個服務元件之間的彼此溝通,就成了很重要的課題。在 Pod 或容器原本的設計理念中,它的存續或生命期間就是短暫的,要如何去找到所需要的服務,這個問題就叫做「服務探索」(service discovery)。瞭解 service discovery 的概念被列在應試目標能力中,比較簡單的說法是剛才提到「服務定位」的問題,另外還有確保服務正常 (health check) ,其他還有那些問題是屬於服務探索的範疇,就請查閱相關資料了。
回到 Service 物件,它在 Kubernetes 中的作用之一就是提供 service discovery。除了前面提到用來揭露 Pod 對外服務的 port 外,它也用來處理叢集裡 Pod 之間互相溝通的任務。這裡直接用一個例子來說明,如何使用 Service 來連接一個作為前端的服務以及一個作為後端的服務。這樣說好像會誤會成先利用 Service 建立一個網路,然後再把前端和後端連接到這個網路上,但它的作法比較像是利用 Service 對後端建立叢集內可見的端點,亦即揭露後端服務的 port,並使用類似 DNS 概念的技術,讓前端可以直接透過後端的名稱取得後端位址,不須直接管理後端各個 Pod 的 IP。接下來就啟動 Minikube 來實驗看看,文件內容請參考 https://kubernetes.io/docs/tasks/access-application-cluster/connecting-frontend-backend/。
首先建立一個後端服務,它的設定檔名為 hello.yaml
的檔案,內容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
spec:
selector:
matchLabels:
app: hello
tier: backend
track: stable
replicas: 7
template:
metadata:
labels:
app: hello
tier: backend
track: stable
spec:
containers:
- name: hello
image: "gcr.io/google-samples/hello-go-gke:1.0"
ports:
- name: http
containerPort: 80
來看一下這個服務是做什麼的,因為它揭露了 80 port,並命名為 http
,所以猜測應該可以透過 http
協定來存取它的 80 port。那要怎麼存取呢,就直接連進容器看看,首先用 kubectl get pods -o wide
來看一下這些 Pod 的 IP。
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
hello-7ff54bc875-5mfbt 1/1 Running 0 6m 172.17.0.10 minikube
hello-7ff54bc875-8ls6w 1/1 Running 0 6m 172.17.0.11 minikube
hello-7ff54bc875-bvf92 1/1 Running 0 6m 172.17.0.9 minikube
hello-7ff54bc875-hp7lh 1/1 Running 0 6m 172.17.0.6 minikube
hello-7ff54bc875-p5lbn 1/1 Running 0 6m 172.17.0.8 minikube
hello-7ff54bc875-xpzml 1/1 Running 0 6m 172.17.0.5 minikube
hello-7ff54bc875-zkstk 1/1 Running 0 6m 172.17.0.7 minikube
隨便連進一個 Pod,可以用下面的指令取得一個 shell。
$ kubectl exec -it hello-7ff54bc875-5mfbt /bin/sh
/ #
接下來要訪問本機的 80 port,本來想用 curl
,但沒有這個指令,所以又試試看用 wget
,指令是 wget localhost:80
,結果可以執行,並寫成 index.html
,用 cat
查看一下,發現它的內容是 {"message":"Hello"}
,所以可以知道這個後端是訪問它的 80 port,它會吐出一個 JSON
字串。實際上在容器中也可以以 IP 來訪問其他 Pod。將 index.html
刪除後離開這個容器。
可以想像如果前端要訪問這個後端,都必須指定 IP 的話很麻煩,因為 IP 不是固定的,如果 ReplicaSet 減少或重建,原本的 IP 可能就無法使用。現在建立一個 Service 來處理這個問題,一樣透過 YAML 設定檔來建立,將設定檔命名為 hello-service.yaml
,內容如下:
kind: Service
apiVersion: v1
metadata:
name: hello
spec:
selector:
app: hello
tier: backend
ports:
- protocol: TCP
port: 80
targetPort: http
在 .spec.selector
中指定了兩個 label,分別是 app: hello
和 tier: backend
,和剛才 Deployment 中 Pod template 的 label 相符。在 targetPort
也是用了 template 中的 port 名稱,而非直接指定 port 號。接下來就建立這個 Service,指令是 kubectl create -f hello-service.yaml
。建立完成後可以查看一下 Service 的 IP。
$ kubectl get service -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
hello ClusterIP 10.108.124.2 <none> 80/TCP 21m app=hello,tier=backend
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 2d <none>
這裡用設定檔來建立 Service,回憶一下在前天的範例中,是用 kubectl expose deployment
來建立的。
接著來建立前端的 Pod,這裡文件把 Deployment 和 Service 放在同一個設定 YAML 中(竟然還有這一招),將它命名為 frontend.yaml
,內容如下:
apiVersion: v1
kind: Service
metadata:
name: frontend
spec:
selector:
app: hello
tier: frontend
ports:
- protocol: "TCP"
port: 80
targetPort: 80
type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
selector:
matchLabels:
app: hello
tier: frontend
track: stable
replicas: 1
template:
metadata:
labels:
app: hello
tier: frontend
track: stable
spec:
containers:
- name: nginx
image: "gcr.io/google-samples/hello-frontend:1.0"
lifecycle:
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"]
這邊的重點在 nginx 的設定檔,在 build 映像檔時已經放進去了,可以連到 Pod 中查看,路徑是 /etc/nginx/conf.d/frontend.conf
,內容如下:
upstream hello {
server hello;
}
server {
listen 80;
location / {
proxy_pass http://hello;
}
}
可以看到這個設定檔中並沒有指定後端的 IP,而是用 hello 來指稱後端的服務,這個名稱的解析就是後端 Service 所負責的。剩下的部分都和第一天的操作類似,再請大家自行嘗試了。
在服務需要對 Kubernetes 叢集之外揭露的情景,例如前端服務,Kubernetes 提供了四種不同的 ServiceType
,在 kubectl get services
時可以看到這個㯗位。這個值在 .spec.type
中可以指定,以下說明這四種類型。
<NodeIP>:<NodePort>
的方式來存取這個服務。在設定檔中可透過 ports 底下的 nodePort
欄位指定要揭露到節點的那一個 port。frontend
服務採用的是 LoadBalancer
此 serviceType
,會發現它的 EXTERNAL-IP
這個欄位會一直處於 pending 的狀態。.spec.externalName
欄位指定。這個 DNS 是在叢集內部維護的,所以在叢集外無法使用,而在叢集內基本上會用服務名稱去存取,所以操作方法本質上和其他類型是一樣的,主要的差異在於服務的導向是發生在 DNS 層級,而不是透過 proxy 或 forwarding。前面有提過 service discovery,那麼在 Kubernetes 中是如何去找到某一個特定服務的呢?主要有兩種方式。
KUBERNETES_SERVICE_PORT=443
MY_NGINX_SERVICE_HOST=10.0.162.149
KUBERNETES_SERVICE_HOST=10.0.0.1
MY_NGINX_SERVICE_PORT=80
KUBERNETES_SERVICE_PORT_HTTPS=443
這個方式有一個缺點,就是在 Pod 建立後才被創建的服務,它的位址不會被寫到環境變數中,所以 Pod 要存取的服務必須先於 Pod 被建立。
$ kubectl get services kube-dns --namespace=kube-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP 3d
看完了 Service,來看一下 Kubernetes 中如何處理資料儲存的問題。
在 Kubernetes 中,volume 的概念我覺得比較接近 Docker 中的 bind mount
,亦即自行指定要讓容器掛載的目錄,它的定義方式也類似 compose file 中的 volume 設定,先在 Pod 中指定 Volume,再於容器中指定 Volume 在容器中被掛載的路徑。對使用者來說,比較大的差異應該是 Kubernetes 的 volume 支援了多種媒介 (medium) 或協定,可指定不同類型的儲存設備,或用雲端儲存空間作為 volume,例如使用 Azure Disk 的範例如下:
apiVersion: v1
kind: Pod
metadata:
name: azure
spec:
containers:
- image: kubernetes/pause
name: azure
volumeMounts:
- name: azure
mountPath: /mnt/azure
volumes:
- name: azure
azureDisk:
diskName: test.vhd
diskURI: https://someaccount.blob.microsoft.net/vhds/test.vhd
除了一般的 volume 外,為了處理跨 Pod 甚至是跨節點的資料儲存問題,Kubernetes 設計了兩種新的資源物件,PersistentVolume和
PersistentVolumeClaim,它們可以視為 Volume 的更高層抽象化概念,將原本的資料儲存媒介,轉變為資料儲存的需求意圖及解決方案。這裡先說明一下,接著依文件中的範例來實作看看。
PersistentVolume 簡稱 PV,是由管理者所開通 (provision) 的儲存,有一點像是 Docker 中的 volume,屬於 Kubernetes 叢集中的資源,其生命週期和使用它的 Pod 無關。而 PersistentVolumeClaim 簡稱 PVC,是指由使用者所要求的儲存需求。文件中打了個比方,PVC 和 PV 的關係,就像 Pod 和 node 的關係,PVC 消費 PV 的資源,而 Pod 消費 node 的資源;Pod 要求 CPU 和 memory,PVC 則要求容量和存取模式。
PV 有兩種開通的方式,一種是靜態的 (static),另一種則是動態的 (dynamic)。靜態的 PV 由管理者事先建立好,若沒有靜態 PV 可以滿足使用者的 PVC,系統會自動開通一個 PV 讓此 PVC 使用,稱為動態的 PV。但動態的 PV 要如何被建立,背後又要依靠一個叫 StorageClass 的東西,它提供了一種讓管理者描述系統提供何種儲存「類別」的方式。這些說明讓人頭昏眼花,直接來看個例子吧,文件中有一個利用 PV 來部署 WordPress 和 MySQL 的範例,看起來不難,來試試看,請參考 https://kubernetes.io/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/。
這個範例也是在 Minikube 中完成的,請先啟動 Minikube。接下來先建立一個給 MySQL 用的 secret,指令如下:
$ kubectl create secret generic mysql-pass --from-literal=password=<YOUR_PASSWORD>
Kubernetes secret 的用法應該類似 Docker secret,generic 表示 secret 的類型,mysql-pass 是這個 secret 的名字。可以用 kubectl create secret --help
查看指令的文件。<YOUR_PASSWORD>
請置換成要使用的密碼,待會在部署 MySQL 時,會帶入這個 secret。建立好之後用 kubectl get secrets
查看。
接著建立 mysql-deployment.yaml
,內容如下:
apiVersion: v1
kind: Service
metadata:
name: wordpress-mysql
labels:
app: wordpress
spec:
ports:
- port: 3306
selector:
app: wordpress
tier: mysql
clusterIP: None
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pv-claim
labels:
app: wordpress
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: wordpress-mysql
labels:
app: wordpress
spec:
selector:
matchLabels:
app: wordpress
tier: mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: wordpress
tier: mysql
spec:
containers:
- image: mysql:5.6
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pv-claim
此份設定檔定義了一個 PVC 物件,它的存取模式 (access mode) 是 read write once,表示只能有一個節點掛載此 volume 並支援讀寫,要求的容量是 20G。而在 Deployment 中,定義了一個 volume,並指定它的類型為 PVC,它會掛載在 /var/lib/mysql
。接下來用 kubectl create
來建立上述資源,建立好了之後用 kubectl get pv
/ kubectl get pvc
來查看。這裡 pv 和 pvc 也可以用全稱。
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
mysql-pv-claim Bound pvc-8ac66875-e257-11e8-93da-08002797b9be 20Gi RWO standard 21s
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-8ac66875-e257-11e8-93da-08002797b9be 20Gi RWO Delete Bound default/mysql-pv-claim standard 14m
這裡可以看到有一個 PV 會被建立出來,在 CLAIM
欄位可以看到它所對應的 PVC。
接下來部署 WordPress,建立 wordpress-deployment.yaml
,內容如下:
apiVersion: v1
kind: Service
metadata:
name: wordpress
labels:
app: wordpress
spec:
ports:
- port: 80
selector:
app: wordpress
tier: frontend
type: LoadBalancer
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wp-pv-claim
labels:
app: wordpress
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: wordpress
labels:
app: wordpress
spec:
selector:
matchLabels:
app: wordpress
tier: frontend
strategy:
type: Recreate
template:
metadata:
labels:
app: wordpress
tier: frontend
spec:
containers:
- image: wordpress:4.8-apache
name: wordpress
env:
- name: WORDPRESS_DB_HOST
value: wordpress-mysql
- name: WORDPRESS_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
ports:
- containerPort: 80
name: wordpress
volumeMounts:
- name: wordpress-persistent-storage
mountPath: /var/www/html
volumes:
- name: wordpress-persistent-storage
persistentVolumeClaim:
claimName: wp-pv-claim
PV 和 PVC 的部分和 MySQL 類似,這裡注意一下 WordPress 是在容器中使用 WORDPRESS_DB_HOST
環境變數來載入 MySQL 服務的名稱 wordpress-mysql。在 Service 的部分,使用 LoadBalancer 此一 service type。接下來建立此設定檔中的資源。
待資源都建立完成後,就可以用瀏覽器連到服務中看看部署的結果。再複習一下,因為沒有外部的負載平衡器,所以必須連到 Minikube 的 IP,使用的 port 是 wordpress 這個 Service 綁定到 Minikube 的 port,應該會在 30000 - 32767 之間。如果都成功的話會看到下面的畫面:
以上就是今天的內容,介紹 Service 物件,這個和 Pod 之間彼此溝通,以及叢集內外互相溝通有關的資源物件,以及 Volume 等與儲存相關的資源物件。明天會很簡單地介紹一下 Kubernetes 的組成元件,並且利用 Vagrant 機器和 kubeadm 來建立一個多節點的 Kubernetes 叢集。那麼就明天繼續了。