【筆記】在本機 Kubernetes 環境佈署容器並串接服務/Ingress 伺服器— — 使用 minikube 與單一 YAML 檔實作
前言
這篇偏向個人筆記性質,因為最近嘗試自己學點 Kubernetes (K8S) 基礎。筆者從一兩年前連 Docker 都不會用,到現在因工作需要而接觸到這類題材,就算身為技術文件寫作者不需真的會搞 DevOps,有實地操作經驗其實還是會有不小的幫助。
問題在於,網路上的文章跟教學多到數不完,但由於想研究怎麼完全用 manifest 來串起 K8S 容器與服務,還是花了些時間整理出想要的資訊。總歸來說雖是蠻基礎的東西,但不寫下來就容易忘掉,就還是記錄一下吧。
在 K8S 中,建立物件(一個 K8S 資源)有以下三種方式:
- Imperative commands(直接用 kubectl 增刪修改物件)
- Imperative object configuration(同上,但改用 YAML 宣告檔搭配 kubectl 來增刪修改物件)
- Declarative object configuration(直接提供 YAML 宣告檔給 K8S,由後者負責管理物件)
在網路上讀到關於 K8S 的諸多優點時,其實經常會提到它是 declarative(聲明式宣告),也就是你只需給一份 YAML 格式的 manifest — — 有點像貨物清單 — — 列出你需要哪種容器並要多少個,K8S 會按你的要求啟動並管理之。身為容器的維運系統,這種特色顯然格外重要:它能在容器掛掉時重啟之,或者在不中斷服務的前提下逐漸將容器抽換成新版。
但也許是 imperative 指令乍看比較易懂,所以書籍和網路文章經常拿它們當起點,對於第三點反而著墨不多。此外,你也不是建了容器就能了事 :若要使用容器,你需要替它們建立「服務」,然後是能讓使用者從 K8S 之外存取這些服務的伺服器。這些都可以用 declarative 的方式來做,但同樣地,相關資訊總是分散在各處,也較少人會統一討論它們跟佈署容器的關係。
本篇的基本目的,便是寫單一一個 YAML 宣告檔,可以很快的將上述的基本東西通通建起來。在本機操作 K8S 的練習跟測試意義大於實際用途,但起碼這樣能讓我們對 K8S 的運作有更貼切的體會。
概念篇
minikube
minikube 是個簡化版的 K8S,本身是個二進位執行檔,可以在本機建一個 single-node cluster。市面上有很多其他類似的工具,現在 Docker Desktop 本身也可以開一個 K8S 環境,但需要花比較多力氣安裝我們需要的服務,所以這裡先講 minikube 吧…(本文結尾有 Kind 和 Docker Desktop 的版本)。
minikube 需要借助一個容器引擎,筆者是用 Docker Desktop,啟動後你會看到它會開一個容器來代表 K8S cluster。
Deployment、Pods 與 Replica Sets
一個 deployment 是一個宣告式定義,用來管理你要佈署的 pods 和 replica sets。你雖然可以個別建立後兩者,但官方的建議是使用 deployment。
Cluster 是 K8S 的典型環境,裡面會包括 control plane(又稱 master node,是 K8S 的管控核心)和若干 worker nodes(但 minikube 只會有一個)。每個 node 代表一個實體或虛擬機器,一個 node 內有若干 pods,而 pods 本身又會包含若干容器(但在本篇每個 pod 就只有一個容器,所以有時我們會把這兩者當成同義詞)。
這裡我就不多談 Control Plane 或 node 內的 kubelet、kube-proxy 是在幹嘛了…
Replica sets 是由一群相同的 pod 組成的集合,這些 pods 合起來可以提供更大容量的服務,也可以避免一兩個容器掛掉時整個服務就掛掉的問題。Deployment 其實就是在宣告要佈署的 replica sets 應該長什麼樣子 — — 裡面的容器用什麼映像檔,要建幾個,更新時如何進行等等。
當你修改 deployment 中的映象檔版本並重新套用到 K8S,它就會自動把舊的 replica set 收掉並建立新的 replica set(新舊兩套怎麼交替就取決於 deployment strategies,可以是 recreate 或 rolling update)。而在容器運行期間,若 replica set 中存活的容器數量少於 deployment 的要求,那麼 K8S 就會建立並啟動新的 pod。所以儘管撰寫 deployment manifest 並不容易,它能讓我們以簡單得多的方式管理容器維運。
Service
每個 pod 在 K8S 中會分配到一個內部 IP。但既然 pod 隨時可能掛掉和開新的出來,你就不可能倚賴 pods 的 IP 來存取它們。
因此在 K8S 中,你得替 pods 建立一個「服務」來當成存取的橋樑。Deployment 中對於 pods 可以指定 label(標籤),而一個 service 可用所謂的 selector(選擇器)來指向這些標籤。Service 本身有固定的內部 IP,這樣就不用擔心容器變動後得重新串接了。
K8S 服務是一個 REST 物件,又分為以下四種:
- ClusterIP:預設類型,顧名思義是在一個 cluster 內部 IP 上啟用服務,並具備負載平衡的功能(把流量分攤給所有的 pods)。這個服務位於 K8S 內部,外人是看不見的,除非你把它連上一個反向代理伺服器。
- NodePort:這是在所有 node 上打開一個 port,好讓 K8S 外部的人可以透過此 port 存取 ClusterIP 服務(K8S 會自動建立 ClusterIP)。看起來好像挺方便,但連線者需要知道這個 port,而且因為有安全性隱憂,不適合直接用在正式佈署環境。
- LoadBalancer:用於自身有提供外部平衡負載服務的雲端平台,這個 load balancer 會有自己的獨立 IP。K8S 會自動建立底下需要的 NodePort 及 ClusterIP 服務。
- ExternalName:使用 K8S 以外的服務,用 DNS 網域名稱來指定,不會建立 ClusterIP 或 NodePort。
Ingress
字面上,ingress 之意是 the action or fact of going in or entering; the capacity or right of entrance (進入的動作或權利)。在 K8S 中,Ingress 是一個反向代理 API 物件,功能是將統一來自 cluster 之外的流量導到正確的服務,而且也具備負載平衡、TLS 等功能。因此,我們在本篇會使用 ClusterIP 服務,並用 Ingress 伺服器來 expose 它。
為了要能使用 Ingress,你必須先在 K8S 內安裝一個 Ingress Controller,也就是 Ingress 的實作類別。從官網可以看到能實作的類別種類很多,幸好 minikube 會自動替我們下載 ingress-nginx。在其他 K8S 環境中,你也可以用 manifest 的方式安裝之,但步驟就繁瑣得多。
宣告篇
我們的目的是寫一個 YAML 宣告檔,佈署幾個 pods(或者應該說一個 replica set)、一個 ClusterIP 服務以及一個 Ingress 伺服器。我們的 pod 使用的映象檔為 echo-server,它是一個會把使用者送出的 HTTP 請求以 JSON 格式丟回來的簡單伺服器。
你可以在同一個檔案撰寫多個資源的宣告,但下面我們先分開來看各個部分。
Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo-deployment
namespace: default
spec:
selector:
matchLabels:
app: echo
replicas: 3
strategy:
type: RollingUpdate
template:
metadata:
labels:
app: echo
spec:
containers:
- name: echo
image: ealen/echo-server:latest
imagePullPolicy: Always
env:
- name: PORT
value: "3000"
ports:
- containerPort: 3000
第一層的 spec 定義了 replica set 的內容,但首先來看它底下的 template,這定義了 replica set 建立的新 pod 應該長得什麼樣,包括其標籤(鍵:app,值:echo)和映像檔、初步的環境變數設置等。label 的寫法沒有特別規範,也可以有不只一個,用來增加配對條件的彈性。
在第一層的 spec 中,replicas 即 pod 的數量,strategy 是更新的方式(預設即 RollingUpdate)。但最重要的是 selector — — 底下的 matchLabels 必須對應到 template 中的 labels,否則會被 K8S 認為宣告無效。
筆者在此還做了一件事,就是用 env 環境變數把 echo-server 容器的 port 設為 3000(預設為 80)。這主要是展示你能如何使用環境變數,並在稍後展示 pods、service 和 Ingress 的 port 如何對應起來。
YAML 內可設置的欄位很多,上面有些像是 namespace 和 imagePullPolicy 已經是預設值,不過我覺得寫出來比較明瞭。
Service
apiVersion: v1
kind: Service
metadata:
name: echo-service
namespace: default
spec:
type: ClusterIP
selector:
app: echo
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 3000
這裡我們宣告一個服務,類型是預設的 ClusterIP,而 selector 則指向前面的 pods(app=echo)。同樣的,selector 可填入的標籤能不只一個,用來增加篩選條件。服務本身的 port 設為 8080,對應到的 pod port 則是 3000。
如果宣告成 NodePort 服務的話,我們可以用 minikube 去對外 expose 這個服務(指令:minikube service echo-service),但本篇就不這樣做了。
Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: echo-ingress
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /echo(/|$)(.*)
pathType: Prefix
backend:
service:
name: echo-service
port:
number: 8080
在 Ingress 的 spec 中,ingressClassName 是實作類別的名稱(ingress-nginx 就是 nginx),rules 則是反向代理的規則,將特定的路徑對應到特定的服務 — — path 是路徑,backend 則是其對應的服務。我們要它將 /echo(或是以這個路徑開頭的任何路徑)導向 echo-service 服務,而後者使用的 port 為 8080。
比較特別的是 metadata 底下的 annotations 是用來設定這個 Ingress 伺服器的一些行為,取決於實作類別而會有所不同。這裡用了 nginx.ingress.kubernetes.io/rewrite-target,就是搭配底下的 path 後面的正規劃字元設定 URL rewriting 規則:
- \echo 會被轉成 \ 傳給服務
- \echo\ 會被轉成 \
- \echo\path1 會被轉成 \path1
- \echo\path1\sub1 會被轉成 \path1\sub1
這麼一來,如果容器在跑的是你自己的應用程式,這樣可以確保它收到的網址不會多一個開頭、導致像是前端 JS 框架或後端程式的路由規則無法運作。
當然 nginx 的網址設定就是另一個學問了。/$2 的意思是去抓正規化表示法 (/|$)(.*) 的第二個 captured group,也就是 (.*) 。/|$ 表示反斜線或字串結尾,而 .* 表示任意數量的任何非換行字元,在此就是第二層與之後的路徑名稱(如果有的話)。
完整的 echo.yaml
下面是把上面三個宣告合成同一個檔案,叫做 echo.yaml,我們之後只要用它就可以同時佈署這些東西:
#
# Deployment
#
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo-deployment
namespace: default
spec:
selector:
matchLabels:
app: echo
replicas: 3
strategy:
type: RollingUpdate
template:
metadata:
labels:
app: echo
spec:
containers:
- name: echo
image: ealen/echo-server:latest
imagePullPolicy: Always
env:
- name: PORT
value: "3000"
ports:
- containerPort: 3000
---
#
# Service
#
apiVersion: v1
kind: Service
metadata:
name: echo-service
namespace: default
spec:
type: ClusterIP
selector:
app: echo
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 3000
---
#
# Ingress
#
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: echo-ingress
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /echo(/|$)(.*)
pathType: Prefix
backend:
service:
name: echo-service
port:
number: 8080
等我們佈署完成時,整個 K8S 的資源關係大概會像這樣:
實作篇
下載與啟動 minikube
首先你需要裝一個容器引擎,筆者是用 Docker Desktop,minikube 建立的 K8S cluster 會以 Docker 容器的形式執行。其實現在 Docker Desktop 本身可以多開一個 K8S cluster,看起來其核心也是 minikube,可是要花更多力氣設定,真是吃力不討好(笑)。
minikube 是個獨立的二進位執行檔,可以從官網下載:
以 Windows 為例,下載的安裝檔會把 minikube.exe 放在 C:\Program Files\Kubernetes\Minikube。把這個路徑加入 PATH 環境變數,方便之後使用。
接著打開一個終端機來啟動 minikube 環境:
minikube start
或者如下來取得最新版 K8S:
minikube start --kubernetes-version=latest
以筆者的機器為例,會出現以下訊息:
😄 minikube v1.26.0 on Microsoft Windows 11 Home 10.0.22000 Build 22000
✨ Automatically selected the docker driver. Other choices: virtualbox, ssh
📌 Using Docker Desktop driver with root privileges
👍 Starting control plane node minikube in cluster minikube
🚜 Pulling base image ...
🔥 Creating docker container (CPUs=2, Memory=3883MB) ...
🐳 Preparing Kubernetes v1.24.1 on Docker 20.10.17 ...
▪ Generating certificates and keys ...
▪ Booting up control plane ...
▪ Configuring RBAC rules ...
🔎 Verifying Kubernetes components...
▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🌟 Enabled addons: storage-provisioner, default-storageclass
🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
第一次執行時會花點時間下載 K8S。還有如果你之前執行過不同的本地端 K8S,記得把 $HOME/.kube 砍掉。
你能如下檢視 K8S 中已經存在的系統 pod:
kubectl get pods --all-namespaces
切回 Docker Desktop,可見 minikube 的 K8S 環境已經啟動:
啟用 Kubernetes Dashboard
Kubernetes Dashboard 是一個圖形化的 K8S 儀表板兼管理介面,對初學者來說幫助不小。你能在終端機中如下啟動 dashboard:
minikube dashboard
第一次啟動時,minikube 會啟用它並抓取其映像檔,所以要稍等一下:
🔌 Enabling dashboard ...
▪ Using image kubernetesui/dashboard:v2.6.0
▪ Using image kubernetesui/metrics-scraper:v1.0.8
🤔 Verifying dashboard health ...
🚀 Launching proxy ...
🤔 Verifying proxy health ...
🎉 Opening http://127.0.0.1:61936/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ in your default browser...
如果網路有點慢的話,映像檔可能會抓取失敗 — — 這時只能試著用 minikube delete 砍掉 cluster 然後重建一次了。
成功啟動後,dashboard 會在瀏覽器自動打開:
只是現在我們還沒佈署任何 pod,所以儀表板裡面空空如也。
啟用 Ingress
接著我們要啟用第二個東西,就是 Ingress:
minikube addons enable ingress
minikube 會下載實作類別 ingress-nginx 並啟用 Ingress:
💡 After the addon is enabled, please run "minikube tunnel" and your ingress resources would be available at "127.0.0.1"
▪ Using image k8s.gcr.io/ingress-nginx/controller:v1.2.1
▪ Using image k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1
▪ Using image k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1
🔎 Verifying ingress addon...
🌟 The 'ingress' addon is enabled
佈署 Deployment、Service 與 Ingress
打開新的終端機,切到 echo.yaml 的所在目錄,並執行以下指令:
kubectl apply -f echo.yaml
K8S 會顯示相關資源被建立起來的訊息:
deployment.apps/echo-deployment created
service/echo-service created
ingress.networking.k8s.io/echo-ingress created
這樣就成了。與其像網路教學一樣使用繁複的命令列指令來查看資源,我們直接到 dashboard 就能看到它們:
「工作負載」顯示了 deployment、pods 以及 replica sets。你可以嘗試改變 deployment 中 pod 的映像檔版本、或者 replica 的數量,重新 apply 後 K8S 都會自行調整。
比如,要是我們修改 echo-server 的映像檔
image: ealen/echo-server:0.5.0
並變更 replicas 數量
replicas: 5
重新 apply 後,K8S 會開始下載新映像檔,關閉舊的pods,並逐次啟動 0.5.0 版 pods,直到有 5 個在運行為止:
至於 service,可點左側選單的 Service/Services 查看:
可見 echo-service 正確對應到 K8S 啟動的三個 pods,每個 pod 都有自己的內部 IP,port 為 3000。
最後來查看 Ingress:
這裡可見 Ingress 底下的類別是 nginx,而其對外端點為 192.168.49.2。
存取 Pods
在正常的電腦上,Ingress 應該會取得一個外部 IP。然而本文的例子是用 Docker,而這個 K8S cluster 仍然位於一個容器內,所以 192.168.49.2 並不是真的對外開放, Ingress 服務對於筆者的機器而言仍然是隔絕的 。
這時我們可以用 minikube 來開一個「通道」,將 minikube 環境的 cluster IP 連到本機的 localhost:
minikube tunnel --cleanup
--cleanup 參數是將之前開啟過但未正確關閉的通道給關起來。此外,minikube 內的任何 LoadBalancer 服務也會透過通道得到外部 IP。
通道啟動後出現訊息如下:
✅ Tunnel successfully started📌 NOTE: Please do not close this terminal as this process must stay alive for the tunnel to be accessible ...❗ Access to ports below 1024 may fail on Windows with OpenSSH clients older than v8.1. For more information, see: https://minikube.sigs.k8s.io/docs/handbook/accessing/#access-to-ports-1024-on-windows-requires-root-permission
🏃 Starting tunnel for service echo-ingress.
可以看到最後一行指出它替 echo-ingress 服務建立了通道。
現在,我們就可以直接在瀏覽器以 localhost/echo 或 127.0.0.1/echo 存取我們佈署的 pods:
訊息中的 originalUrl (\) 顯示了 URL rewriting 的結果,你可以試用幾種不同的網址來看結果。你也可以重複請求幾次,每次顯示的容器 IP 可能都會不同。
恭喜!我們已經成功建置了一個最基本的 K8S 環境,並在上面運行一個容器。現在除非電腦掛掉或被偷,不然 K8S 應該會繼續守護你的容器直到永遠。
停止 deployment、service 與 minikube
當然啦,K8S 會吃記憶體,放 pods 進去更是如此,所以平常沒事還是把它關起來吧 XD
若有開 dashboard 的話,你可以很輕易的直接在儀表板上刪除資源。至於用命令列刪除資源的方式在網路上不難找,但最快的方式或許是直接用 manifest 來刪除你建立的同一批東西:
kubectl delete -f echo.yaml
接著停止 minikube 環境:
minikube stop
刪除 minikube cluster(移除 Docker 內的容器)及裡面的資源:
minikube delete --all
補充
在其他本機環境操作本文範例的方式:
後來我試了另外兩種環境,Kind(需要 Docker)和 Docker Desktop。
Kind 最簡單的方式是下載一個可執行檔,並如下啟動或刪除 K8S cluster:
kind create cluster
kind delete cluster
啟動 cluster 後,Docker 內應該同樣可見一個代表 K8S 環境的容器。
Docker Desktop 則是在選單有一個啟用 K8S 的選項:
啟動後就有一個常駐的 K8S 環境了,需要的時候 reset 即可。
在這些環境內的安裝方式就完全相同。首先你得在 K8S 安裝 Nginx Ingress Controller 資源:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.3.0/deploy/static/provider/cloud/deploy.yaml
稍等片刻等它生效,然後同樣佈署我們自己的 deployment、service 及 Ingress:
kubectl apply -f echo.yaml
那麼,現在沒有 minikube tunnel 這種功能,我們要怎麼存取本機環境的 Ingress 呢?事實是 Ingress Controller 一般都會自帶一個 LoadBalancer 服務。若我們檢視命名空間 ingress-nginx 就能看到:
kubectl get svc --namespace=ingress-nginx
這個服務能連接到 Ingress,因此我們只要用 port-forward 來將該服務的 port 8080 對應到本機 localhost 的 port 80 即可:
kubectl port-forward --namespace=ingress-nginx service/ingress-nginx-controller 8080:80
終端機內會顯示 port-forwarding 的狀況:
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
這麼一來,我們便可以透過 ingress-nginx-controller 連到我們自己架的 echo-ingress、以及背後連接的 echo-service / echo。
此外,使用 Docker Desktop 自身的 K8S 環境比較特別一點,因為 service 和 pod 等等都會以個別容器的形式出現,算是讓我們能在不用 dashboard 的狀況下大概看到 K8S 裡面建了什麼東西。