---
# System prepended metadata

title: 利用 Deployment & Service 部署服務
tags: [K3D]

---

# 利用 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]![image](https://hackmd.io/_uploads/BkJqTVYv6.png)

## 執行
在這之前先建一個 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 最終架構。
> ![image](https://hackmd.io/_uploads/S1ncmv9v6.png)
