对外暴露集群服务
前面我们学习了在 Kubernetes 集群内部使用 kube-dns 实现服务发现的功能,那么我们部署在 Kubernetes 集群中的应用如何暴露给外部的用户使用呢?我们知道可以使用 NodePort 和 LoadBlancer 类型的 Service 可以把应用暴露给外部用户使用,除此之外,Kubernetes 还为我们提供了一个非常重要的资源对象可以用来暴露服务给外部用户,那就是 Ingress。对于小规模的应用我们使用 NodePort 或许能够满足我们的需求,但是当你的应用越来越多的时候,你就会发现对于 NodePort 的管理就非常麻烦了,这个时候使用 Ingress 就非常方便了,可以避免管理大量的端口。
Ingress 其实就是从 Kuberenets 集群外部访问集群的一个入口,将外部的请求转发到集群内不同的 Service 上,其实就相当于 nginx、haproxy 等负载均衡代理服务器,可能你会觉得我们直接使用 nginx 就实现了,但是只使用 nginx 这种方式有很大缺陷,每次有新服务加入的时候怎么改 Nginx 配置?不可能让我们去手动更改或者滚动更新前端的 Nginx Pod 吧?那我们再加上一个服务发现的工具比如 consul 如何?貌似是可以,对吧?Ingress 实际上就是这样实现的,只是服务发现的功能自己实现了,不需要使用第三方的服务了,然后再加上一个域名规则定义,路由信息的刷新依靠 Ingress Controller 来提供。
Ingress Controller 可以理解为一个监听器,通过不断地监听 kube-apiserver,实时的感知后端 Service、Pod 的变化,当得到这些信息变化后,Ingress Controller 再结合 Ingress 的配置,更新反向代理负载均衡器,达到服务发现的作用。其实这点和服务发现工具 consul、 consul-template 非常类似。
现在可以供大家使用的 Ingress Controller 有很多,比如 traefik、nginx-controller、Kubernetes Ingress Controller for Kong、HAProxy Ingress controller,当然你也可以自己实现一个 Ingress Controller,现在普遍用得较多的是 traefik 和 nginx-controller,traefik 的性能较 nginx-controller 差,但是配置使用要简单许多
服务,就需要提前安装一个 Ingress Controller,我们这里就先来安装 NGINX Ingress Controller,由于 nginx-ingress 所在的节点需要能够访问外网,这样域名可以解析到这些节点上直接使用,所以需要让 nginx-ingress 绑定节点的 80 和 443 端口(注意端口冲突问题,关了宿主机的httpd或nginx),所以可以使用 hostPort 来进行访问,
当然对于线上环境来说为了保证高可用,一般是需要运行多个 nginx-ingress 实例的,然后可以用一个 nginx/haproxy (比如用一台nginx的主机,当然也可以对nginx做keepalived,主要是用作反向代理)作为入口,通过 keepalived 来访问边缘节点的 vip 地址。
边缘节点
所谓的边缘节点即集群内部用来向集群外暴露服务能力的节点,集群外部的服务通过该节点来调用集群内部的服务,边缘节点是集群内外交流的一个Endpoint。
正向代理和反向代理其实不是固定的可以根据场景变化来灵活看待,就如同客户端和服务端的看待一样是灵活的,比如现在客户端要访问某个服务端,发送出请求数据包,经由一个nginx服务发出,那这是就能将nginx看成正向代理。
如果这是有个提供各种服务的内网(内网也可以灵活看待的,看你怎么划分),有ftp有web有nfs等等,内网外连着个nginx,请求服务端的流量先经过nginx在转发到内网中的服务器,这是后就是就可以看成反向代理了。
至于反向代理和负载均衡,其实这两个有很大的交际,便于理解,你将上面的内网的各种服务器全换成web服务器,请求的流量到了nginx,nginx根据负载均衡规则算出那部分流量到内网哪台服务器,那部分流量又另外哪台服务器,这就可以看成负载均衡了
这里采用helm方式安装
我们这里需要更改下资源清单文件:
➜ 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
ingress固定到的节点,该节点最好能连通外网,毕竟ingress本身就是为了将集群内部的服务暴露给集群外不,就算是在集群外部流量和集群内部之间加了LB或普通的nginx,LB和nginx一台也是外部的主机或云服务
采用 hostNetwork 模式(生产环境可以使用 LB + DaemonSet hostNetwork 模式),将ingress当成守护进程
新建一个名为 values-prod.yaml 的 Values 文件,用来覆盖 ingress-nginx 默认的 Values 值(可以查看默认的yaml文件然后修改某些字段的内容)
# values-prod.yaml
controller:
name: controller
image:
repository: cnych/ingress-nginx
tag: "v0.41.2"
digest:
dnsPolicy: ClusterFirstWithHostNet #hostNetwork模式下pod最佳的dns策略选择
hostNetwork: true
publishService: # hostNetwork 模式下设置为false,通过节点IP地址上报ingress status数据
enabled: false
kind: DaemonSet
tolerations: # kubeadm 安装的集群默认情况下master是有污点,需要容忍这个污点才可以部署
- key: "node-role.kubernetes.io/master"
operator: "Equal" #Exist会准确点,因为这个污点一般有key没有values,当然values: ''这样也行,表示该标签匹配这个key的任意的values
effect: "NoSchedule"
nodeSelector: # 固定到master1节点
kubernetes.io/hostname: "master1"
service: # HostNetwork 模式不需要创建service,hostNetwork模式的pod没必要在创建service,从架构上看ingress也没必要再创建个service来统一代理
enabled: false
defaultBackend:
enabled: true
name: defaultbackend
image:
repository: cnych/ingress-nginx-defaultbackend
tag: "1.5"
然后使用如下命令安装 ingress-nginx 应用到 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 部署成功了。
kubernetes原生的api资源类型
nginx ingress-controller安装成功后,来为一个 nginx 应用(可以用作代理,负载均衡,当然还有www服务器的老本行提供web服务)创建一个 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,这样可以减少网络跳转,提高性能
写上80端口主要是为来方便区分出到那个pod的那个端口罢了,毕竟不同的端口代表着不同的用途,所以ingress只是通过service来了解后端的pod,直接将流量转发到后端的pod上
直接apply即可
注意我们在 Ingress 资源对象中添加了一个 annotations:kubernetes.io/ingress.class: “nginx”,这就是指定让这个 Ingress 通过 nginx-ingress 来处理。
上面资源创建成功后,然后我们可以将域名 ngdemo.qikqiak.com 解析到 ingress-nginx 所在的**边缘节点(直接理解成集群内部向外提供服务的节点)**中的任意一个(还记得nginx ingress controller是以daemonset方式部署的,可以多个节点都有,当然daemonset控制器也可以用nodeSelector来固定),当然也可以在本地/etc/hosts中添加对应的映射也可以,然后就可以通过域名进行访问了。
这里的映射就是你在哪台主机上用域名进行访问就在/etc/hosts中将域名和对应的服务器的主机的对外ip写上,比如这里是master的ip,就可以用域名区访问了,端口不写默认访问80,而80和443都是nginx ingress controller配置了的
客户端是如果通过 Ingress Controller 连接到其中一个 Pod 的流程,客户端首先对 ngdemo.qikqiak.com 执行 DNS 解析(浏览器缓存–>本地dns缓存–>/etc/hosts–>本地dns服务器–>远程dns服务器,远程的dns服务器的解析流程就像dns服务器的分布式架构,一层一层向上询问,上层dns服务器告诉你对应的域名服务器在哪,最终找到域名对应的域名服务器,找到对应的ip,这就是完整的公网的ip对应的域名的解析流程),得到 Ingress Controller 所在节点的 IP,然后客户端向 Ingress Controller 发送 HTTP 请求,然后根据 Ingress 对象里面的描述匹配域名,找到对应的 Service 对象,并获取关联的 Endpoints 列表,将客户端的请求转发给其中一个 Pod。
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
......
部署了个web应用,使用的数据库是mongo
看看它创建的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
将域名解析后就可以正常通过比如在浏览器用域名访问到应用
(访问服务的命令行工具用curl效果较好,ping主要是用来判断连通性,nslookup和dig是用来检查域名解析的)
现在我们需要对访问的 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(/|$)(.*) #|表示的或的意思,()有捕获分组的功能,这里主要是用来做字符串匹配,当然也可以用来匹配当个字符,.表示必定有一个字符,*是贪婪匹配表示有0至无穷个前面一个RE字符,+表示一个或多个前面一个RE字符,?表示0或1个RE字符
这时候用域名区访问是失败的,因为默认访问的是/(网站的假根),而/这里没有定义,会显示403Not Found
带上 app 的前缀再去访问:
注意不写/但实际上会自动加上/
我们可以看到已经可以访问到页面内容了,这是因为我们在 path 中通过正则表达式 /app(/|$)(.*) 将匹配的路径设置成了 rewrite-target 的目标路径了,所以我们访问 todo.qikqiak.com/app 的时候实际上相当于访问的就是后端服务的 / 路径(pod_ip:port/路径,但是我们也可以发现现在页面的样式没有了:
(重写url,但毕竟还是自己这个url,/app)
其实访问的是后端服务的 / 路径,但是该界面所需要的各种资源的路径没变(你可以通过该web应用比如这里的nginx的配置来改变网站根目录调用的资源的路径,甚至是该代码,比较麻烦),就是web应用程序的设置,原本访问/其实就是访问某一个网站根目录下边,同时该情况下的界面所调用的各种资源的路径自然在后端实际的web服务器中是按照它当时的网站根目录写的,所以重写了url之后,有必要对原本的url的界面调用的资源它们对应路径进行重写,重写成/app下的路径,原本的资源的url路径是在/下,要改成在/app下
这是因为应用的静态资源路径是在 /stylesheets 路径下面的,现在我们做了 url rewrite 过后,要正常访问也需要带上前缀才可以:http://todo.qikqiak.com/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: todo.qikqiak.com
http:
paths:
- backend:
serviceName: todo
servicePort: 3000
path: /app(/|$)(.*)
snippet,代码段,按F12可以分析出请求的url详情,这里是修改代码调用的资源
所以重写url之后,访问测试,按F12,看需要的资源的路径不对的改成在新的前缀下
这个时候因为没有pat匹配/这个路径,所以访问主域名会出现404NotFound
要解决我们访问主域名出现 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/ #修改网站的根目录,/--->/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(/|$)(.*)
这个时候我们更新应用后访问主域名 http://todo.qikqiak.com 就会自动跳转到 http://todo.qikqiak.com/app/ 路径下面去了。但是还有一个问题是我们的 path 路径其实也匹配了 /app 这样的路径,可能我们更加希望我们的应用在最后添加一个 / 这样的 slash,同样我们可以通过 configuration-snippet 配置来完成
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 结尾了
同样我们还可以在 Ingress Controller 上面配置一些基本的 Auth 认证,比如 Basic Auth,可以用 htpasswd (专门用于为web应用用户生成密码)生成一个密码文件来验证身份验证。
yum provides htpasswd
yum install httpd-tools-2.4.6-95.el7.centos.x86_64 -y
➜ htpasswd -c auth foo
New password:
Re-type new password:
Adding password for user foo
-c 表示创建个新文件来存放
generic 从本地 file, directory 或者 literal value 创建一个 secret
tls 创建一个 TLS secret
然后根据上面的 auth 文件创建一个 secret 对象:
generic是缺省选项
➜ 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
然后对上面的 my-nginx 应用创建一个具有 Basic Auth 的 Ingress 对象:
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>
-v:vernose冗长的,详细的
-H:请求头的信息
-u:user[:password],可以只有用户
我们可以看到出现了 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 值,将忽略 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 规则划分为以下两类:
首先创建一个 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 #域名访问是不写端口默认就是80,这里对应service的80端口-->pod的targetPort
直接apply
应用部署成功后,将域名 echo.qikqiak.com 映射到 ingress-nginx 所在的节点的 的外网 IP(/etc/hosts),然后即可正常访问应用了:
➜ 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 版本:
# 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 规则配置
创建一个基于权重的 Canary 版本的应用路由 Ingress 对象。
# 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:
- host: echo.qikqiak.com
http:
paths:
- backend:
serviceName: canary
servicePort: 80
注意这时候echo.qikqiak.com域名对应了两个ingress,即两个service production和canary,对应pod是两个service底下的pod的并集
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 版本的应用,符合我们的预期。
在上面的 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 规则进行优先级的比较。
➜ 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:
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
同样我们更新 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
-s:silence,不输出额外的信息
-b:指定cookie的信息,可以是字符串也可以是文件
我们可以看到应用都被路由到了 Canary 版本的应用中去了,如果我们将这个 Cookie 值设置为 never,则不会路由到 Canary 应用中。
cookie:客户端请求服务器,如果服务器需要记录该用户的状态,就是用responese向客户端返回个cookie,客户端会爆裂cookie,当客户端再次请求该网站,会将请求的url和cookie一块交给服务器,服务器检查该cookie来辩证用户的状态的和获取用户的一些信息。(cookie可以直接自己写发给服务端)
sssion:服务端使用的一种记录客户端状态的机制
如果我们需要用 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: 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 来自动生成合法的证书。
encrypt加密
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 证书使用,对于已经生成的证书,还会定期检查证书的有效期,如即将超过有效期,还会自动续期。
要在 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
➜ 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
创建一个 Issuer 资源对象来测试 webhook 工作是否正常(在开始签发证书之前,必须在群集中至少配置一个 Issuer 或 ClusterIssuer 资源)
这里我们创建了一个名为 cert-manager-test 的命名空间,创建了一个自签名的 Issuer 证书颁发机构,然后使用这个 Issuer 来创建一个证书请求的 Certificate 对象,直接apply上面的资源清单即可
创建完成后可以检查新创建的证书状态,在 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 证书。
Let’s Encrypt 使用 ACME 协议来校验域名是否真的属于你,校验成功后就可以自动颁发免费证书,证书有效期只有 90 天,在到期前需要再校验一次来实现续期,而 cert-manager 是可以自动续期的,所以事实上并不用担心证书过期的问题。目前主要有 HTTP 和 DNS 两种校验方式。
HTTP-01 的校验是通过给你域名指向的 HTTP 服务增加一个临时 location,在校验的时候 Let’s Encrypt 会发送 http 请求到 http://
使用 HTTP 校验这种方式,首先需要将域名解析配置好,也就是需要保证 ACME 服务端可以正常访问到你的 HTTP 服务。这里我们以上面的 TODO 应用为例,我们已经将 todo.qikqiak.com 域名做好了正确的解析。
由于 Let’s Encrypt 的生产环境有着严格的接口调用限制,所以一般我们需要先在 staging 环境测试通过后,再切换到生产环境。首先我们创建一个全局范围 staging 环境使用的 HTTP-01 校验方式的证书颁发机构:
➜ 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: [email protected]
# 用于存放 ACME 帐号 private key 的 secret
privateKeySecretRef:
name: letsencrypt-staging-http01
solvers:
- http01: # ACME HTTP-01 类型
ingress:
class: nginx # 指定ingress的名称
EOF
同样再创建一个用于生产环境使用的 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: [email protected]
privateKeySecretRef:
name: letsencrypt-http01
solvers:
- http01:
ingress:
class: nginx
EOF
直接apply
有了 Issuer/ClusterIssuer 证书颁发机构,接下来我们就可以生成免费证书了,cert-manager 给我们提供了 Certificate 这个用于生成证书的自定义资源对象,不过这个对象需要在一个具体的命名空间下使用,证书最终会在这个命名空间下以 Secret 的资源对象存储。我们这里是要结合 ingress-nginx 一起使用,实际上我们只需要修改 Ingress 对象,添加上 cert-manager 的相关注解即可,不需要手动创建 Certificate 对象了,修改上面的 todo 应用的 Ingress 资源对象,如下所示:
➜ cat <<EOF | kubectl apply -f -
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: todo
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-staging-http01" # 使用哪个issuer
spec:
tls:
- hosts:
- todo.qikqiak.com # TLS 域名
secretName: todo-tls # 用于存储证书的 Secret 对象名字
rules:
- host: todo.qikqiak.com
http:
paths:
- path: /
backend:
serviceName: todo
servicePort: 3000
EOF
直接apply
在校验过程中会自动创建一个 Ingress 对象用于 ACME 服务端访问:
➜ kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
cm-acme-http-solver-tgwlb <none> todo.qikqiak.com 10.151.30.11 80 25s
my-nginx <none> ngdemo.qikqiak.com 10.151.30.11 80 23h
todo <none> todo.qikqiak.com 10.151.30.11 80, 443 33s
校验成功后会将证书保存到 todo-tls 的 Secret 对象中:
➜ kubectl get certificate
NAME READY SECRET AGE
todo-tls True todo-tls 21m
➜ kubectl get secret
NAME TYPE DATA AGE
default-token-hpd7s kubernetes.io/service-account-token 3 55d
todo-tls kubernetes.io/tls 2 20m
➜ 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
certificates其实就是证书请求文件
证书自动获取成功后,现在就可以讲 ClusterIssuer 替换成生产环境的了:
➜ cat <<EOF | kubectl apply -f -
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: todo
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-http01" # 使用生产环境的issuer
spec:
tls:
- hosts:
- todo.qikqiak.com # TLS 域名
secretName: todo-tls # 用于存储证书的 Secret 对象名字
rules:
- host: todo.qikqiak.com
http:
paths:
- path: /
backend:
serviceName: todo
servicePort: 3000
EOF
➜ kubectl get certificate
NAME READY SECRET AGE
todo-tls True todo-tls 25m
校验成功后就可以自动获取真正的 HTTPS 证书了,现在在浏览器中访问 https://todo.qikqiak.com 就可以看到证书是有效的了。
DNS-01 的校验是通过 DNS 提供商的 API 拿到你的 DNS 控制权限, 在 Let’s Encrypt 为 cert-manager 提供 TOKEN 后,cert-manager 将创建从该 TOKEN 和你的帐户密钥派生的 TXT 记录,并将该记录放在 _acme-challenge.
DNS-01 支持多种不同的服务提供商,直接在 Issuer 或者 ClusterIssuer 中可以直接配置,对于一些不支持的 DNS 服务提供商可以使用外部 webhook 来提供支持,比如阿里云的 DNS 解析默认情况下是不支持的,我们可以使用 https://github.com/pragkent/alidns-webhook 这个 webhook 来提供支持。
首先使用如下命令安装 alidns-webhook:
# Install alidns-webhook to cert-manager namespace.
➜ kubectl apply -f https://raw.githubusercontent.com/pragkent/alidns-webhook/master/deploy/bundle.yaml
接着创建一个包含访问阿里云 DNS 认证密钥信息的 Secret 对象,对应的 accessk-key 和 secret-key 可以前往阿里云网站 https://ram.console.aliyun.com/manage/ak 获取:
默认可以查看到 AccessKey ID 的值,点击后面的查看 Secret即可看到对应的 AccessSecret,创建如下所示的 Secret 资源对象:
➜ kubectl create secret generic alidns-secret --from-literal=access-key=YOUR_ACCESS_KEY --from-literal=secret-key=YOUR_SECRET_KEY -n cert-manager
接下来同样首先创建一个 staging 环境的 DNS 类型的证书机构资源对象:
➜ cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging-dns01
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-staging-dns01
solvers:
- dns01: # ACME DNS-01 类型
webhook:
groupName: acme.yourcompany.com
solverName: alidns
config:
region: ""
accessKeySecretRef: # 引用 ak
name: alidns-secret
key: access-key
secretKeySecretRef: # 引用 sk
name: alidns-secret
key: secret-key
EOF
再创建一个用于线上环境正式使用的 DNS 类型的 ClusterIssuer 对象:
➜ cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns01
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-dns01
solvers:
- dns01: # ACME DNS-01 类型
webhook:
groupName: acme.yourcompany.com
solverName: alidns
config:
region: ""
accessKeySecretRef: # 引用 ak
name: alidns-secret
key: access-key
secretKeySecretRef: # 引用 sk
name: alidns-secret
key: secret-key
EOF
➜ kubectl get clusterissuer
NAME READY AGE
letsencrypt-dns01 True 7s
letsencrypt-staging-dns01 True 90s
接下来我们就可以使用上面的 ClusterIssuer 对象来或者证书数据了,创建如下所示的 Certificate 资源对象:
➜ cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: qikqiak-com-cert
spec:
secretName: qikqiak-com-tls
commonName: "*.qikqiak.com"
dnsNames:
- qikqiak.com
- "*.qikqiak.com"
issuerRef:
name: letsencrypt-staging-dns01
kind: ClusterIssuer
EOF
这里我们为 *.qikqiak.com 这个泛域名来获取证书,创建完成后正常就可以获取到证书了:
➜ kubectl get certificate
NAME READY SECRET AGE
qikqiak-com-cert True qikqiak-com-tls 3m21s
生成的证书数据也被存储到了名为 qikqiak-com-tls 的 Secret 对象中,接下来将上面的 Certficate 资源对象更换成正式环境的 ClusterIssuer 证书颁发机构:
➜ cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: qikqiak-com-cert
spec:
secretName: qikqiak-com-tls
commonName: "*.qikqiak.com"
dnsNames:
- qikqiak.com
- "*.qikqiak.com"
issuerRef:
name: letsencrypt-dns01 # 替换成正式的 clusterissuer 对象
kind: ClusterIssuer
EOF
➜ kubectl get certificate
NAME READY SECRET AGE
qikqiak-com-cert True qikqiak-com-tls 10m
➜ kubectl get certificaterequest
NAME READY AGE
qikqiak-com-cert-stct8 True 2m54s
➜ kubectl get order
NAME STATE AGE
qikqiak-com-cert-stct8-3109450950 valid 3m38s
➜ kubectl describe order qikqiak-com-cert-stct8-3109450950
Name: qikqiak-com-cert-stct8-3109450950
Namespace: default
Labels: <none>
......
Spec:
Common Name: *.qikqiak.com
Dns Names:
qikqiak.com
*.qikqiak.com
Issuer Ref:
Kind: ClusterIssuer
Name: letsencrypt-dns01
......
Finalize URL: https://acme-v02.api.letsencrypt.org/acme/finalize/107093987/6878258208
State: valid
URL: https://acme-v02.api.letsencrypt.org/acme/order/107093987/6878258208
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Created 3m45s cert-manager Created Challenge resource "qikqiak-com-cert-stct8-3109450950-1046480550" for domain "qikqiak.com"
Normal Created 3m45s cert-manager Created Challenge resource "qikqiak-com-cert-stct8-3109450950-4189768354" for domain "qikqiak.com"
Normal Complete 80s cert-manager Order completed successfully
更新后生成正式环境的通配符证书,然后我们就可以直接在 Ingress 资源对象中使用上面的 Secret 对象了:
➜ cat <<EOF | kubectl apply -f -
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: todo
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
tls:
- hosts:
- "*.qikqiak.com" # 泛域名
secretName: qikqiak-com-tls # 用于存储通配符证书的 Secret 对象名字
rules:
- host: todo.qikqiak.com
http:
paths:
- path: /
backend:
serviceName: todo
servicePort: 3000
EOF
现在当我们去访问 https://todo.qikqiak.com 的时候就有对应的受浏览器信任的证书了,而且这个证书是支持泛域名的,对 qikqiak.com 的二级域名都适用的。
Traefik 是一个开源的可以使服务发布变得轻松有趣的边缘路由器。它负责接收你系统的请求,然后使用合适的组件来对这些请求进行处理。
除了众多的功能之外,Traefik 的与众不同之处还在于它会自动发现适合你服务的配置。当 Traefik 在检查你的服务时,会找到服务的相关信息并找到合适的服务来满足对应的请求。
Traefik 兼容所有主流的集群技术,比如 Kubernetes,Docker,Docker Swarm,AWS,Mesos,Marathon,等等;并且可以同时处理多种方式。(甚至可以用于在裸机上运行的比较旧的软件。)
使用 Traefik,不需要维护或者同步一个独立的配置文件:因为一切都会自动配置,实时操作的(无需重新启动,不会中断连接)。使用 Traefik,你可以花更多的时间在系统的开发和新功能上面,而不是在配置和维护工作状态上面花费大量时间。
Traefik 是一个边缘路由器,是你整个平台的大门,拦截并路由每个传入的请求:它知道所有的逻辑和规则,这些规则确定哪些服务处理哪些请求;传统的反向代理需要一个配置文件,其中包含路由到你服务的所有可能路由,而 Traefik 会实时检测服务并自动更新路由规则,可以自动服务发现。
首先,当启动 Traefik 时,需要定义 entrypoints(入口点),然后,根据连接到这些 entrypoints 的路由来分析传入的请求,来查看他们是否与一组规则相匹配,如果匹配,则路由可能会将请求通过一系列中间件转换过后再转发到你的服务上去。在了解 Traefik 之前有几个核心概念我们必须要了解:
Providers 用来自动发现平台上的服务,可以是编排工具、容器引擎或者 key-value 存储等,比如 Docker、Kubernetes、File(主要就是用来作为ingress自动发现后端的服务,就是原本的ingress,service,pod流程)
Entrypoints 监听传入的流量(端口等…),是网络入口点,它们定义了接收请求的端口(HTTP 或者 TCP)。
Routers 分析请求(host, path, headers, SSL, …),负责将传入请求连接到可以处理这些请求的服务上去。
Services 将请求转发给你的应用(load balancing, …),负责配置如何获取最终将处理传入请求的实际服务。
Middlewares 中间件,用来修改请求或者根据请求来做出一些判断(authentication, rate limiting, headers, …),中间件被附件到路由上,是一种在请求发送到你的服务之前(或者在服务的响应发送到客户端之前)调整请求的一种方法。(对请求数据包进行一些修改)
由于 Traefik 2.X 版本和之前的 1.X 版本不兼容,我们这里选择功能更加强大的 2.X 版本来和大家进行讲解,我们这里使用的是镜像 traefik:2.3.6。
在 Traefik 中的配置可以使用两种不同的方式:
动态配置:完全动态的路由配置
静态配置:启动配置
静态配置中的元素(这些元素不会经常更改)连接到 providers 并定义 Treafik 将要监听的 entrypoints。
动态配置包含定义系统如何处理请求的所有配置内容,这些配置是可以改变的,而且是无缝热更新的,没有任何请求中断或连接损耗。
在 Traefik 中有三种方式定义静态配置:在配置文件中、在命令行参数中、通过环境变量传递
这里我们还是使用 Helm 来快速安装 traefik,首先获取 Helm Chart 包:
➜ git clone https://github.com/traefik/traefik-helm-chart
创建一个定制的 values 配置文件(用于覆盖原本的默认配置):
# values-prod.yaml
# Create an IngressRoute for the dashboard
ingressRoute:
dashboard:
enabled: false # 禁用helm中渲染的dashboard,我们自己手动创建
# Configure ports
ports:
web:
port: 8000
hostPort: 80 # 使用 hostport 模式
# Use nodeport if set. This is useful if you have configured Traefik in a
# LoadBalancer
# nodePort: 32080
# Port Redirections
# Added in 2.2, you can make permanent redirects via entrypoints.
# https://docs.traefik.io/routing/entrypoints/#redirection
# redirectTo: websecure
websecure:
port: 8443
hostPort: 443 # 使用 hostport 模式
# Options for the main traefik service, where the entrypoints traffic comes
# from.
service: # 使用 hostport 模式就不需要Service了
enabled: false
# Logs
# https://docs.traefik.io/observability/logs/
logs:
general:
level: DEBUG
tolerations: # kubeadm 安装的集群默认情况下master是有污点,需要容忍这个污点才可以部署
- key: "node-role.kubernetes.io/master"
operator: "Equal"
effect: "NoSchedule"
nodeSelector: # 固定到master1节点(该节点才可以访问外网)
kubernetes.io/hostname: "master1"
hostPort
这里我们使用 hostport 模式将 Traefik 固定到 master1 节点上,所以我们这里 master1 是作为流量的入口点。(该节点需要能连通外网,毕竟是对外提供服务)
直接使用上面的 values 文件安装 traefik:
cd traefik-helm-chart/
➜ helm install --namespace kube-system traefik ./traefik -f ./values-prod.yaml
NAME: traefik
LAST DEPLOYED: Thu Dec 24 11:23:51 2020
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
➜ kubectl get pods -n kube-system -l app.kubernetes.io/name=traefik
NAME READY STATUS RESTARTS AGE
traefik-78ff486794-64jbd 1/1 Running 0 3m15s
安装完成后我们可以通过查看 Pod 的资源清单来了解 Traefik 的运行方式:
➜ kubectl get pods traefik-78ff486794-64jbd -n kube-system -o yaml
apiVersion: v1
kind: Pod
metadata:
......
spec:
containers:
- args:
- --global.checknewversion
- --global.sendanonymoususage
- --entryPoints.traefik.address=:9000/tcp
- --entryPoints.web.address=:8000/tcp #对应的是pod的80端口
- --entryPoints.websecure.address=:8443/tcp
- --api.dashboard=true
- --ping=true
- --providers.kubernetescrd
- --providers.kubernetesingress
- --accesslog=true
- --accesslog.fields.defaultmode=keep
- --accesslog.fields.headers.defaultmode=drop
...
其中 entryPoints 属性定义了 web 和 websecure 这两个入口点的,并开启 kubernetesingress 和 kubernetescrd 这两个 provider,也就是我们可以使用 Kubernetes 原本的 Ingress 资源对象,也可以使用 Traefik 自己扩展的 IngressRoute 这样的 CRD 资源对象。
provider:自动发现服务的平台,一般是kubernetes的原生的ingress和traefik引进的crd资源ingressroute
我们可以首先创建一个用于 Dashboard 访问的 IngressRoute 资源清单:
➜ cat <<EOF | kubectl apply -f -
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: traefik-dashboard
namespace: kube-system
spec:
entryPoints: #使用哪个traefik的接收请求入口
- web
routes: #定义路由规则
- match: Host(`traefik.qikqiak.com`) # 指定域名
kind: Rule
services: #匹配该规则的流量路由到哪个service上
- name: api@internal
kind: TraefikService # 引用另外的 Traefik Service
EOF
➜ kubectl get ingressroute -n kube-system
NAME AGE
traefik-dashboard 19m
其中的 TraefikService 是 Traefik Service 的一个 CRD 实现,这里我们使用的 api@internal 这个 TraefikService,表示我们访问的是 Traefik 内置的应用服务。
部署完成后我们可以通过在本地 /etc/hosts 中添加上域名 traefik.qikqiak.com 的映射即可访问 Traefik 的 Dashboard 页面了:
另外需要注意的是默认情况下 Traefik 的 IngressRoute 已经允许跨 namespace 进行通信了,可以通过设置参数 --providers.kubernetescrd.allowCrossNamespace=true 开启(默认已经开启),开启后 IngressRoute 就可以引用 IngressRoute 命名空间以外的其他命名空间中的任何资源了。
ingress ingressRoute service这些资源对象都是有namspace的,默认是default,treafik默认可以跨namespace通信,其实就是跨namespace访问service
Traefik 通过扩展 CRD 的方式来扩展 Ingress 的功能,跟nginx-ingress除了默认的用 Secret 的方式可以支持应用的 HTTPS 之外,还支持自动生成 HTTPS 证书。
apiVersion: v1
kind: Service
metadata:
name: whoami
spec:
ports:
- protocol: TCP
name: web
port: 80
selector:
app: whoami
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: whoami
labels:
app: whoami
spec:
replicas: 2
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: containous/whoami
ports:
- name: web
containerPort: 80
然后定义一个 IngressRoute 对象:
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: simpleingressroute
spec:
entryPoints:
- web
routes:
- match: Host(`who.qikqiak.com`) && PathPrefix(`/notls`)
kind: Rule
services:
- name: whoami
port: 80
prefix前缀
通过 entryPoints 指定了我们这个应用的入口点是 web,然后访问的规则就是要匹配 who.qikqiak.com 这个域名,并且具有 /notls 的路径前缀的请求才会被 whoami 这个 Service 所匹配。
直接创建上面的几个资源对象,然后对域名做对应的解析
在 IngressRoute 对象中我们定义了一些匹配规则,这些规则在 Traefik 中有如下定义方式
如果我们需要用 HTTPS 来访问我们这个应用的话,就需要监听 websecure 这个入口点,也就是通过 443 端口来访问,同样用 HTTPS 访问应用必然就需要证书,这里我们用 openssl 来创建一个自签名的证书:
➜ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=who.qikqiak.com"
然后通过 Secret 对象来引用证书文件:
# 要注意证书文件名称必须是 tls.crt 和 tls.key
➜ kubectl create secret tls who-tls --cert=tls.crt --key=tls.key
secret/who-tls created
这个时候我们就可以创建一个 HTTPS 访问应用的 IngressRoute 对象了:
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: ingressroutetls
spec:
entryPoints:
- websecure
routes:
- match: Host(`who.qikqiak.com`) && PathPrefix(`/tls`)
kind: Rule
services:
- name: whoami
port: 80
tls:
secretName: who-tls
创建完成后就可以通过 HTTPS 来访问应用了,由于我们是自签名的证书,所以证书是不受信任的:
除了手动提供证书的方式之外 Traefik 同样也支持使用 Let’s Encrypt 自动生成证书,要使用 Let’s Encrypt 来进行自动化 HTTPS,就需要首先开启 ACME,开启 ACME 需要通过静态配置的方式,也就是说可以通过环境变量、启动参数等方式来提供。
Traefik用ACME 有多种校验方式 tlsChallenge、httpChallenge 和 dnsChallenge 三种验证方式,之前更常用的是 http 这种验证方式,要使用 tls 校验方式的话需要保证 Traefik 的 443 端口是可达的,dns 校验方式可以生成通配符的证书,只需要配置上 DNS 解析服务商的 API 访问密钥即可校验。这里用 DNS 校验的方式配置 ACME。
我们可以重新修改 Helm 安装的 values 配置文件,添加如下所示的定制参数:
# values-prod.yaml
additionalArguments:
# 使用 dns 验证方式
- --certificatesResolvers.ali.acme.dnsChallenge.provider=alidns
# 先使用staging环境进行验证,验证成功后再使用移除下面一行的配置
# - --certificatesResolvers.ali.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory
# 邮箱配置
- --certificatesResolvers.ali.acme.email=[email protected]
# 保存 ACME 证书的位置
- --certificatesResolvers.ali.acme.storage=/data/acme.json
envFrom:
- secretRef:
name: traefik-alidns-secret
# ALICLOUD_ACCESS_KEY
# ALICLOUD_SECRET_KEY
# ALICLOUD_REGION_ID
persistence:
enabled: true # 开启持久化
accessMode: ReadWriteOnce
size: 128Mi
path: /data
# 由于上面持久化了ACME的数据,需要重新配置下面的安全上下文
securityContext:
readOnlyRootFilesystem: false
runAsGroup: 0
runAsUser: 0
runAsNonRoot: false
这样我们可以通过设置 --certificatesresolvers.ali.acme.dnschallenge.provider=alidns 参数来指定指定阿里云的 DNS 校验,要使用阿里云的 DNS 校验我们还需要配置3个环境变量:ALICLOUD_ACCESS_KEY、ALICLOUD_SECRET_KEY、ALICLOUD_REGION_ID,分别对应我们平时开发阿里云应用的时候的密钥,可以登录阿里云后台获取,由于这是比较私密的信息,所以我们用 Secret 对象来创建:
➜ kubectl create secret generic traefik-alidns-secret --from-literal=ALICLOUD_ACCESS_KEY=<aliyun ak> --from-literal=ALICLOUD_SECRET_KEY=<aliyun sk> --from-literal=ALICLOUD_REGION_ID=cn-beijing -n kube-system
region_id一般是cn-hangzhou,cn-beijing之类
创建用户默认会有一个access key,没什么用,删除新建一个
注意弹窗的提示
创建完成后将这个 Secret 通过环境变量配置到 Traefik 的应用中,还有一个值得注意的是验证通过的证书我们这里存到 /data/acme.json 文件中,我们一定要将这个文件持久化,否则每次 Traefik 重建后就需要重新认证,而 Let’s Encrypt 本身校验次数是有限制的。
➜ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
name: traefik
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 128Mi
hostPath:
path: /data/k8s/traefik
EOF
然后使用如下所示的命令更新 Traefik:
➜ helm upgrade --install traefik --namespace=kube-system ./traefik -f ./values-prod.yaml
Release "traefik" has been upgraded. Happy Helming!
NAME: traefik
LAST DEPLOYED: Thu Dec 24 14:32:04 2020
NAMESPACE: kube-system
STATUS: deployed
REVISION: 2
TEST SUITE: None
更新完成后现在我们来修改上面我们的 whoami 应用:
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: ingressroutetls
spec:
entryPoints:
- websecure
routes:
- match: Host(`who.qikqiak.com`) && PathPrefix(`/tls`)
kind: Rule
services:
- name: whoami
port: 80
tls:
certResolver: ali
domains:
- main: "*.qikqiak.com"
其他的都不变,只需要将 tls 部分改成我们定义的 ali 这个证书解析器,如果我们想要生成一个通配符的域名证书的话可以定义 domains 参数来指定(指定证书类型是通配符证书),然后更新 IngressRoute 对象,这个时候我们再去用 HTTPS 访问我们的应用(当然需要将域名在阿里云 DNS 上做解析):
我们可以看到访问应用已经是受浏览器信任的证书了,查看证书我们还可以发现该证书是一个通配符的证书。(合法证书)
SSL证书是一种数字证书,主要是保护网站信息传输和对服务器进行身份认证,根据一张SSL证书保护的域名数量和类型,可以把SSL证书分为单域名证书、多域名证书和通配符证书三种。单域名证书是一张SSL证书可以保护一个域名,域名是主域名或者子域名都可以;多域名证书是可以一张SSL证书保护多个子域名或者主域名,一般申请证书之后会默认保护3-5个任意域名,想要添加域名就需要继续付费,费用是比再买一个单域名证书要少很多,也更好管理;通配符证书就是可以用一张SSL证书保护主域名及下所有子域名,而且添加子域名时并不用再付费,对于域名比较多,尤其是子域名比较多的网站,通配符证书是性价比较高的一款SSL证书。
通配符证书的作用:
通配符证书适合大部分的网站,因为通配符证书有的可能和一个单域名证书的价格差不多,但是保护的域名却比单域名证书多了很多。通配符证书通常可以用一张SSL证书保护多个网站,网站证书过期时间一样,这样就不用费心注意各个网站的SSL证书过期时间一个个更换,统一管理节省了资金和人力,所以比较受欢迎
中间件是 Traefik2.x 中一个非常有特色的功能,我们可以根据自己的各种需求去选择不同的中间件来满足服务,Traefik 官方已经内置了许多不同功能的中间件,其中一些可以修改请求,头信息,一些负责重定向,一些添加身份验证等等,而且中间件还可以通过链式组合的方式来适用各种情况。
同样比如上面我们定义的 whoami 这个应用,我们可以通过 https://who.qikqiak.com/tls 来访问到应用,但是如果我们用 http 来访问的话呢就不行了,就会404了,因为对于这个域名和路径我们根本就没有监听80端口这个入口点,所以要想通过 http 来访问应用的话自然我们需要监听下 web 这个入口点:
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: ingressroutetls-http
spec:
entryPoints:
- web
routes:
- match: Host(`who.qikqiak.com`) && PathPrefix(`/tls`)
kind: Rule
services:
- name: whoami
port: 80
注意这里我们创建的 IngressRoute 的 entryPoints 是 web,然后创建这个对象,这个时候我们就可以通过 http 访问到这个应用了。
但是我们如果只希望用户通过 https 来访问应用的话呢?按照以前的知识,我们是不是可以让 http 强制跳转到 https 服务去,对的,在 Traefik 中也是可以配置强制跳转的,只是这个功能现在是通过中间件来提供的了。如下所示,我们使用 redirectScheme 中间件来创建提供强制跳转服务:
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: redirect-https
spec:
redirectScheme:
scheme: https
然后将这个中间件附加到 http 的服务上面去,因为 https 的不需要跳转:
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: ingressroutetls-http
spec:
entryPoints:
- web
routes:
- match: Host(`who.qikqiak.com`) && PathPrefix(`/tls`)
kind: Rule
services:
- name: whoami
port: 80
middlewares:
- name: redirect-https
这个时候我们再去访问 http 服务可以发现就会自动跳转到 https 去了
更多的中间件的内容
虽然 Traefik 已经默认实现了很多中间件,可以满足大部分我们日常的需求,但是在实际工作中,用户仍然还是有自定义中间件的需求,这就 Traefik Pilot 的功能了。
Traefik Pilot 是一个 SaaS 平台(软件及服务),和 Traefik 进行链接来扩展其功能,它提供了很多功能,通过一个全局控制面板和 Dashboard 来增强对 Traefik 的观测和控制:
Traefik 代理和代理组的网络活动的指标
服务健康问题和安全漏洞警报
扩展 Traefik 功能的插件
在 Traefik 可以使用 Traefik Pilot 的功能之前,必须先连接它们,我们只需要对 Traefik 的静态配置(一般是不常修改的配置)进行少量更改即可。
Traefik 代理必须要能访问互联网才能连接到 Traefik Pilot,通过 HTTPS 在 443 端口上建立连接。
首先我们需要在 Traefik Pilot 主页上(https://pilot.traefik.io/)创建一个帐户,注册新的 Traefik 实例并开始使用 Traefik Pilot。登录后,可以通过选择 Register New Traefik Instance来创建新实例。
另外,当我们的 Traefik 尚未连接到 Traefik Pilot 时,Traefik Web UI 中将出现一个响铃图标,我们可以选择 Connect with Traefik Pilot 导航到 Traefik Pilot UI 进行操作。
登录完成后,Traefik Pilot 会生成一个新实例的令牌,我们需要将这个 Token 令牌添加到 Traefik 静态配置中。
我们这里就是在 values-prod.yaml 文件中启用 Pilot 的配置:
# values-prod.yaml
# Activate Pilot integration
pilot:
enabled: true
token: "e079ea6e-536a-48c6-b3e3-f7cfaf94f477"
然后重新更新 Traefik:
➜ helm upgrade --install traefik --namespace=kube-system ./traefik -f ./values-prod.yaml
更新完成后,我们在 Traefik 的 Web UI 中就可以看到 Traefik Pilot UI 相关的信息了。
接下来我们就可以在 Traefik Pilot 的插件页面选择我们想要使用的插件,比如我们这里使用 Demo Plugin 这个插件。
点击右上角的 Install Plugin 按钮安装插件会弹出一个对话框提示我们如何安装。
册到 Traefik Pilot(已完成),然后需要以静态配置的方式添加这个插件到 Traefik 中,这里我们同样更新 values-prod.yaml 文件中的 Values 值即可:
# values-prod.yaml
# Activate Pilot integration
pilot:
enabled: true
token: "e079ea6e-536a-48c6-b3e3-f7cfaf94f477"
additionalArguments:
# 添加 demo plugin 的支持
- --experimental.plugins.plugindemo.modulename=github.com/traefik/plugindemo
- --experimental.plugins.plugindemo.version=v0.2.1
# 其他配置
同样重新更新 Traefik:
➜ helm upgrade --install traefik --namespace=kube-system ./traefik -f ./values-prod.yaml
更新完成后创建一个如下所示的 Middleware 对象:
➜ cat <<EOF | kubectl apply -f -
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: myplugin
spec:
plugin:
plugindemo: # 插件名
Headers:
X-Demo: test
Foo: bar
EOF
然后添加到上面的 whoami 应用的 IngressRoute 对象中去:
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: simpleingressroute
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(`who.qikqiak.com`) && PathPrefix(`/notls`)
kind: Rule
services:
- name: whoami # K8s Service
port: 80
middlewares:
- name: myplugin # 使用上面新建的 middleware
更新完成后,当我们去访问 http://who.qikqiak.com/notls 的时候就可以看到新增了两个上面插件中定义的两个 Header。
当然除了使用 Traefik Pilot 上开发者提供的插件之外,我们也可以根据自己的需求自行开发自己的插件
Traefik2.0 的一个更强大的功能就是灰度发布,灰度发布我们有时候也会称为金丝雀发布(Canary),主要就是让一部分测试的服务也参与到线上去(或者说将某部分的用户流量导入到这里),经过测试观察看是否符号上线要求。
比如现在我们有两个名为 appv1 和 appv2 的服务,我们希望通过 Traefik 来控制我们的流量,将 3⁄4 的流量路由到 appv1,¼ 的流量路由到 appv2 去,这个时候就可以利用 Traefik2.0 中提供的**带权重的轮询(WRR)**来实现该功能,首先在 Kubernetes 集群中部署上面的两个服务。为了对比结果我们这里提供的两个服务一个是 whoami,一个是 nginx,方便测试。
appv1 服务的资源清单如下所示:(appv1.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: appv1
spec:
selector:
matchLabels:
app: appv1
template:
metadata:
labels:
use: test
app: appv1
spec:
containers:
- name: whoami
image: containous/whoami
ports:
- containerPort: 80
name: portv1
---
apiVersion: v1
kind: Service
metadata:
name: appv1
spec:
selector:
app: appv1
ports:
- name: http
port: 80
targetPort: portv1
appv2 服务的资源清单如下所示:(appv2.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: appv2
spec:
selector:
matchLabels:
app: appv2
template:
metadata:
labels:
use: test
app: appv2
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
name: portv2
---
apiVersion: v1
kind: Service
metadata:
name: appv2
spec:
selector:
app: appv2
ports:
- name: http
port: 80
targetPort: portv2
直接创建上面两个服务:
➜ kubectl apply -f appv1.yaml
➜ kubectl apply -f appv2.yaml
# 通过下面的命令可以查看服务是否运行成功
➜ kubectl get pods -l use=test
NAME READY STATUS RESTARTS AGE
appv1-58f856c665-shm9j 1/1 Running 0 12s
appv2-ff5db55cf-qjtrf 1/1 Running 0 12s
在 Traefik2.1 中新增了一个 TraefikService 的 CRD 资源,我们可以直接利用这个对象来配置 WRR(做负载均衡),之前的版本需要通过 File Provider,比较麻烦,新建一个描述 WRR 的资源清单:(wrr.yaml)
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: app-wrr
spec:
weighted:
services:
- name: appv1
weight: 3 # 定义权重
port: 80
kind: Service # 可选,默认就是 Service
- name: appv2
weight: 1
port: 80
然后为我们的灰度发布的服务创建一个 IngressRoute 资源对象:(ingressroute.yaml)
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: wrringressroute
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(`wrr.qikqiak.com`)
kind: Rule
services:
- name: app-wrr
kind: TraefikService
不过需要注意的是现在我们配置的 Service 不再是直接的 Kubernetes 对象了,而是上面我们定义的 TraefikService 对象,直接创建上面的两个资源对象,这个时候我们对域名 wrr.qikqiak.com 做上解析,去浏览器中连续访问 4 次,我们可以观察到 appv1 这应用会收到 3 次请求,而 appv2 这个应用只收到 1 次请求,符合上面我们的 3:1 的权重配置。
除了灰度发布之外,Traefik 2.0 还引入了流量镜像服务,是一种可以将流入流量复制并同时将其发送给其他服务的方法,镜像服务可以获得给定百分比的请求同时也会忽略这部分请求的响应。(就是只转发请求到对应的服务测试,但不接受服务器的应答)
现在我们部署两个 whoami 的服务,资源清单文件如下所示:
apiVersion: v1
kind: Service
metadata:
name: v1
spec:
ports:
- protocol: TCP
name: web
port: 80
selector:
app: v1
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: v1
labels:
app: v1
spec:
selector:
matchLabels:
app: v1
template:
metadata:
labels:
app: v1
spec:
containers:
- name: v1
image: nginx
ports:
- name: web
containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: v2
spec:
ports:
- protocol: TCP
name: web
port: 80
selector:
app: v2
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: v2
labels:
app: v2
spec:
selector:
matchLabels:
app: v2
template:
metadata:
labels:
app: v2
spec:
containers:
- name: v2
image: nginx
ports:
- name: web
containerPort: 80
直接创建上面的资源对象:
➜ kubectl get pods
NAME READY STATUS RESTARTS AGE
v1-77cfb86999-wfbl2 1/1 Running 0 94s
v2-6f45d498b7-g6qjt 1/1 Running 0 91s
➜ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
v1 ClusterIP 10.96.218.173 <none> 80/TCP 99s
v2 ClusterIP 10.99.98.48 <none> 80/TCP 96s
现在我们创建一个 IngressRoute 对象,将服务 v1 的流量复制 50% 到服务 v2,如下资源对象所示:(mirror-ingress-route.yaml)
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: app-mirror
spec:
mirroring:
name: v1 # 发送 100% 的请求到 K8S 的 Service "v1"
port: 80
mirrors:
- name: v2 # 然后复制 50% 的请求到 v2
percent: 50
port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: mirror-ingress-route
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(`mirror.qikqiak.com`)
kind: Rule
services:
- name: app-mirror
kind: TraefikService # 使用声明的 TraefikService 服务,而不是 K8S 的 Service
然后直接创建这个资源对象即可:
➜ kubectl apply -f mirror-ingress-route.yaml
ingressroute.traefik.containo.us/mirror-ingress-route created
traefikservice.traefik.containo.us/mirroring-example created
这个时候我们在浏览器中去连续访问4次 mirror.qikqiak.com 可以发现有一半的请求也出现在了 v2 这个服务中
另外 Traefik2.X 已经支持了 TCP 服务的,下面我们以 mongo 为例来了解下 Traefik 是如何支持 TCP 服务的
首先部署一个普通的 mongo 服务,资源清单文件如下所示:(mongo.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: mongo-traefik
labels:
app: mongo-traefik
spec:
selector:
matchLabels:
app: mongo-traefik
template:
metadata:
labels:
app: mongo-traefik
spec:
containers:
- name: mongo
image: mongo:4.0
ports:
- containerPort: 27017
---
apiVersion: v1
kind: Service
metadata:
name: mongo-traefik
spec:
selector:
app: mongo-traefik
ports:
- port: 27017
直接创建 mongo 应用:
➜ kubectl apply -f mongo.yaml
deployment.apps/mongo-traefik created
service/mongo-traefik created
创建成功后就可以来为 mongo 服务配置一个路由了。由于 Traefik 中使用 TCP 路由配置需要 SNI,而 SNI 又是依赖 TLS 的,所以我们需要配置证书才行,如果没有证书的话,我们可以使用通配符 * 进行配置,我们这里创建一个 IngressRouteTCP 类型的 CRD 对象(前面我们就已经安装了对应的 CRD 资源):(mongo-ingressroute-tcp.yaml)
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteTCP
metadata:
name: mongo-traefik-tcp
spec:
entryPoints:
- mongo
routes:
- match: HostSNI(`*`)
services:
- name: mongo-traefik
port: 27017
要注意的是这里的 entryPoints 部分,是根据我们启动的 Traefik 的静态配置中的 entryPoints 来决定的,我们当然可以使用前面我们定义得 80 和 443 这两个入口点,但是也可以可以自己添加一个用于 mongo 服务的专门入口点,更新 values-prod.yaml 文件,新增 mongo 这个入口点:
# values-prod.yaml
# Configure ports
ports:
web:
port: 8000
hostPort: 80
websecure:
port: 8443
hostPort: 443
mongo:
port: 27017
hostPort: 27017
然后更新 Traefik 即可:
➜ helm upgrade --install traefik --namespace=kube-system ./traefik -f ./values-prod.yaml
这里给入口点添加 hostPort 是为了能够通过节点的端口访问到服务,关于 entryPoints 入口点的更多信息,可以查看文档 entrypoints 了解更多信息。
然后更新 Traefik 后我们就可以直接创建上面的资源对象:
➜ mongo-ingressroute-tcp.yaml
ingressroutetcp.traefik.containo.us/mongo-traefik-tcp created
创建完成后,同样我们可以去 Traefik 的 Dashboard 页面上查看是否生效:
然后我们配置一个域名 mongo.local 解析到 Traefik 所在的节点,然后通过 27017 端口来连接 mongo 服务:
➜ mongo --host mongo.local --port 27017
mongo(75243,0x1075295c0) malloc: *** malloc_zone_unregister() failed for 0x7fffa56f4000
MongoDB shell version: 2.6.1
connecting to: mongo.local:27017/test
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
到这里我们就完成了将 mongo(TCP)服务暴露给外部用户了。
上面我们部署的 mongo 是一个普通的服务,然后用 Traefik 代理的,但是有时候为了安全 mongo 服务本身还会使用 TLS 证书的形式提供服务,下面是用来生成 mongo tls 证书的脚本文件:(generate-certificates.sh)
#!/bin/bash
#
# From https://medium.com/@rajanmaharjan/secure-your-mongodb-connections-ssl-tls-92e2addb3c89
set -eu -o pipefail
DOMAINS="${1}"
CERTS_DIR="${2}"
[ -d "${CERTS_DIR}" ]
CURRENT_DIR="$(cd "$(dirname "${0}")" && pwd -P)"
GENERATION_DIRNAME="$(echo "${DOMAINS}" | cut -d, -f1)"
rm -rf "${CERTS_DIR}/${GENERATION_DIRNAME:?}" "${CERTS_DIR}/certs"
echo "== Checking Requirements..."
command -v go >/dev/null 2>&1 || echo "Golang is required"
command -v minica >/dev/null 2>&1 || go get github.com/jsha/minica >/dev/null
echo "== Generating Certificates for the following domains: ${DOMAINS}..."
cd "${CERTS_DIR}"
minica --ca-cert "${CURRENT_DIR}/minica.pem" --ca-key="${CURRENT_DIR}/minica-key.pem" --domains="${DOMAINS}"
mv "${GENERATION_DIRNAME}" "certs"
cat certs/key.pem certs/cert.pem > certs/mongo.pem
echo "== Certificates Generated in the directory ${CERTS_DIR}/certs"
将上面证书放置到 certs 目录下面,然后我们新建一个 02-tls-mongo 的目录,在该目录下面执行如下命令来生成证书:
➜ bash ../certs/generate-certificates.sh mongo.local .
== Checking Requirements...
== Generating Certificates for the following domains: mongo.local...
最后的目录如下所示,在 02-tls-mongo 目录下面会生成包含证书的 certs 目录:
➜ tree .
.
├── 01-mongo
│ ├── mongo-ingressroute-tcp.yaml
│ └── mongo.yaml
├── 02-tls-mongo
│ └── certs
│ ├── cert.pem
│ ├── key.pem
│ └── mongo.pem
└── certs
├── generate-certificates.sh
├── minica-key.pem
└── minica.pem
在 02-tls-mongo/certs 目录下面执行如下命令通过 Secret 来包含证书内容:
➜ kubectl create secret tls traefik-mongo-certs --cert=cert.pem --key=key.pem
secret/traefik-mongo-certs created
然后重新更新 IngressRouteTCP 对象,增加 TLS 配置:
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteTCP
metadata:
name: mongo-traefik-tcp
spec:
entryPoints:
- mongo
routes:
- match: HostSNI(`mongo.local`)
services:
- name: mongo-traefik
port: 27017
tls:
secretName: traefik-mongo-certs
同样更新后,现在我们直接去访问应用就会被 hang 住,因为我们没有提供证书:
➜ mongo --host mongo.local --port 27017
MongoDB shell version: 2.6.1
connecting to: mongo1.local:27017/test
这个时候我们可以带上证书来进行连接:
➜ mongo --host mongo.local --port 27017 --ssl --sslCAFile=../certs/minica.pem --sslPEMKeyFile=./certs/mongo.pem
MongoDB shell version v4.0.3
connecting to: mongodb://mongo.local:27017/
Implicit session: session { "id" : UUID("e7409ef6-8ebe-4c5a-9642-42059bdb477b") }
MongoDB server version: 4.0.14
......
> show dbs;
admin 0.000GB
config 0.000GB
local 0.000GB
可以看到现在就可以连接成功了,这样就完成了一个使用 TLS 证书代理 TCP 服务的功能,这个时候如果我们使用其他的域名去进行连接就会报错了,因为现在我们指定的是特定的 HostSNI:
➜ mongo --host mongo.k8s.local --port 27017 --ssl --sslCAFile=../certs/minica.pem --sslPEMKeyFile=./certs/mongo.pem
MongoDB shell version v4.0.3
connecting to: mongodb://mongo.k8s.local:27017/
2019-12-29T15:03:52.424+0800 E NETWORK [js] SSL peer certificate validation failed: Certificate trust failure: CSSMERR_TP_NOT_TRUSTED; connection rejected
2019-12-29T15:03:52.429+0800 E QUERY [js] Error: couldn't connect to server mongo.qikqiak.com:27017, connection attempt failed: SSLHandshakeFailed: SSL peer certificate validation failed: Certificate trust failure: CSSMERR_TP_NOT_TRUSTED; connection rejected :
connect@src/mongo/shell/mongo.js:257:13
@(connect):1:6
exception: connect failed
此外 Traefik2.3.x 版本也已经提供了对 UDP 的支持,所以我们可以用于诸如 DNS 解析的服务提供负载。同样首先部署一个如下所示的 UDP 服务:
apiVersion: v1
kind: Service
metadata:
name: whoamiudp
spec:
ports:
- protocol: UDP
name: udp
port: 8080
selector:
app: whoamiudp
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: whoamiudp
labels:
app: whoamiudp
spec:
replicas: 2
selector:
matchLabels:
app: whoamiudp
template:
metadata:
labels:
app: whoamiudp
spec:
containers:
- name: whoamiudp
image: containous/whoamiudp
ports:
- name: udp
containerPort: 8080
直接部署上面的应用,部署完成后我们需要在 Traefik 中定义一个 UDP 的 entryPoint 入口点,修改我们部署 Traefik 的 values-prod.yaml 文件,增加 UDP 协议的入口点:
# values-prod.yaml
# Configure ports
ports:
web:
port: 8000
hostPort: 80
websecure:
port: 8443
hostPort: 443
mongo:
port: 27017
hostPort: 27017
udpep:
port: 18080
hostPort: 18080
protocol: UDP
我们这里定义了一个名为 udpep 的入口点,但是 protocol 协议是 UDP(此外 TCP 和 UDP 共用同一个端口也是可以的,但是协议一定要声明为不一样),然后重新更新 Traefik:
➜ helm upgrade --install traefik --namespace=kube-system ./traefik -f ./values-prod.yaml
更新完成后我们可以导出 Traefik 部署的资源清单文件来检测是否增加上了 UDP 的入口点:
➜ kubectl get deploy traefik -n kube-system -o yaml
......
containers:
- args:
- --entryPoints.mongo.address=:27017/tcp
- --entryPoints.traefik.address=:9000/tcp
- --entryPoints.udpep.address=:18080/udp
- --entryPoints.web.address=:8000/tcp
- --entryPoints.websecure.address=:8443/tcp
- --api.dashboard=true
- --ping=true
- --providers.kubernetescrd
- --providers.kubernetesingress
......
UDP 的入口点增加成功后,接下来我们可以创建一个 IngressRouteUDP 类型的资源对象,用来代理 UDP 请求:
➜ cat <<EOF | kubectl apply -f -
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteUDP
metadata:
name: whoamiudp
spec:
entryPoints:
- udpep
routes:
- services:
- name: whoamiudp
port: 8080
EOF
➜ kubectl get ingressrouteudp
NAME AGE
whoamiudp 31s
创建成功后我们首先在集群上通过 Service 来访问上面的 UDP 应用:
➜ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
whoamiudp ClusterIP 10.106.10.185 <none> 8080/UDP 36m
➜ echo "WHO" | socat - udp4-datagram:10.106.10.185:8080
Hostname: whoamiudp-d884bdb64-6mpk6
IP: 127.0.0.1
IP: 10.244.1.145
➜ echo "othermessage" | socat - udp4-datagram:10.106.10.185:8080
Received: othermessage
我们这个应用当我们输入 WHO 的时候,就会打印出访问的 Pod 的 Hostname 这些信息,如果不是则打印接收到字符串。现在我们通过 Traefik 所在节点的 IP(10.151.30.11)与 18080 端口来访问 UDP 应用进行测试:
➜ echo "othermessage" | socat - udp4-datagram:10.151.30.11:18080
Received: othermessage
➜ echo "WHO" | socat - udp4-datagram:10.151.30.11:18080
Hostname: whoamiudp-d884bdb64-hkw6k
IP: 127.0.0.1
IP: 10.244.2.87
我们可以看到测试成功了,证明我就用 Traefik 来代理 UDP 应用成功了。
除此之外 Traefik 还有很多功能,特别是强大的中间件和自定义插件的功能,为我们提供了不断扩展其功能的能力,我们完全可以根据自己的需求进行二次开发。
附加:traefik