Try  HackMD Logo HackMD

How does Ingress Nginx Controller work?

本文介紹 Ingress 基本概念、Ingress Controller 工作原理和 Nginx Ingress Controller 的使用說明。

1. Ingress 基本概念

在 Kubernetes 叢集中,Ingress 作為叢集內服務對外暴露的存取存取點,幾乎承載著叢集內服務存取的所有流量。 Ingress 是 Kubernetes 中的一個 resource object,用來管理叢集外部存取叢集內部服務的方式。可以透過 Ingress 資源來設定不同的轉送規則,從而實現根據不同的規則設定存取叢集內不同的 Service 所對應的後端 Pod。

2. What is Ingress?

Ingress 將叢集外部的 HTTP 和 HTTPS 請求路由到叢集內的服務。流量路由是根據 Ingress 資源上定義的規則來控制的。

以下是一個簡單的例子,Ingress 將所有的流量導向一個服務:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Ingress 可以設定為給服務一個外部可訪問的網址、負載均衡的流量、terminate SSL / TLS,以及提供 name-based ( 基於名稱 ) 的 virtual hosting 功能。Ingress Controller 負責實現 Ingress,一般會搭配 load balancer 一起使用,也可能設定 edge router 或額外的前端來幫助處理流量。

Ingress 不會暴露任意的 ports (埠) 或 protocols (通訊協定)。如果需要將 HTTP 和 HTTPS 以外的服務暴露到互聯網,通常會使用 Service.Type=NodePortService.Type=LoadBalancer 類型的服務來實現。

3. Ingress Controllers 使用須知

要讓 Ingress 在你的 K8s 叢集中運作,必須有一個 Ingress controller 在運行。你需要選擇至少一個 Ingress controller 並確保它已經在叢集中設定好。

和其他在 kube-controller-manager 二進位程式中運行的 controller 不同,Ingress controller 並不會隨著叢集自動啟動。

4. Ingress Controller 工作原理

Ingress Controller 用於解析 Ingress 的轉送規則。 Ingress Controller 收到請求,匹配 Ingress 轉發規則轉發到後端 Service 所對應的 Pod,由 Pod 處理請求。 Kubernetes 中 Service、Ingress 與 Ingress Controller 有著以下關係:

  • Service 是後端真實服務的抽象,一個 Service 可以代表多個相同的後端服務。
  • Ingress 是反向代理規則,用來規定 HTTP、HTTPS 請求應該被轉送到哪個 Service 所對應的 Pod。例如根據請求中不同的 Host 和 URL 路徑,讓請求落在不同 Service 所對應的 Pod上。
  • Ingress Controller 是一個反向代理程序,負責解析 Ingress 的反向代理規則。如果Ingress 有增刪改的變動,Ingress Controller 會及時更新自己對應的轉送規則,當 Ingress Controller 收到請求後就會根據這些規則將請求轉送到對應 Service 的 Pod 上。

Ingress Controller 透過 API Server 取得 Ingress 資源的變化,動態地產生 Load Balancer 所需的設定文件,然後依序產生新的路由轉送規則。

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

5. Deploying the Ingress Nginx Controller as a Deployment Workload

# curl -sL https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.11.2/deploy/static/provider/cloud/deploy.yaml | kubectl apply -f -

執行結果 :

namespace/ingress-nginx created
serviceaccount/ingress-nginx created
serviceaccount/ingress-nginx-admission created

確認 pod 的狀態是 up 和 running

# kubectl wait -n ingress-nginx pod -l app.kubernetes.io/component=controller --for=condition=Ready --timeout=120s

執行結果 :

pod/ingress-nginx-controller-7d8d8c7b4c-pctt4 condition met

6. 了解 Ingress Nginx Controller Kubernetes Resources

$ kubectl get all -n ingress-nginx -o name

執行結果 :

pod/ingress-nginx-admission-create-lgfnk
pod/ingress-nginx-admission-patch-55p8j
pod/ingress-nginx-controller-7d8d8c7b4c-pctt4
service/ingress-nginx-controller
service/ingress-nginx-controller-admission
deployment.apps/ingress-nginx-controller
replicaset.apps/ingress-nginx-controller-7d8d8c7b4c
job.batch/ingress-nginx-admission-create
job.batch/ingress-nginx-admission-patch

這是一個典型的 Deployment,前面有兩個 Service 在支援,以下是為什麼需要這樣做的原因:

  • 第一個 service 叫做 ingress-nginx-controller,它是一個 load balancer 類型的 service,用來將 Ingress controller 的功能暴露給外部。它透過穩定的 IP 地址和 DNS 名稱,讓外部客戶端可以連接到 Ingress controller。load balancer service 也幫助 Ingress controller 將進來的流量分散到多個 Pod 分身上,提升可用性和擴展性。
  • 第二個 service 叫做 ingress-nginx-controller-admission,它是一個 cluster IP service,用來控制 Ingress controller 的准入。這個 service 在 Ingress 資源被 Ingress controller 處理之前,用於驗證和修改這些資源。它在叢集內部運行,不對外部客戶端暴露,使用 Kubernetes 的 admission control mechanism (准入控制機制) 攔截並修改 Ingress 資源。
  • 簡而言之,第一個 service 提供 Ingress controller 的外部進入點,負責 traffic routing 和 load balancing;而第二個 service 負責在 Ingress 資源被 Ingress controller 處理前進行驗證和修改。

如果我們查看 Endpoints,就會發現兩個服務都指向相同的 Pod

$ kubectl -n ingress-nginx get endpoints

執行結果 :

NAME                                 ENDPOINTS                        AGE
ingress-nginx-controller             10.244.2.30:443,10.244.2.30:80   24m
ingress-nginx-controller-admission   10.244.2.30:8443                 24m

而這個 pod 就是 ingress-nginx-controller

$ kubectl -n ingress-nginx get pods -l "app.kubernetes.io/component=controller" -o wide

執行結果 :

NAME                                        READY   STATUS    RESTARTS   AGE   IP            NODE      NOMINATED NODE   READINESS GATES
ingress-nginx-controller-7d8d8c7b4c-pctt4   1/1     Running   0          27m   10.244.2.30   worker2   <none>           <none>

7. 設定 ingress nginx controller 使用 host network

在某些環境中,如果沒有 load balancer 可用,而且不能使用 NodePorts,可以設定 ingress-nginx 的 Pod 使用它們所在主機的網路,而不是使用獨立的 network namespace。這樣做的好處是 Ingress-Nginx Controller 可以直接將 80 和 443 埠綁定到 Kubernetes 節點的網卡上,而不需要透過 NodePort 服務進行額外的網路轉換。

!!! 注意:這種做法不會利用任何 Service 物件來暴露 Ingress-Nginx Controller。如果目標叢集中存在 ingress-nginx 的 Service,建議將其刪除。

$ kubectl edit -n ingress-nginx deployment ingress-nginx-controller

執行結果 :

…
    spec:
      ## 1. 新增以下這行 
      hostNetwork: true
      containers:
      - args:
        - /nginx-ingress-controller
        ## 2. 註解掉以下這行的設定
        #- --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller
        ...
        ## 3. 新增以下這行
        - --report-node-internal-ip-address

!!! 危險 "安全考量"
啟用這個選項會讓每個系統的 daemon 都會在任何網路介面(包括主機的 loopback)上暴露給 Ingress-Nginx Controller。請仔細評估這樣做對系統安全性的影響。

!!! 例子
考慮這個由 2 個副本組成的 ingress-nginx-controller 部署,NGINX Pod 繼承了它們所在主機的 IP 地址,而不是使用內部的 Pod IP。

$ kubectl -n ingress-nginx get pod -o wide
NAME                                        READY   STATUS      RESTARTS   AGE     IP             NODE      NOMINATED NODE   READINESS GATES
ingress-nginx-admission-create-lgfnk        0/1     Completed   0          49m     10.244.2.28    worker2   <none>           <none>
ingress-nginx-admission-patch-55p8j         0/1     Completed   0          49m     10.244.2.29    worker2   <none>           <none>
ingress-nginx-controller-67f877d9d8-9sr6q   1/1     Running     0          3m24s   172.22.8.203   worker2   <none>           <none>

這種部署方式的一個主要限制是,每個叢集節點上只能調度一個 Ingress-Nginx 控制器 Pod,因為在同一個網路介面上綁定相同的埠多次在技術上是不可能的。因為這個原因而無法調度的 Pod 會出現以下的事件錯誤:

$ kubectl -n ingress-nginx get events \
  --sort-by='.lastTimestamp' \
  --field-selector involvedObject.kind=Pod

執行結果

LAST SEEN   TYPE      REASON             OBJECT                                          MESSAGE
5m13s       Warning   FailedScheduling   pod/ingress-nginx-controller-67f877d9d8-8z2ws   0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 2 node(s) didn't have free ports for the requested pod ports. preemption: 0/3 nodes are available: 1 Preemption is not helpful for scheduling, 2 No preemption victims found for incoming pod.
...

確保只建立可排程 Pod 的方法之一,是將 Ingress-Nginx Controller 部署為 DaemonSet,而非傳統的 Deployment。

除非某個節點被設定為拒絕這些 Pod,否則 DaemonSet 只會為每個叢集節點(包括主節點)調度一種 Pod。

像使用 NodePorts 一樣,這種方法也有一些需要注意的小問題。

  • DNS 解析
    設定了 hostNetwork: true 的 Pod 不會使用內部的 DNS resolver(例如 kube-dns 或 CoreDNS),除非它們的 dnsPolicy 設定為 ClusterFirstWithHostNet。如果 NGINX 需要解析內部名稱,建議使用這個設定。
  • Ingress 狀態
    由於使用主機網路的設定中沒有任何 Service 來暴露 Ingress-Nginx Controller,因此在標準雲端環境中常用的 --publish-service flag 不適用,所有 Ingress 物件的狀態會保持為空白。
$ kubectl get ingress
NAME           HOSTS               ADDRESS   PORTS
test-ingress   myapp.example.com             80

相反,由於 bare-metal 節點通常沒有 ExternalIP,因此必須啟用 --report-node-internal-ip-address flag,將所有 Ingress 物件的狀態設定為執行 Ingress-Nginx Controller 的所有節點的內部 IP 位址。

!!! 範例 給定一個由 2 個 replicas 組成的 ingress-nginx-controller DaemonSet

$ kubectl -n ingress-nginx get pod -o wide
NAME                                       READY   STATUS    IP            NODE
default-http-backend-7c5bc89cc9-p86md      1/1     Running   172.17.1.1    host-2
ingress-nginx-controller-5b4cf5fc6-7lg6c   1/1     Running   203.0.113.3   host-3
ingress-nginx-controller-5b4cf5fc6-lzrls   1/1     Running   203.0.113.2   host-2

controller 會將其管理的所有 Ingress 物件的狀態設定為下列值:

$ kubectl get ingress -o wide
NAME           HOSTS               ADDRESS                   PORTS
test-ingress   myapp.example.com   203.0.113.2,203.0.113.3   80

!!! 注意 另外,也可以使用 --publish-status-address 標誌來覆寫寫入 Ingress 物件的位址。請參閱命令列參數

8. Deploying an Application (Comprising of Deployment, Service, and Ingress)

$ kubectl create ns ing-internal
namespace/ing-internal created

$ echo 'kind: Pod
apiVersion: v1
metadata:
  name: hi-app
  namespace: ing-internal
  labels:
    app: hi-app
spec:
  containers:
  - name: hi-app
    image: quay.io/flysangel/hashicorp/http-echo
    args:
    - "-text=hi"
---
kind: Service
apiVersion: v1
metadata:
  name: hi-service
  namespace: ing-internal
spec:
  selector:
    app: hi-app
  ports:
  - port: 5678' | kubectl apply -f - 

$ echo 'apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ping
  namespace: ing-internal
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /hi
        pathType: Prefix
        backend:
          service:
            name: hi-service
            port:
              number: 5678' | kubectl apply -f -

9. Observing the Ingress Nginx Controller Pod

如果我們現在查看 Ingress Nginx Controller pod log,我們會發現上一步建立的 Ingress 有一些 log 資訊:

$ kubectl -n ingress-nginx logs deployments/ingress-nginx-controller

執行結果 :

...
"successfully validated configuration, accepting" ingress="ing-internal/ping"
"Found valid IngressClass" ingress="ing-internal/ping" ingressclass="nginx"
"Configuration changes detected, backend reload required"
Event(v1.ObjectReference{Kind:"Ingress", Namespace:"ing-internal", Name:"ping", UID:"b77f8877-1a41-4530-95ce-6aaa4032cbb8", APIVersion:"networking.k8s.io/v1", ResourceVersion:"78690", FieldPath:""}): type: 'Normal' reason: 'Sync' Scheduled for sync
"Backend successfully reloaded"
Event(v1.ObjectReference{Kind:"Pod", Namespace:"ingress-nginx", Name:"ingress-nginx-controller-67f877d9d8-9sr6q", UID:"a371a524-53c5-4e3a-8ea3-6b3858275c52", APIVersion:"v1", ResourceVersion:"71406", FieldPath:""}): type: 'Normal' reason: 'RELOAD' NGINX reload triggered due to a change in configuration
"updating Ingress status" namespace="ing-internal" ingress="ping" currentValue=null newValue=[{"ip":"172.22.8.203"}]
Event(v1.ObjectReference{Kind:"Ingress", Namespace:"ing-internal", Name:"ping", UID:"b77f8877-1a41-4530-95ce-6aaa4032cbb8", APIVersion:"networking.k8s.io/v1", ResourceVersion:"78765", FieldPath:""}): type: 'Normal' reason: 'Sync' Scheduled for sync

基本上,Ingress Nginx Controller pod 會一直觀察 Kubernetes API Server。一旦接收到 Ingress 資源已建立的事件,它就會檢查其語法,以及是否已定義了 IngressClass=nginx

如果新的 Ingress manifest 符合規定,Nginx Controller 就會更新用於 routing 的設定檔,並在不關閉 Pod 的情況下啟動重新載入。

10. 查看 Ingress Nginx Controller Pod Configuration File

routing 設定是存儲在 Ingress Nginx Controller Pod 的設定文件中。這裡我提取了一些高層次的相關部分:

# 將 Ingress Nginx Controller Pod 名稱設為變數
$ PN=$(kubectl -n ingress-nginx get pods -l "app.kubernetes.io/component=controller" -o name | head -n 1)

$ kubectl -n ingress-nginx exec ${PN#*/} -- cat nginx.conf

執行結果 :

(...)
http {
  (...)
  server {
    (...)
    location ~* "^/hi" {
      set $namespace      "ing-internal";
      set $ingress_name   "ping";
      set $service_name   "hi-service";
      set $service_port   "5678";
      set $location_path  "/hi";
      set $global_rate_limit_exceeding n;

      rewrite_by_lua_block {
              lua_ingress.rewrite({
                      force_ssl_redirect = false,
                      ssl_redirect = true,
                      force_no_ssl_redirect = false,
                      preserve_trailing_slash = false,
                      use_port_in_redirects = false,
                      global_throttle = { namespace = "", limit = 0, window_size = 0, key = { }, ignored_cidrs = { } },
              })
              balancer.rewrite()
              plugins.run()
              
              ...
              
              # Custom Response Headers

              rewrite "(?i)/hi" / break;
              proxy_pass http://upstream_balancer;

              proxy_redirect                          off;
      }
      (...)
    }
  }
}

基本上,如果這個 Nginx 伺服器收到針對根路徑(也就是請求中沒有指定路徑)的請求,它會使用 OpenResty 的 ngx_http_lua_module 模組來重寫請求,並將其傳遞給應用服務。

讓我們來看看目前為止的設定:

  • 通過 hi-service Service 將流量以 load balanced 或 random 的方式傳給網站應用的 Pod。
    • load balanced 或 random 將取決於 kube-proxy 的 mode
  • 創建了一個帶有 class=nginx 屬性的 Ingress ,來將流量 routes 到這個 Service。這會在 Kubernetes 中觸發一個事件。
  • Ingress Nginx Controller Pod 看到這個事件並做出反應:它會驗證 Ingress 設定是否語法正確,以及是否設置了必要的屬性。
  • 如果所有檢查都通過,Ingress Nginx Controller 會將 routing 細節加入到其內部設定文件中,並進行 hot reloads。
  • Ingress Nginx Controller Pod 作為反向代理,將它收到的流量 routing 到叢集中的相應服務。
    • 使用 Ingress 資源來創建這些路由規則。

檢視 Ingress 狀態

$ kubectl -n ing-internal get ing
NAME   CLASS   HOSTS   ADDRESS        PORTS   AGE
ping   nginx   *       172.22.8.203   80      28m

可以看到 Ingress 的 Address 和 Ports 對應的就是 Ingress Nginx Controller Pod 的 IP 跟 Port。

11. Ref