昨天我們研究了 Admission Controllers,補齊了 Kubernetes API 安全鏈條的最後一塊拼圖。回顧這幾天的學習,從 Kube Config
的身份驗證、RBAC
的操作授權,到 Admission Controller
的最終驗證,雖然不敢說精通,但至少比剛看到這些名詞時要清楚不少,在實務上如果遇到問題,也大概知道是哪個環節出了狀況。
接下來要來看看 Ingress。可以設想一下,當我們的應用程式 Pod 已經安全地在 Cluster 內部運行後,該如何將服務 暴露 (expose) 給外部的使用者呢?雖然我們之前用過 NodePort
,但不可能要使用者在網址後面加上 30000~32767 這種又長又難記的 Port。要如何用更標準、更靈活的方式來管理來自外部的 HTTP/S 流量,這就是今天要研究的主題 Ingress。
我們在 Service 看到的是 NodePort
和 LoadBalancer
這兩種 Service Type。它們很直觀,但在實務上都有各自的「痛點」。
NodePort
時,Kubernetes 會在每個 Node 上開啟一個 30000 以上的 Port。如果叢集裡每個服務都對應一個 NodePort,那管理起來會非常混亂且難記。實務上,我們通常得在前面再架設一層反向代理(如 Nginx、HAProxy 等)來將標準的 80/443 port 轉發到節點的 NodePort 上,這無疑增加了架構的複雜性。LoadBalancer
類型雖然方便,它會自動向雲服務(如 GCP, AWS)申請一個外部負載均衡器,直接對應到我們的服務。但問題在於,每建立一個 LoadBalancer
類型的 Service,通常就會產生一個新的、需要付費的外部 IP 和 Load Balancer。如果 Cluster 裡有十幾個服務需要對外暴露,這不僅管理上變得混亂,成本也會直線上升。這就是 Ingress 登場的時刻。透過 Ingress,我們只需要 一個對外 IP,就能根據使用者訪問的 URL 路徑或主機 Host 名稱,智慧地將流量分發到後端不同的 Service,同時還能集中處理 SSL/TLS 設定。而且這些複雜的路由規則,都能透過標準的 YAML 定義檔來描述與維護。
在理解 Ingress 時,我發現最關鍵的一點是要區分兩個概念:
kind: Ingress
),我們在裡面定義路由規則,例如「當有請求訪問 my-store.com/video
時,請將流量轉發到 video-service
」。簡單來說,Ingress Controller
讀取 Ingress Resource
的內容,然後將其轉換成對應的代理設定,並開始工作。如果沒有部署 Controller,光建立 Ingress Resource 是沒有任何作用的。
在 Ingress 實戰之前,要先搞懂它最主要的兩種路由模式:
web.example.com
的流量導向 web-service
,而 api.example.com
的流量導向 api-service
,即使它們背後都共用同一個 Ingress Controller 的 IP。sean.example.com/web
的流量導向 web-service
,而 sean.example.com/api
的流量導向 api-service
。由於 Kubernetes 預設沒有 Ingress Controller,因此要先安裝。我這裡使用 Helm 安裝 ingress-nginx
:
安裝 Helm:
# 下載安裝腳本
curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get-helm-3 > get_helm.sh
# 授予執行權限
chmod +x get_helm.sh
# 執行
./get_helm.sh
把 ingress-nginx
加入 Helm Repo
# 將 ingress-nginx 加入本地 Repo
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
# 確保 Helm Repo 中都是最新狀態
helm repo update
# 查看 Helm Repo
helm repo list
建立 Ingress 專屬的 Namespace:
kubectl create ns ingress-nginx
安裝 Ingress Nginx:
helm -n ingress-nginx install ingress-nginx ingress-nginx/ingress-nginx --version 4.9.0
查看 Pod 與 Service:
查看 Ingress Controller 版本:
POD_NAMESPACE=ingress-nginx
POD_NAME=$(kubectl get pods -n $POD_NAMESPACE -l app.kubernetes.io/name=ingress-nginx --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}')
kubectl exec -it $POD_NAME -n $POD_NAMESPACE -- /nginx-ingress-controller --version
那為了方便後續操作,把預設的 Namespace 切換到 ingress-nginx
:
kubectl config set-context kind-kind --namespace=ingress-nginx
建立兩個 Deployment:
kubectl create deployment web --image=nginx
kubectl create deployment api --image=nginx
建立對應 Deployment 的 Service:
kubectl expose deployment web --port=80
kubectl expose deployment api --port=80
接下來要來建立 Ingress Rule。在寫 YAML 前,先來理解一下 rules
裡的核心欄位:
host
:定義此規則對應的主機名稱。http.paths
:一個列表,定義了不同的路徑規則。path
:URL 的路徑,例如 /
或 /api
。pathType
:路徑的匹配類型,Prefix
(前綴匹配) 和 Exact
(精確匹配) 是最常用的。backend.service
:指定流量要被轉發到的後端 Service 名稱與 Port。現在要來建立一個 Host-Based Routing 的規則,將 web.example.com
和 api.example.com
分別導向 web
和 api
服務。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-web
annotations:
# use the shared ingress-nginx
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: web.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-api
annotations:
# use the shared ingress-nginx
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80
建立 Ingress 後,為了讓本機電腦能正確解析這些域名,我們需要修改 hosts
檔案來模擬 DNS。這裡的 web.example.com
就是一個完全限定域名 (FQDN, Fully Qualified Domain Name),它代表了主機在 DNS 樹狀結構中的絕對路徑。
不知道 Node IP 的話可以先查看 IP,隨便拿 Master 或 Worker Node 都可以:
kubectl get nodes -o wide
172.18.0.3 web.example.com
172.18.0.3 api.example.com
172.18.0.3 sean.example.com
172.18.0.3 sub.example.com
接著,為了方便辨識,我們分別進入 web
和 api
的 Pod,修改它們的歡迎頁面。
kubectl exec -it web-68bdbdcb94-xs7vw -- bash -c "echo web >> /usr/share/nginx/html/index.html"
kubectl exec -it api-678f78d9b8-mlhk7 -- bash -c "echo api >> /usr/share/nginx/html/index.html"
最後來驗證結果!先找出 Ingress Controller 的 NodePort:
NODE_PORT=$(kubectl get svc ingress-nginx-controller -o jsonpath='{.spec.ports[0].nodePort}')
可以直接透過 curl
加上 -H
指定 Host 來測試:
curl -H "Host: web.example.com" http://172.18.0.2:31717/
curl -H "Host: api.example.com" http://172.18.0.2:31717/
可以看到隨著我們 Host 指定的域名不同,根據 Rules,Ingress 會導至對應的 Service:
利用我們前面設定好的 FQDN:
curl http://web.example.com:31717/
curl http://api.example.com:31717/
接下來,我們在同一個 Host (sean.example.com
) 下,根據不同的 URL 路徑將流量路由到不同 Service。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-multipath
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: sean.example.com
http:
paths:
- path: /web
pathType: Prefix
backend:
service:
name: web
port:
number: 80
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 80
查看一下剛剛所建立的 ingress:
kubectl describe ingress ingress-multipath
接下來一樣來驗證結果,因為我有設定 FQDN,因此我就不用指定 Host 的方式,直接使用我們指定在 FQDN 的域名:
curl http://sean.example.com:31717/web
curl http://sean.example.com:31717/api
今天實際操作 Ingress 後,搞懂了如何透過 Host-Based 和 Path-Based 路由,解決 NodePort
的痛點,成為 Cluster 流量的統一入口。
雖然在 kind
這種學習環境中直接存取 NodePort
很方便,但在生產環境中,標準架構是在 Cluster 前端配置一個 外部負載均衡器 (External Load Balancer)。由 LB 接收 80/443 Port 的流量,再均勻分發到後端所有 Worker Node 上的 Ingress Controller NodePort,以此實現單一入口與高可用性。
如果是在 本地 On-Premise 環境,則常會以一組 反向代理伺服器(如 Nginx)來扮演相同角色,確保服務既集中管理、又能保持冗餘,目的與 LB 是一樣的。
我們已經搞懂了服務之間的流量分發,但回到最小部署單位 — Pod 本身。一個 Pod 裡不只能運行一個容器。如果主應用需要輔助程式來處理日誌或代理連線,該怎麼辦?這就帶出了我們明天要研究的主題:Multi-Container Pod 設計模式,包含在主容器運行前執行任務的 Init Container,以及與主容器並行運作的 Sidecar 等模式。