跳转至

ingress 灰度发版

在日常工作中我们经常需要对服务进行版本更新升级,所以我们经常会使用到滚动升级、蓝绿发布、灰度发布等不同的发布操作。而 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 规则划分为以下两类:

  • 基于权重的 Canary (金丝雀) 规则 image-20240403155732485

  • 基于用户请求的 Canary (金丝雀) 规则 image-20240403155654482

部署 Production (生产环境) 和 Canary (金丝雀) 版本应用

1. 编辑 production-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: production
  labels:
    app: production
spec:
  replicas: 1
  selector:
    matchLabels:
      app: production
  template:
    metadata:
      labels:
        app: production
    spec:
      containers:
      - name: production
        image: mirrorgooglecontainers/echoserver:1.10
        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
    protocol: TCP
    name: http
  selector:
    app: production
2. 创建并查看 production-deployment.yml
[root@k8smaster002 ingress]# kubectl apply -f production-deployment.yml 
deployment.apps/production created
service/production created

[root@k8smaster002 ingress]# kubectl get pod -l app=production
NAME                          READY   STATUS    RESTARTS   AGE
production-64cfc46b65-2459s   1/1     Running   0          87s

[root@k8smaster002 ingress]# kubectl get svc -l app=production
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
production   ClusterIP   10.97.100.168   <none>        80/TCP    3m37s
3. 编写 production.ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: production
spec:
  ingressClassName: nginx
  rules:
  - host: production.k8s.huichengcheng.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: production
            port:
              number: 80
4. 创建并查看 production.ingress.yml
[root@k8smaster002 ingress]# kubectl apply -f production.ingress.yml 
ingress.networking.k8s.io/production created

[root@k8smaster002 ingress]# kubectl get ingress 
NAME         CLASS   HOSTS                              ADDRESS         PORTS   AGE
production   nginx   production.k8s.huichengcheng.com   192.168.3.204   80      75s

[root@k8smaster002 ingress]# kubectl describe ingress 
Name:             production
Labels:           <none>
Namespace:        default
Address:          192.168.3.204
Ingress Class:    nginx
Default backend:  <default>
Rules:
  Host                              Path  Backends
  ----                              ----  --------
  production.k8s.huichengcheng.com  
                                    /   production:80 (10.244.35.58:8080)
Annotations:                        <none>
Events:
  Type    Reason  Age                   From                      Message
  ----    ------  ----                  ----                      -------
  Normal  Sync    2m5s (x2 over 2m25s)  nginx-ingress-controller  Scheduled for sync
5. 访问 Production 版本的应用
❯ curl https://production.k8s.huichengcheng.com:60000/
Hostname: production-64cfc46b65-2459s

Pod Information:
        node name:      k8sworker002
        pod name:       production-64cfc46b65-2459s
        pod namespace:  default
        pod IP: 10.244.35.58

Server values:
        server_version=nginx: 1.13.3 - lua: 10008

Request Information:
        client_address=10.244.87.252
        method=GET
        real path=/
        query=
        request_version=1.1
        request_scheme=http
        request_uri=http://production.k8s.huichengcheng.com:8080/

Request Headers:
        accept=*/*
        accept-encoding=gzip
        host=production.k8s.huichengcheng.com:60000
        user-agent=curl/7.81.0
        x-forwarded-for=192.168.3.202
        x-forwarded-host=production.k8s.huichengcheng.com:60000
        x-forwarded-port=80
        x-forwarded-proto=http
        x-forwarded-scheme=http
        x-original-forwarded-for=192.168.1.99
        x-real-ip=192.168.3.202
        x-request-id=14976def2dea3555cbf3a3c2060ef6ba
        x-scheme=http

Request Body:
        -no body in request-
1. 编辑 Canary-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: canary
  labels:
    app: canary
spec:
  replicas: 1
  selector:
    matchLabels:
      app: canary
  template:
    metadata:
      labels:
        app: canary
    spec:
      containers:
      - name: canary
        image: mirrorgooglecontainers/echoserver:1.10
        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
    protocol: TCP
    name: http
  selector:
    app: canary
2. 创建并查看 Canary-deployment.yml
[root@k8smaster002 ingress]# kubectl apply -f Canary-deployment.yml
deployment.apps/canary created
service/canary created

[root@k8smaster002 ingress]# kubectl get pod -l app=canary
NAME                      READY   STATUS    RESTARTS   AGE
canary-7d97679b67-9dn5c   1/1     Running   0          18s

[root@k8smaster002 ingress]# kubectl get svc -l app=canary
NAME     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
canary   ClusterIP   10.110.80.240   <none>        80/TCP    37s

配置 Annotation 规则进行流量切分

基于权重

基于权重的流量切分的典型应用场景就是蓝绿部署,可通过将权重设置为 0 或 100 来实现。例如,可将 Green 版本设置为主要部分,并将 Blue 版本的入口配置为 Canary。最初,将权重设置为 0,因此不会将流量代理到 Blue 版本。一旦新版本测试和验证都成功后,即可将 Blue 版本的权重设置为 100,即所有流量从 Green 版本转向 Blue。

1. 要开启灰度发布机制,首先需设置 nginx.ingress.kubernetes.io/canary: true 启用 Canary,以下 Ingress 示例的 Canary 版本使用了基于权重进行流量切分的 annotation 规则,将分配 30% 的流量请求发送至 Canary 版本。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "30"
spec:
  ingressClassName: nginx
  rules:
  - host: production.k8s.huichengcheng.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: canary
            port:
              number: 80
说明: 应用的 Canary 版本基于权重 (30%) 进行流量切分后,访问到 Canary 版本的概率接近 30%,流量比例可能会有小范围的浮动。
❯ for i in $(seq 1 10); do curl -s https://production.k8s.huichengcheng.com:60000 | grep "Hostname"; done

Hostname: production-64cfc46b65-2459s
Hostname: production-64cfc46b65-2459s
Hostname: production-64cfc46b65-2459s
Hostname: production-64cfc46b65-2459s
Hostname: canary-7d97679b67-9dn5c
Hostname: canary-7d97679b67-9dn5c
Hostname: canary-7d97679b67-9dn5c
Hostname: production-64cfc46b65-2459s
Hostname: production-64cfc46b65-2459s
Hostname: production-64cfc46b65-2459s

基于 Request Header

基于 Request Header 进行流量切分的典型应用场景即灰度发布或 A/B 测试场景。

1. 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 规则进行优先级的比较。

1. Canary 版本的 Ingress 对象中新增一条 annotation 配置 nginx.ingress.kubernetes.io/canary-by-header: canary(这里的 value 可以是任意值),使当前的 Ingress 实现基于 Request Header 进行流量切分,由于 canary-by-header 的优先级大于 canary-weight,所以会忽略原有的 canary-weight 的规则。
❯ for i in $(seq 1 10); do curl -s -H "canary: never"  https://production.k8s.huichengcheng.com:60000 | grep "Hostname"; done

Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx

# 请求的时候设置了 canary: never 这个 Header 值,所以请求没有发送到 Canary 应用中去
2. Canary 版本的 Ingress 对象中新增一条 annotation 配置 nginx.ingress.kubernetes.io/canary-by-header: canary(这里的 value 可以是任意值),使当前的 Ingress 实现基于 Request Header 进行流量切分,由于 canary-by-header 的优先级大于 canary-weight,所以会忽略原有的 canary-weight 的规则。
❯ for i in $(seq 1 10); do curl -s -H "canary: never"  https://production.k8s.huichengcheng.com:60000 | grep "Hostname"; done

Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx

# 请求的时候设置了 canary: never 这个 Header 值,所以请求没有发送到 Canary 应用中去
3. 请求设置的 Header 值为 canary: other-value
❯ for i in $(seq 1 10); do curl -s -H "canary: user-value"  https://production.k8s.huichengcheng.com:60000 | grep "Hostname"; done

Hostname: production-64cfc46b65-6wkqx
Hostname: canary-7d97679b67-klfq7
Hostname: production-64cfc46b65-6wkqx
Hostname: canary-7d97679b67-klfq7
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: canary-7d97679b67-klfq7

# ingress-nginx 会通过优先级将请求与其他 Canary 规则进行优先级的比较,我们这里也就会进入 canary-weight: "30" 这个规则去
4. 在上一个 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-nginx 会通过优先级将请求与其他 Canary 规则进行优先级的比较,我们这里也就会进入 canary-weight: "30" 这个规则去
5. 在上一个 annotation (即 canary-by-header)的基础上添加一条 nginx.ingress.kubernetes.io/canary-by-header-value: user-value 这样的规则,就可以将请求路由到 Canary Ingress 中指定的服务了。
❯ for i in $(seq 1 10); do curl -s -H "canary: user-value"  https://production.k8s.huichengcheng.com:60000 | grep "Hostname"; done

Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7

与基于 Request Header 的 annotation 用法规则类似。例如在 A/B 测试场景下,需要让地域为北京的用户访问 Canary 版本。那么当 cookie 的 annotation 设置为 nginx.ingress.kubernetes.io/canary-by-cookie: "users_from_Beijing",此时后台可对登录的用户请求进行检查,如果该用户访问源来自北京则设置 cookie users_from_Beijing 的值为 always,这样就可以确保北京的用户仅访问 Canary 版本。

1. 更新 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
2. 更新上面的 Ingress 资源对象后,我们在请求中设置一个 users_from_Beijing=always 的 Cookie 值,再次访问应用的域名
❯ for i in $(seq 1 10); do curl -s -b "users_from_Beijing=always"  https://production.k8s.huichengcheng.com:60000 | grep "Hostname"; done

Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7

# 应用都被路由到了 Canary 版本的应用中去了
❯ for i in $(seq 1 10); do curl -s -b "users_from_Beijing=never"  https://production.k8s.huichengcheng.com:60000 | grep "Hostname"; done

Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx

# 如果我们将这个 Cookie 值设置为 never,则不会路由到 Canary 应用中