什么是Ingress
Ingress是kubernetes中用來對集群外部進來的請求進行負載、路由控制的一種機制。通過ingress,可以方便的將集群內的service以http或https方式對外提供服務,而且不用將各個服務再單獨暴露。Ingress功能由Ingress resource和Ingress Controllers共同協作完成。
Ingress resource
Ingress resource是kubernetes中的一種資源,是類似於pod、deployment的一種API對象,可以通過kubectl命令創建、銷毀、更新,其對應的Kind是Ingress,可以在其spec屬性中定義服務路由的規則信息。通常Ingress resource中定義的規則需要支持以下的特性:
-
基於內容路由
- 基於目的主機路由:根據請求頭中的請求地址(域名),將請求路由到不同的服務中。如請求地址是foo.example.com的分發到一組服務中,請求地址是bar.example.com分發到別的服務組中
- 基於請求路徑路由:根據請求URL中的路徑信息,將請求路由到不同的服務中。如將請求路徑/serviceA/**的請求轉發到服務A中,將請求路徑/serviceB/**的請求轉發到服務B中
-
對每一個主機都可以單獨設置TLS/SSL連接信息
Ingress Controllers
Ingress resource只是定義了路由的規則信息,真正利用這些信息對請求進行控制是通過Ingress Controllers來實現的。不像kube-controller-manager中的其他controller組件,它們是被作為集群的一部分隨着集群直接安裝好,Ingress controllers 需要我們自己來安裝啟動,並且kubernetes支持很多種實現來滿足不同需求,具體參考Ingress Controllers。
在kubernetes中,Ingress Controller以pod形式運行,監控API Server的/ingress接口后端的backend services,如果service發生變化,Ingress Controller自動更新轉發規則。如Nginx Ingress Controller的工作過程如下:
- 監聽API Server獲取所有的ingress 的定義
- 基於Ingress的定義,進行規則合並,生成Nginx所需的配置文件/etc/nginx/nginx.conf
- 執行nginx -s reload命令,重新加載nginx.conf
為什么需要Ingress
-
端口競爭
通常情況下,我們部署在kubernetes集群中的應用需要給外部訪問,這時我們需要在kubernetes中定義NodePort、LoadBalancer等類型的Servcie來實現,具體參考Service。其中LoadBalancer需要在提供相應機制的雲環境中才能使用,所以在自建的kubernetes集群中都是通過NodePort類型的Service來實現,就是將宿主機的port的Service的Port做個映射,通過訪問宿主機的端口來對service進行訪問。在kubernetes集群中只有一個應用,或者應用數量比較少,能夠正確分配各個應用對應的宿主機端口時還可以應付。隨着應用的追加,端口的映射就變的混亂起來,有的應用還會因為限制必須使用特定的端口號,而這些端口號可能前期已經分配給了別的應用,這時就出現了端口競爭
-
服務動態更新
為了避免端口競爭,可以在系統中部署反向代理服務(nginx、HAproxy)等,這時可以把對外的集群服務都通過反向代理服務來暴露,這樣就帶來了另一個問題,當有新的服務追加進來,或者舊的服務需要刪除,這時還要重新編輯反向代理的配置文件,然后對反向代理服務進行升級。上線/下線一個應用卻需要編輯另一個應用的配置文件,服務的升級、回滾等等都需要考慮,不僅麻煩還容易出錯
Ingress如何解決上面問題
當采用Ingress機制時,部署新應用,只需要創建針對新應用的Ingress resource,Ingress Controllers就會自動把Ingress resource中的規則合並起來,作為整體路由規則對外服務,並且服務都通過Ingress Controllers統一對外提供,也解決了端口競爭的問題。接下來以nginx-ingress為例來講解具體的原理以及在集群中部署Ingress。
nginx-ingress
-
nginx-ingress實現的原理

- 用戶通過kubectl命令向API server發送創建ingress source對象的請求,
- ingress-controller監聽API server獲取到自己對應的ingress source的變化(當集群中有多個ingress controller時,創建ingress source對象時可以指定ingress.class屬性,參考Using multiple Ingress controllers),
- ingress-controller獲取ingress source中的規則后,根據自己的模版生成相應的配置文件,之后reload配置文件,使新的配置生效
- 外部請求進入到ingress,訪問到實際ingress-controller后,ingress-controller根據配置文件中的規則將請求分發到具體的service
- 其中nginx-ingress-controller是在nginx之上又額外添加了一些功能,如監聽API server,自動合並規則,並重新加載等,當用容器形式部署時可以通過Deployment、RC等形式啟動,和啟動普通容器方法是一樣的,想要讓nginx-ingress-controller能被外部訪問,也可以用NodePort形式的service來實現,或者通過Daemonset形式發布時直接指定暴露端口的形式。
-
部署nginx-ingress-controller
目前部署nginx-ingress-controller有兩個官方的安裝方式:nginxinc/kubernetes-ingress、kubernetes/ingress-nginx,具體對比參考區別部署前可參閱下這個異同,下面以kubernetes/ingress-nginx為例,進行nginx-ingress-controller的部署
-
下載啟動文件
$ mkdir -p /opt/k8s/yml/ingress-nginx $ cd /opt/k8s/yml/ingress-nginx $ wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.30.0/deploy/static/mandatory.yaml- 如果搭建的kubernetes是1.14之前的版本,需要把下載的mandatory.yaml文件中217行的kubernetes.io/os改成beta.kubernetes.io/os
-
下載鏡像,上傳到自己的私有鏡像倉庫
mandatory.yaml中指定的原始鏡像是quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.30.0,在國內可能沒有辦法下載,改成從阿里鏡像倉庫下載,為了啟動時不用重新從互聯網下載,把這個鏡像push到我們自己的鏡像倉庫中
$ docker pull registry.aliyuncs.com/google_containers/nginx-ingress-controller:0.30.0 $ docker tag registry.aliyuncs.com/google_containers/nginx-ingress-controller:0.30.0 192.168.0.107/k8s/nginx-ingress-controller:0.30.0 $ docker push 192.168.0.107/k8s/nginx-ingress-controller:0.30.0
-
-
修改下載的mandatory.yaml,將鏡像名稱改成我們私有倉庫中的鏡像名稱,之后啟動服務
``` $ cd /opt/k8s/yml/ingress-nginx $ kubectl create -f mandatory.yaml namespace/ingress-nginx created configmap/nginx-configuration created configmap/tcp-services created configmap/udp-services created serviceaccount/nginx-ingress-serviceaccount created clusterrole.rbac.authorization.k8s.io/nginx-ingress-clusterrole created role.rbac.authorization.k8s.io/nginx-ingress-role created rolebinding.rbac.authorization.k8s.io/nginx-ingress-role-nisa-binding created clusterrolebinding.rbac.authorization.k8s.io/nginx-ingress-clusterrole-nisa-binding created deployment.apps/nginx-ingress-controller created limitrange/ingress-nginx created ``` * 主要是發布了deployment類型的nginx-ingress-controller,默認replicate是1,權限通過nginx-ingress-serviceaccount來設置 * 創建了一個serviceaccount:nginx-ingress-serviceaccount,並賦予相關的權限-
因為我們是在裸機上部署的kubernetes,還需要部署一個service,將nginx-ingress-controller暴露出去,使集群外的服務能夠訪問,此處采用NodePort類型的service
$ cd /opt/k8s/yml/ingress-nginx $ wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.30.0/deploy/static/provider/baremetal/service-nodeport.yaml $ kubectl create -f service-nodeport.yaml service/ingress-nginx created-
這樣我們通過創建一個service,把nginx-ingress-controller暴露出去,通過如下命令查看具體暴露的端口號
$ kubectl get svc -n ingress-nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE ingress-nginx NodePort 10.254.10.208 <none> 80:18797/TCP,443:29468/TCP 108s- 如果kubernetes給我們隨機選擇的端口號不能滿足要求,可以通過修改service-nodeport.yaml,指定自己需要的端口號
-
-
驗證安裝情況
$ kubectl get pods -n ingress-nginx NAME READY STATUS RESTARTS AGE nginx-ingress-controller-7fdc95bf86-rgmdn 1/1 Running 0 22m- 如果狀態不是running,通過kubectl describe查看具體原因
-
查看安裝的controller版本
$ kubectl exec -it nginx-ingress-controller-7fdc95bf86-rgmdn -n ingress-nginx -- /nginx-ingress-controller --version NGINX Ingress controller Release: 0.30.0 Build: git-7e65b90c4 Repository: https://github.com/kubernetes/ingress-nginx nginx version: nginx/1.17.8- 其中nginx-ingress-controller-7fdc95bf86-rgmdn是上一步驟獲取到的pod的名稱
-
驗證
-
利用官方給我們提供的例子http-svc,啟動一個服務,通過Ingress controller的端口訪問我們的服務。因為官方例子http-svc.yaml中的鏡像在gcr.io中,國內無法訪問,需要修改成阿里倉庫中的鏡像registry.aliyuncs.com/google_containers/echoserver:1.4,這個鏡像的作用是接收客戶端的請求,返回服務端和客戶端的header信息。
啟動文件:
$ cd /opt/k8s/yml/ingress-nginx $ cat > http-svc.yaml<< EOF apiVersion: apps/v1 kind: Deployment metadata: apiVersion: apps/v1 name: http-svc spec: replicas: 1 selector: matchLabels: app: http-svc template: metadata: labels: app: http-svc spec: containers: - name: http-svc image: registry.aliyuncs.com/google_containers/echoserver:1.4 ports: - containerPort: 8080 env: - name: NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP --- apiVersion: v1 kind: Service metadata: name: http-svc labels: app: http-svc spec: ports: - port: 80 targetPort: 8080 protocol: TCP name: http selector: app: http-svc EOF啟動命令
$ cd /opt/k8s/yml/ingress-nginx $ kubectl create -f http-svc.yaml -
創建一個Ingress resources
目前我們只創建了一個后端服務,所以創建一個簡單的Ingress,把所有請求都路由到http-svc:80
$ cd /opt/k8s/yml/ingress-nginx $ cat > single-ingress.yml <<EOF apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: single-ingress annotations: kubernetes.io/ingress.class: "nginx" spec: backend: serviceName: http-svc servicePort: 80 EOF創建 ingress
$ cd /opt/k8s/yml/ingress-nginx $ kubectl create -f single-ingress.yml查看ingress信息
$ kubectl get ingress -o wide NAME HOSTS ADDRESS PORTS AGE single-ingress * 10.254.10.208 80 11m $ kubectl describe ingresses single-ingress Name: single-ingress Namespace: default Address: 10.254.10.208 Default backend: http-svc:80 (172.30.22.3:8080) Rules: Host Path Backends ---- ---- -------- * * http-svc:80 (172.30.22.3:8080) Annotations: kubernetes.io/ingress.class: nginx Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal CREATE 11m nginx-ingress-controller Ingress default/single-ingress Normal UPDATE 11m nginx-ingress-controller Ingress default/single-ingress -
通過Ingress controller暴露出來的http端口或者https端口訪問http-svc服務,查看Ingress controller服務暴露的地址(按照前面的部署流程,我們是通過一個NodePort類型的service來暴露的)
$ kubectl get svc -n ingress-nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE ingress-nginx NodePort 10.254.10.208 <none> 80:18797/TCP,443:29468/TCP 12h通過18797端口訪問服務
# http端口訪問 $ curl http://192.168.0.107:18797 # https端口訪問 $curl --insecure https://192.168.0.107:29468 CLIENT VALUES: client_address=172.30.22.7 command=GET real path=/ query=nil request_version=1.1 request_uri=http://192.168.0.107:8080/ SERVER VALUES: server_version=nginx: 1.10.0 - lua: 10001 HEADERS RECEIVED: accept=*/* host=192.168.0.107:18797 user-agent=curl/7.58.0 x-forwarded-for=172.30.22.1 x-forwarded-host=192.168.0.107:18797 x-forwarded-port=80 x-forwarded-proto=http x-real-ip=172.30.22.1 x-request-id=9c9b6f86b5a0d0a664c0f9f01a0bde47 x-scheme=http BODY: -no body in request-- 其中192.168.0.107是集群中一個節點的IP地址
- 返回信息中client_address是nginx-ingress-controller對應的pod的地址,說明請求先進入到nginx-ingress-controller中,之后路由到具體的服務中。對於如何正確拿到client IP地址,可以參照Source IP address,也可參照我的另一篇文章[kubernetes 網絡](kubernetes 網絡.md)
- 頭信息x-forwarded-host中存放了我們真實訪問的服務器的地址
這樣我們就完成了通過nginx-ingress-controller對后端服務的訪問(如果用spring cloud做過微服務開發,會對這個操作很熟悉,因為和zuul、getway等網關功能很類似)。
追加服務
假如我們又在集群中新啟動了一個服務,為了能從外部訪問這個服務,我們也要把這個新服務通過nginx-ingress-controller對外暴露,我們可以新追加一個ingress source對象,定義我們這個新服務相關的信息.
-
啟動新服務,我們這個新服務是自己編寫的一個spring boot 工程,里面只有一個restful接口
@RequestMapping("/header/list") public String listHeader(HttpServletRequest request) { log.info("host is" + request.getHeader("host")); log.info("remoteAddr is " + request.getRemoteHost()); log.info("remotePort is " + request.getRemotePort()); return "OK"; }- 接收外部請求后打印出client相關信息
-
啟動這個新服務
$ cd /opt/k8s/yml/ingress-nginx $ cat > clientip.yml <<EOF apiVersion: v1 kind: Service metadata: name: clientip spec: selector: app: clientip ports: - name: http port: 8080 targetPort: 8080 --- apiVersion: apps/v1 kind: Deployment metadata: name: clientip-deployment spec: selector: matchLabels: app: clientip replicas: 1 template: metadata: labels: app: clientip spec: nodeSelector: sample: slave containers: - name: clientip image: 192.168.0.107/k8s/client-ip-test:0.0.2 ports: - containerPort: 8080 EOF啟動服務
$ cd /opt/k8s/yml/ingress-nginx $ kubectl create -f clientip.yml -
新服務對應的ingress source文件
$ cd /opt/k8s/yml/ingress-nginx $ cat > clientip-ingress.yml<< EOF apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: clientip-ingress annotations: kubernetes.io/ingress.class: "nginx" nginx.ingress.kubernetes.io/rewrite-target: /$2 spec: rules: - http: paths: - path: /clientip backend: serviceName: clientip servicePort: 8080 path: /clientip(/|$)(.*) EOF- 表示請求uri以clientip開頭的服務會路由到這個新啟動的服務中
- 追加了rewrite-target的注解,因為雖然請求以/clientip開頭,但是我么真實的請求地址中並沒有clientip,所以需要用rewrite-target功能把請求地址重寫,詳情參考rewrite
創建新的ingress source
$ cd /opt/k8s/yml/ingress-nginx $ kubectl create -f clientip-ingress.yml ingress.networking.k8s.io/clientip-ingress created -
啟動后通過nginx-ingress-controller訪問
# 訪問新服務,注意我們在請求路徑中的clientip $ curl http://192.168.0.107:18797/clientip/header/list OK # 訪問原來部署好的服務 $ curl http://192.168.0.107:18797 CLIENT VALUES: client_address=172.30.22.7 command=GET real path=/ query=nil request_version=1.1 request_uri=http://192.168.0.107:8080/ SERVER VALUES: server_version=nginx: 1.10.0 - lua: 10001 HEADERS RECEIVED: accept=*/* host=192.168.0.107:18797 user-agent=curl/7.58.0 x-forwarded-for=172.30.22.1 x-forwarded-host=192.168.0.107:18797 x-forwarded-port=80 x-forwarded-proto=http x-real-ip=172.30.22.1 x-request-id=883570a193258f151a6d8bd5f96761af x-scheme=http BODY: -no body in request-- 可以看到,新追加的服務也可以通過nginx-ingress-controller進行訪問,並且原來部署好的服務不受影響仍舊可以訪問
