介紹
原文鏈接:k8s技術圈-陽明
在Kubernetes中,服務和Pod的IP地址僅可以在集群網絡內部使用,對於集群外的應用是不可見的。為了使外部的應用能夠訪問集群內的服務,Kubernetes目前提供了以下幾種方案:
- NodePort
- LoadBalancer
- Ingress
Ingress只是Kubernetes中的一個普通資源對象,需要一個對應的Ingress Controller來解析 Ingress 的規則,暴露服務到外部,如ingress-nginx。
ingress-nginx和traefik都是熱門的ingress-controller。
相對於traefik來說,nginx-ingress性能更加優秀,但配置比 traefik 復雜,當然功能也要強大一些,支持的功能多。
官網 有一些常見的ingress controller。
Ingress工作原理
Ingress其實就是從kubernets集群外部去訪問集群的一個統一管理入口,它會將外部請求轉發到集群內不同service的endpoint列表中的pod。
ingress controller監聽kube-apiserver,實時感知后端service、pod的變化。得到的信息變化后,ingress controller再結合ingress的配置,更新反向代理負載均衡器,達到服務發現的作用。
這個邏輯和consul非常類似。
部署nginx Ingress
Ingress組成
- ingress controller:將新加入的Ingress轉化成Nginx的配置文件並使之生效。
- ingress服務:將Nginx的配置抽象成一個Ingress對象,每添加一個新的服務只需寫一個新的Ingress的yaml文件,然后應用到kubernetes集群。
要使用Ingress對外暴露服務,就需要提前安裝一個Ingress Controller。
生產環境使用LB + DaemonSet hostNetwork模式需要修改values.yaml
- .Values.kind設置為DaemonSet,生產環境使用 LB + DaemonSet hostNetwork 模式
- .Values.hostNetwork設置為true,表示ingress-nginx這個pod啟動的端口直接在ingress nginx DaemonSet運行的節點上開啟。
比如DaemonSet啟動在nodeSelector設置為http-endpoint:here的k8s-node1,k8s-node2兩個節點上運行,那只會在這兩個節點開啟80,443端口。而不會在k8s-node3上啟動 - .Values.publishService.enabled設置為false,hostNetwork 模式下設置為false,通過節點IP地址上報ingress status數據
- .Values.controller.digest,鏡像手動上傳的,這行一定要注釋掉。否則一直提示找不到拉取不到鏡像
- .Values.dnsPolicy設置為ClusterFirstWithHostNet,如果pod工作在主機網絡,效率更高
- .Values.defaultbackend.enabled設置為true,默認后端pod
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm fetch ingress-nginx/ingress-nginx
tar -xvf ingress-nginx-3.23.0.tgz
kubectl create ns ingress-nginx
helm install --debug --namespace ingress-nginx ingress-nginx ingress-nginx
注意:
- 默認配置監聽所有命名空間的ingress對象。--watch-namespace 可以限制監聽的namespace
- 如果多個Ingresses定義了同一個host的不同路徑,ingress控制器會合並這些規則
- 如果使用的是GKE,則需要使用以下命令將用戶初始化為cluster-admin:
console kubectl create clusterrolebinding cluster-admin-binding \ --clusterrole cluster-admin \ --user $(gcloud config get-value account)
線上環境為了保證高可用,需要運行多個nginx-ingress實例,然后用一個nginx/haproxy 作為入口,通過keepalived來訪問邊緣節點(集群內部用來向集群外暴露服務能力的節點)的vip地址。
檢查
# pod運行狀態,可以看到這兩個pod的IP直接就是node節點的IP
[root@k8s-master1 ingress-nginx]# kubectl get pods -n ingress-nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
ingress-nginx-controller-cgf99 1/1 Running 0 16m 192.168.1.120 k8s-node1 <none> <none>
ingress-nginx-controller-krgkp 1/1 Running 0 16m 192.168.1.121 k8s-node2 <none> <none>
ingress-nginx-defaultbackend-cb7bcf6d7-hkmhs 1/1 Running 0 16m 100.2.4.26 k8s-node2 <none> <none>
#service
[root@k8s-master1 ingress-nginx]# kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller-admission ClusterIP 172.16.206.255 <none> 443/TCP 28m
ingress-nginx-defaultbackend ClusterIP 172.16.81.103 <none> 80/TCP 28m
# nginx ingress controller 日志
[root@k8s-master1 ingress-nginx]# kubectl logs -n ingress-nginx ingress-nginx-controller-cgf99
-------------------------------------------------------------------------------
NGINX Ingress controller
Release: v0.44.0
Build: f802554ccfadf828f7eb6d3f9a9333686706d613
Repository: https://github.com/kubernetes/ingress-nginx
nginx version: nginx/1.19.6
-------------------------------------------------------------------------------
創建一個簡單的nginx應用的ingress資源
cat > test-app-nginx.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-nginx
spec:
selector:
matchLabels:
app: test-nginx
template:
metadata:
labels:
app: test-nginx
spec:
containers:
- name: test-nginx
image: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: test-nginx
labels:
app: test-nginx
spec:
ports:
- port: 80
protocol: TCP
name: http
selector:
app: test-nginx
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test-nginx
annotations:
kubernetes.io/ingress.class: "nginx" #指定這個ingress資源用ingress-nginx來處理
spec:
rules:
- host: erbiao.com # 將域名映射到 test-nginx 服務
http:
paths:
- path: /
backend:
serviceName: test-nginx # 將所有請求發送到 test-nginx 服務的 80 端口
servicePort: 80 # 不過需要注意大部分Ingress controller都不是直接轉發到Service
# 而是只是通過Service來獲取后端的Endpoints列表,直接轉發到Pod,這樣可以減少網絡跳轉,提高性能
EOF
kubectl create ns test
kubectl apply -f test-app-nginx.yaml -n test
資源創建成功后將域名erbiao.com解析到ingress-nginx所在邊緣節點。就可以通過域名訪問。

請求流程:客戶端解析域名erbiao.com得到邊緣節點IP,然后向節點上的Ingress Controller發送http請求。依據Ingress對象里的描述匹配域名,找到對應的service對象,獲取關聯的endpoint列表,最后將客戶端請求轉發給其中某個pod。

在ingress-nginx-controller的pod的nginx.conf配置中,生成了nginx配置段
## start server erbiao.com
server {
server_name erbiao.com ;
listen 80 ;
listen 443 ssl http2 ;
set $proxy_upstream_name "-";
ssl_certificate_by_lua_block {
certificate.call()
}
location / {
set $namespace "test";
set $ingress_name "test-nginx";
set $service_name "test-nginx";
set $service_port "80";
set $location_path "/";
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,
use_port_in_redirects = false,
global_throttle = { namespace = "", limit = 0, window_size = 0, key = { }, ignored_cidrs = { } },
})
balancer.rewrite()
plugins.run()
}
# be careful with `access_by_lua_block` and `satisfy any` directives as satisfy any
# will always succeed when there's `access_by_lua_block` that does not have any lua code doing `ngx.exit(ngx.DECLINED)`
# other authentication method such as basic auth or external auth useless - all requests will be allowed.
#access_by_lua_block {
#}
header_filter_by_lua_block {
lua_ingress.header()
plugins.run()
}
body_filter_by_lua_block {
plugins.run()
}
log_by_lua_block {
balancer.log()
monitor.call()
plugins.run()
}
port_in_redirect off;
set $balancer_ewma_score -1;
set $proxy_upstream_name "test-test-nginx-80";
set $proxy_host $proxy_upstream_name;
set $pass_access_scheme $scheme;
set $pass_server_port $server_port;
set $best_http_host $http_host;
set $pass_port $pass_server_port;
set $proxy_alternative_upstream_name "";
client_max_body_size 1m;
proxy_set_header Host $best_http_host;
# Pass the extracted client certificate to the backend
# Allow websocket connections
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Request-ID $req_id;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Host $best_http_host;
proxy_set_header X-Forwarded-Port $pass_port;
proxy_set_header X-Forwarded-Proto $pass_access_scheme;
proxy_set_header X-Scheme $pass_access_scheme;
# Pass the original X-Forwarded-For
proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;
# mitigate HTTPoxy Vulnerability
# https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/
proxy_set_header Proxy "";
# Custom headers to proxied server
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering off;
proxy_buffer_size 4k;
proxy_buffers 4 4k;
proxy_max_temp_file_size 1024m;
proxy_request_buffering on;
proxy_http_version 1.1;
proxy_cookie_domain off;
proxy_cookie_path off;
# In case of errors try the next upstream server before returning an error
proxy_next_upstream error timeout;
proxy_next_upstream_timeout 0;
proxy_next_upstream_tries 3;
proxy_pass http://upstream_balancer;
proxy_redirect off;
}
}
## end server erbiao.com
URL Rewrite功能
NGINX Ingress Controller很多高級的用法可以通過Ingress對象的annotation進行配置,如URL Rewrite功能。
比如一個 todo 的前端應用。
kubectl apply -f https://github.com/cnych/todo-app/raw/master/k8s/mongo.yaml
kubectl apply -f https://github.com/cnych/todo-app/raw/master/k8s/web.yaml
對應的Ingress資源對象:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: todo
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: erbiao.me
http:
paths:
- path: /
backend:
serviceName: todo
servicePort: 3000
部署,解析域名后就可正常訪問到。

現在針對URL路徑做一個rewrite:在URI中添加一個app的前綴。做法就是在annotations中添加rewrite-target的注解。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: todo
namespace: default
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- host: erbiao.me
http:
paths:
- backend:
serviceName: todo
servicePort: 3000
path: /app(/|$)(.*)
github中還有其他annotations的介紹。
更新后再訪問就需要加上/app這個URI了


可看到靜態資源在/stylesheets路徑下,做了URL Rewrite后,要正常訪問那也需要加上前綴http://erbiao.me/app/stylesheets/screen.css。對於圖片或者其他靜態資源也是如此,當然去更改頁面引入靜態資源的方式為相對路徑也是可以的,但畢竟要修改代碼,此時可借助ingress-nginx 中的configuration-snippet來對靜態資源做一次跳轉。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: todo
namespace: default
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/configuration-snippet: |
rewrite ^/stylesheets/(.*)$ /app/stylesheets/$1 redirect; # 添加 /app 前綴
rewrite ^/images/(.*)$ /app/images/$1 redirect; # 添加 /app 前綴
spec:
rules:
- host: erbiao.me
http:
paths:
- backend:
serviceName: todo
servicePort: 3000
path: /app(/|$)(.*)
現在訪問主域名erbiao.me還是404,要解決此問題可設置app-root的注解,如此當訪問主域名時會自動跳轉到指定的app-root資源下。即訪問http://erbiao.me會自動跳轉到http://erbiao.me/app。但是還有一個問題是path 路徑其實也匹配了/app 這樣的路徑,可能我們更加希望應用在最后添加一個 / 這樣的slash,同樣,可通過configuration-snippet配置來完成。更新后應用訪問地址就都會以/樣的slash結尾了
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: todo
namespace: default
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/app-root: /app/
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/configuration-snippet: |
rewrite ^(/app)$ $1/ redirect;
rewrite ^/stylesheets/(.*)$ /app/stylesheets/$1 redirect;
rewrite ^/images/(.*)$ /app/images/$1 redirect;
spec:
rules:
- host: erbiao.me
http:
paths:
- backend:
serviceName: todo
servicePort: 3000
path: /app(/|$)(.*)
Basic Auth
在 Ingress Controller上面可配置一些基本的認證,如Basic Auth,可用 htpasswd生成一個密碼文件來驗證身份驗證。
# htpasswd 生成一個密碼文件
[root@k8s-master1 test]# htpasswd -c auth erbiao
New password:
Re-type new password:
Adding password for user erbiao
#創建依據生成的auth文件創建secret對象。
#創建的secret一定要和應用是在同一命名空間,否則會報503
[root@k8s-master1 test]# kubectl create secret generic basic-auth --from-file=auth
對上述todo應用做個認證
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: todo
annotations:
kubernetes.io/ingress.class: "nginx"
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 - erbiao' #要顯示的帶有適當上下文的消息,說明需要身份驗證的原因
spec:
rules:
- host: erbiao.me
http:
paths:
- path: /
backend:
serviceName: todo
servicePort: 3000
NGINX Ingress Controller 還支持一些其他高級的認證,比如OAUTH認證之類的
灰度發布
在日常工作中經常需要對服務進行版本更新升級,所以經常會使用到滾動升級、藍綠發布、灰度發布等不同的發布操作。而ingress-nginx支持通過Annotations配置來實現不同場景下的灰度發布和測試,可以滿足金絲雀發布、藍綠部署與 A/B 測試等業務場景。ingress-nginx的Annotation支持以下幾種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-by-header-pattern:與上面的
canary-by-header-value類似,唯一的區別是它是用正則表達式(PCRE Regex matching)對來匹配請求頭的值,而不是只固定某一個值。注意:當與canary-by-header-value同時存在,注解canary-by-header-pattern將被忽略。當指定的正則表達式在請求處理過程中導致錯誤時,該請求將被視為不匹配。 - 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
當Ingress被標記為Canary Ingress,除nginx.ingress.kubernetes.io/load-balance和nginx.ingress.kubernetes.io/upstream-hash-by外,所有其他非Canary注解都被忽略。
已知局限性:每個Ingress規則最多可以應用一個Canary Ingress
總的來說可以把以上 annotation 規則划分為以下兩類:
- 基於權重的 Canary 規則

- 基於用戶請求的 Canary 規則

下面通過一個示例應用來對灰度發布功能進行說明。
1. 部署一個production版本的應用
cat > production.yaml << EOF
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
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: production
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: echo.erbiao.me
http:
paths:
- backend:
serviceName: production
servicePort: 80
EOF
kubectl apply -f production.yaml
應用部署成功后,將域名echo.erbiao.me解析到邊緣節點即可訪問:
Hostname: production-856d5fb99-zvbd2
Pod Information:
node name: k8s-node1
pod name: production-856d5fb99-zvbd2
pod namespace: default
pod IP: 100.2.3.39
Server values:
server_version=nginx: 1.13.3 - lua: 10008
Request Information:
client_address=100.2.4.0
method=GET
real path=/
query=
request_version=1.1
request_scheme=http
request_uri=http://echo.erbiao.me:8080/
Request Headers:
accept=*/*
host=echo.erbiao.me
user-agent=curl/7.29.0
x-forwarded-for=192.168.1.105
x-forwarded-host=echo.erbiao.me
x-forwarded-port=80
x-forwarded-proto=http
x-real-ip=192.168.1.105
x-request-id=20a18db31ea6693a0207e5fc73a14baf
x-scheme=http
Request Body:
-no body in request-
2. 依據上述版本,創建Canary版本的應用
cat > canary.yaml << EOF
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
EOF
kubectl apply -f canary.yaml
3.Annotation 規則配置
- 基於權重:基於權重的流量切分的典型應用場景就是藍綠部署,可通過將權重設置為 0 或 100 來實現。
可將 Green 版本設置為主要部分,並將 Blue 版本的入口配置為 Canary。最初,將權重設置為 0,因此不會將流量代理到 Blue 版本。一旦新版本測試和驗證都成功后,即可將 Blue 版本的權重設置為 100,即所有流量從 Green 版本轉向 Blue。
創建一個基於權重的 Canary 版本的應用路由 Ingress 對象。
cat > canary-ingress.yaml << EOF
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: canary
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/canary: "true" # 要開啟灰度發布機制,首先需要啟用 Canary
nginx.ingress.kubernetes.io/canary-weight: "30" # 分配30%流量到當前Canary版本
spec:
rules:
- host: echo.erbiao.me
http:
paths:
- backend:
serviceName: canary
servicePort: 80
EOF
kubectl apply -f canary-ingress.yaml
Canary版本應用創建成功后,在命令行終端中來不斷訪問這個應用,觀察 Hostname 變化。Canary版本應用分配了30%左右權重的流量,符合我們的預期。:
[root@k8s-master1 ~]# for i in $(seq 1 20); do echo -n "count:$i ," && curl -s echo.erbiao.me | grep "Hostname" ; done
count:1 ,Hostname: canary-66cb497b7f-pnqq9
count:2 ,Hostname: canary-66cb497b7f-pnqq9
count:3 ,Hostname: production-856d5fb99-zvbd2
count:4 ,Hostname: production-856d5fb99-zvbd2
count:5 ,Hostname: canary-66cb497b7f-pnqq9
count:6 ,Hostname: production-856d5fb99-zvbd2
count:7 ,Hostname: production-856d5fb99-zvbd2
count:8 ,Hostname: production-856d5fb99-zvbd2
count:9 ,Hostname: canary-66cb497b7f-pnqq9
count:10 ,Hostname: production-856d5fb99-zvbd2
count:11 ,Hostname: canary-66cb497b7f-pnqq9
count:12 ,Hostname: production-856d5fb99-zvbd2
count:13 ,Hostname: production-856d5fb99-zvbd2
count:14 ,Hostname: production-856d5fb99-zvbd2
count:15 ,Hostname: production-856d5fb99-zvbd2
count:16 ,Hostname: canary-66cb497b7f-pnqq9
count:17 ,Hostname: canary-66cb497b7f-pnqq9
count:18 ,Hostname: production-856d5fb99-zvbd2
count:19 ,Hostname: canary-66cb497b7f-pnqq9
count:20 ,Hostname: production-856d5fb99-zvbd2
- 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:
kubernetes.io/ingress.class: nginx
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 規則進行優先級的比較。
#請求時設置了 canary: never 這個 Header 值,所以請求沒有發送到 Canary 應用中去。
[root@k8s-master1 ~]# for i in $(seq 1 20); do echo -n "count:$i ," && curl -s -H "canary: never" echo.erbiao.me | grep "Hostname" ; done
count:1 ,Hostname: production-856d5fb99-zvbd2
count:2 ,Hostname: production-856d5fb99-zvbd2
count:3 ,Hostname: production-856d5fb99-zvbd2
count:4 ,Hostname: production-856d5fb99-zvbd2
count:5 ,Hostname: production-856d5fb99-zvbd2
count:6 ,Hostname: production-856d5fb99-zvbd2
count:7 ,Hostname: production-856d5fb99-zvbd2
count:8 ,Hostname: production-856d5fb99-zvbd2
count:9 ,Hostname: production-856d5fb99-zvbd2
count:10 ,Hostname: production-856d5fb99-zvbd2
count:11 ,Hostname: production-856d5fb99-zvbd2
count:12 ,Hostname: production-856d5fb99-zvbd2
count:13 ,Hostname: production-856d5fb99-zvbd2
count:14 ,Hostname: production-856d5fb99-zvbd2
count:15 ,Hostname: production-856d5fb99-zvbd2
count:16 ,Hostname: production-856d5fb99-zvbd2
count:17 ,Hostname: production-856d5fb99-zvbd2
count:18 ,Hostname: production-856d5fb99-zvbd2
count:19 ,Hostname: production-856d5fb99-zvbd2
count:20 ,Hostname: production-856d5fb99-zvbd2
#請求設置的 Header 值為 canary: other-value(不匹配never或always),所以 ingress-nginx 會通過優先級將請求與其他 Canary 規則進行優先級的比較,我們這里也就會進入 canary-weight: "30" 這個規則去。
[root@k8s-master1 ~]# for i in $(seq 1 20); do echo -n "count:$i ," && curl -s -H "canary: other-value" echo.erbiao.me | grep "Hostname" ; done
count:1 ,Hostname: production-856d5fb99-zvbd2
count:2 ,Hostname: canary-66cb497b7f-pnqq9
count:3 ,Hostname: canary-66cb497b7f-pnqq9
count:4 ,Hostname: production-856d5fb99-zvbd2
count:5 ,Hostname: production-856d5fb99-zvbd2
count:6 ,Hostname: production-856d5fb99-zvbd2
count:7 ,Hostname: production-856d5fb99-zvbd2
count:8 ,Hostname: production-856d5fb99-zvbd2
count:9 ,Hostname: production-856d5fb99-zvbd2
count:10 ,Hostname: production-856d5fb99-zvbd2
count:11 ,Hostname: production-856d5fb99-zvbd2
count:12 ,Hostname: canary-66cb497b7f-pnqq9
count:13 ,Hostname: canary-66cb497b7f-pnqq9
count:14 ,Hostname: production-856d5fb99-zvbd2
count:15 ,Hostname: canary-66cb497b7f-pnqq9
count:16 ,Hostname: canary-66cb497b7f-pnqq9
count:17 ,Hostname: production-856d5fb99-zvbd2
count:18 ,Hostname: production-856d5fb99-zvbd2
count:19 ,Hostname: production-856d5fb99-zvbd2
count:20 ,Hostname: canary-66cb497b7f-pnqq9
在上述ingress對象的基礎上,再添加nginx.ingress.kubernetes.io/canary-by-header-value: user-value注解,結合nginx.ingress.kubernetes.io/canary-by-header就可以將請求路由到canary版本的服務中。
annotations:
kubernetes.io/ingress.class: nginx
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版本
# 請求時設置canary: user-value,所有請求都會被路由到canary版本
[root@k8s-master1 ~]# for i in $(seq 1 20); do echo -n "count:$i ," && curl -s -H "canary: user-value" echo.erbiao.me | grep "Hostname" ; done
count:1 ,Hostname: canary-66cb497b7f-pnqq9
count:2 ,Hostname: canary-66cb497b7f-pnqq9
count:3 ,Hostname: canary-66cb497b7f-pnqq9
count:4 ,Hostname: canary-66cb497b7f-pnqq9
count:5 ,Hostname: canary-66cb497b7f-pnqq9
count:6 ,Hostname: canary-66cb497b7f-pnqq9
count:7 ,Hostname: canary-66cb497b7f-pnqq9
count:8 ,Hostname: canary-66cb497b7f-pnqq9
count:9 ,Hostname: canary-66cb497b7f-pnqq9
count:10 ,Hostname: canary-66cb497b7f-pnqq9
count:11 ,Hostname: canary-66cb497b7f-pnqq9
count:12 ,Hostname: canary-66cb497b7f-pnqq9
count:13 ,Hostname: canary-66cb497b7f-pnqq9
count:14 ,Hostname: canary-66cb497b7f-pnqq9
count:15 ,Hostname: canary-66cb497b7f-pnqq9
count:16 ,Hostname: canary-66cb497b7f-pnqq9
count:17 ,Hostname: canary-66cb497b7f-pnqq9
count:18 ,Hostname: canary-66cb497b7f-pnqq9
count:19 ,Hostname: canary-66cb497b7f-pnqq9
count:20 ,Hostname: canary-66cb497b7f-pnqq9
3. 基於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 版本。
annotations:
kubernetes.io/ingress.class: nginx
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
#請求時設置一個 users_from_Beijing=always 的 Cookie 值,所有請求都會被路由到canary版本
[root@k8s-master1 ~]# for i in $(seq 1 20); do echo -n "count:$i ," && curl -s -b "users_from_Beijing=always" echo.erbiao.me | grep "Hostname" ; done
count:1 ,Hostname: canary-66cb497b7f-pnqq9
count:2 ,Hostname: canary-66cb497b7f-pnqq9
count:3 ,Hostname: canary-66cb497b7f-pnqq9
count:4 ,Hostname: canary-66cb497b7f-pnqq9
count:5 ,Hostname: canary-66cb497b7f-pnqq9
count:6 ,Hostname: canary-66cb497b7f-pnqq9
count:7 ,Hostname: canary-66cb497b7f-pnqq9
count:8 ,Hostname: canary-66cb497b7f-pnqq9
count:9 ,Hostname: canary-66cb497b7f-pnqq9
count:10 ,Hostname: canary-66cb497b7f-pnqq9
count:11 ,Hostname: canary-66cb497b7f-pnqq9
count:12 ,Hostname: canary-66cb497b7f-pnqq9
count:13 ,Hostname: canary-66cb497b7f-pnqq9
count:14 ,Hostname: canary-66cb497b7f-pnqq9
count:15 ,Hostname: canary-66cb497b7f-pnqq9
count:16 ,Hostname: canary-66cb497b7f-pnqq9
count:17 ,Hostname: canary-66cb497b7f-pnqq9
count:18 ,Hostname: canary-66cb497b7f-pnqq9
count:19 ,Hostname: canary-66cb497b7f-pnqq9
count:20 ,Hostname: canary-66cb497b7f-pnqq9
ssl證書手動管理
通過 Secret 對象來引用證書文件:
kubectl create secret tls erbiao-tls --cert=tls.crt --key=tls.key
#如何自簽證書
# openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=ssl.erbiao.me"
創建一個帶有使用ssl證書的應用
cat > ssl-nginx.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: ssl-nginx
spec:
selector:
matchLabels:
app: ssl-nginx
template:
metadata:
labels:
app: ssl-nginx
spec:
containers:
- name: ssl-nginx
image: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: ssl-nginx
labels:
app: ssl-nginx
spec:
ports:
- port: 80
protocol: TCP
name: http
selector:
app: ssl-nginx
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: ingress-with-auth
spec:
rules:
- host: ssl.erbiao.me
http:
paths:
- path: /
backend:
serviceName: ssl-nginx
servicePort: 80
tls:
- hosts:
- ssl.erbiao.me
secretName: erbiao-tls
EOF
kubectl apply -f ssl-nginx.yaml
除了自簽名證書或者購買正規機構的 CA 證書之外,我們還可以通過 letsencrypt 來自動生成合法的證書。
ssl證書自動續期工具:cert-manager
cert-manager 是一個雲原生證書管理開源項目,用於在 Kubernetes 集群中提供 HTTPS 證書並自動續期,支持Let's Encrypt/HashiCorp/Vault這些免費證書的簽發。
在Kubernetes中,可以通過Kubernetes Ingress和Let's Encrypt 實現外部服務的自動化HTTPS。
下面是官方給出的架構圖,可以看到 cert-manager 在 Kubernetes 中定義了兩個自定義類型資源:Issuer(ClusterIssuer) 和 Certificate。

- Issuer 代表的是證書頒發者,可以定義各種提供者的證書頒發者,當前支持基於 Let's Encrypt/HashiCorp/Vault 和 CA 的證書頒發者,還可以定義不同環境下的證書頒發者。
- Certificate 代表的是生成證書的請求,一般其中存入生成證書元信息,如域名等。
當在Kubernetes中定義了上述兩類資源,部署的cert-manager會根據Issuer和Certificate 生成TLS證書,並將證書保存進Kubernetes的Secret資源中,然后在Ingress資源中就可引用到這些生成的Secret資源作為TLS證書使用,對於已經生成的證書,還會定期檢查證書的有效期,如將超過有效期,還會自動續期。
安裝cert-manager也簡單,官方提供一個單一的資源清單文件,包含所有的資源對象,直接安裝
# Kubernetes 1.16+
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.1.0/cert-manager.yaml
# Kubernetes <1.16
kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.1.0/cert-manager-legacy.yaml
#檢查狀態
[root@k8s-master1 test]# kubectl get pods -n cert-manager
NAME READY STATUS RESTARTS AGE
cert-manager-5597cff495-ptg2n 1/1 Running 0 5m39s
cert-manager-cainjector-bd5f9c764-jmfrt 1/1 Running 0 5m39s
cert-manager-webhook-5f57f59fbc-4lctx 1/1 Running 0 5m39s
在簽發證書前,群集中至少配置一個Issuer或ClusterIssuer資源
創建一個Issuer資源對象來測試webhook工作是否正常。創建了一個名為 cert-manager-test 的命名空間,創建了一個自簽名的 Issuer 證書頒發機構,然后使用這個 Issuer 來創建一個證書請求的 Certificate 對象,直接創建資源清單
cat <<EOF > test-selfsigned.yaml
apiVersion: v1
kind: Namespace
metadata:
name: cert-manager-test
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: test-selfsigned
namespace: cert-manager-test
spec:
selfSigned: {} # 配置自簽名的證書機構類型
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: selfsigned-cert
namespace: cert-manager-test
spec:
dnsNames:
- example.com
secretName: selfsigned-cert-tls
issuerRef:
name: test-selfsigned
EOF
kubectl apply -f test-selfsigned.yaml
創建完成后可以檢查新創建的證書狀態,在 cert-manager 處理證書請求之前,可能需要稍微等幾秒:
[root@k8s-master1 ~]# kubectl describe certificate -n cert-manager-test
Name: selfsigned-cert
Namespace: cert-manager-test
Labels: <none>
Annotations: <none>
API Version: cert-manager.io/v1
Kind: Certificate
......
Spec:
Dns Names:
example.com
Issuer Ref:
Name: test-selfsigned #證書機構
Secret Name: selfsigned-cert-tls #secret對象名稱
Status:
Conditions:
Last Transition Time: 2021-02-22T06:08:39Z
Message: Certificate is up to date and has not expired #證書狀態
Reason: Ready
Status: True
Type: Ready
Not After: 2021-05-23T06:08:38Z #證書有效期
Not Before: 2021-02-22T06:08:38Z #證書有效期
Renewal Time: 2021-04-23T06:08:38Z #證書下次更新時間
Revision: 1
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Issuing 12m cert-manager Issuing certificate as Secret does not exist
Normal Generated 12m cert-manager Stored new private key in temporary Secret resource "selfsigned-cert-tqkwd"
Normal Requested 12m cert-manager Created new CertificateRequest resource "selfsigned-cert-zpdwq"
Normal Issuing 12m cert-manager The certificate has been successfully issued
從上面的Events中可看到證書已經成功簽發,生成的證書存放在一個名為selfsigned-cert-tls的Secret對象:
[root@k8s-master1 ~]# kubectl get secret -n cert-manager-test selfsigned-cert-tls -o yaml
apiVersion: v1
data:
ca.crt: ...
tls.crt: ...
tls.key: ...
kind: Secret
metadata:
...
name: selfsigned-cert-tls
namespace: cert-manager-test
resourceVersion: "815281"
selfLink: /api/v1/namespaces/cert-manager-test/secrets/selfsigned-cert-tls
uid: 4e7fac45-284c-4755-bb89-5435021048ed
type: kubernetes.io/tls
到這里證明cert-manager已經安裝成功。需要注意的是cert-manager功能非常強大,不只是支持 ACME 類型證書簽發,還支持其他眾多類型,如SelfSigned(自簽名)、CA、Vault、Venafi、External、ACME,只是一般主要是使用ACME來生成自動化的證書。
cert-manager結合ingress-nginx為Kubernetes應用自動簽發Let's Encrypt類型的ssl證書
Let's Encrypt使用ACME協議來校驗域名的歸屬(目前主要有HTTP和DNS兩種校驗方式),校驗成功后就可自動頒發免費證書,證書有效期為90天,到期前需要再校驗一次來實現續期,而cert-manager是可以自動續期的,所以不用擔心證書過期問題。
HTTP-01校驗
HTTP-01的校驗通過給域名指向的HTTP服務增加一個臨時 location,校驗時 Let's Encrypt會發送http請求到http://<YOUR_DOMAIN>/.well-known/acme-challenge/
使用HTTP校驗這種方式,首先需要配置域名解析,即需要保證ACME 服務端可以正常訪問到你的HTTP服務。
由於Let's Encrypt的生產環境有着嚴格的接口調用限制,所以一般需要在 staging環境測試通過后,再切換到生產環境。
此處以一個todo項目為例
1. 創建Issuer/ClusterIssuer證書頒發機構
首先創建一個全局范圍staging環境使用的HTTP-01校驗方式的(ClusterIssuer對象)
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging-http01
spec:
acme:
# ACME 服務端地址
server: https://acme-staging-v02.api.letsencrypt.org/directory
# 用於 ACME 注冊的郵箱
email: icnych@gmail.com
# 用於存放 ACME 帳號 private key 的 secret
privateKeySecretRef:
name: letsencrypt-staging-http01
solvers:
- http01: # ACME HTTP-01 類型
ingress:
class: nginx # 指定ingress的名稱
EOF
再創建一個用於生產環境使用的HTTP-01校驗方式的(ClusterIssuer對象)
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-http01
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: icnych@gmail.com
privateKeySecretRef:
name: letsencrypt-http01
solvers:
- http01:
ingress:
class: nginx
EOF
#生成兩個全局范圍ClusterIssuer的對象
[root@k8s-master1 ~]# kubectl get clusterissuers
NAME READY AGE
letsencrypt-http01 True 106s
letsencrypt-staging-http01 True 2m2s
2. 生成證書
cert-manager提供了用於生成證書的自定義資源對象Certificate,不過這個對象需要在一個具體的命名空間下使用,證書最終會在這個命名空間下以 Secret 的資源對象存儲。這里是要結合ingress-nginx一起使用,只需要修改Ingress對象,添加上cert-manager的相關注解即可,不需要手動創建Certificate 對象。:
# 修改前的ingress對象
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: todo-web
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: todo.erbiao.me
http:
paths:
- path: /
backend:
serviceName: todo-web
servicePort: 3000
# 修改后的ingress對象
cat <<EOF | kubectl apply -f -
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: todo-mongo
spec:
selector:
matchLabels:
app: todo-mongo
template:
metadata:
labels:
app: todo-mongo
spec:
volumes:
- name: todo-mongo-data
emptyDir: {}
containers:
- name: todo-mongo
image: todo-mongo
ports:
- containerPort: 27017
volumeMounts:
- name: data
mountPath: /data/db
---
apiVersion: v1
kind: Service
metadata:
name: todo-mongo
spec:
selector:
app: todo-mongo
type: ClusterIP
ports:
- name: todo-mongo
port: 27017
targetPort: 27017
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: todo-web
spec:
selector:
matchLabels:
app: todo-web
template:
metadata:
labels:
app: todo-web
spec:
containers:
- name: todo-web
image: cnych/todo:v1.1
env:
- name: "DBHOST"
value: "mongodb://mongo.default.svc.cluster.local:27017" #不同的命名空間地址不同,此處為default
ports:
- containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: todo-web
spec:
selector:
app: todo-web
type: ClusterIP
ports:
- name: todo-web
port: 3000
targetPort: 3000
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: todo-web
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-staging-http01" # 使用哪個issuer
spec:
tls:
- hosts:
- todo.erbiao.me # TLS 域名
secretName: todo-tls # 用於存儲證書的 Secret 對象名字
rules:
- host: todo.erbiao.me
http:
paths:
- path: /
backend:
serviceName: todo-web
servicePort: 3000
EOF
上述執行完成,就會自動創建一個 Certificate 對象
kubectl get certificate
NAME READY SECRET AGE
todo-tls False todo-tls 34s
在校驗過程中會自動創建一個 Ingress 對象用於 ACME 服務端訪問:
kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
todo-web <none> todo.erbiao.me 192.168.1.120 80, 443 33s
校驗成功后會將證書保存到 todo-tls 的 Secret 對象中
[root@k8s-master1]# kubectl get certificate
NAME READY SECRET AGE
todo-tls True todo-tls 21m
[root@k8s-master1]# kubectl get secret
NAME TYPE DATA AGE
default-token-hpd7s kubernetes.io/service-account-token 3 55d
todo-tls kubernetes.io/tls 2 20m
[root@k8s-master1]# kubectl describe certificate todo-tls
Name: todo-tls
Namespace: default
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Issuing 22m cert-manager Issuing certificate as Secret does not exist
Normal Generated 22m cert-manager Stored new private key in temporary Secret resource "todo-tls-tr4pq"
Normal Requested 22m cert-manager Created new CertificateRequest resource "todo-tls-2gchg"
Normal Issuing 21m cert-manager The certificate has been successfully issued
3. 將ClusterIssuer替換成生產環境的
證書自動獲取成功后,就可將 ClusterIssuer 替換成生產環境
cat <<EOF | kubectl apply -f -
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: todo-web
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-http01" # 使用生產環境的issuer
spec:
tls:
- hosts:
- todo.erbiao.me # TLS 域名
secretName: todo-tls # 用於存儲證書的 Secret 對象名字
rules:
- host: todo.erbiao.me
http:
paths:
- path: /
backend:
serviceName: todo-web
servicePort: 3000
EOF
[root@k8s-master1 ]# kubectl get certificate
NAME READY SECRET AGE
todo-tls True todo-tls 25m
校驗成功后就可自動獲取真正的HTTPS證書,在瀏覽器中訪問 https://todo.erbiao.me就可看到證書有效。
DNS-01 校驗
DNS-01的校驗通過 DNS 提供商的 API 拿到你的 DNS 控制權限,在 Let's Encrypt 為 cert-manager 提供 TOKEN 后,cert-manager 將創建從該 TOKEN 和你的帳戶密鑰派生的 TXT 記錄,並將該記錄放在 _acme-challenge.<YOUR_DOMAIN>。然后Let's Encrypt將向 DNS 系統查詢該記錄,若找到匹配項,就頒發證書,這種方法是支持泛域名證書。
