【Kubernetes】K8s筆記(十一):Ingress 叢集進出流量總管

2022-10-22 12:00:34

0. Ingress 解決了什麼問題

上一篇筆記中講解了 Service 的功能和執行機制。Service 本質上是一個由 kube-proxy 控制的四層負載均衡,在 TCP/IP 協定棧上轉發流量。然而四層負載均衡能做的很有限,現在絕大多應用執行在應用層(五層/ OSI 七層)的 HTTP/HTTPS 協定之上,有更多的高階路由條件,而這些在傳輸層是不可見的。

Service 比較適合代理叢集內部的服務。如果想要把服務暴露到叢集外部,就只能使用 NodePort 或者 LoadBalancer 這兩種方式,而它們都缺乏足夠的靈活性,難以管控。

Kubernetes 為了解決這個問題,引入了一個新的 API 物件做七層負載均衡。除了七層負載均衡,這個物件還承擔了更多的職責——作為流量的總入口,管理進出叢集的資料(南北向流量),讓外部使用者能夠安全便捷地存取叢集內部的服務。這個 API 物件被命名為 Ingress,意思就是叢集內外邊界上的入口。

*圖示是一個將所有流量都傳送到同一 Service 的簡單 Ingress 範例 圖片來源

1. Ingress Controller

Service 本身是沒有服務能力的,它只是一些 iptables 規則,真正設定、應用這些規則的實際上是節點裡的 kube-proxy 元件。如果沒有 kube-proxy,Service 定義得再完善也沒有用。

同樣的,Ingress 也只是一些 HTTP 路由規則的集合,相當於一份靜態的描述檔案,真正要把這些規則在叢集裡實施執行,還需要有另外一個東西,這就是 Ingress Controller,它的作用就相當於 Service 的 kube-proxy,能夠讀取、應用 Ingress 規則,處理、排程流量。

為了讓 Ingress 資源工作,叢集必須有一個正在執行的 Ingress 控制器。Ingress 控制器不是隨叢集自動啟動的,使用者可以選擇最適合叢集的 Ingress 控制器實現。Kubernetes 目前支援和維護 AWS、 GCE 和 Nginx Ingress 控制器。還有很多控制器可供選擇。

從 Ingress Controller 的描述上我們也可以看到,HTTP 層面的流量管理、安全控制等功能其實就是經典的反向代理,而 Nginx 則是其中穩定性最好、效能最高的產品,所以它也理所當然成為了 Kubernetes 裡應用得最廣泛的 Ingress Controller。不過,因為 Nginx 是開源的,誰都可以基於原始碼做二次開發,所以它又有很多的變種。這裡我們選取 Nginx 對 Ingress Controller 的開發實現 NGINX Ingress Controller

*圖示展示了 Ingress Controller 在叢集中的位置 圖片來源

2. 指定 Ingress Class 使用多個 Ingress Controller

IngressClass Docs

起初,Kubernetes 叢集內只有一個 Ingress Controller,這樣的用法會帶來一些問題:

  • 由於某些原因,專案組需要引入不同的 Ingress Controller,但 Kubernetes 不允許這樣做

  • Ingress 規則太多,都交給一個 Ingress Controller 處理會讓它不堪重負

  • 多個 Ingress 物件沒有很好的邏輯分組方式,管理和維護成本很高

  • 叢集裡有不同的租戶,他們對 Ingress 的需求差異很大甚至有衝突,無法部署在同一個 Ingress Controller 上

Kubernetes 就又提出了一個 Ingress Class 的概念,讓它插在 Ingress 和 Ingress Controller 中間,作為流量規則和控制器的協調人,解除了 Ingress 和 Ingress Controller 的強繫結關係。

Kubernetes 使用者可以轉向管理 Ingress Class,用它來定義不同的業務邏輯分組,簡化 Ingress 規則的複雜度。比如說,我們可以用 Class A 處理部落格流量、Class B 處理短視訊流量、Class C 處理購物流量。這些 Ingress 和 Ingress Controller 彼此獨立,不會發生衝突。

3. 使用 YAML 描述 Ingress / Ingress Class

首先用命令 kubectl api-resources 檢視它們的基本資訊:

NAME                              SHORTNAMES   APIVERSION                             NAMESPACED   KIND
ingressclasses                                 networking.k8s.io/v1                   false        IngressClass
ingresses                         ing          networking.k8s.io/v1                   true         Ingress

*Ingress Controller 是一個處理流量的應用程式,稍後可以使用 Deployment 和 DaemonSet 來部署

3.1 Ingress

Ingress 可以使用 kubectl create 來建立樣板檔案,它需要用兩個附加引數:

  • --class - 指定 Ingress 從屬的 Ingress Class 物件

  • --rule - 指定路由規則,基本形式是 URI=Service,也就是說是存取 HTTP 路徑就轉發到對應的 Service 物件,再由 Service 物件轉發給後端的 Pod

$ export out="--dry-run=client -o yaml"
$ kubectl create ing ngx-ing --rule="ngx.test/=ngx-svc:80" --class=ngx-ink $out
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ngx-ing

spec:
  ingressClassName: ngx-ink

  rules:
  - host: ngx.test
    http:
      paths:
      - backend:
          service:
            name: ngx-svc
            port:
              number: 80
        path: /
        pathType: Exact

這份 YAML 檔案中有兩個關鍵欄位 ingressClassNamerulesrules 的格式稍顯複雜:它將路由規則拆散為 hosthttp path,在 path 裡又指定了路徑的匹配方式,可以是精確匹配 Exact 或者是字首匹配 Prefix,再用 backend 來指定轉發的目標 Service 物件。

3.2 Ingress Class

Ingress Class 本身並沒有什麼實際的功能,只是起到聯絡 Ingress 和 Ingress Controller 的作用,所以它的定義非常簡單,在 spec 裡只有一個必需的欄位 controller ,表示要使用哪個 Ingress Controller,具體的名字就要看實現檔案了。

比如,要使用 Nginx 開發的 Ingress Controller,那麼就要用名字 nginx.org/ingress-controller

apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  name: ngx-ink

spec:
  controller: nginx.org/ingress-controller

4. 使用 Ingress / Ingress Class

使用 kubectl apply 建立 Ingress 和 Ingress Class 這兩個物件:

$ kubectl apply -f ngx-ing-class.yaml 
ingressclass.networking.k8s.io/ngx-ink created
$ kubectl apply -f ngx-ing.yaml 
ingress.networking.k8s.io/ngx-ing created

然後我們檢視 Ingress 和 Ingress Class 的狀態:

$ kubectl get ingress -o wide 
NAME      CLASS     HOSTS      ADDRESS   PORTS   AGE
ngx-ing   ngx-ink   ngx.test             80      67s
$ kubectl get ingressclass -o wide 
NAME      CONTROLLER                     PARAMETERS   AGE
ngx-ink   nginx.org/ingress-controller   <none>       84s

可以使用 kubectl describe 檢視詳細的資訊:

$ kubectl describe ing ngx-ing 
Name:             ngx-ing
Labels:           <none>
Namespace:        default
Address:          
Ingress Class:    ngx-ink
Default backend:  <default>
Rules:
  Host        Path  Backends
  ----        ----  --------
  ngx.test    
              /   ngx-svc:80 (10.10.1.38:80,10.10.1.40:80,10.10.1.41:80)
Annotations:  <none>
Events:       <none>

可以看到 Ingress 物件的路由規則 Host/Path 就是在 YAML 裡設定的域名 ngx.test/

5. 使用 Ingress Controller

準備好了 Ingress 和 Ingress Class,接下來就需要部署真正處理路由規則的 Ingress Controller。

Nginx Ingress Controller 以 Pod 的形式執行在 Kubernetes 裡,同時支援 Deployment 和 DaemonSet 兩種部署方式。我們現在根據 Nginx Ingress Controller Installation Docs 部署 Nginx Ingress Controller。

在使用 kubectl 的主機上首先克隆倉庫並進入部署資料夾:

$ git clone https://github.com/nginxinc/kubernetes-ingress.git --branch v2.4.1
$ cd kubernetes-ingress/deployments

Nginx Ingress Controller 的安裝略微麻煩一些,有很多個 YAML 需要執行,但如果只是做簡單的試驗,就只需要用到 4 個 YAML:

$ kubectl apply -f common/ns-and-sa.yaml
namespace/nginx-ingress created
serviceaccount/nginx-ingress created

$ kubectl apply -f rbac/rbac.yaml
clusterrole.rbac.authorization.k8s.io/nginx-ingress created
clusterrolebinding.rbac.authorization.k8s.io/nginx-ingress created

$ kubectl apply -f common/nginx-config.yaml
configmap/nginx-config created

$ kubectl apply -f common/default-server-secret.yaml
secret/default-server-secret created

前兩條命令為 Ingress Controller 建立了一個獨立的名稱空間 nginx-ingress,還有相應的賬號和許可權,這是為了存取 apiserver 獲取 Service、Endpoint 資訊用的;後兩條則是建立了一個 ConfigMap 和 Secret,用來設定 HTTP/HTTPS 服務。

接下來我們還需要部署一些 Custom Resources,沒有它們我們部署的 Ingress Controller 就無法執行:

預設情況下,需要為虛擬伺服器、虛擬伺服器路由、傳輸伺服器和策略建立自定義資源的定義。否則,Ingress Controller Pod 將不會變為 Ready 狀態。如果要禁用該要求,請將 -enable-custom-resources 命令列引數設定為 Readyfalse 並跳過此部分。

$ kubectl apply -f common/crds/k8s.nginx.org_policies.yaml 
customresourcedefinition.apiextensions.k8s.io/policies.k8s.nginx.org created

$ kubectl apply -f common/crds/k8s.nginx.org_transportservers.yaml 
customresourcedefinition.apiextensions.k8s.io/transportservers.k8s.nginx.org created
     
$ kubectl apply -f common/crds/k8s.nginx.org_virtualserverroutes.yaml 
customresourcedefinition.apiextensions.k8s.io/virtualserverroutes.k8s.nginx.org created

$ kubectl apply -f common/crds/k8s.nginx.org_virtualservers.yaml 
customresourcedefinition.apiextensions.k8s.io/virtualservers.k8s.nginx.org created

部署 Ingress Controller 不需要我們自己從頭編寫 Deployment,Nginx 已經為我們提供了範例 YAML (位置是:kubernetes-ingress/deployments/deployment/nginx-ingress.yaml),現在我們對其進行一些小小的改動:

  • metadata 裡的 name 要改成自己的名字,比如 ngx-ing-dep

  • spec.selectortemplate.metadata.labels 也要修改成自己的名字,比如還是用 ngx-ing-dep

  • containers.image 可以改用 apline 版本,加快下載速度,比如 nginx/nginx-ingress:2.2-alpine

  • 最下面的 args 要加上 -ingress-class=ngx-ink,也就是前面建立的 Ingress Class 的名字,這是讓 Ingress Controller 管理 Ingress 的關鍵

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ngx-ing-dep
  namespace: nginx-ingress
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ngx-ing-dep

  template:
    metadata:
      labels:
        app: ngx-ing-dep

    spec:
      serviceAccountName: nginx-ingress
      automountServiceAccountToken: true
      containers:
      - image: nginx/nginx-ingress:2.2-alpine
        imagePullPolicy: IfNotPresent
        name: nginx-ingress
        ports:
        - name: http
          containerPort: 80
        - name: https
          containerPort: 443
        - name: readiness-port
          containerPort: 8081
        - name: prometheus
          containerPort: 9113
        readinessProbe:
          httpGet:
            path: /nginx-ready
            port: readiness-port
          periodSeconds: 1
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"

        securityContext:
          allowPrivilegeEscalation: true
          runAsUser: 101 #nginx
          runAsNonRoot: true
          capabilities:
            drop:
            - ALL
            add:
            - NET_BIND_SERVICE
        env:
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        args:
          - -nginx-configmaps=$(POD_NAMESPACE)/nginx-config
          - -default-server-tls-secret=$(POD_NAMESPACE)/default-server-secret
          - -ingress-class=ngx-ink

有了 Ingress Controller,這些 API 物件的關聯就更復雜了。然後我們建立物件:

$ kubectl apply -f ngx-ing-dep.yaml 
deployment.apps/ngx-ing-dep created

注意 Ingress Controller 位於名稱空間 nginx-ingress,所以檢視狀態需要用 -n 引數顯式指定,否則我們只能看到 default 名稱空間裡的 Pod:

$ kubectl get deploy -n nginx-ingress
NAME          READY   UP-TO-DATE   AVAILABLE   AGE
ngx-ing-dep   1/1     1            1           10m

$ kubectl get pod -n nginx-ingress
NAME                           READY   STATUS    RESTARTS   AGE
ngx-ing-dep-7c48c74865-vzmnf   1/1     Running   0          11m

現在 Ingress Controller 就算是執行起來了。還有最後一道工序,因為 Ingress Controller 本身也是一個 Pod,想要向外提供服務還是要依賴於 Service 物件。所以至少還要再為它定義一個 Service,使用 NodePort 或者 LoadBalancer 暴露埠,才能真正把叢集的內外流量打通。

這裡還有個取巧的辦法,使用 kubectl port-forward 直接把原生的埠對映到 Kubernetes 叢集的某個 Pod 裡,在測試驗證的時候非常方便。

$ kubectl port-forward -n nginx-ingress ngx-ing-dep-7c48c74865-vzmnf 8080:80 &

可以修改 /etc/hosts 來手工新增域名解析,也可以使用 --resolve 引數,指定域名的解析規則,比如在這裡把 ngx.test 強制解析到 127.0.0.1,也就是被 kubectl port-forward 轉發的本地地址。

和 Service 一樣,Ingress 把請求轉發到了叢集內部的 Pod,但 Ingress 的路由規則不再是 IP 地址,而是 HTTP 協定裡的域名、URI 等要素。

再補充一點,目前的 Kubernetes 流量管理功能主要集中在 Ingress Controller 上,已經遠不止於管理「入口流量」了,它還能管理「出口流量」,也就是 egress,甚至還可以管理叢集內部服務之間的「東西向流量」。此外,Ingress Controller 通常還有很多的其他功能,比如 TLS 終止、網路應用防火牆、限流限速、流量拆分、身份認證、存取控制等等,完全可以認為它是一個全功能的反向代理或者閘道器。