Progressive Delivery on Kubernetes: Automating Canary Releases with Flagger and Istio

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

The Real Problem with Production Deployments

Every Friday release used to bring that familiar knot in my stomach. Standard rolling updates sound great in theory, but they fall short during silent failures. You roll out version 2.0, pods pass their basic readiness probes, and Kubernetes immediately routes 100% of user traffic to them. Then the 500 errors start rolling in.

Staging rarely catches edge-case bugs. A memory leak in our order-processing worker once stayed hidden until hit by 800 concurrent requests in production. Within four minutes, 30% of active checkouts dropped before anyone noticed the alert. That disaster forced us to adopt Progressive Delivery using Flagger and Istio, removing manual traffic management entirely.

Instead of exposing every user to unverified code, you route a tiny slice of live traffic—say, 5%—to the new version. You observe real telemetry for error spikes or latency regressions. If the canary stays healthy, you incrementally widen the pipe. If errors appear, the system rolls back instantly without human intervention.

How the Components Fit Together

Progressive Delivery vs. Standard Rollouts

Progressive Delivery couples gradual traffic shifting with automated analysis loops. A standard rolling update stops only if a container crashes or fails an HTTP health check. In contrast, progressive rollouts query live application metrics like HTTP error rates and P99 latency before shifting each additional batch of users.

Flagger: The Automation Controller

Flagger is a Kubernetes operator that manages the canary lifecycle from start to finish. When you bump a Deployment’s container image, Flagger intercepts the release, builds an isolated canary replica set, and kicks off an evaluation loop:

  • Routes an initial 5%–10% of traffic to the canary deployment.
  • Queries Prometheus every 60 seconds against configured SLO thresholds.
  • Increases traffic weight after every successful evaluation window.
  • Reverts routing to the stable primary pods if error thresholds are exceeded.

Istio: The Service Mesh Data Plane

Standard Kubernetes ClusterIP Services distribute traffic randomly across matching pod labels. They cannot split traffic 90/10 by percentage. Istio solves this at Layer 7 using VirtualService and DestinationRule resources. Flagger manipulates these Istio routing rules dynamically, so engineers never have to tweak routing weights by hand.

Setting Up the Stack

Prerequisites

You need a Kubernetes cluster running version 1.24 or higher, kubectl access, and helm v3 installed. You also need Prometheus collecting mesh telemetry, which comes out of the box with the standard kube-prometheus-stack chart.

Install Istio

curl -L https://istio.io/downloadIstio | ISTIO_VERSION=1.20.0 sh -
cd istio-1.20.0
export PATH=$PWD/bin:$PATH

istioctl install --set profile=default -y

# Enable automatic Envoy sidecar injection on the target namespace
kubectl label namespace default istio-injection=enabled --overwrite

Install Flagger

helm repo add flagger https://flagger.app
helm repo update

helm upgrade -i flagger flagger/flagger \
  --namespace=istio-system \
  --set meshProvider=istio \
  --set metricsServer=http://kube-prometheus-stack-prometheus.monitoring:9090

Point metricsServer directly to your cluster’s Prometheus endpoint. Flagger queries this address during every analysis interval to evaluate canary health.

Configuring an Automated Canary

Base Deployment and Service

Deploy your application using a standard Deployment manifest. Flagger clones this resource into my-app-primary (the stable baseline) and manages the canary instances for you.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app
        image: my-registry/my-app:1.0.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
  name: my-app
  namespace: default
spec:
  selector:
    app: my-app
  ports:
  - port: 80
    targetPort: 8080

Canary Custom Resource

The Canary custom resource controls the progression rules, traffic increments, and rollback criteria:

apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
  name: my-app
  namespace: default
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  progressDeadlineSeconds: 120
  service:
    port: 80
    targetPort: 8080
    gateways:
    - istio-system/ingressgateway
    hosts:
    - my-app.example.com
  analysis:
    interval: 1m
    threshold: 5
    maxWeight: 50
    stepWeight: 10
    metrics:
    - name: request-success-rate
      thresholdRange:
        min: 99
      interval: 1m
    - name: request-duration
      thresholdRange:
        max: 500
      interval: 1m

Key configuration details:

  • interval (1m): Flagger collects and evaluates metrics once every minute.
  • threshold (5): Five consecutive metric check failures trigger an immediate rollback.
  • maxWeight (50): Canary traffic peaks at 50% before full promotion to primary.
  • stepWeight (10): Each successful check increases traffic by 10% (10%, 20%, 30%, 40%, 50%).
  • request-success-rate: Requires an HTTP success rate of at least 99% based on Envoy telemetry.
  • request-duration: P99 latency must remain under 500ms.

Triggering a Rollout

Apply your Canary manifest and wait for Flagger to initialize. Once the primary deployment is ready, update the container image to launch a release:

kubectl set image deployment/my-app \
  my-app=my-registry/my-app:2.0.0 \
  -n default

Monitor progress in your terminal:

kubectl get canary my-app -n default -w

# NAME     STATUS        WEIGHT   LASTTRANSITIONTIME
# my-app   Progressing   10       2024-01-15T10:00:00Z
# my-app   Progressing   20       2024-01-15T10:01:00Z
# my-app   Progressing   30       2024-01-15T10:02:00Z
# my-app   Progressing   40       2024-01-15T10:03:00Z
# my-app   Progressing   50       2024-01-15T10:04:00Z
# my-app   Promoting     0        2024-01-15T10:05:00Z
# my-app   Succeeded     0        2024-01-15T10:06:00Z

Testing the Rollback Mechanism

Never trust a rollback policy you haven’t intentionally broken. Deploy an image version configured to return 500 errors on 10% of requests, then stream Flagger’s controller logs:

kubectl logs -n istio-system \
  -l app.kubernetes.io/name=flagger \
  --tail=100 -f | grep my-app

As errors hit Prometheus, Flagger catches the breach and halts progression:

Halt my-app.default advancement: request-success-rate 94.20 < 99
Halt my-app.default advancement: request-success-rate 93.80 < 99
Rolling back my-app.default: failed checks threshold reached (5/5)
Canary failed! Scaling down my-app.canary

Istio immediately resets traffic routing to 100% on my-app-primary. The faulty release affected only 10% of users during a 5-minute window instead of knocking down the entire cluster.

Writing Custom Business Metric Gates

HTTP status codes do not tell the whole story. You can gate deployments on application-specific Prometheus metrics, such as checkout failures or database query latencies, using a MetricTemplate:

apiVersion: flagger.app/v1beta1
kind: MetricTemplate
metadata:
  name: checkout-error-rate
  namespace: default
spec:
  provider:
    type: prometheus
    address: http://kube-prometheus-stack-prometheus.monitoring:9090
  query: |
    sum(rate(http_requests_total{
      app="my-app",
      path="/checkout",
      status=~"5.."
    }[1m]))
    /
    sum(rate(http_requests_total{
      app="my-app",
      path="/checkout"
    }[1m])) * 100

Attach the metric template to your Canary definition:

  analysis:
    metrics:
    - name: checkout-error-rate
      templateRef:
        name: checkout-error-rate
        namespace: default
      thresholdRange:
        max: 0.5
      interval: 1m

If checkout failures exceed 0.5% during any step, Flagger freezes the release immediately.

Production Field Notes

Align your Prometheus scrape configs with Flagger’s analysis cadence. If Prometheus scrapes every 30 seconds, a 1-minute Flagger interval ensures at least two fresh metric data points per evaluation. A 60-second scrape on a 60-second check risks querying stale calculations.

Low-traffic microservices need synthetic traffic generation. If your service handles fewer than 10 requests per second, Flagger metrics can fluctuate wildly. Use Flagger’s built-in webhooks feature to spin up load-testing tools like ghz or hey against the canary pod during analysis.

Teams that prefer all-or-nothing cutovers can still run blue-green releases. Set stepWeight: 100 in your Canary spec. Flagger will run health evaluations against the isolated candidate before flipping all traffic over at once.

Automating canary analysis removes guesswork from the deployment pipeline. Define your reliability criteria up front in code, let Istio handle routing weights, and let Flagger manage the rollbacks.

Share: