Configure a canary deployment

Canary deployments release applications incrementally to a subset of users, which allows for the gradual deployment of new application versions without any downtime.

NGINX Ingress Controller supports traffic-splitting policies based on header, cookie, and weight. Whereas header- and cookie-based policies serve to provide a new service version to a subset of users, weight-based policies serve to divert a percentage of traffic to a new service version.

NGINX Ingress Controller uses the following annotations to enable canary deployments:

  • nginx.ingress.kubernetes.io/canary-by-header

  • nginx.ingress.kubernetes.io/canary-by-header-value

  • nginx.ingress.kubernetes.io/canary-by-header-pattern

  • nginx.ingress.kubernetes.io/canary-by-cookie

  • nginx.ingress.kubernetes.io/canary-weight

Canary rules are evaluated in the following order:

  1. canary-by-header

  2. canary-by-cookie

  3. canary-weight

Canary deployments require that you create two ingresses: one for regular traffic and one for alternative traffic. Be aware that you can apply only one canary ingress.

You enable a particular traffic-splitting policy by setting the associated canary annotation to true in the Kubernetes Ingress resource, as in the following example:

nginx.ingress.kubernetes.io/canary-by-header: "true"

Refer to Ingress Canary Annotations in the NGINX Ingress Controller documentation for more information.

Example canary setup

  1. Deploy two services, echo-v1 and echo-v2, using either the MKE web UI or kubectl.

    To deploy echo-v1:
    apiVersion: v1
    kind: Service
    metadata:
      name: echo-v1
    spec:
      type: ClusterIP
      ports:
        - port: 80
          protocol: TCP
          name: http
      selector:
        app: echo
        version: v1
    
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: echo-v1
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: echo
          version: v1
      template:
        metadata:
          labels:
            app: echo
            version: v1
        spec:
          containers:
            - name: echo
              image: "docker.io/hashicorp/http-echo"
              args:
                - -listen=:80
                - --text="echo-v1"
              ports:
                - name: http
                  protocol: TCP
                  containerPort: 80
    
    To deploy echo-v2:
    apiVersion: v1
    kind: Service
    metadata:
      name: echo-v2
    spec:
      type: ClusterIP
      ports:
        - port: 80
          protocol: TCP
          name: http
      selector:
        app: echo
        version: v2
    
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: echo-v2
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: echo
          version: v2
      template:
        metadata:
          labels:
            app: echo
            version: v2
        spec:
          containers:
            - name: echo
              image: "docker.io/hashicorp/http-echo"
              args:
                - -listen=:80
                - --text="echo-v2"
              ports:
                - name: http
                  protocol: TCP
                  containerPort: 80
    
  2. Create an Ingress to route the traffic for the regular service:

    Example Ingress
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      annotations:
        ingress.kubernetes.io/rewrite-target: /
      name: ingress-echo
    spec:
      ingressClassName: nginx-default
      rules:
        - host: canary.example.com
          http:
            paths:
              - path: /echo
                pathType: Exact
                backend:
                  service:
                    name: echo-v1
                    port:
                      number: 80
    
  3. Verify that traffic is successfully routed:

    curl -H "Host: canary.example.com" http://<IP_ADDRESS>:<NODE_PORT>
    

    Expected output:

    echo-v1
    

Canary deployment use cases

To provide a subset of users with a new service version using a request header:

  1. Create a canary ingress that routes traffic to the echo-v2 service using the request header x-region: us-east:

    Header-based policy
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      annotations:
        ingress.kubernetes.io/rewrite-target: /
        nginx.ingress.kubernetes.io/canary: "true"
        nginx.ingress.kubernetes.io/canary-by-header: "x-region"
        nginx.ingress.kubernetes.io/canary-by-header-value: "us-east"
      name: ingress-echo-canary
    spec:
      ingressClassName: nginx-default
      rules:
        - host: canary.example.com
          http:
            paths:
              - path: /echo
                pathType: Exact
                backend:
                  service:
                    name: nginx-v2
                    port:
                      number: 80
    
  2. Verify that traffic is properly routed:

    curl -H "Host: canary.example.com" -H "x-region: us-east" \
    http://<IP_ADDRESS>:<NODE_PORT>
    curl -H "Host: canary.example.com" -H "x-region: us-west" \
    http://<IP_ADDRESS>:<NODE_PORT>
    curl -H "Host: canary.example.com" \
    http://<IP_ADDRESS>:<NODE_PORT>
    

    Expected output:

    echo-v2
    echo-v1
    echo-v1
    

To provide a subset of users with a new service version using a cookie:

  1. Create a canary ingress that routes traffic to the echo-v2 service using a cookie:

  2. Verify that traffic is properly routed:

    curl -s -H "Host: canary.example.com" --cookie "my_cookie=always" \
    http://<IP_ADDRESS>:<NODE_PORT>
    curl -s -H "Host: canary.example.com" --cookie "other_cookie=always" \
    http://<IP_ADDRESS>:<NODE_PORT>
    curl -s -H "Host: canary.example.com" \
    http://<IP_ADDRESS>:<NODE_PORT>
    

    Expected output:

    echo-v2
    echo-v1
    echo-v1
    

To route a segment of traffic to a new service version:

  1. Create a canary ingress that routes 20% of traffic to the echo-v2 service using the nginx.ingress.kubernetes.io/canary-weight annotation:

    Weight-based policy
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      annotations:
        ingress.kubernetes.io/rewrite-target: /
        nginx.ingress.kubernetes.io/canary: "true"
        nginx.ingress.kubernetes.io/canary-weight: "20"
      name: ingress-echo-canary
    spec:
      ingressClassName: nginx-default
      rules:
        - host: canary.example.com
          http:
            paths:
              - path: /echo
                pathType: Exact
                backend:
                  service:
                    name: echo-v2
                    port:
                      number: 80
    
  2. Verify that traffic is properly routed:

    for i in {1..10}; do curl -H "Host: canary.example.com" \
    http://<IP_ADDRESS>:<NODE_PORT>
    

    Example output:

    "echo-v1"
    "echo-v2"
    "echo-v2"
    "echo-v1"
    "echo-v1"
    "echo-v1"
    "echo-v1"
    "echo-v1"
    "echo-v1"
    "echo-v1"