Try   HackMD

GKE Hand's Up Lab (施工中)

藉由實際的案例來學習如何將服務部署到GKE上

tags: RD1

Quick Intro

簡介

此教學主要是希望藉由一個小專案部署到GKE上的過程,讓大家學習如何將服務部署到GKE上。操作這個Lab之後應該會對k8s的這些元件更加熟悉:

  • Pod
  • Deployment
  • Service
  • PVC、Storage Class

另外,由於是將服務部署到GKE上,所以也會學習到一些GCP提供的功能:

  • Kubernetes Engine - GCP 提供的k8s代管服務
  • Container Registry - GCP 提供的容器image版本庫服務
  • Cloud Source Repository - GCP 提供的git程式碼版本庫服務

甚至是一些GCP工具的操作

  • GCP Console(UI)
  • gcloud(CLI)

要部署的專案簡介

這次要部署的專案可以使用git指令下載回來

git clone https://github.com/TerryHuangchungyo/gallery.git

或是直接到github頁面下載:
https://github.com/TerryHuangchungyo/gallery

由於該專案的說明與啟動方式在專案的README文件已經有說明了,這邊就不再贅述了;不妨使用docker-compose本地啟動該專案並操作看看,並閱讀README文件,可以幫助了解該專案的架構。

Start with Container Registry

這個章節我們先會操作如何將專案部署到GKE上,首先會先用docker在本地將映象檔打包好,上傳到GCP的Container Registry上,之後利用這些打包好的映象檔部署服務。如果想知道如何使用GCP Cloud build服務觸發打包部署的話,建議可以直接跳到後面Cloud build的章節。

在GCP新增一個專案

首先我們必須在GCP上新增一個專案,可以使用google的免費方案,google會給你的帳號90天300美元的額度。

首先進到gcp平台頁面,然後點選建立專案:

輸入專案名稱後按建立:

建立好之後,可以選擇專案,並看到專案資訊頁。

這邊需要看到每個專案都會有一個專案ID,專案建立之後專案ID是不能修改的,許多操作都是會用這個專案ID而不是使用專案名稱,可以把專案ID當作是這個專案的身分證號。

Container Registry

Container Registry是GCP提供的映象檔版本管理服務,是本章節會用到的GCP服務。

要到Container Registry的頁面,點選導覽選單 > Container Registry。

Google有提供如何將映象檔推上Container Register的教學以及配置。

Container Registry - Pushing and pulling images

Container Registry - Authentication methods

在本地端打包

將這次要部署的專案使用git指令下載回來。

git clone https://github.com/TerryHuangchungyo/gallery.git

切換到專案資料夾底下,並觀察專案目錄。

cd gallery
ls
doc/                gallery-backend/  gallery-frontend/
docker-compose.yml  gallery-db/       README.md

這次將打包的會是gallery-backend與gallery-frontend服務的映象檔,可以觀察一下兩個服務的Dockerfile,應該會發現沒有差別,主要是複製到/go/src資料夾底下的程式碼不同而已。

# gallery-backend/Dockerfile
FROM golang:1.16
COPY . /go/src
WORKDIR /go/src/cmd
RUN go mod download && go build main.go
CMD ["./main"]

現在,我們切換到gallery-frontend的資料夾底下進行打包並推到GCR(Container Registry)上。

cd gallery-frontend
docker build -t="gcr.io/<PROJECT_ID>/gallery-frontend" .
docker push gcr.io/<PROJECT_ID>/gallery-frontend

PROJECT_ID就是前面講的專案ID,可以到GCP的專案頁進行查看

gallery-backend的打包方式也相同

cd ../gallery-backend
docker build -t="gcr.io/<PROJECT_ID>/gallery-backend" .
docker push gcr.io/<PROJECT_ID>/gallery-backend

如果上傳成功的話Container Registry上就會有兩個映像檔

接下來準備要開始部署專案了,可以考慮使用GCP提供的Cloud Shell進行操作,裡面已經將kubectl工具安裝好了。

專案部署


首先我們先看一下專案原本在本地的架構,gallery-frontend跟gallery-backend的映象檔我們已經上傳到GCR了,而mysql與paint-image的儲存空間我們必須用到PVC來取得儲存空間。

對PV、PVC或GCE Persistent disk不熟的話可以參考一些教學:

GKE - Persistent volumes and dynamic provisioning

K8S - Storage

創建叢集

要創建叢集我們必須選擇要在哪一個專案底下創建,這時我們選擇剛剛創建好的專案。

接下來到導覽列的Kubernetes Engine的叢集選項來使用UI介面創建叢集。

按下建立

這邊會詢問你要使用哪一種叢集設定,這邊我們選擇標準,然後按下設定。

接下來設定叢集基本設定,這邊我們設定名稱為
"gallery"與叢集所在地區"asia-east1-a。

叢集的種類說明可以參考這篇文章
Google Cloud - Types of clusters

創建之後我們可以在叢集頁面看到創建好的叢集,叢集預設會產生的node數是3個,這個可以在創建時或創建後修改,這邊我使用預設數量就好。

創建好之後記得用gcloud指令取得cluster的授權,這樣才有辦法使用kubectl指令對該cluster操作。

gcloud config set <PROJECT_ID> gcloud container clusters get-credentials gallery --zone asia-east1-a

如果有正確授權的話,使用以下命令應該可以看到kubectl的context被設定為該叢集

$ kubectl config get-contexts CURRENT NAME CLUSTER AUTHINFO NAMESPACE * gke_constant-wonder-305916_asia-east1-a_gallery gke_constant-wonder-305916_asia-east1-a_gallery gke_constant-wonder-305916_a sia-east1-a_gallery

部署MySQL資料庫與phpmyadmin

部署MySQL

首先我們要先創建一個StorageClass元件,之後創建PVC時就可以依此StorageClass去要一塊PV。這邊要注意的是StorageClass的reclaimPolicy設置成Retain代表說如果Pod被刪除了,綁定在Pod上PV裡的資料會被保存下來,畢竟應該沒有人希望mysql服務重啟後裡面的資料不見吧?:worried:

# gallery-sc-retain.yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: gallery-sc-retain provisioner: kubernetes.io/gce-pd parameters: type: pd-standard fstype: ext4 reclaimPolicy: Retain volumeBindingMode: Immediate

這邊我們打開文字編輯器將上面的yaml檔複製貼上

nano gallery-sc-retain.yaml Ctrl + x 保存存檔

然後使用kubectl用yaml檔創建物件

$ kubectl create -f gallery-sc-retain.yaml

創建好後可以到Kubernetes Engine > 儲存空間 查看剛剛創建好的StorageClass。

接下來我們要來創建一個PVC來跟StorageClass要一塊儲存空間了,只要有設定 StorageClass ,K8S就會依照StorageClass的Spec幫我們創建一個PV。

# gallery-db-pvc.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: gallery-db-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 50Gi storageClassName: gallery-sc-retain

這邊可以看到storageClassName設置的就是我們剛剛創建的StorageClass;然後一樣使用kubectl命令從檔案創建物件。

nano gallery-db-pvc.yaml # 複製上面的yaml 貼下後按下Crtl + x 儲存
$ kubectl create -f gallery-db-pvc.yaml persistentvolumeclaim/gallery-db-pvc created

創建好後可以到Kubernetes Engine > 儲存空間 > 永久磁碟區要求標籤 查看 查看剛剛創建好的PVC。

紅色框框標起來寫PV的部分就是由StorageClass創建出來的PV,點進去並觀察他的yaml檔,你會發現有一些有的沒的設定其實是GKE幫我們自動設定的。

如果想知道儲存空間是從哪裡產生出來的,可以到導覽標籤 > Compute Engine > 磁碟 查看,有時候想對gce Persistent Disk做管理也是會到這個頁面管理。

好了之後我們要來部署mysql伺服器了,首先我們需要創建一個secret物件來保存mysql root帳號的密碼。

# gallery-db-secret.yaml apiVersion: v1 kind: Secret metadata: name: gallery-db-secret type: Opaque data: root-password: cm9vdA==

上面看到root-password有一串奇怪的代碼"cm9vdA==",其實這個是由base64編碼後的內容,原本的內容應該是"root"。這是secret物件設定上的一些規定,可以到k8s官方文件看更詳細的說明:

k8s - secrets

一樣也是複製到檔案後,用k8s指令創建。

$ kubectl create -f gallery-db-secret.yaml
MySQL服務部署

這邊我們會用StatefulSets進行mysql伺服器服務的部署。通常Deployment用在無狀態服務的部署,而StatefulSets則是用在有狀態服務的部署。

為甚麼這邊會使用StatefulSets而不是使用Deployment,主要原因是因為gcePersistentDisk 只能同時被一個node進行讀寫,如果用Deployment進行部署的話有可能會發生死結的狀況;更詳細的原因這邊就不詳細說明了,可以參考以下文件說明:

GKE - Persistent volumes and dynamic provisioning

k8s - StatefulSets

以下是這次部署使用到的yaml檔:

# gallery-db.yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: gallery-mysql-db spec: selector: matchLabels: db: gallery serviceName: "gallery-db" replicas: 1 # by default is 1 template: metadata: labels: db: gallery spec: terminationGracePeriodSeconds: 10 containers: - name: mysql image: mysql:8.0.21 ports: - containerPort: 3306 name: mysql-db-port env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: gallery-db-secret key: root-password volumeMounts: - name: gallery-db-pvc-volume mountPath: /var/lib/mysql volumes: - name: gallery-db-pvc-volume persistentVolumeClaim: claimName: gallery-db-pvc --- apiVersion: v1 kind: Service metadata: name: gallery-db labels: app: gallery spec: ports: - port: 3306 name: mysql-db-port clusterIP: None selector: db: gallery

可以看到MySQL儲存的volume是用我們剛剛創建的PVC物件掛上去的,而root的密碼則是剛剛我們創建secret物件以環境變數的方式帶入,這次部署replicas設為1,代表我們只維持一個mysql伺服器的pod運行就好。

而service的種類使用的是無頭服務(headless service),就是沒有Cluster IP的Service,原因是StatefulSets的一些規定,這邊可以去查看官方的說明。

一樣複製到文件後,使用kubectl指令部署

$ kubectl create -f gallery-db.yaml

好了之後查看工作負載與Service頁面應該能看到以下畫面。

工作負載

有時候會看到一些warning訊息,像是pending之類的,代表pod在部署中等待一下重整應該就好了。

Service的確沒有ClusterIP

不過點進去看時還是能看到他有綁到mysql pod服務的端點。

想確認mysql的服務是否有部署成功,除了看GCP UI的介面以外,也可以下以下命令看Pod的狀態

$ kubectl get pods NAME READY STATUS RESTARTS AGE gallery-mysql-db-0 1/1 Running 0 8m19s

然後進到Pod裡面執行mysql看看

$ kubectl exec -it gallery-mysql-db-0 /bin/sh $ mysql -uroot -proot
部署phpmyadmin

由於使用指令去管理mysql不是很方便,可以部署phpmyadmin,這樣就可以用GUI對mysql操作。

# gallery-db-phpmyadmin.yaml apiVersion: apps/v1 kind: Deployment metadata: name: phpmyadmin labels: app: phpmyadmin spec: replicas: 1 selector: matchLabels: app: phpmyadmin template: metadata: labels: app: phpmyadmin spec: containers: - name: phpmyadmin image: phpmyadmin ports: - containerPort: 80 env: - name: PMA_HOST value: "gallery-db"

可以看到這邊phpmyadmin部署時,環境參數PMA_HOST是帶gallery-db,就是我們剛剛創建資料庫service的名字。可以用service就能獲取服務的IP這個要歸功於kube-dns,有興趣的話可以搜尋看看kube-dns的功能,這邊就不過多說明了。

一樣下kubectl創建物件。

$ kubeclt create -f gallery-db-phpmyadmin.yaml

可以使用kubectl port-forward獲取這個工具服務。

# 要先看Pod的name $ kubectl get pods NAME READY STATUS RESTARTS AGE gallery-mysql-db-0 1/1 Running 0 26m phpmyadmin-ff944bb55-679qm 1/1 Running 0 2m35s # port-forward 連結 $ kubectl port-forward phpmyadmin-ff944bb55-679qm 8080:80

如果是在本地端port-forward的話,打開瀏覽器輸入localhost:8080應該就能看到phpmyadmin的登入頁面了。

如果是使用gcloud shell的話,可以使用以下方法。

這邊我們需要登入mysql資料庫做一些資料表的初始化。(密碼是"root")

進入後點選SQL標籤。

複製以下SQL語法

CREATE DATABASE `gallery`; USE `gallery`; DROP TABLE IF EXISTS `exhibition`; CREATE TABLE `exhibition` ( `id` int NOT NULL AUTO_INCREMENT, `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `description` text COLLATE utf8mb4_unicode_ci, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; DROP TABLE IF EXISTS `exhibition_has_paint`; CREATE TABLE `exhibition_has_paint` ( `exibition_id` int NOT NULL, `paint_id` int NOT NULL, UNIQUE KEY `exibition_id` (`exibition_id`,`paint_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; DROP TABLE IF EXISTS `paint`; CREATE TABLE `paint` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `image_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

貼到框框內,然後按執行。

完成後應該可以看到資料庫與資料表被建好了。

部署MySQL到這邊就算完成了,接下來我們要來部署圖片上傳儲存的空間。

部署NFS伺服器


接下來我們要來部署圖片的上傳空間,不過有一個議題還是要去克服,就是gcePersistentDisk只能有兩種存取模式ReadWriteOnce與ReadOnlyMany。

Persistent Volume的存取模式有三種:

  • ReadWriteOnce - 只能被同一個node讀或寫
  • ReadOnlyMany - 可以被很多node讀,但不能寫
  • ReadWriteMany - 可以被很多node讀跟寫

不同種類或廠商提供的儲存方案對於存取模式的支援也會有所不同,這邊可以參考以下文件來了解。
Persistent Volumes - Access Mode

如果使用ReadWriteOnce模式來當圖片儲存空間的話,未來gallery-frontend跟gallery-backend在node之間的調度會很麻煩,對於自動擴展也是一大問題。而使用ReadOnlyMany模式的話,就沒有辦法實現上傳圖片的功能了。

這邊可以架設一個nfs伺服器,nfs對於三種模式都有支援。GCP本身有提供一個叫FileStore的NFS服務,不過FileStore對於小型應用似乎不是那麼划算,所以可以架設一個自己的NFS伺服器。

首先必須從gcePersistent Disk要一塊空間

$ gcloud compute disks create --size=10GB --zone=asia-east1-a gce-nfs-disk NAME ZONE SIZE_GB TYPE STATUS gce-nfs-disk asia-east1-a 10 pd-standard READY

好了之後應該就能在 Compute Engine > 磁碟 看到請求到的硬碟。

創建好之後,我們要來部署NFS伺服器。

# gallery-nfs-server.yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: gallery-nfs-server spec: replicas: 1 selector: matchLabels: app: gallery role: nfs-server serviceName: "gallery-nfs-server" template: metadata: labels: app: gallery role: nfs-server spec: terminationGracePeriodSeconds: 10 containers: - name: gallery-nfs-server image: gcr.io/google_containers/volume-nfs ports: - name: nfs containerPort: 2049 - name: mountd containerPort: 20048 - name: rpcbind containerPort: 111 securityContext: privileged: true volumeMounts: - mountPath: /exports name: disk-for-nfs volumes: - name: disk-for-nfs gcePersistentDisk: pdName: gce-nfs-disk fsType: ext4 --- apiVersion: v1 kind: Service metadata: name: gallery-nfs-server spec: ports: - name: nfs port: 2049 - name: mountd port: 20048 - name: rpcbind port: 111 clusterIP: None selector: app: gallery role: nfs-server

NFS伺服器的部署一樣是用StatefulSets來部署(也是因為ReadWriteOnce的關係),用的volumes就是我們剛剛創建的gcePersistentDisk,而Service也是剛剛所說的headless service。

一樣用kubectl指令部署

$ kubectl create -f gallery-nfs-server.yaml statefulset.apps/gallery-nfs-server created service/gallery-nfs-server created

接下來就可以創建圖片儲存空間的PV與PVC了。

# gallery-image-hub.yaml apiVersion: v1 kind: PersistentVolume metadata: name: gallery-image-hub-pv labels: app: gallery role: nfs-pv spec: capacity: storage: 10Gi accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain nfs: server: gallery-nfs-server.default.svc.cluster.local path: "/" --- kind: PersistentVolumeClaim apiVersion: v1 metadata: name: gallery-image-hub-pvc spec: accessModes: - ReadWriteMany storageClassName: "" resources: requests: storage: 10Gi selector: matchLabels: app: gallery role: nfs-pv

這邊我們是預先創建一個PV,而不是像之前一樣使用StorageClass動態配置的方式。可以發現到accessModes的設定已經是ReadWriteMany了,這樣就可以由多個node進行讀跟寫了。

指令

$ kubectl create -f gallery-image-hub.yaml persistentvolume/gallery-image-hub-pv created persistentvolumeclaim/gallery-image-hub-pvc created

之後部署的gallery-frontend跟gallery-backend就可以以創建好的PV掛載volume了。

部署gallery-backend

還記得一開始我們有在本地端打包映象檔並推到GCR上面嗎? 現在我們要來利用剛剛推送的映象檔進行部署了。

部署Deployment
# gallery-backend-deploy.yaml apiVersion: apps/v1 kind: Deployment metadata: name: gallery-backend labels: app: gallery-backend spec: replicas: 1 selector: matchLabels: app: gallery-backend template: metadata: labels: app: gallery-backend spec: containers: - name: gallery-backend image: gcr.io/<PROJECT_ID>/gallery-backend ports: - containerPort: 8080 env: - name: PROJECT_ENV value: "gcp" volumeMounts: - name: gallery-image-hub mountPath: /go/src/assets/image volumes: - name: gallery-image-hub persistentVolumeClaim: claimName: gallery-image-hub-pvc

可以看到這次部署是containers的image就是剛剛我們推送上去的映象檔(<PROJECT_ID> 要填您的專案ID),並且volumeMounts掛載的就是我們創建圖片儲存空間的PVC。

指令

$ kubectl create -f gallery-backend-deploy.yaml deployment.apps/gallery-backend created
部署Service

這邊我們希望從Cluster外也能拿到服務,所以必須創建一個種類是Loadbalancer的Service。

對Loadbalancer不熟悉的話可以參考k8s官方文件。
k8s - Loadbalancer

# gallery-backend-svc.yaml apiVersion: v1 kind: Service metadata: name: gallery-backend-svc spec: selector: app: gallery-backend type: LoadBalancer ports: - port: 80 targetPort: 8080

指令

$ kubectl create -f gallery-backend-svc.yaml service/gallery-backend-svc created

部署好了到 Kubernetes Engine > Service 頁面查看service創建狀況,應該能看到service已經有對外端點了,透過此對外端點我們就能從Cluster外獲取服務了。

部署gallery-frontend

這邊我們要來部署gallery-frontend了,流程與剛剛部署gallery-backend時類似。

部署Deployment
# gallery-frontend-deploy.yaml apiVersion: apps/v1 kind: Deployment metadata: name: gallery-frontend labels: app: gallery-frontend spec: replicas: 3 selector: matchLabels: app: gallery-frontend template: metadata: labels: app: gallery-frontend spec: containers: - name: gallery-frontend image: gcr.io/<PROJECT_ID>/gallery-frontend ports: - containerPort: 8080 env: - name: PROJECT_ENV value: "gcp" volumeMounts: - name: gallery-image-hub mountPath: /go/src/assets/image volumes: - name: gallery-image-hub persistentVolumeClaim: claimName: gallery-image-hub-pvc

指令

$ kubectl create -f gallery-frontend-deploy.yaml service/gallery-frontendend-svc created
部署Service
# gallery-frontend-svc.yaml apiVersion: v1 kind: Service metadata: name: gallery-frontend-svc spec: selector: app: gallery-frontend type: LoadBalancer ports: - port: 80 targetPort: 8080

指令

$ kubectl create -f gallery-frontend-svc.yaml deployment.apps/gallery-frontend created

部署好了之後一樣到 Kubernetes Engine > Service 頁面查看service創建狀況。

結語

恭喜! 這個章節已經完成了~

CI/CD with Cloud Build

之前的章節,我們已經成功部署專案到GKE上了。而gallery-frontend與gallery-backend的部署則是由我們從本地端將映象檔打包後推上GCP後,使用kubectl進行服務的部署與更新。

除了在本地端打包映象檔之外,是否有其他的方法能夠將打包更新部署這個動作自動化呢?這個時候可能就可以使用GCP提供的Cloud Build工具來達到自動化部署,更進階一點甚至能建構一個CI/CD Pipeline。

現代DevOps實踐涉及軟體應用程式在整個開發生命周期內的持續開發、持續測試、持續集成、持續部署和持續監控。CI/CD實踐或CI/CD管道(CI/CD pipeline)構成了現代DevOps業務的主幹。

以上截取自維基百科 - CI/CD

關於DevOps與CI/CD的介紹可以參考以下文章:

GitOps-style continuous delivery with Cloud Build

此次教學會參考此篇google cloud build的教學,為gallery-frontend與gallery-backend建構一個GitOps-style的CI/CD pipeline。

PROJECT_ID=$(gcloud config get-value project)