Cloud Native 11 min read

How to Implement Canary Deployments with Ingress‑Nginx Annotations in Kubernetes

This guide shows how to use Ingress‑Nginx annotations to perform canary releases in Kubernetes, covering header‑based, cookie‑based, and weight‑based traffic splitting with complete YAML manifests and curl commands for verification, providing step‑by‑step examples and demonstrating practical testing.

Full-Stack DevOps & Kubernetes
Full-Stack DevOps & Kubernetes
Full-Stack DevOps & Kubernetes
How to Implement Canary Deployments with Ingress‑Nginx Annotations in Kubernetes

Overview

Ingress‑Nginx is a Kubernetes ingress controller that supports canary annotations for gradual rollouts and testing. The annotations nginx.ingress.kubernetes.io/canary-by-header, nginx.ingress.kubernetes.io/canary-by-header-value, nginx.ingress.kubernetes.io/canary-weight and nginx.ingress.kubernetes.io/canary-by-cookie enable traffic splitting based on request headers, header values, service weight, or cookies.

Deploy Two Versions of a Service

First create a simple nginx deployment (v1) and a corresponding ConfigMap that serves a custom nginx.conf. Then expose it with a ClusterIP service.

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"
        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:
  name: nginx-v1
  labels:
    app: nginx
    version: 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

Repeat the same steps for version v2, changing the names and the string printed in access_by_lua to nginx-v2.

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"
        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:
  name: nginx-v2
  labels:
    app: nginx
    version: v2
data:
  nginx.conf: |-
    (same content as v1, but access_by_lua prints "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

Create a Base Ingress for the Stable Version

Expose the v1 service through an Ingress that routes all traffic to it.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: nginx
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  rules:
  - host: canary.example.com
    http:
      paths:
      - backend:
          serviceName: nginx-v1
          servicePort: 80
        path: /

Verify the setup: curl -H "Host:canary.example.com" http://EXTERNAL-IP Response should be nginx-v1.

Header‑Based Canary Routing

Add a second Ingress marked as a canary. The annotation nginx.ingress.kubernetes.io/canary-by-header: "Region" together with

nginx.ingress.kubernetes.io/canary-by-header-pattern: "cd|sz"

routes only requests whose Region header matches cd or sz to the v2 service.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: nginx-canary
  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"
spec:
  rules:
  - host: canary.example.com
    http:
      paths:
      - backend:
          serviceName: nginx-v2
          servicePort: 80
        path: /

Test the routing:

curl -H "Host:canary.example.com" -H "Region: cd" http://EXTERNAL-IP

Should return nginx-v2. Requests with Region: bj or without the header continue to hit nginx-v1.

Cookie‑Based Canary Routing

Replace the header‑based canary with a cookie‑based rule. The annotation nginx.ingress.kubernetes.io/canary-by-cookie: "user_from_cd" forwards traffic to v2 only when the request carries a cookie named user_from_cd (value can be anything, e.g., always).

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: nginx-canary
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-cookie: "user_from_cd"
spec:
  rules:
  - host: canary.example.com
    http:
      paths:
      - backend:
          serviceName: nginx-v2
          servicePort: 80
        path: /

Test:

curl -s -H "Host:canary.example.com" --cookie "user_from_cd=always" http://EXTERNAL-IP

Response should be nginx-v2. Requests without that cookie receive nginx-v1.

Weight‑Based Canary Routing

For simple percentage‑based traffic splitting, use the nginx.ingress.kubernetes.io/canary-weight annotation. Setting it to 10 directs roughly 10 % of requests to the canary service.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: nginx-canary
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  rules:
  - host: canary.example.com
    http:
      paths:
      - backend:
          serviceName: nginx-v2
          servicePort: 80
        path: /

Run a quick test to see the distribution:

for i in {1..10}; do curl -H "Host: canary.example.com" http://EXTERNAL-IP; done

Approximately one out of ten responses should be nginx-v2, confirming the 10 % weight.

Conclusion

Ingress‑Nginx canary annotations provide flexible mechanisms for gray‑release, A/B testing, and gradual rollouts in Kubernetes. By adjusting headers, cookies, or weight percentages, operators can control traffic flow to new versions with minimal risk, and the provided YAML manifests and curl commands make verification straightforward.

KubernetesYAMLtraffic splittingcanary deploymentingress-nginxHeader RoutingCookie Routing
Full-Stack DevOps & Kubernetes
Written by

Full-Stack DevOps & Kubernetes

Focused on sharing DevOps, Kubernetes, Linux, Docker, Istio, microservices, Spring Cloud, Python, Go, databases, Nginx, Tomcat, cloud computing, and related technologies.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.