部署在k8s集群中的应用如何暴露给外部的用户使用呢?可以通过NodePort和LoadBLancer类型的Service可以把应用暴露给外部用户使用,除此之外,k8s还提供了一个非常重要的资源对象可以用来暴露服务给外部用户,那就是Ingress。对于小规模的应用使用NodePort或许能够满足需求,但是当应用越来越多的时候,就会发现对于NodePort的管理就非常麻烦,这时候使用Ingress就非常方便,可以避免管理大量的端口。
Ingress其实就是从k8s集群外部访问集群的一个入口,将外部的请求转发到集群内不同的Service上,其实就相当于nginx、haproxy等负载均衡代理服务一样。
是否能够直接使用nginx呢?其实是不可以的,只使用nginx这种方式其实有很大缺陷,每次有新服务加入的时候怎么修改nginx配置?不可能手动或滚动更新前端的Nginx Pod,那在加上一个服务发现的工具如consul,貌似可以。但是Ingress实际上就是这么实现的。只是服务发现的功能自己实现了,不需要使用第三方的服务,然后加上一个域名规则定义,路由信息的刷新依靠Ingress Controller来提供。
Ingress Controller可以理解为一个监听器,通过不断的监听kube-apiserver,实时的感知后端Service、Pod的变化,当得到这些信息变化后,Ingress Controller再结合Ingress的配置,更新反向代理负载均衡器,达到服务发现的作用。其实这点和发现工具consul、soncul-template非常类似。
现在可以使用的Ingress Controller有很多,比如traefik、nginx-controller、kubernetes Ingress for Kong、HAproxy Ingress controller,当然也可以自己实现一个Ingress Controller,现在普遍用的较多的是traefik和nginx-controller,traefik的性能较nginx-controller差,但是配置使用要简单许多。
安装
NGINX Ingress Controller是使用k8s ingress资源对象构建的,用ConfigMap来存储Nginx配置的一种Ingress Controller实现。
要使用Ingress对外暴露服务,就需要提前安装一个Ingress Controller,这里就先来安装 NGINX Ingress Controller,由于nginx-ingress所在的节点需要能够访问外网,这样域名可以解析到这些节点上直接使用,当然对于线上环境来说为了保证高可用,一般是需要运行多个nginx-ingress实例的,然后可以用一个nginx/haproxy作为入口,通过keepalived来访问边缘节点的VIP地址。
所谓边缘节点即集群内部用来向集群外暴露服务能力的节点,集群外部的服务通过该节点来调用集群内的服务,边缘节点是集群内外交流的一个Endpoint。
修改下资源清单文件:
➜ 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.15.2.tgz
测试环境只有master节点可以访问外网,在测试ingress-nginx固定到master节点,采用hostNetwork模式(生产环境可以使用LB+DaemonSet hostNetwork模式)。然后新建一个名为values-prod.yaml的values文件,用来覆盖ingress-nginx默认的value值,对应的数据如下:
# values-prod.yaml
controller:
name: controller
image:
repository: cnych/ingress-nginx
tag: "v0.41.2"
digest:
dnsPolicy: ClusterFirstWithHostNet
hostNetwork: true
publishService: # hostNetwork 模式下设置为false,通过节点IP地址上报ingress status数据
enabled: false
kind: DaemonSet
tolerations: # kubeadm 安装的集群默认情况下master是有污点,需要容忍这个污点才可以部署
- key: "node-role.kubernetes.io/master"
operator: "Equal"
effect: "NoSchedule"
nodeSelector: # 固定到master1节点
kubernetes.io/hostname: "master1"
service: # HostNetwork 模式不需要创建service
enabled: false
defaultBackend:
enabled: true
name: defaultbackend
image:
repository: cnych/ingress-nginx-defaultbackend
tag: "1.5"
然后使用helm安装ingress-nginx到指定的命名空间中:
➜ kubectl create ns ingress-nginx
➜ helm install --namespace ingress-nginx ingress-nginx ./ingress-nginx -f ./ingress-nginx/values-prod.yaml
NAME: ingress-nginx
LAST DEPLOYED: Fri Dec 11 14:19:05 2020
NAMESPACE: ingress-nginx
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
The ingress-nginx controller has been installed.
Get the application URL by running these commands:
export POD_NAME=$(kubectl --namespace ingress-nginx get pods -o jsonpath="{.items[0].metadata.name}" -l "app=ingress-nginx,component=controller,release=ingress-nginx")
kubectl --namespace ingress-nginx port-forward $POD_NAME 8080:80
echo "Visit http://127.0.0.1:8080 to access your application."
An example Ingress that makes use of the controller:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
name: example
namespace: foo
spec:
rules:
- host: www.example.com
http:
paths:
- backend:
serviceName: exampleService
servicePort: 80
path: /
# This section is only required if TLS is to be enabled for the Ingress
tls:
- hosts:
- www.example.com
secretName: example-tls
If TLS is enabled for the Ingress, a Secret containing the certificate and key must also be provided:
apiVersion: v1
kind: Secret
metadata:
name: example-tls
namespace: foo
data:
tls.crt: <base64 encoded cert>
tls.key: <base64 encoded key>
type: kubernetes.io/tls
部署完查看pod运行状态:
➜ kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller-admission ClusterIP 10.110.143.167 <none> 443/TCP 2m21s
ingress-nginx-defaultbackend ClusterIP 10.104.156.141 <none> 80/TCP 2m21s
➜ kubectl get pods -n ingress-nginx
NAME READY STATUS RESTARTS AGE
ingress-nginx-controller-596955b554-vfmhq 1/1 Running 0 31s
ingress-nginx-defaultbackend-7bf9445d94-lkgw5 1/1 Running 0 3m52s
➜ POD_NAME=$(kubectl get pods -l app.kubernetes.io/name=ingress-nginx -n ingress-nginx -o jsonpath='{.items[0].metadata.name}')
➜ kubectl exec -it $POD_NAME -n ingress-nginx -- /nginx-ingress-controller --version
-------------------------------------------------------------------------------
NGINX Ingress controller
Release: v0.41.2
Build: d8a93551e6e5798fc4af3eb910cef62ecddc8938
Repository: https://github.com/kubernetes/ingress-nginx
nginx version: nginx/1.19.4
-------------------------------------------------------------------------------
看到上面信息证明ingress-nginx部署成功了。
Ingress
创建一个ingress资源,如下:
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: extensions/v1beta1
kind: Ingress
metadata:
name: my-nginx
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: ngdemo.qikqiak.com # 将域名映射到 my-nginx 服务
http:
paths:
- path: /
backend:
serviceName: my-nginx # 将所有请求发送到 my-nginx 服务的 80 端口
servicePort: 80 # 不过需要注意大部分Ingress controller都不是直接转发到Service
# 而是只是通过Service来获取后端的Endpoints列表,直接转发到Pod,这样可以减少网络跳转,提高性能
直接创建上面的资源对象:
➜ kubectl apply -f ngdemo.yaml
deployment.apps "my-nginx" created
service "my-nginx" created
ingress.extensions "my-nginx" created
在ingress资源对象中添加一个annotations: kubernetes.io/ingress.class: "nginx",这就是指定让这个Ingress通过nginx-ingress来处理。
上面的资源创建成功后,可以将域名ngdemo.qikqiak.com解析到ingress-nginx所在的边缘节点中的任意一个,当然也可以在本地/etc/hosts中添加对应的映射。
下图显示了客户端是如何通过Ingress Controller连接到其中一个Pod的流程,客户端首先对ngdemo.qikqiak.com执行DNS解析,得到Ingress Controller所在节点的IP,然后客户端像Ingress Controller发送HTTP请求,然后根据Ingress对象里面的描述匹配域名,找到对应的Service对象,并获取关联的Endpoints列表,将客户端请求转发给其中一个Pod。
URL Rewrite
Nginx Ingress Controller很多高级用法可以通过Ingress对象的annotation进行配置,比如常用的URL Rewrite功能,比如有个前端todo应用,代码地址:https://github.com/cnych/todo-app,直接部署这个应用进行测试:
➜ 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
➜ kubectl get pods
NAME READY STATUS RESTARTS AGE
mongo-5c9fd978bb-txn9j 1/1 Running 0 149m
todo-566957d785-tdgs6 1/1 Running 0 3m31s
......
➜ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 54d
mongo ClusterIP 10.96.95.11 <none> 27017/TCP 150m
todo ClusterIP 10.111.105.47 <none> 3000/TCP 145m
......
对应的Ingress资源对象如下:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: todo
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: todo.qikqiak.com
http:
paths:
- path: /
backend:
serviceName: todo
servicePort: 3000
这就是一个很常规的Ingress对象,部署该对象后,将域名解析后就可以正常访问到应用:
现在对访问的URL路径做一个Rewrite,比如在PATH中添加一个app的前缀,关于Rewrite的操作在Ingress-nginx官方文档中也给出对应的说明:
按照要求需要在path中匹配前缀app,然后通过rewrite-target制定目标,修改后的ingress对象如下:
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: todo.qikqiak.com
http:
paths:
- backend:
serviceName: todo
servicePort: 3000
path: /app(/|$)(.*)
带上app的前缀去访问:
这时候页面可以正常访问,这是因为在path中通过正则表达式/app(/|$)(.*)将匹配的路径设置成rewrite-target的目标路径,所以访问todo.qikqiak/app的时候实际上相当于访问的就是后端服务的/路径,但是发现页面的样式丢失了:
这是因为静态资源路径是在/stylesheets路径下面,现在的 url rewrite过后,要正常访问也需要带上前缀才可以,这时候可以借助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: todo.qikqiak.com
http:
paths:
- backend:
serviceName: todo
servicePort: 3000
path: /app(/|$)(.*)
更新ingress对象后,刷新页面显示正常:
要解决访问主域名出现404的问题,可以给应用设置一个app-root的注解,这样当访问主域名的时候会自动太哦转到指定的app-root目录下面,如下所示:
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 ^/stylesheets/(.*)$ /app/stylesheets/$1 redirect; # 添加 /app 前缀
rewrite ^/images/(.*)$ /app/images/$1 redirect; # 添加 /app 前缀
spec:
rules:
- host: todo.qikqiak.com
http:
paths:
- backend:
serviceName: todo
servicePort: 3000
path: /app(/|$)(.*)
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: todo.qikqiak.com
http:
paths:
- backend:
serviceName: todo
servicePort: 3000
path: /app(/|$)(.*)
更新后就都会以/这样的slash结尾。
Basic Auth
还可以在Ingress Controller上面配置一些基本的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: Zm9vOiRhcHIxJFNjcVhZcFN6JDc4Nm5ISFNaeDdwN2VscDM2WUo0YS8K
kind: Secret
metadata:
creationTimestamp: "2019-12-08T06:40:39Z"
name: basic-auth
namespace: default
resourceVersion: "9197951"
selfLink: /api/v1/namespaces/default/secrets/basic-auth
uid: 6b2aa299-b511-412e-85ea-d0e91e578af0
type: Opaque
然后创建一个具有Basic Auth的Ingress对象的my-nginx应用:
apiVersion: extensions/v1beta1
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:
rules:
- host: foo.bar.com
http:
paths:
- path: /
backend:
serviceName: my-nginx
servicePort: 80
创建资源对象,然后执行下面的命令:
➜ curl -v http://k8s.qikqiak.com -H 'Host: foo.bar.com'
* Rebuilt URL to: http://k8s.qikqiak.com/
* Trying 123.59.188.12...
* TCP_NODELAY set
* Connected to k8s.qikqiak.com (123.59.188.12) port 80 (#0)
> GET / HTTP/1.1
> Host: foo.bar.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Server: openresty/1.15.8.2
< Date: Sun, 08 Dec 2019 06:44:35 GMT
< Content-Type: text/html
< Content-Length: 185
< 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>openresty/1.15.8.2</center>
</body>
</html>
出现401认证错误,然后带上配置的用户名和密码进行认证:
➜ curl -v http://k8s.qikqiak.com -H 'Host: foo.bar.com' -u 'foo:foo'
* Rebuilt URL to: http://k8s.qikqiak.com/
* Trying 123.59.188.12...
* TCP_NODELAY set
* Connected to k8s.qikqiak.com (123.59.188.12) port 80 (#0)
* Server auth using Basic with user 'foo'
> GET / HTTP/1.1
> Host: foo.bar.com
> Authorization: Basic Zm9vOmZvbw==
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: openresty/1.15.8.2
< Date: Sun, 08 Dec 2019 06:46:27 GMT
< Content-Type: text/html
< Content-Length: 612
< Connection: keep-alive
< Vary: Accept-Encoding
< Last-Modified: Tue, 19 Nov 2019 12:50:08 GMT
< ETag: "5dd3e500-264"
< Accept-Ranges: bytes
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
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>
可以看到已经认证成功了。当然出来 Basic Auth 这一种简单的认证方式之外,NGINX Ingress Controller 还支持一些其他高级的认证,比如 OAUTH 认证之类的。
灰度发布
在日常工作中需要进行版本更新升级,所以经常会使用到滚动升级、蓝绿发布、灰度发布等不同的发布操作。而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值,将忽略,并通过优先级将请求与其他金丝雀规则进行优先级的比较。nginx.ingress.kubernetes.io/canary-by-header-value:要匹配的Request Header的值,用于通知Ingress将请求路由到Canary Ingress中指定的服务。当Request Heade设置为此值时,它将会被路由到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-ny-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: extensions/v1beta1
kind: Ingress
metadata:
name: production
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: echo.qikqiak.com
http:
paths:
- backend:
serviceName: production
servicePort: 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映射到master节点(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版本的文件,再创建一个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对象
```yaml
canary-ingress.yaml
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:
- 创建一个基于权重的Canary版本的应用路由Ingress对象
```yaml
host: echo.qikqiak.com http: paths:
- backend:
serviceName: canary
servicePort: 80
Canary 版本应用创建成功后,接下来我们在命令行终端中来不断访问这个应用,观察 Hostname 变化:直接创建上面的资源对象即可: ```shell ➜ 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 版本应用分配了 30% 左右权重的流量,所以上面访问10次有3次访问到了 Canary 版本的应用,符合预期。➜ 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
- backend:
serviceName: canary
servicePort: 80
基于Request Header:基于Request Header进行流量切分的典型应用场景即灰度发布或A/B测试场景。
- 基于权重:基于权重的流量切分的典型应用场景就是蓝绿部署,可通过将权重设置为0或100来实现。例如,可将Green版本设置为主要部分,并将Blue版本的入口配置为Canary。最初,将权重设置为0,因此不会将流量代理到Blue版本,一旦新版本测试和验证都成功后,即可将Blue版本的权重设置为100,即所有流量从Green版本转向Blue。
在上面的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,发现请求没有发送到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
将请求的Header值为canary:other-value,所以ingress-nginx会通过优先级将请求与其他Canary规则进行优先级的比较,这里会进入canary-weight:30这个规则
➜ 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
这时候在上一个版本的annotation的基础上添加一个nginx.ingress.kubernetes.io/canary-by-header-value: user-value这样的规则,就可以将请求路由到Canary Ingress中指定的服务
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版本
同样更新 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: "user_from_beijing",此时后台可对登录的用户请求进行检查,如果该用户访问源来自北京则设置cookie users_from_beijing的值为always,这样就可以确保北京的用户仅访问canary版本。
更新Canary版本的Ingress资源对象,基于cookie来进行流量切分:
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
更新上面的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端口,且需要证书,这里用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: extensions/v1beta1
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:
rules:
- host: foo.bar.com
http:
paths:
- path: /
backend:
serviceName: my-nginx
servicePort: 80
tls:
- hosts:
- foo.bar.com
secretName: foo-tls
除了自签名证书或者购买正规机构的 CA 证书之外,还可以通过 letsencrypt 来自动生成合法的证书。
Certmanager自动HTTPS
安装配置
cert-manager是一个云原生证书管理开源项目,用于在kubernetes集群中提供https证书并自动续费,支持Let's Encrypt/HashiCorp/Vault这些免费证书的签发。在kubernetes中,可通过kubernetes ingress和let’s encrypt实现外部服务的自动化https。
上面是官方给出的架构图,可以看到 cert-manager 在 Kubernetes 中定义了两个自定义类型资源:Issuer(ClusterIssuer)和Certificate。
Issure代表的是证书颁发者,可以定义各种提供者的证书颁发者,当前支持基于Let’s Encrypt/HashiCorp/Vault和CA的证书颁发者,还可以定义不同环境下的证书颁发者。Certificate代表的是生成证书的请求,一般其中存在生成证书的元信息,如域名等等。
一旦在k8s中定义了上述两类资源,部署的cert-manager则会根据Issure和certificate生成tls证书,并将证书保存进k8s的Secret资源中,然后在Ingress资源中就可以引用到这些生成的Secret资源作为tls证书使用,对于已经生成的证书,还会定期检查证书的有效期,如即将超过有效期,还会自动续期。
要在 Kubernetes 集群上安装 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
上面的命令会创建一个名为cert-manager的命名空间,安装大量的CRD以及AdmissionWebhook对象,可通过如下命令查看是否安装成功:
➜ kubectl get pods -n cert-manager
NAME READY STATUS RESTARTS AGE
cert-manager-5597cff495-q6rzh 1/1 Running 0 5m31s
cert-manager-cainjector-bd5f9c764-5sc7d 1/1 Running 0 5m31s
cert-manager-webhook-5f57f59fbc-mvcq4 1/1 Running 0 5m30s
正常情况下可以看到 cert-manager、cert-manager-cainjector 以及 cert-manager-webhook 这几个 Pod 处于 Running 状态。我们可以通过下面的测试来验证下是否可以签发基本的证书类型,创建一个 Issuer 资源对象来测试 webhook 工作是否正常(在开始签发证书之前,必须在群集中至少配置一个 Issuer 或 ClusterIssuer 资源):
➜ 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
这里我们创建了一个名为 cert-manager-test 的命名空间,创建了一个自签名的 Issuer 证书颁发机构,然后使用这个 Issuer 来创建一个证书请求的 Certificate 对象,直接创建上面的资源清单即可:
➜ kubectl apply -f test-selfsigned.yaml
namespace/cert-manager-test created
issuer.cert-manager.io/test-selfsigned created
certificate.cert-manager.io/selfsigned-cert created
创建完成后可以检查新创建的证书状态,在cert-manager处理证书请求之前,可能需要稍等几秒:
➜ kubectl describe certificate -n cert-manager-test
Name: selfsigned-cert
Namespace: cert-manager-test
......
Spec:
Dns Names:
example.com
Issuer Ref:
Name: test-selfsigned
Secret Name: selfsigned-cert-tls
Status:
Conditions:
Last Transition Time: 2020-12-12T03:29:07Z
Message: Certificate is up to date and has not expired
Reason: Ready
Status: True
Type: Ready
Not After: 2021-03-12T03:29:06Z
Not Before: 2020-12-12T03:29:06Z
Renewal Time: 2021-02-10T03:29:06Z
Revision: 1
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Issuing 6s cert-manager Issuing certificate as Secret does not exist
Normal Generated 6s cert-manager Stored new private key in temporary Secret resource "selfsigned-cert-sppz7"
Normal Requested 6s cert-manager Created new CertificateRequest resource "selfsigned-cert-z4nvl"
Normal Issuing 5s cert-manager The certificate has been successfully issued
从上面的 Events 事件中我们可以证书已经成功签发了,生成的证书存放在一个名为 selfsigned-cert-tls 的 Secret 对象下面:
➜ kubectl get secret -n cert-manager-test
NAME TYPE DATA AGE
default-token-t928x kubernetes.io/service-account-token 3 64s
selfsigned-cert-tls kubernetes.io/tls 3 63s
➜ kubectl get secret -n cert-manager-test selfsigned-cert-tls -o yaml
apiVersion: v1
data:
ca.crt: ......
tls.crt: ......
tls.key: ......
kind: Secret
......
name: selfsigned-cert-tls
namespace: cert-manager-test
resourceVersion: "13461084"
selfLink: /api/v1/namespaces/cert-manager-test/secrets/selfsigned-cert-tls
uid: 42e456dc-6d34-4269-b207-f1f3bd50db8b
type: kubernetes.io/tls
到这里证明cert-manager已经安装成功了。需要注意的是cert-manager的功能非常强大,不只是可以支持 ACME 类型的证书签发,还支持其他众多的类型,比如 SelfSigned(自签名)、CA、Vault、Venafi、External、ACME,只是我们一般主要是使用 ACME 来帮我们生成自动化的证书。
下面就来使用cert-manager结合ingress-nginx为Kubernetes应用自动签发 Let’s Encrypt 类型的 HTTPS 证书。
自动化HTTPS
Let’s Encrypt使用ACME协议来校验域名是否真的属于你,校验成功后就可以自动颁发免费证书,证书有效期只有90天,在到期前需要再校验一次来实现续期,而cert-manager是可以自动续期的,所以事实上并不用担心证书过期问题。目前主要有HTTP和DNS两种校验方式。
HTTP-01校验
HTTP-01的校验是通过给你的域名指向的HTTP服务增加一个临时location,在校验的时候Let’s Encrypt会发送http请求到http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>,其中YOUR_DOMAIN就是被校验的域名,TOKEN是cert-manager生成的一个路径,它通过修改Ingress规则来增加这个临时校验路径并指向提供TOEKN的服务。Let’s Encrypt会对比TOKEN是否符合预期,校验成功后就会颁发证书了,不过这种不支持泛域名证书。
