Ingress 資源對象只是一個路由請求描述配置文件,要讓其真正生效還需要對應的 Ingress 控制器才行,Ingress 控制器有很多,這里我們先介紹使用最多的 ingress-nginx,它是基於 Nginx 的 Ingress 控制器。
運行原理
ingress-nginx 控制器主要是用來組裝一個 nginx.conf 的配置文件,當配置文件發生任何變動的時候就需要重新加載 Nginx 來生效,但是並不會只在影響 upstream 配置的變更后就重新加載 Nginx,控制器內部會使用一個 lua-nginx-module 來實現該功能。
我們知道 Kubernetes 控制器使用控制循環模式來檢查控制器中所需的狀態是否已更新或是否需要變更,所以 ingress-nginx 需要使用集群中的不同對象來構建模型,比如 Ingress、Service、Endpoints、Secret、ConfigMap 等可以生成反映集群狀態的配置文件的對象,控制器需要一直 Watch 這些資源對象的變化,但是並沒有辦法知道特定的更改是否會影響到最終生成的 nginx.conf 配置文件,所以一旦 Watch 到了任何變化控制器都必須根據集群的狀態重建一個新的模型,並將其與當前的模型進行比較,如果模型相同則就可以避免生成新的 Nginx 配置並觸發重新加載,否則還需要檢查模型的差異是否只和端點有關,如果是這樣,則然后需要使用 HTTP POST 請求將新的端點列表發送到在 Nginx 內運行的 Lua 處理程序,並再次避免生成新的 Nginx 配置並觸發重新加載,如果運行和新模型之間的差異不僅僅是端點,那么就會基於新模型創建一個新的 Nginx 配置了,這樣構建模型最大的一個好處就是在狀態沒有變化時避免不必要的重新加載,可以節省大量 Nginx 重新加載。
下面簡單描述了需要重新加載的一些場景:
- 創建了新的 Ingress 資源
- TLS 添加到現有 Ingress
- 從 Ingress 中添加或刪除 path 路徑
- Ingress、Service、Secret 被刪除了
- Ingress 的一些缺失引用對象變可用了,例如 Service 或 Secret
- 更新了一個 Secret
對於集群規模較大的場景下頻繁的對 Nginx 進行重新加載顯然會造成大量的性能消耗,所以要盡可能減少出現重新加載的場景。
由於 ingress-nginx 所在的節點需要能夠訪問外網(不是強制的),這樣域名可以解析到這些節點上直接使用,所以需要讓 ingress-nginx 綁定節點的 80 和 443 端口,所以可以使用 hostPort 來進行訪問,當然對於線上環境來說為了保證高可用,一般是需要運行多個 ·ingress-nginx 實例的,然后可以用一個 nginx/haproxy 作為入口,通過 keepalived 來訪問邊緣節點的 vip 地址。
!!! info "邊緣節點" 所謂的邊緣節點即集群內部用來向集群外暴露服務能力的節點,集群外部的服務通過該節點來調用集群內部的服務,邊緣節點是集群內外交流的一個 Endpoint。
第一個示例
安裝成功后,現在我們來為一個 nginx 應用創建一個 Ingress 資源,如下所示:
# my-nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
spec:
selector:
matchLabels:
app: my-nginx
template:
metadata:
labels:
app: my-nginx
spec:
containers:
- name: my-nginx
image: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: my-nginx
labels:
app: my-nginx
spec:
ports:
- port: 80
protocol: TCP
name: http
selector:
app: my-nginx
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-nginx
namespace: default
spec:
ingressClassName: nginx # 使用 nginx 的 IngressClass(關聯的 ingress-nginx 控制器)
rules:
- host: ngdemo.qikqiak.com # 將域名映射到 my-nginx 服務
http:
paths:
- path: /
pathType: Prefix
backend:
service: # 將所有請求發送到 my-nginx 服務的 80 端口
name: my-nginx
port:
number: 80
# 不過需要注意大部分Ingress控制器都不是直接轉發到Service, 而是只是通過Service來獲取后端的Endpoints列表,直接轉發到Pod,這樣可以減少網絡跳轉,提高性能
直接創建上面的資源對象:
➜ kubectl apply -f my-nginx.yaml
deployment.apps/my-nginx created
service/my-nginx created
ingress.networking.k8s.io/my-nginx created
➜ kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
my-nginx nginx ngdemo.qikqiak.com 192.168.31.31 80 30m
在上面的 Ingress 資源對象中我們使用配置 ingressClassName: nginx 指定讓我們安裝的 ingress-nginx 這個控制器來處理我們的 Ingress 資源,配置的匹配路徑類型為前綴的方式去匹配 /,將來自域名 ngdemo.qikqiak.com 的所有請求轉發到 my-nginx 服務的后端 Endpoints 中去。
上面資源創建成功后,然后我們可以將域名 ngdemo.qikqiak.com 解析到 ingress-nginx 所在的邊緣節點中的任意一個,當然也可以在本地 /etc/hosts 中添加對應的映射也可以,然后就可以通過域名進行訪問了。
下圖顯示了客戶端是如何通過 Ingress 控制器連接到其中一個 Pod 的流程,客戶端首先對 ngdemo.qikqiak.com 執行 DNS 解析,得到 Ingress 控制器所在節點的 IP,然后客戶端向 Ingress 控制器發送 HTTP 請求,然后根據 Ingress 對象里面的描述匹配域名,找到對應的 Service 對象,並獲取關聯的 Endpoints 列表,將客戶端的請求轉發給其中一個 Pod。
前面我們也提到了 ingress-nginx 控制器的核心原理就是將我們的 Ingress 這些資源對象映射翻譯成 Nginx 配置文件 nginx.conf,我們可以通過查看控制器中的配置文件來驗證這點:
➜ kubectl exec -it $POD_NAME -n ingress-nginx -- cat /etc/nginx/nginx.conf
......
upstream upstream_balancer {
server 0.0.0.1; # placeholder
balancer_by_lua_block {
balancer.balance()
}
keepalive 320;
keepalive_timeout 60s;
keepalive_requests 10000;
}
......
## start server ngdemo.qikqiak.com
server {
server_name ngdemo.qikqiak.com ;
listen 80 ;
listen [::]:80 ;
listen 443 ssl http2 ;
listen [::]:443 ssl http2 ;
set $proxy_upstream_name "-";
ssl_certificate_by_lua_block {
certificate.call()
}
location / {
set $namespace "default";
set $ingress_name "my-nginx";
set $service_name "my-nginx";
set $service_port "80";
set $location_path "/";
set $global_rate_limit_exceeding n;
......
proxy_next_upstream_timeout 0;
proxy_next_upstream_tries 3;
proxy_pass http://upstream_balancer;
proxy_redirect off;
}
}
## end server ngdemo.qikqiak.com
......
我們可以在 nginx.conf 配置文件中看到上面我們新增的 Ingress 資源對象的相關配置信息,不過需要注意的是現在並不會為每個 backend 后端都創建一個 upstream 配置塊,現在是使用 Lua 程序進行動態處理的,所以我們沒有直接看到后端的 Endpoints 相關配置數據。
Nginx 配置
如果我們還想進行一些自定義配置,則有幾種方式可以實現:使用 Configmap 在 Nginx 中設置全局配置、通過 Ingress 的 Annotations 設置特定 Ingress 的規則、自定義模板。接下來我們重點給大家介紹使用注解來對 Ingress 對象進行自定義。
Basic Auth
我們可以在 Ingress 對象上配置一些基本的 Auth 認證,比如 Basic Auth,可以用 htpasswd 生成一個密碼文件來驗證身份驗證。
➜ htpasswd -c auth foo
New password:
Re-type new password:
Adding password for user foo
然后根據上面的 auth 文件創建一個 secret 對象:
➜ kubectl create secret generic basic-auth --from-file=auth
secret/basic-auth created
➜ kubectl get secret basic-auth -o yaml
apiVersion: v1
data:
auth: Zm9vOiRhcHIxJFUxYlFZTFVoJHdIZUZQQ1dyZTlGRFZONTQ0dXVQdC4K
kind: Secret
metadata:
name: basic-auth
namespace: default
type: Opaque
然后對上面的 my-nginx 應用創建一個具有 Basic Auth 的 Ingress 對象:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-auth
namespace: default
annotations:
nginx.ingress.kubernetes.io/auth-type: basic # 認證類型
nginx.ingress.kubernetes.io/auth-secret: basic-auth # 包含 user/password 定義的 secret 對象名
nginx.ingress.kubernetes.io/auth-realm: 'Authentication Required - foo' # 要顯示的帶有適當上下文的消息,說明需要身份驗證的原因
spec:
ingressClassName: nginx # 使用 nginx 的 IngressClass(關聯的 ingress-nginx 控制器)
rules:
- host: bauth.qikqiak.com # 將域名映射到 my-nginx 服務
http:
paths:
- path: /
pathType: Prefix
backend:
service: # 將所有請求發送到 my-nginx 服務的 80 端口
name: my-nginx
port:
number: 80
直接創建上面的資源對象,然后通過下面的命令或者在瀏覽器中直接打開配置的域名:
➜ kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress-with-auth nginx bauth.qikqiak.com 192.168.31.31 80 6m55s
➜ curl -v http://192.168.31.31 -H 'Host: bauth.qikqiak.com'
* Trying 192.168.31.31...
* TCP_NODELAY set
* Connected to 192.168.31.31 (192.168.31.31) port 80 (#0)
> GET / HTTP/1.1
> Host: bauth.qikqiak.com
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Date: Thu, 16 Dec 2021 10:49:03 GMT
< Content-Type: text/html
< Content-Length: 172
< Connection: keep-alive
< WWW-Authenticate: Basic realm="Authentication Required - foo"
<
<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx</center>
</body>
</html>
* Connection #0 to host 192.168.31.31 left intact
* Closing connection 0
我們可以看到出現了 401 認證失敗錯誤,然后帶上我們配置的用戶名和密碼進行認證:
➜ curl -v http://192.168.31.31 -H 'Host: bauth.qikqiak.com' -u 'foo:foo'
* Trying 192.168.31.31...
* TCP_NODELAY set
* Connected to 192.168.31.31 (192.168.31.31) port 80 (#0)
* Server auth using Basic with user 'foo'
> GET / HTTP/1.1
> Host: bauth.qikqiak.com
> Authorization: Basic Zm9vOmZvbw==
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Thu, 16 Dec 2021 10:49:38 GMT
< Content-Type: text/html
< Content-Length: 615
< Connection: keep-alive
< Last-Modified: Tue, 02 Nov 2021 14:49:22 GMT
< ETag: "61814ff2-267"
< Accept-Ranges: bytes
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
* Connection #0 to host 192.168.31.31 left intact
* Closing connection 0
可以看到已經認證成功了。除了可以使用我們自己在本地集群創建的 Auth 信息之外,還可以使用外部的 Basic Auth 認證信息,比如我們使用 https://httpbin.org 的外部 Basic Auth 認證,創建如下所示的 Ingress 資源對象:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
# 配置外部認證服務地址
nginx.ingress.kubernetes.io/auth-url: https://httpbin.org/basic-auth/user/passwd
name: external-auth
namespace: default
spec:
ingressClassName: nginx
rules:
- host: external-bauth.qikqiak.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-nginx
port:
number: 80
上面的資源對象創建完成后,再進行簡單的測試:
➜ kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
external-auth <none> external-bauth.qikqiak.com 80 72s
➜ curl -k http://192.168.31.31 -v -H 'Host: external-bauth.qikqiak.com'
* Trying 192.168.31.31...
* TCP_NODELAY set
* Connected to 192.168.31.31 (192.168.31.31) port 80 (#0)
> GET / HTTP/1.1
> Host: external-bauth.qikqiak.com
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Date: Thu, 16 Dec 2021 10:57:25 GMT
< Content-Type: text/html
< Content-Length: 172
< Connection: keep-alive
< WWW-Authenticate: Basic realm="Fake Realm"
<
<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx</center>
</body>
</html>
* Connection #0 to host 192.168.31.31 left intact
* Closing connection 0
然后使用正確的用戶名和密碼測試:
➜ curl -k http://192.168.31.31 -v -H 'Host: external-bauth.qikqiak.com' -u 'user:passwd'
* Trying 192.168.31.31...
* TCP_NODELAY set
* Connected to 192.168.31.31 (192.168.31.31) port 80 (#0)
* Server auth using Basic with user 'user'
> GET / HTTP/1.1
> Host: external-bauth.qikqiak.com
> Authorization: Basic dXNlcjpwYXNzd2Q=
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Thu, 16 Dec 2021 10:58:31 GMT
< Content-Type: text/html
< Content-Length: 615
< Connection: keep-alive
< Last-Modified: Tue, 02 Nov 2021 14:49:22 GMT
< ETag: "61814ff2-267"
< Accept-Ranges: bytes
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
* Connection #0 to host 192.168.31.31 left intact
* Closing connection 0
如果用戶名或者密碼錯誤則同樣會出現401的狀態碼:
➜ curl -k http://192.168.31.31 -v -H 'Host: external-bauth.qikqiak.com' -u 'user:passwd123'
* Trying 192.168.31.31...
* TCP_NODELAY set
* Connected to 192.168.31.31 (192.168.31.31) port 80 (#0)
* Server auth using Basic with user 'user'
> GET / HTTP/1.1
> Host: external-bauth.qikqiak.com
> Authorization: Basic dXNlcjpwYXNzd2QxMjM=
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Date: Thu, 16 Dec 2021 10:59:18 GMT
< Content-Type: text/html
< Content-Length: 172
< Connection: keep-alive
* Authentication problem. Ignoring this.
< WWW-Authenticate: Basic realm="Fake Realm"
<
<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx</center>
</body>
</html>
* Connection #0 to host 192.168.31.31 left intact
* Closing connection 0
當然除了 Basic Auth 這一種簡單的認證方式之外,ingress-nginx 還支持一些其他高級的認證,比如我們可以使用 GitHub OAuth 來認證 Kubernetes 的 Dashboard。
URL Rewrite
ingress-nginx 很多高級的用法可以通過 Ingress 對象的 annotation 進行配置,比如常用的 URL Rewrite 功能。很多時候我們會將 ingress-nginx 當成網關使用,比如對訪問的服務加上 /app 這樣的前綴,在 nginx 的配置里面我們知道有一個 proxy_pass 指令可以實現:
location /app/ {
proxy_pass http://127.0.0.1/remote/;
}
proxy_pass 后面加了 /remote 這個路徑,此時會將匹配到該規則路徑中的 /app 用 /remote 替換掉,相當於截掉路徑中的 /app。同樣的在 Kubernetes 中使用 ingress-nginx 又該如何來實現呢?我們可以使用 rewrite-target 的注解來實現這個需求,比如現在我們想要通過 rewrite.qikqiak.com/gateway/ 來訪問到 Nginx 服務,則我們需要對訪問的 URL 路徑做一個 Rewrite,在 PATH 中添加一個 gateway 的前綴,關於 Rewrite 的操作在 ingress-nginx 官方文檔中也給出對應的說明:
按照要求我們需要在 path 中匹配前綴 gateway,然后通過 rewrite-target 指定目標,Ingress 對象如下所示:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rewrite
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- host: rewrite.qikqiak.com
http:
paths:
- path: /gateway(/|$)(.*)
pathType: Prefix
backend:
service:
name: my-nginx
port:
number: 80
更新后,我們可以預見到直接訪問域名肯定是不行了,因為我們沒有匹配 / 的 path 路徑:
➜ curl rewrite.qikqiak.com
default backend - 404
但是我們帶上 gateway 的前綴再去訪問:
我們可以看到已經可以訪問到了,這是因為我們在 path 中通過正則表達式 /gateway(/|$)(.*) 將匹配的路徑設置成了 rewrite-target 的目標路徑了,所以我們訪問 rewite.qikqiak.com/gateway/ 的時候實際上相當於訪問的就是后端服務的 / 路徑。
要解決我們訪問主域名出現 404 的問題,我們可以給應用設置一個 app-root 的注解,這樣當我們訪問主域名的時候會自動跳轉到我們指定的 app-root 目錄下面,如下所示:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rewrite
annotations:
nginx.ingress.kubernetes.io/app-root: /gateway/
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- host: rewrite.qikqiak.com
http:
paths:
- path: /gateway(/|$)(.*)
pathType: Prefix
backend:
service:
name: my-nginx
port:
number: 80
這個時候我們更新應用后訪問主域名 rewrite.qikqiak.com 就會自動跳轉到 rewrite.qikqiak.com/gateway/ 路徑下面去了。但是還有一個問題是我們的 path 路徑其實也匹配了 /app 這樣的路徑,可能我們更加希望我們的應用在最后添加一個 / 這樣的 slash,同樣我們可以通過 configuration-snippet 配置來完成,如下 Ingress 對象:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rewrite
annotations:
nginx.ingress.kubernetes.io/app-root: /gateway/
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/configuration-snippet: |
rewrite ^(/gateway)$ $1/ redirect;
spec:
ingressClassName: nginx
rules:
- host: rewrite.qikqiak.com
http:
paths:
- path: /gateway(/|$)(.*)
pathType: Prefix
backend:
service:
name: my-nginx
port:
number: 80
更新后我們的應用就都會以 / 這樣的 slash 結尾了。這樣就完成了我們的需求,如果你原本對 nginx 的配置就非常熟悉的話應該可以很快就能理解這種配置方式了。
灰度發布
在日常工作中我們經常需要對服務進行版本更新升級,所以我們經常會使用到滾動升級、藍綠發布、灰度發布等不同的發布操作。而 ingress-nginx 支持通過 Annotations 配置來實現不同場景下的灰度發布和測試,可以滿足金絲雀發布、藍綠部署與 A/B 測試等業務場景。
ingress-nginx 的 Annotations 支持以下 4 種 Canary 規則:
-
nginx.ingress.kubernetes.io/canary-by-header:基於 Request Header 的流量切分,適用於灰度發布以及 A/B 測試。當 Request Header 設置為 always 時,請求將會被一直發送到 Canary 版本;當 Request Header 設置為 never 時,請求不會被發送到 Canary 入口;對於任何其他 Header 值,將忽略 Header,並通過優先級將請求與其他金絲雀規則進行優先級的比較。
-
nginx.ingress.kubernetes.io/canary-by-header-value:要匹配的 Request Header 的值,用於通知 Ingress 將請求路由到 Canary Ingress 中指定的服務。當 Request Header 設置為此值時,它將被路由到 Canary 入口。該規則允許用戶自定義 Request Header 的值,必須與上一個 annotation (canary-by-header) 一起使用。
-
nginx.ingress.kubernetes.io/canary-weight:基於服務權重的流量切分,適用於藍綠部署,權重范圍 0 - 100 按百分比將請求路由到 Canary Ingress 中指定的服務。權重為 0 意味着該金絲雀規則不會向 Canary 入口的服務發送任何請求,權重為 100 意味着所有請求都將被發送到 Canary 入口。
-
nginx.ingress.kubernetes.io/canary-by-cookie:基於 cookie 的流量切分,適用於灰度發布與 A/B 測試。用於通知 Ingress 將請求路由到 Canary Ingress 中指定的服務的cookie。當 cookie 值設置為 always 時,它將被路由到 Canary 入口;當 cookie 值設置為 never 時,請求不會被發送到 Canary 入口;對於任何其他值,將忽略 cookie 並將請求與其他金絲雀規則進行優先級的比較。
需要注意的是金絲雀規則按優先順序進行排序:canary-by-header - > canary-by-cookie - > canary-weight
總的來說可以把以上的四個 annotation 規則划分為以下兩類:
-
基於權重的 Canary 規則
-
基於用戶請求的 Canary 規則
下面我們通過一個示例應用來對灰度發布功能進行說明。
第一步. 部署 Production 應用
首先創建一個 production 環境的應用資源清單:
# production.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: production
labels:
app: production
spec:
selector:
matchLabels:
app: production
template:
metadata:
labels:
app: production
spec:
containers:
- name: production
image: cnych/echoserver
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: production
labels:
app: production
spec:
ports:
- port: 80
targetPort: 8080
name: http
selector:
app: production
然后創建一個用於 production 環境訪問的 Ingress 資源對象:
# production-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: production
spec:
ingressClassName: nginx
rules:
- host: echo.qikqiak.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: production
port:
number: 80
直接創建上面的幾個資源對象:
➜ kubectl apply -f production.yaml
➜ kubectl apply -f production-ingress.yaml
➜ kubectl get pods -l app=production
NAME READY STATUS RESTARTS AGE
production-856d5fb99-d6bds 1/1 Running 0 2m50s
➜ kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
production <none> echo.qikqiak.com 10.151.30.11 80 90s
應用部署成功后,將域名 echo.qikqiak.com 映射到 master1 節點(ingress-nginx 所在的節點)的 IP即可正常訪問應用:
➜ curl http://echo.qikqiak.com
Hostname: production-856d5fb99-d6bds
Pod Information:
node name: node1
pod name: production-856d5fb99-d6bds
pod namespace: default
pod IP: 10.244.1.111
Server values:
server_version=nginx: 1.13.3 - lua: 10008
Request Information:
client_address=10.244.0.0
method=GET
real path=/
query=
request_version=1.1
request_scheme=http
request_uri=http://echo.qikqiak.com:8080/
Request Headers:
accept=*/*
host=echo.qikqiak.com
user-agent=curl/7.64.1
x-forwarded-for=171.223.99.184
x-forwarded-host=echo.qikqiak.com
x-forwarded-port=80
x-forwarded-proto=http
x-real-ip=171.223.99.184
x-request-id=e680453640169a7ea21afba8eba9e116
x-scheme=http
Request Body:
-no body in request-
第二步. 創建 Canary 版本參考將上述 Production 版本的 production.yaml 文件,再創建一個 Canary 版本的應用。
# canary.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: canary
labels:
app: canary
spec:
selector:
matchLabels:
app: canary
template:
metadata:
labels:
app: canary
spec:
containers:
- name: canary
image: cnych/echoserver
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: canary
labels:
app: canary
spec:
ports:
- port: 80
targetPort: 8080
name: http
selector:
app: canary
接下來就可以通過配置 Annotation 規則進行流量切分了。
第三步. Annotation 規則配置
- 基於權重:基於權重的流量切分的典型應用場景就是藍綠部署,可通過將權重設置為 0 或 100 來實現。例如,可將 Green 版本設置為主要部分,並將 Blue 版本的入口配置為 Canary。最初,將權重設置為 0,因此不會將流量代理到 Blue 版本。一旦新版本測試和驗證都成功后,即可將 Blue 版本的權重設置為 100,即所有流量從 Green 版本轉向 Blue。
創建一個基於權重的 Canary 版本的應用路由 Ingress 對象。
# canary-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: canary
annotations:
nginx.ingress.kubernetes.io/canary: "true" # 要開啟灰度發布機制,首先需要啟用 Canary
nginx.ingress.kubernetes.io/canary-weight: "30" # 分配30%流量到當前Canary版本
spec:
ingressClassName: nginx
rules:
- host: echo.qikqiak.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: canary
port:
number: 80
直接創建上面的資源對象即可:
➜ kubectl apply -f canary.yaml
➜ kubectl apply -f canary-ingress.yaml
➜ kubectl get pods
NAME READY STATUS RESTARTS AGE
canary-66cb497b7f-48zx4 1/1 Running 0 7m48s
production-856d5fb99-d6bds 1/1 Running 0 21m
......
➜ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
canary ClusterIP 10.106.91.106 <none> 80/TCP 8m23s
production ClusterIP 10.105.182.15 <none> 80/TCP 22m
......
➜ kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
canary <none> echo.qikqiak.com 10.151.30.11 80 108s
production <none> echo.qikqiak.com 10.151.30.11 80 22m
Canary 版本應用創建成功后,接下來我們在命令行終端中來不斷訪問這個應用,觀察 Hostname 變化:
➜ for i in $(seq 1 10); do curl -s echo.qikqiak.com | grep "Hostname"; done
Hostname: production-856d5fb99-d6bds
Hostname: canary-66cb497b7f-48zx4
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: production-856d5fb99-d6bds
由於我們給 Canary 版本應用分配了 30% 左右權重的流量,所以上面我們訪問10次有3次訪問到了 Canary 版本的應用,符合我們的預期。
- 基於 Request Header: 基於 Request Header 進行流量切分的典型應用場景即灰度發布或 A/B 測試場景。
在上面的 Canary 版本的 Ingress 對象中新增一條 annotation 配置 nginx.ingress.kubernetes.io/canary-by-header: canary(這里的 value 可以是任意值),使當前的 Ingress 實現基於 Request Header 進行流量切分,由於 canary-by-header 的優先級大於 canary-weight,所以會忽略原有的 canary-weight 的規則。
annotations:
nginx.ingress.kubernetes.io/canary: "true" # 要開啟灰度發布機制,首先需要啟用 Canary
nginx.ingress.kubernetes.io/canary-by-header: canary # 基於header的流量切分
nginx.ingress.kubernetes.io/canary-weight: "30" # 會被忽略,因為配置了 canary-by-headerCanary版本
更新上面的 Ingress 資源對象后,我們在請求中加入不同的 Header 值,再次訪問應用的域名。
注意:當 Request Header 設置為 never 或 always 時,請求將不會或一直被發送到 Canary 版本,對於任何其他 Header 值,將忽略 Header,並通過優先級將請求與其他 Canary 規則進行優先級的比較。
➜ for i in $(seq 1 10); do curl -s -H "canary: never" echo.qikqiak.com | grep "Hostname"; done
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
這里我們在請求的時候設置了 canary: never 這個 Header 值,所以請求沒有發送到 Canary 應用中去。如果設置為其他值呢:
➜ for i in $(seq 1 10); do curl -s -H "canary: other-value" echo.qikqiak.com | grep "Hostname"; done
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: canary-66cb497b7f-48zx4
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: canary-66cb497b7f-48zx4
Hostname: production-856d5fb99-d6bds
Hostname: canary-66cb497b7f-48zx4
由於我們請求設置的 Header 值為 canary: other-value,所以 ingress-nginx 會通過優先級將請求與其他 Canary 規則進行優先級的比較,我們這里也就會進入 canary-weight: "30" 這個規則去。
這個時候我們可以在上一個 annotation (即 canary-by-header)的基礎上添加一條 nginx.ingress.kubernetes.io/canary-by-header-value: user-value 這樣的規則,就可以將請求路由到 Canary Ingress 中指定的服務了。
annotations:
nginx.ingress.kubernetes.io/canary: "true" # 要開啟灰度發布機制,首先需要啟用 Canary
nginx.ingress.kubernetes.io/canary-by-header-value: user-value
nginx.ingress.kubernetes.io/canary-by-header: canary # 基於header的流量切分
nginx.ingress.kubernetes.io/canary-weight: "30" # 分配30%流量到當前Canary版本
同樣更新 Ingress 對象后,重新訪問應用,當 Request Header 滿足 canary: user-value時,所有請求就會被路由到 Canary 版本:
➜ for i in $(seq 1 10); do curl -s -H "canary: user-value" echo.qikqiak.com | grep "Hostname"; done
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
- 基於 Cookie:與基於 Request Header 的 annotation 用法規則類似。例如在 A/B 測試場景下,需要讓地域為北京的用戶訪問 Canary 版本。那么當 cookie 的 annotation 設置為 nginx.ingress.kubernetes.io/canary-by-cookie: "users_from_Beijing",此時后台可對登錄的用戶請求進行檢查,如果該用戶訪問源來自北京則設置 cookie users_from_Beijing 的值為 always,這樣就可以確保北京的用戶僅訪問 Canary 版本。
同樣我們更新 Canary 版本的 Ingress 資源對象,采用基於 Cookie 來進行流量切分,
annotations:
nginx.ingress.kubernetes.io/canary: "true" # 要開啟灰度發布機制,首先需要啟用 Canary
nginx.ingress.kubernetes.io/canary-by-cookie: "users_from_Beijing" # 基於 cookie
nginx.ingress.kubernetes.io/canary-weight: "30" # 會被忽略,因為配置了 canary-by-cookie
更新上面的 Ingress 資源對象后,我們在請求中設置一個 users_from_Beijing=always 的 Cookie 值,再次訪問應用的域名。
➜ for i in $(seq 1 10); do curl -s -b "users_from_Beijing=always" echo.qikqiak.com | grep "Hostname"; done
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
我們可以看到應用都被路由到了 Canary 版本的應用中去了,如果我們將這個 Cookie 值設置為 never,則不會路由到 Canary 應用中。
HTTPS
如果我們需要用 HTTPS 來訪問我們這個應用的話,就需要監聽 443 端口了,同樣用 HTTPS 訪問應用必然就需要證書,這里我們用 openssl 來創建一個自簽名的證書:
➜ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=foo.bar.com"
然后通過 Secret 對象來引用證書文件:
# 要注意證書文件名稱必須是 tls.crt 和 tls.key
➜ kubectl create secret tls foo-tls --cert=tls.crt --key=tls.key
secret/who-tls created
這個時候我們就可以創建一個 HTTPS 訪問應用的:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-auth
annotations:
# 認證類型
nginx.ingress.kubernetes.io/auth-type: basic
# 包含 user/password 定義的 secret 對象名
nginx.ingress.kubernetes.io/auth-secret: basic-auth
# 要顯示的帶有適當上下文的消息,說明需要身份驗證的原因
nginx.ingress.kubernetes.io/auth-realm: 'Authentication Required - foo'
spec:
ingressClassName: nginx
tls: # 配置 tls 證書
- hosts:
- foo.bar.com
secretName: foo-tls
rules:
- host: foo.bar.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-nginx
port:
number: 80
除了自簽名證書或者購買正規機構的 CA 證書之外,我們還可以通過一些工具來自動生成合法的證書,cert-manager 是一個雲原生證書管理開源項目,可以用於在 Kubernetes 集群中提供 HTTPS 證書並自動續期,支持 Let's Encrypt/HashiCorp/Vault 這些免費證書的簽發。在 Kubernetes 中,可以通過 Kubernetes Ingress 和 Let's Encrypt 實現外部服務的自動化 HTTPS。
TCP與UDP
由於在 Ingress 資源對象中沒有直接對 TCP 或 UDP 服務的支持,要在 ingress-nginx 中提供支持,需要在控制器啟動參數中添加 --tcp-services-configmap 和 --udp-services-configmap 標志指向一個 ConfigMap,其中的 key 是要使用的外部端口,value 值是使用格式 <namespace/service name>:
比如現在我們要通過 ingress-nginx 來暴露一個 MongoDB 服務,首先創建如下的應用:
# mongo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mongo
labels:
app: mongo
spec:
selector:
matchLabels:
app: mongo
template:
metadata:
labels:
app: mongo
spec:
volumes:
- name: data
emptyDir: {}
containers:
- name: mongo
image: mongo:4.0
ports:
- containerPort: 27017
volumeMounts:
- name: data
mountPath: /data/db
---
apiVersion: v1
kind: Service
metadata:
name: mongo
spec:
selector:
app: mongo
ports:
- port: 27017
直接創建上面的資源對象:
➜ kubectl apply -f mongo.yaml
➜ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
mongo ClusterIP 10.98.117.228 <none> 27017/TCP 2m26s
➜ kubectl get pods -l app=mongo
NAME READY STATUS RESTARTS AGE
mongo-84c587f547-gd7pv 1/1 Running 0 2m5s
現在我們要通過 ingress-nginx 來暴露上面的 MongoDB 服務,我們需要創建一個如下所示的 ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: tcp-services
namespace: ingress-nginx
data:
"27017": default/mongo:27017
然后在 ingress-nginx 的啟動參數中添加 --tcp-services-configmap=$(POD_NAMESPACE)/ingress-nginx-tcp 這樣的配置即可,由於我們這里使用的是 Helm Chart 進行安裝的,我們只需要去覆蓋 Values 值重新安裝即可,修改 ci/daemonset-prod.yaml 文件:
# ci/daemonset-prod.yaml
# ...... 其他部分省略,和之前的保持一致
tcp: # 配置 tcp 服務
27017: "default/mongo:27017" # 使用 27017 端口去映射 mongo 服務
# 9000: "default/test:8080" # 如果還需要暴露其他 TCP 服務,繼續添加即可
配置完成后重新更新當前的 ingress-nginx:
➜ helm upgrade --install ingress-nginx . -f ./ci/daemonset-prod.yaml --namespace ingress-nginx
重新部署完成后會自動生成一個名為 ingress-nginx-tcp 的 ConfigMap 對象,如下所示:
➜ kubectl get configmap -n ingress-nginx ingress-nginx-tcp -o yaml
apiVersion: v1
data:
"27017": default/mongo:27017
kind: ConfigMap
metadata:
......
name: ingress-nginx-tcp
namespace: ingress-nginx
在 ingress-nginx 的啟動參數中也添加上 --tcp-services-configmap=$(POD_NAMESPACE)/ingress-nginx-tcp 這樣的配置:
➜ kubectl get pods -n ingress-nginx
NAME READY STATUS RESTARTS AGE
ingress-nginx-controller-gc582 1/1 Running 0 5m17s
➜ kubectl get pod ingress-nginx-controller-gc582 -n ingress-nginx -o yaml
apiVersion: v1
kind: Pod
......
containers:
- args:
- /nginx-ingress-controller
- --default-backend-service=$(POD_NAMESPACE)/ingress-nginx-defaultbackend
- --election-id=ingress-controller-leader
- --controller-class=k8s.io/ingress-nginx
- --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
- --tcp-services-configmap=$(POD_NAMESPACE)/ingress-nginx-tcp # tcp 配置參數
- --validating-webhook=:8443
- --validating-webhook-certificate=/usr/local/certificates/cert
- --validating-webhook-key=/usr/local/certificates/key
......
ports:
......
- containerPort: 27017
hostPort: 27017
name: 27017-tcp
protocol: TCP
......
現在我們就可以通過 ingress-nginx 暴露的 27017 端口去訪問 Mongo 服務了:
➜ mongo --host 192.168.31.31 --port 27017
MongoDB shell version v4.0.3
connecting to: mongodb://192.168.31.31:27017/
Implicit session: session { "id" : UUID("10f462eb-32b8-443b-ad85-99820db1aaa0") }
MongoDB server version: 4.0.27
......
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
>
同樣的我們也可以去查看最終生成的 nginx.conf 配置文件:
➜ kubectl exec -it ingress-nginx-controller-gc582 -n ingress-nginx -- cat /etc/nginx/nginx.conf
......
stream {
......
# TCP services
server {
preread_by_lua_block {
ngx.var.proxy_upstream_name="tcp-default-mongo-27017";
}
listen 27017;
listen [::]:27017;
proxy_timeout 600s;
proxy_next_upstream on;
proxy_next_upstream_timeout 600s;
proxy_next_upstream_tries 3;
proxy_pass upstream_balancer;
}
# UDP services
}
TCP 相關的配置位於 stream 配置塊下面。從 Nginx 1.9.13 版本開始提供 UDP 負載均衡,同樣我們也可以在 ingress-nginx 中來代理 UDP 服務,比如我們可以去暴露 kube-dns 的服務,同樣需要創建一個如下所示的 ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: udp-services
namespace: ingress-nginx
data:
53: "kube-system/kube-dns:53"
然后需要在 ingress-nginx 參數中添加一個 - --udp-services-configmap=$(POD_NAMESPACE)/udp-services 這樣的配置,當然我們這里只需要去修改 Values 文件值即可,修改 ci/daemonset-prod.yaml 文件:
# ci/daemonset-prod.yaml
# ...... 其他部分省略,和之前的保持一致
tcp: # 配置 tcp 服務
27017: "default/mongo:27017" # 使用 27017 端口去映射 mongo 服務
# 9000: "default/test:8080" # 如果還需要暴露其他 TCP 服務,繼續添加即可
udp: # 配置 udp 服務
53: "kube-system/kube-dns:53"
然后重新更新即可。
全局配置
除了可以通過 annotations 對指定的 Ingress 進行定制之外,我們還可以配置 ingress-nginx 的全局配置,在控制器啟動參數中通過標志 --configmap 指定了一個全局的 ConfigMap 對象,我們可以將全局的一些配置直接定義在該對象中即可:
containers:
- args:
- /nginx-ingress-controller
- --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
......
比如這里我們用於全局配置的 ConfigMap 名為 ingress-nginx-controller:
➜ kubectl get configmap -n ingress-nginx
NAME DATA AGE
ingress-nginx-controller 1 5d2h
比如我們可以添加如下所示的一些常用配置:
➜ kubectl edit configmap ingress-nginx-controller -n ingress-nginx
apiVersion: v1
data:
allow-snippet-annotations: "true"
client-header-buffer-size: 32k # 注意不是下划線
client-max-body-size: 5m
use-gzip: "true"
gzip-level: "7"
large-client-header-buffers: 4 32k
proxy-connect-timeout: 11s
proxy-read-timeout: 12s
keep-alive: "75" # 啟用keep-alive,連接復用,提高QPS
keep-alive-requests: "100"
upstream-keepalive-connections: "10000"
upstream-keepalive-requests: "100"
upstream-keepalive-timeout: "60"
disable-ipv6: "true"
disable-ipv6-dns: "true"
max-worker-connections: "65535"
max-worker-open-files: "10240"
kind: ConfigMap
......
修改完成后 Nginx 配置會自動重載生效,我們可以查看 nginx.conf 配置文件進行驗證:
➜ kubectl exec -it ingress-nginx-controller-gc582 -n ingress-nginx -- cat /etc/nginx/nginx.conf |grep large_client_header_buffers
large_client_header_buffers 4 32k;
由於我們這里是 Helm Chart 安裝的,為了保證重新部署后配置還在,我們同樣需要通過 Values 進行全局配置:
# ci/daemonset-prod.yaml
controller:
config:
allow-snippet-annotations: "true"
client-header-buffer-size: 32k # 注意不是下划線
client-max-body-size: 5m
use-gzip: "true"
gzip-level: "7"
large-client-header-buffers: 4 32k
proxy-connect-timeout: 11s
proxy-read-timeout: 12s
keep-alive: "75" # 啟用keep-alive,連接復用,提高QPS
keep-alive-requests: "100"
upstream-keepalive-connections: "10000"
upstream-keepalive-requests: "100"
upstream-keepalive-timeout: "60"
disable-ipv6: "true"
disable-ipv6-dns: "true"
max-worker-connections: "65535"
max-worker-open-files: "10240"
# 其他省略
此外往往我們還需要對 ingress-nginx 部署的節點進行性能優化,修改一些內核參數,使得適配 Nginx 的使用場景,一般我們是直接去修改節點上的內核參數,為了能夠統一管理,我們可以使用 initContainers 來進行配置:
initContainers:
- command:
- /bin/sh
- -c
- |
mount -o remount rw /proc/sys
sysctl -w net.core.somaxconn=65535 # 具體的配置視具體情況而定
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
sysctl -w fs.file-max=1048576
sysctl -w fs.inotify.max_user_instances=16384
sysctl -w fs.inotify.max_user_watches=524288
sysctl -w fs.inotify.max_queued_events=16384
image: busybox
imagePullPolicy: IfNotPresent
name: init-sysctl
securityContext:
capabilities:
add:
- SYS_ADMIN
drop:
- ALL
......
由於我們這里使用的是 Helm Chart 安裝的 ingress-nginx,同樣只需要去配置 Values 值即可,模板中提供了對 initContainers 的支持,配置如下所示:
controller:
# 其他省略,配置 initContainers
extraInitContainers:
- name: init-sysctl
image: busybox
securityContext:
capabilities:
add:
- SYS_ADMIN
drop:
- ALL
command:
- /bin/sh
- -c
- |
mount -o remount rw /proc/sys
sysctl -w net.core.somaxconn=65535 # socket監聽的backlog上限
sysctl -w net.ipv4.tcp_tw_reuse=1 # 開啟重用,允許將 TIME-WAIT sockets 重新用於新的TCP連接
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
sysctl -w fs.file-max=1048576
sysctl -w fs.inotify.max_user_instances=16384
sysctl -w fs.inotify.max_user_watches=524288
sysctl -w fs.inotify.max_queued_events=16384
同樣重新部署即可:
➜ helm upgrade --install ingress-nginx . -f ./ci/daemonset-prod.yaml --namespace ingress-nginx
部署完成后通過 initContainers 就可以修改節點內核參數了,生產環境建議對節點內核參數進行相應的優化。