目录

通过 Ingress-nginx 实现灰度发布

一、Ingress Controller 多种发布策略介绍

场景一:将新版本灰度给部分用户

场景二:切一定比例的流量给新版本 

二、模拟部署生产测试版本 Web 服务

2.1 部署一个 v1 版本:

2.2 再部署一个 v2 版本:

2.3 再创建一个 Ingress,对外暴露服务,指向 v1 版本的服务:

2.4 访问验证一下:

三、Ingress Controller 基于请求头、cookie 和权重发布业务

3.1 基于 Header 的流量切分:

3.2 基于 Cookie 的流量切分: 

3.3  基于服务权重的流量切分


通过 Ingress-nginx 实现灰度发布

一、Ingress Controller 多种发布策略介绍

场景一:将新版本灰度给部分用户

假设线上运行了一套对外提供 7 层服务的 Service A 服务,后来开发了个新版本 Service A’ 想要上线,但又不想直接替换掉原来的 Service A,希望先灰度一小部分用户,等运行一段时间足够稳定了再逐渐全量上线新版本,最后平滑下线旧版本。这个时候就可以利用 Nginx Ingress 基于 Header 或 Cookie 进行流量切分的策略来发布,业务使用 Header 或 Cookie 来标识不同类型的用户,我们通过配置 Ingress 来实现让带有指定 Header 或 Cookie 的请求被转发到新版本,其它的仍然转发到旧版本,从而实现将新版本灰度给部分用户:

k8s 部署的nginx service外部无法访问 k8s nginx ingress_nginx

场景二:切一定比例的流量给新版本 

        假设线上运行了一套对外提供 7 层服务的 Service B 服务,后来修复了一些问题,需要灰度上线一个新版本 Service B’,但又不想直接替换掉原来的 Service B,而是让先切 10% 的流量到新版本,等观察一段时间稳定后再逐渐加大新版本的流量比例直至完全替换旧版本,最后再滑下线旧版本,从而实现切一定比例的流量给新版本:

k8s 部署的nginx service外部无法访问 k8s nginx ingress_新版本_02

        Ingress-Nginx 是一个 K8S ingress 工具,支持配置 Ingress Annotations 来实现不同场景下的灰度发布和测试。 Nginx Annotations 支持以下几种 Canary 规则:

假设我们现在部署了两个版本的服务,老版本和 canary 版本。

  • nginx.ingress.kubernetes.io/canary-by-header:基于 Request Header 的流量切分,适用于灰度发布以及 A/B 测试。当 Request Header 设置为 always 时,请求将会被一直发送到 Canary 版本;当 Request Header 设置为 never 时,请求不会被发送到 Canary 入口。
  • nginx.ingress.kubernetes.io/canary-by-header-value:要匹配的 Request Header 的值,用于通知 Ingress 将请求路由到 Canary Ingress 中指定的服务。当 Request Header 设置为此值时,它将被路由到 Canary 入口。
  • nginx.ingress.kubernetes.io/canary-weight:基于服务权重的流量切分,适用于蓝绿部署,权重范围 0 - 100 按百分比将请求路由到 Canary Ingress 中指定的服务。权重为 0 意味着该金丝雀规则不会向 Canary 入口的服务发送任何请求。权重为 60 意味着 60% 流量转到 canary。权重为 100 意味着所有请求都将被发送到 Canary 入口。
  • nginx.ingress.kubernetes.io/canary-by-cookie:基于 Cookie 的流量切分,适用于灰度发布与 A/B 测试。用于通知 Ingress 将请求路由到 Canary Ingress 中指定的服务的 cookie。当 cookie 值设置为 always 时,它将被路由到 Canary 入口;当 cookie 值设置为 never 时,请求不会被发送到 Canary 入口。

二、模拟部署生产测试版本 Web 服务

相关资料下载地址:

部署两个版本的服务,这里以简单的 nginx 为例。

2.1 部署一个 v1 版本:

# 上传镜像并解压
[root@k8s-node1 ~]# ctr -n=k8s.io images import openresty.tar.gz
[root@k8s-node2 ~]# ctr -n=k8s.io images import openresty.tar.gz

[root@k8s-master01 ~]# vim v1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
      version: v1
  template:
    metadata:
      labels:
        app: nginx
        version: v1
    spec:
      containers:
      - name: nginx
        image: "openresty/openresty:centos"
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          protocol: TCP
          containerPort: 80
        volumeMounts:
        - mountPath: /usr/local/openresty/nginx/conf/nginx.conf
          name: config
          subPath: nginx.conf
      volumes:
      - name: config
        configMap:
          name: nginx-v1
---
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app: nginx
    version: v1
  name: nginx-v1
data:
  nginx.conf: |-
    worker_processes  1;
    events {
        accept_mutex on;
        multi_accept on;
        use epoll;
        worker_connections  1024;
    }
    http {
        ignore_invalid_headers off;
        server {
            listen 80;
            location / {
                access_by_lua '
                    local header_str = ngx.say("nginx-v1")
                ';
            }
        }
    }
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-v1
spec:
  type: ClusterIP
  ports:
  - port: 80
    protocol: TCP
    name: http
  selector:
    app: nginx
    version: v1

[root@k8s-master01 ~]# kubectl apply -f v1.yaml 

[root@k8s-master01 ~]# kubectl get pods 
NAME                               READY   STATUS    RESTARTS       AGE
nginx-v1-6d9846d69d-zrfcq          1/1     Running   0              13s

2.2 再部署一个 v2 版本:

[root@k8s-master01 ~]# vim v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
      version: v2
  template:
    metadata:
      labels:
        app: nginx
        version: v2
    spec:
      containers:
      - name: nginx
        image: "openresty/openresty:centos"
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          protocol: TCP
          containerPort: 80
        volumeMounts:
        - mountPath: /usr/local/openresty/nginx/conf/nginx.conf
          name: config
          subPath: nginx.conf
      volumes:
      - name: config
        configMap:
          name: nginx-v2
---
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app: nginx
    version: v2
  name: nginx-v2
data:
  nginx.conf: |-
    worker_processes  1;
    events {
        accept_mutex on;
        multi_accept on;
        use epoll;
        worker_connections  1024;
    }
    http {
        ignore_invalid_headers off;
        server {
            listen 80;
            location / {
                access_by_lua '
                    local header_str = ngx.say("nginx-v2")
                ';
            }
        }
    }
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-v2
spec:
  type: ClusterIP
  ports:
  - port: 80
    protocol: TCP
    name: http
  selector:
    app: nginx
    version: v2

[root@k8s-master01 ~]# kubectl apply -f v2.yaml 

[root@k8s-master01 ~]# kubectl get pods 
NAME                               READY   STATUS    RESTARTS       AGE
nginx-v1-6d9846d69d-zrfcq          1/1     Running   0              2m5s
nginx-v2-7cd8cfb784-fztvw          1/1     Running   0              19s

2.3 再创建一个 Ingress,对外暴露服务,指向 v1 版本的服务:

[root@k8s-master01 ~]# vim v1-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  rules:
  - host: canary.example.com
    http:
      paths:
      - path: /           # 配置访问路径,如果通过 url 进行转发,需要修改;默认为访问的路径为"/"
        pathType:  Prefix
        backend:          # 配置后端服务
         service:
           name: nginx-v1
           port:
            number: 80

[root@k8s-master01 ~]# kubectl apply -f v1-ingress.yaml

2.4 访问验证一下:

# 工作节点物理机 ip
[root@k8s-master01 ~]# curl -H "Host: canary.example.com" http://192.168.78.131
nginx-v1
[root@k8s-master01 ~]# curl -H "Host: canary.example.com" http://192.168.78.132
nginx-v1

# 虚拟 ip
[root@k8s-master01 ~]# curl -H "Host: canary.example.com" http://192.168.78.199
nginx-v1

三、Ingress Controller 基于请求头、cookie 和权重发布业务

3.1 基于 Header 的流量切分:

        创建 Canary Ingress,指定 v2 版本的后端服务,且加上一些 annotation,实现仅将带有名为 Region 且值为 cd 或 sz 的请求头的请求转发给当前 Canary Ingress,模拟灰度新版本给成都和深圳地域的用户:

[root@k8s-master01 ~]# vim v2-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-header: "Region"
    nginx.ingress.kubernetes.io/canary-by-header-pattern: "cd|sz"
  name: nginx-canary
spec:
  rules:
  - host: canary.example.com
    http:
      paths:
      - path: /           # 配置访问路径,如果通过 url 进行转发,需要修改;默认为访问的路径为"/"
        pathType:  Prefix
        backend:          # 配置后端服务
         service:
           name: nginx-v2
           port:
            number: 80

[root@k8s-master01 ~]# kubectl apply -f v2-ingress.yaml 

# 测试访问:
[root@k8s-master01 ~]# curl -H "Host: canary.example.com" -H "Region: cd" http://192.168.78.199
nginx-v2

[root@k8s-master01 ~]# curl -H "Host: canary.example.com" -H "Region: sz" http://192.168.78.199
nginx-v2

[root@k8s-master01 ~]# curl -H "Host: canary.example.com" -H "Region: bj" http://192.168.78.199
nginx-v1

[root@k8s-master01 ~]# curl -H "Host: canary.example.com" http://192.168.78.199
nginx-v1

可以看到,只有 header Region 为 cd 或 sz 的请求才由 v2 版本服务响应。

3.2 基于 Cookie 的流量切分: 

        与前面 Header 类似,不过使用 Cookie 就无法自定义 value 了,这里以模拟灰度成都地域用户为例,仅将带有名为 user_from_cd 的 cookie 的请求转发给当前 Canary Ingress 。先删除前面基于 Header 的流量切分的 Canary Ingress,然后创建下面新的 Canary Ingress:

[root@k8s-master01 ~]# kubectl delete -f v2-ingress.yaml 

[root@k8s-master01 ~]# vim v1-cookie.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-cookie: "user_from_cd"
  name: nginx-canary
spec:
  rules:
  - host: canary.example.com
    http:
      paths:
      - path: /              # 配置访问路径,如果通过url进行转发,需要修改;默认为访问的路径为"/"
        pathType:  Prefix
        backend:             # 配置后端服务
         service:
           name: nginx-v2
           port:
            number: 80

[root@k8s-master01 ~]# kubectl apply -f v1-cookie.yaml 

# 测试访问:
[root@k8s-master01 ~]# curl -s -H "Host: canary.example.com" --cookie "user_from_cd=always" http://192.168.78.199
nginx-v2

[root@k8s-master01 ~]# curl -s -H "Host: canary.example.com" --cookie "user_from_bj=always" http://192.168.78.199
nginx-v1

[root@k8s-master01 ~]# curl -s -H "Host: canary.example.com" http://192.168.78.199
nginx-v1

可以看到,只有 cookie user_from_cd 为 always 的请求才由 v2 版本的服务响应。

3.3  基于服务权重的流量切分

        基于服务权重的 Canary Ingress 就简单了,直接定义需要导入的流量比例,这里以导入 10% 流量到 v2 版本为例 (先删除之前的 Canary Ingress):

[root@k8s-master01 ~]# kubectl delete -f v1-cookie.yaml 
[root@k8s-master01 ~]# vim v1-weight.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
  name: nginx-canary
spec:
  rules:
  - host: canary.example.com
    http:
      paths:
      - path: /              # 配置访问路径,如果通过url进行转发,需要修改;默认为访问的路径为"/"
        pathType:  Prefix
        backend:             # 配置后端服务
         service:
           name: nginx-v2
           port:
            number: 80

[root@k8s-master01 ~]# kubectl apply -f v1-weight.yaml 

# 测试访问
[root@k8s-master01 ~]# for i in {1..10}; do curl -H "Host: canary.example.com" http://192.168.78.199; done;
nginx-v1
nginx-v2
nginx-v1
nginx-v1
nginx-v1
nginx-v1
nginx-v1
nginx-v1
nginx-v2
nginx-v1

可以看到,大概只有十分之一的几率由 v2 版本的服务响应,符合 10% 服务权重的设置。