iT邦幫忙

2025 iThome 鐵人賽

DAY 21
1
DevOps

牧場主的 K8s 放牧日記系列 第 21

Day 21: Ingress Controller 流量路由實戰 - 牧場的智慧導航系統

  • 分享至 

  • xImage
  •  

牧場主今日工作

今天要來建立牧場的智慧導航系統!就像現代牧場需要清楚的指示牌告訴訪客「要看乳牛請往左,要看羊群請往右」,Ingress Controller 就是 Kubernetes 世界的智慧導航員。有了昨天的 MetalLB 提供外部 IP,今天要讓這些 IP 變得更聰明,能根據域名和路徑把流量精準送到對的地方!

https://ithelp.ithome.com.tw/upload/images/20250904/20141794gBwhgujpnS.png

技術背景與概念

從 LoadBalancer 到 Ingress 的進化

昨天我們用 MetalLB 解決了「如何讓外部訪問到集群」的問題,但還有更精細的需求:

LoadBalancer Service 的限制

  • 每個 Service 需要一個獨立的外部 IP
  • 無法根據 HTTP 路徑做路由
  • 不能處理虛擬主機(多域名共用一個 IP)
  • SSL 終止需要在應用層處理

Ingress 的價值
https://ithelp.ithome.com.tw/upload/images/20250904/20141794z6wFrMSjvR.png

Ingress Controller vs Ingress Resource

重要概念區分

  • Ingress Resource:Kubernetes 的標準資源,定義路由規則的「藍圖」
  • Ingress Controller:實際執行路由的「工人」,將藍圖變成真實的網路配置

nginx-ingress 的工作原理
nginx-ingress Controller 會監聽所有 Ingress Resource,然後將它們轉換成 nginx 配置檔案。

# 這是 Ingress Resource(規則定義)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: frontend-ingress
spec:
  rules:
  - http:
      paths:
      - path: /frontend(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: frontend-service
            port:
              number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
spec:
  rules:
  - http:
      paths:
      - path: /api(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: api-service
            port:
              number: 80

nginx-ingress Controller 會將上述 Ingress 轉換成類似這樣的 nginx.conf

# nginx-ingress 自動生成的配置(簡化版)
server {
    listen 80;
    listen [::]:80;
    
    # 來自 frontend-ingress 的規則
    location ~ ^/frontend(/|$)(.*) {
        # rewrite 規則
        rewrite ^/frontend(/|$)(.*) /$2 break;
        
        # 代理到後端 Service
        proxy_pass http://default-frontend-service-80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    
    # 來自 api-ingress 的規則  
    location ~ ^/api(/|$)(.*) {
        # rewrite 規則
        rewrite ^/api(/|$)(.*) /$2 break;
        
        # 代理到後端 Service
        proxy_pass http://default-api-service-80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

# upstream 定義(Service 的後端 Pod)
upstream default-frontend-service-80 {
    # 這些 IP 來自 Service 的 Endpoints
    server 10.42.1.10:80 max_fails=1 fail_timeout=1s;
    server 10.42.2.10:80 max_fails=1 fail_timeout=1s;
}

upstream default-api-service-80 {
    server 10.42.1.11:80 max_fails=1 fail_timeout=1s;
    server 10.42.2.11:80 max_fails=1 fail_timeout=1s;
}

整個流程

  1. 你建立 Ingress Resource(YAML 檔案)
  2. nginx-ingress Controller 監聽這些 Ingress Resource
  3. 自動轉換 成對應的 nginx location 和 upstream 配置
  4. 重新載入 nginx 配置,新的路由規則立即生效
  5. 動態更新:當 Pod IP 變化時,upstream 會自動更新

實際驗證方式

# RKE2 中的 nginx-ingress 是 DaemonSet,需要指定具體 Pod
kubectl get pods -n kube-system | grep ingress

# 進入任一 nginx-ingress Pod 查看實際生成的 nginx.conf
kubectl exec -it -n kube-system <ingress-nginx-controller-pod名稱> -- cat /etc/nginx/nginx.conf

# 查看 upstream 配置
kubectl exec -it -n kube-system <ingress-nginx-controller-pod名稱> -- nginx -T | grep -A 5 "upstream.*service"

常見的 Ingress Controller 選項

  • nginx-ingress:最成熟穩定,生成標準的 nginx 配置
  • Traefik:生成 Traefik 的路由配置
  • HAProxy Ingress:生成 HAProxy 的配置檔案
  • Istio Gateway:生成 Envoy Proxy 配置

RKE2 預設 Nginx Ingress Controller

RKE2 內建優勢

RKE2 預設就包含了 nginx-ingress controller,這為我們節省了大量的選擇和配置時間:

# 檢查 RKE2 預設的 Ingress Controller
kubectl get pods -n kube-system | grep nginx

# 檢查 Ingress Class
kubectl get ingressclass

# 檢查 Ingress Controller 的 Service
kubectl get svc -n kube-system | grep nginx

RKE2 nginx-ingress 特色

  • 零配置啟動:安裝 RKE2 就自動可用
  • 企業級穩定:經過 SUSE 團隊驗證的穩定版本
  • 與 RKE2 深度整合:無需擔心版本相容性問題
  • 預設安全配置:包含基本的安全最佳實踐

RKE2 的 nginx-ingress 部署方式

RKE2 預設使用 hostNetwork 模式部署 nginx-ingress:

# 檢查 RKE2 預設的 nginx-ingress 部署
kubectl get pods -n kube-system | grep ingress

# 檢查是否使用 hostNetwork
kubectl get pod -n kube-system <具體的pod名稱> -o yaml | grep hostNetwork

# 檢查 nginx-ingress 是否為 DaemonSet
kubectl get daemonset -n kube-system

RKE2 nginx-ingress 的特色

  • hostNetwork: true:直接使用主機網路
  • DaemonSet 部署:每個 worker 節點都運行一個 Pod
  • 綁定主機端口:直接佔用節點的 80/443 端口
  • 無需 LoadBalancer Service:外部直接訪問節點 IP

RKE2 nginx-ingress 實際架構
https://ithelp.ithome.com.tw/upload/images/20250904/20141794ZQmuIfXD1C.png

正確的職責說明

  1. Worker 節點:每個節點的 80/443 端口被 nginx-ingress 佔用
  2. nginx-ingress Pod
    • 使用 hostNetwork,直接監聽節點的 80/443 端口
    • DaemonSet 確保每個節點都有一個 Pod
    • 讀取 Ingress 資源進行 HTTP 路由
  3. Ingress Resource:定義路由規則,告訴所有 nginx Pod 如何轉發
  4. 外部訪問:直接訪問任何一個 worker 節點的 IP,無需 LoadBalancer

SSL 憑證管理策略

現有 SSL 架構

根據我們在 Day 11 的設定,目前的 SSL 架構是:

  • 外部 SSL 終止:HAProxy 在 bastion 節點處理 SSL (ithome-rancher.duckdns.org)
  • 內部集群通訊:Ingress Controller 處理 HTTP 流量
  • 憑證集中管理:只需要維護一份 Let's Encrypt 憑證

現有架構優勢
https://ithelp.ithome.com.tw/upload/images/20250904/20141794AlBZQhdjv6.png

HTTP-only Ingress 配置策略

由於我們已有外部 SSL 終止,集群內的 Ingress 主要處理 HTTP 路由:

# 基本 HTTP Ingress 配置(無需 SSL)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: basic-http-ingress
  namespace: default
  annotations:
    # 路徑重寫規則
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  ingressClassName: nginx
  rules:
  - host: app.ranch.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web-service
            port:
              number: 80

配置重點說明

  • 使用路徑重寫,將 /frontend/something 重寫為 /something
  • 主要使用 80 端口進行內部通訊
  • 針對此 Custom Cluster,不依賴特定域名或 SSL 配置

實戰範例:多應用路由配置

場景設定

SSL 憑證說明
可以參考之前的流程(Day 7-8),為新的服務註冊新的域名並申請 SSL 憑證,但在這個範例中我們不重複執行該流程。接下來的服務將:

  • 只在 PowerDNS 中註冊 DNS 記錄
  • 在 HAProxy 中加入這座 RKE2 cluster 的 worker 節點
  • 服務本身使用 HTTP(無 SSL 憑證)

測試場景:使用路徑型路由測試多個應用:

  • /frontend:前端應用
  • /api:API 服務
  • /admin:管理介面

部署測試應用

# 建立多個測試應用
cat > ingress-test-apps.yaml << 'EOF'
# Frontend Application
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-app
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
      volumes:
      - name: html
        configMap:
          name: frontend-html

---
apiVersion: v1
kind: Service
metadata:
  name: frontend-service
  namespace: default
spec:
  selector:
    app: frontend
  ports:
  - port: 80
    targetPort: 80

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: frontend-html
  namespace: default
data:
  index.html: |
    <!DOCTYPE html>
    <html>
    <head><title>Frontend App</title></head>
    <body>
        <h1>🎯 Frontend Application</h1>
        <p>This is the main frontend service</p>
        <p>Served from: frontend.ranch.local</p>
    </body>
    </html>

---
# API Application  
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-app
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
      volumes:
      - name: html
        configMap:
          name: api-html

---
apiVersion: v1
kind: Service  
metadata:
  name: api-service
  namespace: default
spec:
  selector:
    app: api
  ports:
  - port: 80
    targetPort: 80

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: api-html
  namespace: default
data:
  index.html: |
    <!DOCTYPE html>
    <html>
    <head><title>API Service</title></head>
    <body>
        <h1>🔧 API Service</h1>
        <p>This is the backend API service</p>
        <p>Served from: api.ranch.local</p>
    </body>
    </html>

---
# Admin Application
apiVersion: apps/v1
kind: Deployment
metadata:
  name: admin-app
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: admin
  template:
    metadata:
      labels:
        app: admin
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
      volumes:
      - name: html
        configMap:
          name: admin-html

---
apiVersion: v1
kind: Service
metadata:
  name: admin-service
  namespace: default
spec:
  selector:
    app: admin
  ports:
  - port: 80
    targetPort: 80

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: admin-html
  namespace: default
data:
  index.html: |
    <!DOCTYPE html>
    <html>
    <head><title>Admin Panel</title></head>
    <body>
        <h1>⚙️ Admin Panel</h1>
        <p>This is the administration interface</p>
        <p>Served from: admin.ranch.local</p>
    </body>
    </html>
EOF

# 部署測試應用
kubectl apply -f ingress-test-apps.yaml

# 確認部署狀態
kubectl get pods | grep -E "(frontend|api|admin)"
kubectl get svc

建立路徑型 Ingress 規則

對於這個 Custom Cluster,我們使用通用的路徑型路由:

# 建立路徑型 Ingress 規則
cat > path-based-ingress.yaml << 'EOF'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: path-based-ingress
  namespace: default
  annotations:
    # 路徑重寫規則
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  ingressClassName: nginx
  rules:
  # 不指定特定 host,接受任何 host header
  - http:
      paths:
      # /frontend/* 路由到 frontend service
      - path: /frontend(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: frontend-service
            port:
              number: 80
      # /api/* 路由到 api service  
      - path: /api(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: api-service
            port:
              number: 80
      # /admin/* 路由到 admin service
      - path: /admin(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: admin-service
            port:
              number: 80
EOF

# 套用 Ingress 規則
kubectl apply -f path-based-ingress.yaml

# 檢查 Ingress 狀態
kubectl get ingress
kubectl describe ingress path-based-ingress

路徑重寫說明

  • rewrite-target: /$2:將 /frontend/something 重寫為 /something
  • path: /frontend(/|$)(.*):使用正規表達式捕獲路徑
  • pathType: ImplementationSpecific:使用 nginx 特定的正規表達式功能

功能驗證測試

直接測試 hostNetwork Ingress

RKE2 的 nginx-ingress 使用 hostNetwork,直接測試節點 IP:

# 檢查 worker 節點 IP
kubectl get nodes -o wide

# 直接測試 worker 節點 IP(最簡單的方式)
curl http://192.168.0.238/frontend/
curl http://192.168.0.238/api/
curl http://192.168.0.238/admin/

# 測試時可以添加任何 Host header(因為 Ingress 不限制 host)
curl -H "Host: test.example.com" http://192.168.0.238/frontend/
curl -H "Host: anything.local" http://192.168.0.238/api/

# 檢查 nginx ingress 日誌
kubectl logs -n kube-system <具體的pod名稱> -f

整合到 HAProxy(推薦)

為了統一 SSL 管理,在 HAProxy 中添加路由到 worker 節點:

# SSH 到 bastion 節點
ssh calvin@192.168.0.135

# 編輯 HAProxy 配置
sudo vim /etc/haproxy/haproxy.cfg

# 在 frontend https_frontend 區塊中添加:
# 檢查是否為應用路徑
acl is_app_path path_beg /frontend /api /admin

# 在路由規則中添加(注意順序,放在 rancher 規則之前):
use_backend nginx_backend if is_app_path

# 添加新的後端配置:
backend nginx_backend
    balance roundrobin
    option httpchk GET /healthz
    http-check expect status 404  # nginx ingress 預設 404 是正常的
    server worker-1 192.168.0.238:80 check
    # 如果有多個 worker 節點,繼續添加

# 重新載入 HAProxy
sudo systemctl reload haproxy

測試完整的流量路徑

# 方式1:直接訪問 worker 節點(HTTP)
curl -H "Host: ithome-rancher.duckdns.org" http://192.168.0.238/frontend/

# 方式2:透過其他域名指向(如果有設定)
curl http://custom-app.local/frontend/

# 方式3:如果想要 HTTPS,需要額外的反向代理設定

# 檢查路由是否正確工作
curl -v http://192.168.0.238/frontend/ | head -10

# 檢查 nginx ingress 日誌
kubectl logs -n kube-system <具體的pod名稱> -f

訪問方式的特點

  • 直接 worker IP:最簡單直接,適合測試和開發
  • 自定義域名:如果需要域名,可以設定其他 DNS 記錄
  • 反向代理:如果需要 HTTPS,需要額外設定 nginx 或 HAProxy

適用場景建議

  • 開發測試:直接使用 worker IP,快速簡便
  • 內部服務:可以考慮設定內部 DNS 記錄
  • 生產環境:建議搭配外部反向代理提供 SSL 和負載均衡

多租戶環境考量

自助式 Ingress 管理

在多租戶環境中,管理員不可能為每個應用手動建立 Ingress 配置。建議的做法:

讓租戶自行管理

  • 提供 Ingress 建立權限給各 namespace 的管理者
  • 每個 namespace 的使用者自行建立和維護自己的 Ingress
  • 平台管理員只負責基礎設施和全域策略

基本指導原則

平台管理員職責

  • 維護 nginx-ingress Controller 正常運行
  • 設定全域的安全策略和資源限制
  • 提供基本的 Ingress 使用說明文件

租戶職責

  • 根據自己的應用需求建立 Ingress 規則
  • 管理自己的域名和路徑規劃
  • 負責應用層級的故障排除

監控與日誌

# 檢查 Ingress Controller 狀態
kubectl get pods -n kube-system | grep ingress

# 查看即時日誌
kubectl logs -n kube-system <具體的pod名稱> -f

# 檢查 Ingress 配置是否正確載入
kubectl exec -n kube-system <具體的pod名稱> -- nginx -T | grep -A 5 -B 5 "location"

# 檢查後端健康狀況
kubectl get endpoints

故障排除常見問題

問題 1:404 Not Found

# 檢查項目:
# 1. Ingress 規則是否正確建立
kubectl get ingress
kubectl describe ingress ranch-ingress

# 2. Service 和 Pod 是否正常運行
kubectl get svc
kubectl get pods

# 3. DNS 解析是否正確
nslookup frontend.ranch.local
# 或檢查 /etc/hosts

# 4. nginx 配置是否載入
kubectl logs -n kube-system <具體的pod名稱> | grep "路由相關關鍵字"

問題 2:502 Bad Gateway

# 檢查後端服務健康狀況
kubectl get endpoints
kubectl describe endpoints frontend-service

# 檢查 Pod 是否能正常回應
kubectl port-forward svc/frontend-service 8080:80
curl http://localhost:8080

# 檢查 Service selector 是否正確
kubectl get svc frontend-service -o yaml
kubectl get pods -o wide

問題 3:SSL 相關問題

# 如果使用 SSL Bridging
# 1. 檢查後端服務是否支援 HTTPS
# 2. 檢查憑證是否有效
# 3. 檢查 backend-protocol 註解是否正確

# 檢查 SSL 配置
kubectl describe ingress <ingress名稱> | grep -i tls

今日總結與明日預告

今天我們成功建立了 Ingress Controller 流量路由系統!從基本概念到實際配置,從 Ingress Resource 轉換成 nginx.conf 的原理,到多租戶環境的自助式管理策略,我們的牧場現在有了智慧導航系統。RKE2 預設的 nginx-ingress 使用 hostNetwork 模式,讓應用能透過路徑型路由優雅地提供服務。

明天我們要開始建立牧場的觀測系統,學習 Prometheus 監控平台的部署與設定,為我們的 Kubernetes 環境建立完整的指標收集機制!

💡 牧場主小提示:Ingress Controller 就像牧場的智慧導航系統,一個入口分流到不同目的地!記住核心概念:Ingress Resource 就是 nginx.conf 的 YAML 版本,nginx-ingress Controller 會自動轉換並重載配置。有了這個理解,除錯和配置都會變得直觀許多!


上一篇
Day 20: MetalLB 負載均衡實戰 - 牧場的智慧流量分配系統
下一篇
Day 22: Prometheus 監控平台部署 - 牧場的數據收集中心
系列文
牧場主的 K8s 放牧日記22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言