iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 25
0
Cloud Native

30 天準備 LPI DevOps Tools Engineer 證照系列 第 25

[Day 25] Kubernetes (3)

Service 物件及網路

今天來介紹服務 (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: hellotier: 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 所負責的。剩下的部分都和第一天的操作類似,再請大家自行嘗試了。

Service Type

在服務需要對 Kubernetes 叢集之外揭露的情景,例如前端服務,Kubernetes 提供了四種不同的 ServiceType,在 kubectl get services 時可以看到這個㯗位。這個值在 .spec.type 中可以指定,以下說明這四種類型。

  1. ClusterIP:這是預設的類型,它會替服務建立一個叢集內部的 IP,使得這個服務只能由叢集內部存取。
  2. NodePort:它會將服務揭露於叢集中每個節點的固定 IP 上,在叢集外部可以透過 <NodeIP>:<NodePort> 的方式來存取這個服務。在設定檔中可透過 ports 底下的 nodePort 欄位指定要揭露到節點的那一個 port。
  3. LoadBalancer:這個類別基本上要有外部的負載平衡器 (load balancer) 配合。文件中提到部署在雲端供應商的環境,有些會支援提供外部負載平衡器。如果是自己架設負載平衡器,不確定是否可以使用這個設定。基本上這個設定會視不同的雲端供應商而定,若有需要請再參考文件。在前面的範例中,frontend 服務採用的是 LoadBalancerserviceType,會發現它的 EXTERNAL-IP 這個欄位會一直處於 pending 的狀態。
  4. ExternalName: 會將服務對應到一個 DNS 名稱 (CNAME record),這個名稱可在 .spec.externalName 欄位指定。這個 DNS 是在叢集內部維護的,所以在叢集外無法使用,而在叢集內基本上會用服務名稱去存取,所以操作方法本質上和其他類型是一樣的,主要的差異在於服務的導向是發生在 DNS 層級,而不是透過 proxy 或 forwarding。

前面有提過 service discovery,那麼在 Kubernetes 中是如何去找到某一個特定服務的呢?主要有兩種方式。

  1. 環境變數:這是在 Pod 被創立時會被一併設定的資訊,Kubernetes 會把此 Pod 會存取到的 Service 或其他物件的 IP 以環境變數的方式記錄下來,例如:
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 被建立。

  1. DNS:就是利用 DNS 查詢的方式,這個方式需要 DNS cluster addon,可以用以下指令確認是否有安裝:
$ 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 中如何處理資料儲存的問題。

Volume

在 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

Persistent Volume

除了一般的 volume 外,為了處理跨 Pod 甚至是跨節點的資料儲存問題,Kubernetes 設計了兩種新的資源物件,PersistentVolumePersistentVolumeClaim,它們可以視為 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/

利用 PV 來部署 WordPress 和 MySQL

這個範例也是在 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 之間。如果都成功的話會看到下面的畫面:
https://ithelp.ithome.com.tw/upload/images/20181109/20111953NkuczS3EPa.png

以上就是今天的內容,介紹 Service 物件,這個和 Pod 之間彼此溝通,以及叢集內外互相溝通有關的資源物件,以及 Volume 等與儲存相關的資源物件。明天會很簡單地介紹一下 Kubernetes 的組成元件,並且利用 Vagrant 機器和 kubeadm 來建立一個多節點的 Kubernetes 叢集。那麼就明天繼續了。


上一篇
[Day 24] Kubernetes (2)
下一篇
[Day 26] Kubernetes (4)
系列文
30 天準備 LPI DevOps Tools Engineer 證照30

尚未有邦友留言

立即登入留言