Skip to content

Traefik 灰度发布

作者: ryan 发布于: 1970/1/1 更新于: 1970/1/1 字数: 0 字 阅读: 0 分钟

Traefik2.0 的一个更强大的功能就是灰度发布,灰度发布我们有时候也会称为金丝雀发布(Canary),主要就是让一部分测试的服务也参与到线上去,经过测试观察看是否符号上线要求。

img

比如现在我们有两个名为 appv1 和 appv2 的服务,我们希望通过 Traefik 来控制我们的流量,将 3⁄4 的流量路由到 appv1,1/4 的流量路由到 appv2 去,这个时候就可以利用

Traefik2.0 中提供的带权重的轮询(WRR)来实现该功能,首先在 Kubernetes 集群中部署上面的两个服务。

为了对比结果我们这里提供的两个服务一个是 whoami,一个是 nginx,方便测试。

创建应用

appv1 服务的资源清单如下所示:

yaml
# canary-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: registry.cn-beijing.aliyuncs.com/xxk8s/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 服务的资源清单如下所示:

yaml
# canary-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: registry.cn-beijing.aliyuncs.com/xxk8s/nginx:1.26.2
          ports:
            - containerPort: 80
              name: portv2
---
apiVersion: v1
kind: Service
metadata:
  name: appv2
spec:
  selector:
    app: appv2
  ports:
    - name: http
      port: 80
      targetPort: portv2

直接创建上面两个服务:

shell
root@master01:/k8s-traefik# kubectl apply -f canary-appv1.yaml -n test-pod
deployment.apps/appv1 created
service/appv1 created
root@master01:/k8s-traefik#
root@master01:/k8s-traefik# kubectl apply -f canary-appv2.yaml -n test-pod
deployment.apps/appv2 created
service/appv2 created


# 通过下面的命令可以查看服务是否运行成功
root@master01:/k8s-traefik# kubectl get pods -l use=test -n test-pod
NAME                     READY   STATUS    RESTARTS   AGE
appv1-85fc57d66b-k2vf8   1/1     Running   0          31s
appv2-f45bb89d9-hrn2w    1/1     Running   0          27s

创建 TraefikService

在 Traefik2.1 中新增了一个 TraefikService 的 CRD 资源,我们可以直接利用这个对象来配置 WRR,之前的版本需要通过 File Provider,比较麻烦,新建一个描述 WRR 的资源清单:

yaml
# canary-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

在 dashboard 中查看

img

创建 IngressRout

然后为我们的灰度发布的服务创建一个 IngressRoute 资源对象:

yaml
# canary-wrr-ingressroute.yaml
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: wrr-ingressroute
  namespace: default
spec:
  entryPoints:
    - web
  routes:
    - match: Host(`wrr.xinn.cc`)
      kind: Rule
      services:
        - name: app-wrr
          kind: TraefikService
bash
root@master01:/k8s-traefik# kubectl get traefikservices -n test-pod
NAME      AGE
app-wrr   2m24s

不过需要注意的是现在我们配置的 Service 不再是直接的 Kubernetes 对象了,而是上面我们定义的 TraefikService 对象,直接创建上面的两个资源对象,这个时候我们对域名 wrr.xinn.cc 做上解析,去浏览器中连续访问 4 次,我们可以观察到 appv1 这应用会收到 3 次请求,而 appv2 这个应用只收到 1 次请求,符合上面我们的 3:1 的权重配置。

img

流量复制

除了灰度发布之外,Traefik 2.0 还引入了流量镜像服务,是一种可以将流入流量复制并同时将其发送给其他服务的方法,镜像服务可以获得给定百分比的请求同时也会忽略这部分请求的响应。

img

创建应用

现在我们部署两个 nginx 服务,资源清单文件如下所示:

yaml
#mirror-nginx.yaml
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: registry.cn-beijing.aliyuncs.com/xxk8s/nginx:1.26.2
          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: registry.cn-beijing.aliyuncs.com/xxk8s/nginx:1.26.2
          ports:
            - name: web
              containerPort: 80

直接创建上面的资源对象:

shell
root@master01:/k8s-traefik# kubectl get pod -n test-pod
NAME                      READY   STATUS    RESTARTS   AGE
v1-66ff54f898-bjpg5       1/1     Running   0          64s
v2-5988dc45b6-4xs8n       1/1     Running   0          63s

root@master01:/k8s-traefik# kubectl get svc -n test-pod
NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
v1               ClusterIP   10.96.156.180   <none>        80/TCP           95s
v2               ClusterIP   10.106.250.53   <none>        80/TCP           94s

创建流量复制 IngressRoute

现在我们创建一个 IngressRoute 对象,将服务 v1 的流量复制 50% 到服务 v2,如下资源对象所示:

yaml
# 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.xinn.cc`)
      kind: Rule
      services:
        - name: app-mirror
          kind: TraefikService # 使用声明的 TraefikService 服务,而不是 K8S 的 Service

然后直接创建这个资源对象即可:

shell
root@master01:/k8s-traefik# kubectl apply -f mirror-ingress-route.yaml -n test-pod
traefikservice.traefik.containo.us/app-mirror unchanged
ingressroute.traefik.containo.us/mirror-ingress-route created

测试镜像流量

这个时候我们在浏览器中去连续访问 4 次 mirror.xinn.cc 可以发现有一半的请求也出现在了 v2 这个服务中:

img

TCP 代理

另外 Traefik2.X 已经支持了 TCP 服务的,下面我们以 mongo 为例来了解下 Traefik 是如何支持 TCP 服务得。

创建一个简单 TCP 服务

首先部署一个普通的 mongo 服务,资源清单文件如下所示:(mongo.yaml)

yaml
#mongo-tcp.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: registry.cn-beijing.aliyuncs.com/xxk8s/mongo:4.0
          ports:
            - containerPort: 27017
---
apiVersion: v1
kind: Service
metadata:
  name: mongo-traefik
spec:
  selector:
    app: mongo-traefik
  ports:
    - port: 27017

直接创建 mongo 应用:

shell
root@master01:/k8s-traefik# kubectl apply -f mongo-tcp.yaml -n test-pod
deployment.apps/mongo-traefik created
service/mongo-traefik created

创建成功后就可以来为 mongo 服务配置一个路由了。

创建 IngressRouteTCP

由于 Traefik 中使用 TCP 路由配置需要 SNI,而 SNI 又是依赖 TLS 的,所以我们需要配置证书才行,如果没有证书的话,我们可以使用通配符 * 进行配置,我们这里创建一个 IngressRouteTCP 类型的 CRD 对象(前面我们就已经安装了对应的 CRD 资源):

yaml
# 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
bash
root@master01:/k8s-traefik# kubectl apply -f mongo-ingressroute-tcp.yaml -n test-pod
ingressroutetcp.traefik.containo.us/mongo-traefik-tcp created

要注意的是这里的 entryPoints 部分,是根据我们启动的 Traefik 的静态配置中的 entryPoints 来决定的,我们当然可以使用前面我们定义得 80 和 443 这两个入口点,但是也可以可以自己添加一个用于 mongo 服务的专门入口点,更新 xin-traefik-valuse.yaml 文件,新增 mongo 这个入口点:

yaml
# xin-traefik-valuse.yaml
ports:
  mongo:
    expose: true
    port: 27017
    protocol: TCP
    nodePort: 31117

然后更新 Traefik 即可:

shell
$ helm upgrade --install traefik ./ -f ./xin-traefik-valuse.yaml --namespace kube-system

在节点主机上查看端口是否监听

bash
root@master01:/k8s-traefik/traefik# netstat -lntup | grep 31888
udp        0      0 0.0.0.0:31888           0.0.0.0:*                           2798428/kube-proxy
root@master01:/k8s-traefik/traefik#
root@master01:/k8s-traefik/traefik# netstat -lntup | grep 31117
tcp        0      0 0.0.0.0:31117           0.0.0.0:*               LISTEN      2798428/kube-proxy

这里给入口点添加 hostPort 是为了能够通过节点的端口访问到服务,关于 entryPoints 入口点的更多信息,可以查看文档 entrypoints 了解更多信息。

然后更新 Traefik 后我们就可以直接创建上面的资源对象:

shell
$ kubectl apply -f mongo-ingressroute-tcp.yaml

创建完成后,同样我们可以去 Traefik 的 Dashboard 页面上查看是否生效:

img

在其他节点上安装mongodb-clients

然后我们配置一个域名 mongo.local 解析到 Traefik 所在的节点,然后通过 31117 端口来连接 mongo 服务:

shell
root@harbor01[17:06:08]~ #:apt install mongodb-clients

root@harbor01[17:00:18]~ #:mongo --host mongo.local --port 31117
MongoDB shell version v3.6.8
connecting to: mongodb://mongo.local:31117/
Implicit session: session { "id" : UUID("177d54e9-a8ea-4ca3-b702-a6cf955b359b") }
MongoDB server version: 4.0.28
...
> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB

到这里我们就完成了将 mongo(TCP)服务暴露给外部用户了。

创建 MongoDB 证书

生成 CA 密钥和证书

bash
# 生成 CA 私钥
openssl genpkey -algorithm RSA -out ca.key -aes256 -pass pass:dasdcc123

# 生成 CA 自签名证书
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt -passin pass:dasdcc123 \
  -subj "/CN=MongoDB CA"

生成 MongoDB 服务端证书

bash
# 生成服务端私钥
openssl genpkey -algorithm RSA -out mongo-server.key -aes256 -pass pass:dasdcc123

# 生成服务端证书签名请求 (CSR)
openssl req -new -key mongo-server.key -out mongo-server.csr -passin pass:dasdcc123 \
  -subj "/CN=mongo.local"

# 使用 CA 签署服务端证书
openssl x509 -req -in mongo-server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out mongo-server.crt -days 365 -sha256 -passin pass:dasdcc123

# 将私钥转换为不加密的格式(MongoDB 需要不加密的私钥)
openssl rsa -in mongo-server.key -out mongo-server.key -passin pass:dasdcc123

生成 MongoDB 客户端证书

bash
# 生成客户端私钥
openssl genpkey -algorithm RSA -out mongo-client.key -aes256 -pass pass:dasdcc123

# 生成客户端证书签名请求 (CSR)
openssl req -new -key mongo-client.key -out mongo-client.csr -passin pass:dasdcc123 \
  -subj "/CN=mongo-client"

# 使用 CA 签署客户端证书
openssl x509 -req -in mongo-client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out mongo-client.crt -days 365 -sha256 -passin pass:dasdcc123

# 将私钥转换为不加密的格式(MongoDB 需要不加密的私钥)
openssl rsa -in mongo-client.key -out mongo-client.key -passin pass:dasdcc123
bash
root@master01:/k8s-traefik/certs# ls
ca.crt  ca.srl                    mongo-client.crt  mongo-client.key  mongo-server.csr
ca.key  generate-certificates.sh  mongo-client.csr  mongo-server.crt  mongo-server.key

在 Kubernetes 中创建 Secret

接下来将这些文件转换为 Kubernetes Secret。

创建 MongoDB 服务端 Secret

bash
kubectl create secret generic traefik-mongo-certs --from-file=tls.crt=mongo-server.crt --from-file=tls.key=mongo-server.key --from-file=ca.crt=ca.crt
bash
cat mongo-client.crt mongo-client.key > mongo-client.pem

创建IngressRouteTCP-TLS

然后重新更新 IngressRouteTCP 对象,增加 TLS 配置:

yaml
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
bash
root@master01:/k8s-traefik/certs# kubectl apply -f ../mongo-ingressroute-tcp.yaml -n test-pod
ingressroutetcp.traefik.containo.us/mongo-traefik-tcp configured

使用证书测试访问

同样更新后,现在我们直接去访问应用就会被 hang 住,因为我们没有提供证书:

shell
root@harbor01[17:44:50]~ #:mongo --host mongo.local --port 31117
MongoDB shell version v3.6.8
connecting to: mongodb://mongo.local:31117/

这个时候我们可以带上证书来进行连接:

shell
root@harbor01[17:51:43]~ #:mongo --host mongo.local --port 31117 --ssl --sslCAFile=./ca.crt --sslPEMKeyFile=./mongo-client.pem
MongoDB shell version v3.6.8
connecting to: mongodb://mongo.local:31117/
Implicit session: session { "id" : UUID("3c058482-b6c6-4aa0-90d8-3b64f1b8139e") }
MongoDB server version: 4.0.28

> show dbs;
admin   0.000GB
config  0.000GB
local   0.000GB

可以看到现在就可以连接成功了,这样就完成了一个使用 TLS 证书代理 TCP 服务的功能,这个时候如果我们使用其他的域名去进行连接就会报错了,因为现在我们指定的是特定的 HostSNI:

shell
root@harbor01[17:52:21]~ #:mongo --host k8s.mongo.local --port 31117 --ssl --sslCAFile=./ca.crt --sslPEMKeyFile=./mongo-client.pem
MongoDB shell version v3.6.8
connecting to: mongodb://k8s.mongo.local:31117/
2024-08-29T17:52:29.610+0800 I NETWORK  [thread1] getaddrinfo("k8s.mongo.local") failed: Temporary failure in name resolution
2024-08-29T17:52:29.610+0800 E QUERY    [thread1] Error: couldn't initialize connection to host k8s.mongo.local, address is invalid :
connect@src/mongo/shell/mongo.js:257:13
@(connect):1:6
exception: connect failed

UDP 代理

此外 Traefik2.3.x 版本也已经提供了对 UDP 的支持,所以我们可以用于诸如 DNS 解析的服务提供负载。

whoamiudp

同样首先部署一个如下所示的 UDP 服务:

yaml
#udp-whoami-app.yaml
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: registry.cn-beijing.aliyuncs.com/xxk8s/whoamiudp:latest
          ports:
            - name: udp
              containerPort: 8080
bash
root@master01:/k8s-traefik# kubectl apply -f udp-whoami-app.yaml -n test-pod
service/whoamiudp created
deployment.apps/whoamiudp created

创建entryPoint 入口点

直接部署上面的应用,部署完成后我们需要在 Traefik 中定义一个 UDP 的 entryPoint 入口点,修改我们部署 Traefik 的 values-prod.yaml 文件,增加 UDP 协议的入口点:

yaml
# Configure ports
ports:
  mongo:
    expose: true
    port: 27017
    nodePort: 31117
  udpep:
    expose: true
    port: 31888
    nodePort: 3
    protocol: UDP

我们这里定义了一个名为 udpep 的入口点,但是 protocol 协议是 UDP(此外 TCP 和 UDP 共用同一个端口也是可以的,但是协议一定要声明为不一样),然后重新更新 Traefik:

shell
$ helm upgrade --install traefik ./ -f ./xin-traefik-valuse.yaml --namespace kube-system

更新完成后我们可以导出 Traefik 部署的资源清单文件来检测是否增加上了 UDP 的入口点:

shell
$ kubectl get deploy traefik -n kube-system -o yaml
......
 containers:
 - args:
   - --entrypoints.metrics.address=:9100/tcp
   - --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
   - --metrics.prometheus=true
   - --metrics.prometheus.entrypoint=metrics
   - --providers.kubernetescrd
   - --providers.kubernetescrd.allowCrossNamespace=true
   - --providers.kubernetescrd.allowExternalNameServices=true
   - --providers.kubernetescrd.allowEmptyServices=true
   - --providers.kubernetesingress
   - --providers.kubernetesingress.allowExternalNameServices=true
   - --providers.kubernetesingress.allowEmptyServices=true
 
......

创建 IngressRouteUDP

UDP 的入口点增加成功后,接下来我们可以创建一个 IngressRouteUDP 类型的资源对象,用来代理 UDP 请求:

yaml
# udp-whoami-ingressroute.yaml
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteUDP
metadata:
  name: whoamiudp
spec:
  entryPoints:
  - udpep
  routes:
  - services:
    - name: whoamiudp
      port: 8080
shell
root@master01:/k8s-traefik# kubectl apply -f udp-whoami-ingressroute.yaml -n test-pod
ingressrouteudp.traefik.containo.us/whoamiudp created
root@master01:/k8s-traefik#

root@master01:/k8s-traefik# kubectl get ingressrouteudp -n test-pod
NAME        AGE
whoamiudp   13s

测试访问 UDP 应用

创建成功后我们首先在集群上通过 Service 来访问上面的 UDP 应用:

shell
root@master01:/k8s-traefik# kubectl get svc -n test-pod
NAME             TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
whoamiudp        ClusterIP   10.109.146.211   <none>        8080/UDP         7m5s



$ echo "WHO" | socat - udp4-datagram:10.109.146.211:8080
Hostname: whoamiudp-799555c775-tscfq
IP: 127.0.0.1
IP: ::1
IP: 10.244.0.60
IP: fe80::dceb:f0ff:fe27:2ab5

$ echo "othermessage" | socat - udp4-datagram:10.109.146.211:8080
Received: othermessage

我们这个应用当我们输入 WHO 的时候,就会打印出访问的 Pod 的 Hostname 这些信息,如果不是则打印接收到字符串。现在我们通过 Traefik 所在节点的 IP(10.1.0.16)与 31888 端口来访问 UDP 应用进行测试:

shell
root@master01:/k8s-traefik# echo "othermessage" | socat - udp4-datagram:10.1.0.17:31888
Received: othermessage
root@master01:/k8s-traefik# echo "WHO" | socat - udp4-datagram:10.1.0.16:31888
Hostname: whoamiudp-799555c775-tscfq
IP: 127.0.0.1
IP: ::1
IP: 10.244.0.60
IP: fe80::dceb:f0ff:fe27:2ab5

我们可以看到测试成功了,证明我就用 Traefik 来代理 UDP 应用成功了。除此之外 Traefik 还有很多功能,特别是强大的中间件和自定义插件的功能,为我们提供了不断扩展其功能的能力,我们完成可以根据自己的需求进行二次开发。

imgimg

多控制器

有的业务场景下可能需要在一个集群中部署多个 traefik,不同的实例控制不同的 IngressRoute 资源对象,要实现该功能有两种方法:

第一种方法:通过 annotations 注解筛选:

  • 首先在 traefik 中增加启动参数 --providers.kubernetescrd.ingressclass=traefik-in
  • 然后在 IngressRoute 资源对象中添加 kubernetes.io/ingress.class: traefik-in 注解即可

第二种方法:通过标签选择器进行过滤:

  • 首先在 traefik 中增加启动参数 --providers.kubernetescrd.labelselector=ingressclass=traefik-out
  • 然后在 IngressRoute 资源对象中添加 ingressclass: traefik-out 这个标签即可