# 利用 Deployment & Service 部署服務 以下 app 範例有兩部分,一個為前端介面(利用 gradio),另一個為 Model serving 的後端 API (利用 fastapi)。 因此會需要兩個 Deployment,並將前端的 port 接上 Service 讓我們能夠在本機使用瀏覽器訪問。本篇只示範後端 API 部分。 ## [Deployment](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/deployment-v1/) Deployment 是一種用於自動管理一組相同 Pods 的資源類型。它不僅能夠確保指定數量的 Pod 副本始終運行,還能夠在 Pod 運行時實現自動替換和擴展。 當我們建了 Deployment,它會建立 ReplicaSet,然後再看需要多少 replicas 去建立相對應數量的 Pods。 ```yaml # deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: namespace: chest-ct name: chest-ct-api-deployment spec: replicas: 2 # Pod 數量 selector: matchLabels: app: chest-ct-api # 需和 template.metadata.labels 一致 template: metadata: labels: app: chest-ct-api # 設定 Pod 的標籤為 app: chest-ct-api spec: containers: - name: chest-ct-api image: chest-ct-serving:latest ports: - containerPort: 9528 imagePullPolicy: Never envFrom: - secretRef: name: s3-secret # app 本身不會用到,這邊只是示範語法 ``` + **`apiVersion`**:指定 Kubernetes API 版本,**`apps/v1`** 是常用於 Deployment 的穩定版本。 + **`kind`**:指定資源類型為 Deployment,用於管理應用的部署和更新, + **`metadata`**:Deployment 的名稱,用於在 Kubernetes Cluster 中標識這個 Deployment。也可以加上 **`labels`** 作為識別的用途,此外,可指定 **`namespace`**,在 Cluster 中可以利用 Namespace 來做資源區隔,Deployment 就是個 Namespace level object。 + **`spec`**: + **`replicas`**:副本數量,指定 Kubernetes 應該維持的 Pod 實例數量為 2。 + **`selector`**:用於識別應該由此 Deployment 管理的 Pod,這裡選擇 **`label`** 為 **`app: chest-ct-api`** 的 Pod。 + **`template`**:Pod template,要定義的東西跟 Pod 一模一樣除了 **`apiVersion`** & **`kind`**。如果數量小於 replicas number 的話,這邊的資訊會被 Deployment 用來建立新的 Pod。因此在 **`spec.template.metadata.labels`** 的地方,**`labels`** 要跟 **`selector`** 符合才會作用。另外這裡的 Pod 重啟策略只會是 **`Always`** ,因為使用 Deployment 就是希望能夠自動控管 Pod 數量如我們預期,所以 Pod 如果死掉會重啟直到數量 = **`replicas`**。 + **`image`**:要使用的 Image。 + **`containerPort`**:Container 對外暴露的端口。 + **`imagePullPolicy`**:指定映像拉取策略為 **`Never`**,這表示不從遠程倉庫拉取映像,只使用本地映像。 + **`envFrom`**:可以掛環境變數進去給 Container 使用。在 Kubernetes 的世界中通常會用 Secret & ConfigMap 兩種 object 來掛環境變數。Secret 是給敏感資訊用的。**`secretRef`** 即是說從 Secret 中設定 env variables,底下的 **`name`** 會需要跟 Secret name 一致。 :::warning :notebook: **使用本地 Image 導入功能:** k3d 提供了一個功能,允許你直接從本地 Docker 宿主機的 Image 倉庫導入 Image 到 k3d Cluster 中。這可以通過 **`k3d image import <image-name> -c <cluster-name>`** 命令完成。首先,確保你的 Image 已經在本地 Docker 宿主機上構建或拉取。 ```bash k3d image import chest-ct-serving:latest -c mycluster ``` 進入到 Node 查看 Image 是否有導入成功:(**`crictl`** 是 containerd 的指令) ```bash docker exec -it k3d-mycluster-server-0 sh crictl images # IMAGE TAG IMAGE ID SIZE # docker.io/kubernetesui/dashboard v2.7.0 07655ddf2eebe 75.8MB # docker.io/kubernetesui/metrics-scraper v1.0.8 115053965e86b 19.7MB # docker.io/library/chest-ct-serving latest b933f3d1d2832 4.33GB # docker.io/rancher/klipper-helm v0.8.0-build20230510 6f42df210d7fa 95MB # docker.io/rancher/klipper-lb v0.4.4 af74bd845c4a8 4.92MB # docker.io/rancher/mirrored-pause 3.6 6270bb605e12e 301kB ``` ::: ## [Secret](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/secret-v1/) Pod 所需要用到的環境變數如果包含敏感資料,可以利用 Secret 這個類型的 Object 傳進去。 ```yaml # secret.yaml apiVersion: v1 kind: Secret metadata: namespace: chest-ct name: s3-secret type: Opaque data: AWS_ACCESS_KEY_ID: bHh6WlRGTzRvMlc2OEZ6bkVmR1Q= AWS_SECRET_ACCESS_KEY: Q1dZdnh2NkcxRFFwUExUM05DdXdiYmJxTnp6MXJBaE9IekFHa2E3ZQ== MLFLOW_S3_ENDPOINT_URL: aHR0cHM6Ly9zMy5taW5pby53aW5nZW5lLms4cw== MLFLOW_S3_IGNORE_TLS: dHJ1ZQ== ``` Secret spec 中有 **`type`** 的 field。預設其實就是 **`Opaque`**,但 Secret 還有很多種類型。**`Opaque`** 可以讓使用者定義要的 **`data`** 是什麼。 **`data`** 是內容的部分。這邊都是以 key/value pair 的方式來訂變數。如果使用 **`data`** ,則 value 的部分必須是 base64-encoded 字串,因 Kubernetes 在使用時會幫你做 base64 decode。如果想要用明碼的話則是要用 **`stringData`** 。 base64 轉換可以用以下指令: ```bash echo -n 'test' | base64 echo -n 'dGVzdA==' | base64 --decode ``` 要注意的一點是這邊只做了編碼,並非加密喔! 這邊定義的 S3 連線相關資訊,到時會進行測試是否能存取資料。 ## [Service](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/) 在 Cluster 中每個 Pod 其實都能跟別的 Pod 溝通,每個 Pod 都會有自己的 IP。但是!如果 Pod 死掉後重開一個,IP 就會改變。在 Kubernetes 中,Service 是一個虛擬的概念,使用 Service 可以讓外界的流量傳給 Pod,或是做 Pod 之間的通信。而 Service 要將流量傳給誰呢?在 configuration file 中會設定 **`selector`** 去指向特定的 Pods,也會設定要如何訪問這些 Pods。 ```yaml # service.yaml apiVersion: v1 kind: Service metadata: namespace: chest-ct name: chest-ct-api-service spec: type: NodePort ports: - port: 9977 # Service 將公開的端口 targetPort: 9528 # 和連接 Pod 中的 containerPort 一致 nodePort: 30001 selector: app: chest-ct-api # 跟 Deployment 中 template 定義的 metadata.labels 是一致的 ``` + **`type`**:可以是 **`ClusterIP`**、**`NodePort`**、**`LoadBalancer`**、**`ExternalName`**。Service 的類型為 **`NodePort`**,這意味著 Service 會在 Cluster 的每個節點上打開一個端口,允許從節點外部訪問,之後傳給 Service,然後再傳給 Pods。 + **`ports`**: + **`port`**:這是 Service 在 Cluster 內部的端口,Cluster 內部的流量會使用這個端口與 Service 通信。 + **`targetPort`**:這是要連接的 Pods 上 Container 開的端口,Service 會將接收到的流量轉發到此端口上的 Pods。 + **`nodePort`**:這是 Cluster 外部用於訪問 Service 的端口,Cluster 中的每個節點都會監聽這個端口。如果不指定的話就是由 control plane 從下述範圍隨機分配一個。(如果 **`type`** 是 **`NodePort`**,default:30000-32767) + **`selector`**:這個選擇器決定了哪些 Pods 會被這個 Service 管理。在這個例子中,所有帶有標籤 "**`app: chest-ct-api`**" 的 Pods 都會被 Service 管理。 :notebook: **總結:** > 這個 Service 定義了一個名為 **`chest-ct-api-service`** 的 **`NodePort`** 類型的 Service,它在 Cluster 內部監聽端口 **`9977`** 並將流量轉發到標籤為 **`app: chest-ct-api`** 的 Pods 上的 **`9528`** 端口。同時,它在 Cluster 的每個節點上開放了 **`30001`** 端口,允許從 Cluster 外部訪問這個 Service。[color=grey] ## 執行 在這之前先建一個 Namespace **`chest-ct`**,也可以透過 yaml 創建。執行 **`kubectl apply -f .`** 可 **`apply`** 所在資料夾下的所有檔案。 ```bash kubectl create ns chest-ct # namespace/chest-ct created ``` ```bash kubectl apply -f . # deployment.apps/chest-ct-api-deployment created # service/chest-ct-api-service created # secret/s3-secret created ``` 因這些 objects 都是在 **`chest-ct`** 這個 Namespace 下,在 list objects 需要加上 **`-n`** 指定 Namespace。 ```bash kubectl get all,secrets -n chest-ct # NAME READY STATUS RESTARTS AGE # pod/chest-ct-api-deployment-59fc549d7d-42xrf 1/1 Running 0 107s # pod/chest-ct-api-deployment-59fc549d7d-7sj76 1/1 Running 0 107s # NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE # service/chest-ct-api-service NodePort 10.43.194.231 <none> 9977:30001/TCP 107s # NAME READY UP-TO-DATE AVAILABLE AGE # deployment.apps/chest-ct-api-deployment 2/2 2 2 107s # NAME DESIRED CURRENT READY AGE # replicaset.apps/chest-ct-api-deployment-59fc549d7d 2 2 2 107s # NAME TYPE DATA AGE # secret/s3-secret Opaque 4 4m11s ``` 可以發現有 Deployment 也有 ReplicaSet,會列出 Desired & Current & Ready Pod Number。而 ReplicaSet 及 Pod 名稱後面都有一串隨機字串。為什麼要有 Deployment 又有 ReplicaSet 呢?Deployment 有比 ReplicaSet 更多的功能,在進行版本更新時能夠使用不同的策略來維持服務。之後會更詳細比較不同的 Deployment 部署策略。 上面可以看到 Service 的部分,顯示了 9977 port 對應到 Node 30001 port。 ## 透過 Host 存取應用程式 使用 k3d 我要怎麼在本機用瀏覽器連上這個應用呢? 我們使用了 **`NodePort`** 方式讓 Pod 可以接收外部流量,但我們的 Node 是由 Container 啟動的,所以還要將 Host port 映射到 Node 30001 port上。 :::success :notebook: **參考資源:** [**Exposing Services**](https://k3d.io/v5.4.6/usage/exposing_services/) [**K3d cluster edit**](https://k3d.io/v5.4.6/usage/commands/k3d_cluster_edit/) ::: 必須幫這個 Cluster 開個接口,把 Host port 映射到 Node port。 ```bash k3d cluster edit mycluster --port-add "5566:30001@server:0" ``` 這邊還不曉得為什麼 k3d 指令只對 **`server:0`** 做 port mapping 卻還是能讓所有 Node 都接到流量:question:似乎是因為作用在 **`serverlb`**,而不是只有 **`server:0`**。 如果要進入 Pod 裡面,可以使用 **`kubectl exec -it`** 指令。 ```bash kubectl exec -it -n chest-ct <pod name> -- sh ``` **`--`** 後面接的是 commands。 另外這篇使用的 NodePort 其實比較不建議使用,因為 Port 數量固定,還要去記得是開哪個 Port。透過 Ingress 讓外部請求接到 Service 的方式是比較建議使用的。 :notebook: **總結:** > 這是一開始提到的 app 最終架構。 > 
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up