Skip to content
HoginHogin
Go back

Argo Rollouts + Gateway API: canary with auto-rollback, no service mesh required

10 мин чтения

Canary deploys with automatic rollback were long the privilege of clusters running a service mesh: splitting live traffic by percentage was something Istio VirtualService or Linkerd’s SMI splits could do, and everyone else made do with ingress-controller annotations. Argo Rollouts removed that constraint: the official trafficrouter-gatewayapi plugin splits traffic directly in a standard HTTPRoute, and an AnalysisTemplate reading Prometheus metrics decides on its own whether to promote the release or roll it back. No service mesh required — just Gateway API, which by 2026 almost everyone already has.

Why now: starting with Rollouts 1.9 the plugin supports maxTrafficWeight — you can stretch the weight scale to a million and split traffic finer than 1%, and the ecosystem (Cilium, kgateway, VPC Lattice) is building integrations on top of Gateway API. Progressive delivery has moved from mesh-specific resources to the cluster’s standard API, and the barrier to entry has collapsed.

Table of contents

Open Table of contents

Rollout instead of Deployment: what changes

A Deployment with RollingUpdate replaces pods on a schedule and never asks the metrics — a problem we covered in detail in our Flagger article. Argo Rollouts solves it at the manifest level: instead of a Deployment you describe a Rollout — same pod template, but a canary strategy with explicit steps. It plays well with Argo CD out of the box: for a GitOps pipeline it’s just another CRD in git.

In canary mode the Rollout keeps two ReplicaSets — stable and canary — and two Services with the same selector: web-stable and web-canary. No, they won’t grab the same pods: the Rollout stamps each ReplicaSet’s pods with a rollouts-pod-template-hash label, so the stable Service selects the old pods and the canary Service the new ones. And the traffic between them is split not by kube-proxy but by the Gateway — according to the weights in an HTTPRoute with two backendRefs:

How traffic is split: one HTTPRoute, two backendRefs

The key mechanism: the argoproj-labs/gatewayAPI plugin inside the controller rewrites the weight field of both backendRefs whenever the Rollout advances a step. The Gateway — Envoy Gateway, Cilium, Traefik, kgateway, anything implementing Gateway API v1 — executes the percentages faithfully. Git holds the manifest without weights; the live state is changed by the controller: steps are declarative, execution is automatic.

AnalysisTemplate: the metrics that kill a release

Shifting traffic is half a canary. The other half is the “promote or not” decision, and Argo Rollouts takes that away from humans too. An AnalysisTemplate is a CR describing a metric: which query to run against Prometheus, how often, what value counts as a failure, and how many failures we tolerate.

When the Rollout reaches an analysis step, it creates an AnalysisRun object that blocks further progress: every interval it runs the query and checks the result against the condition. Once failed measurements exceed failureLimit, the AnalysisRun is marked Failed and the Rollout aborts: the plugin instantly returns 100% of the weight to the stable backendRef, canary pods scale to zero, and the Rollout status becomes Degraded. No kubectl rollout undo at 3 a.m. — the controller rolls back in seconds.

Canary by steps: metric analysis decides — proceed or roll back

The metrics don’t have to be infrastructure ones: the query is arbitrary PromQL, so the threshold can be payment success rate or conversion, not just p95 and error-rate.

Comparison in one table

Deployment + RollingUpdateRollout + Gateway API
Traffic shiftas pods become readyin steps of 5 → 25 → 50 → 100% via HTTPRoute
Metric analysisnone, alerts after the factan AnalysisRun at every step
Rollback decisiona human, pagedthe controller, on a threshold from git
Minimal blast radius~25% of traffic (maxUnavailable)5% or finer
Service meshnot needed, but no splits eithernot needed — splits a standard HTTPRoute
GitOpsimage in git, then blindsame manifests + native Argo CD fit
Rollbackminutes and a humanseconds and a controller

What you need to run a canary

Preconditions: a cluster with a v1 Gateway API implementation (Envoy Gateway, Cilium, Traefik, kgateway) and Prometheus carrying your app’s metrics. Then four steps.

1. Controller, CLI and the plugin. Install Argo Rollouts and the kubectl plugin:

kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts \
  -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
kubectl krew install rollouts

The plugin is wired in through a ConfigMap with the fixed name argo-rollouts-config:

apiVersion: v1
kind: ConfigMap
metadata:
  name: argo-rollouts-config   # the name is fixed
  namespace: argo-rollouts
data:
  trafficRouterPlugins: |-
    - name: "argoproj-labs/gatewayAPI"
      location: "https://github.com/argoproj-labs/rollouts-plugin-trafficrouter-gatewayapi/releases/download/v0.5.0/gatewayapi-plugin-linux-amd64"

Apply it, restart the controller (kubectl -n argo-rollouts rollout restart deployment argo-rollouts) and check the logs for the line Downloading plugin argoproj-labs/gatewayAPI. For offline clusters there’s an init-container variant using an image from ghcr.io — see the plugin docs.

2. RBAC. The plugin needs to read Services and patch HTTPRoutes (and GRPCRoutes if you use them):

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: argo-rollouts-gatewayapi
rules:
  - apiGroups: [""]
    resources: [services]
    verbs: [get]
  - apiGroups: [gateway.networking.k8s.io]
    resources: [httproutes, grpcroutes]
    verbs: [get, list, patch, update]

Bind the role to the argo-rollouts ServiceAccount with a ClusterRoleBinding — and the cluster is ready.

3. Two Services and an HTTPRoute. The selectors are identical — the Rollout will sort it out:

apiVersion: v1
kind: Service
metadata:
  name: web-stable
  namespace: prod
spec:
  selector: { app: web }
  ports: [{ port: 80, targetPort: http }]
---
apiVersion: v1
kind: Service
metadata:
  name: web-canary
  namespace: prod
spec:
  selector: { app: web }
  ports: [{ port: 80, targetPort: http }]
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: web
  namespace: prod
spec:
  parentRefs: [{ name: eg }]
  rules:
    - matches: [{ path: { type: PathPrefix, value: / } }]
      backendRefs:
        - { name: web-stable, kind: Service, port: 80 }
        - { name: web-canary, kind: Service, port: 80 }

No weights in the manifest — the plugin writes those on the very first rollout.

4. AnalysisTemplate and the Rollout itself. A p95 analysis template with failureLimit: 2:

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: p95-latency
  namespace: prod
spec:
  args:
    - name: service
  metrics:
    - name: p95
      interval: 1m
      count: 3
      failureLimit: 2
      failureCondition: result[0] >= 500
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            histogram_quantile(0.95,
              sum by (le) (rate(http_request_duration_seconds_bucket
                {service="{{args.service}}", version="canary"}[2m]))
            ) * 1000

Three measurements a minute apart; two worse than 500 ms — and the AnalysisRun goes Failed. Now the Rollout with 5/25/50/100 steps:

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: web
  namespace: prod
spec:
  replicas: 10
  selector: { matchLabels: { app: web } }
  template:
    metadata: { labels: { app: web } }
    spec:
      containers:
        - name: web
          image: registry.example.com/web:v1
          ports: [{ name: http, containerPort: 8080 }]
  strategy:
    canary:
      stableService: web-stable
      canaryService: web-canary
      maxSurge: 1
      maxUnavailable: 0
      trafficRouting:
        plugins:
          argoproj-labs/gatewayAPI:
            httpRoute: web
            namespace: prod
      steps:
        - setWeight: 5
        - pause: { duration: 2m }
        - analysis:
            templates: [{ templateName: p95-latency }]
            args: [{ name: service, value: web }]
        - setWeight: 25
        - pause: { duration: 5m }
        - analysis:
            templates: [{ templateName: p95-latency }]
            args: [{ name: service, value: web }]
        - setWeight: 50
        - pause: { duration: 5m }
        - analysis:
            templates: [{ templateName: p95-latency }]
            args: [{ name: service, value: web }]
        - setWeight: 100

Each step: raise the weight, let the metrics accumulate, ask Prometheus. Want a manual gate — insert pause: {} with no duration: the Rollout freezes until kubectl argo rollouts promote. And if you need sub-percent precision, add maxTrafficWeight: 10000 under trafficRouting (Rollouts 1.9+): a setWeight: 100 step then means 1% of traffic.

How to verify the auto-rollback

A run against a deliberately broken release is the only honest test. Deploy a version that sinks the p95:

kubectl argo rollouts set image web web=registry.example.com/web:v2-broken
kubectl argo rollouts get rollout web -n prod --watch

The expected picture: the weight climbs to 5%, pause, the AnalysisRun takes its measurements; on the third one (two failures at failureLimit: 2) the status flips to Failed, the Rollout to Degraded, and the weight arrow returns to zero. Check the actual route state:

kubectl -n prod get httproute web -o jsonpath='{.spec.rules[0].backendRefs}'

After the rollback it reads 100/0 — all traffic back on web-stable. The events (via kubectl -n prod describe rollout web) will show which metric exceeded the threshold and by how much. Standing advice: rerun this negative drill after every change to the thresholds — an AnalysisTemplate that has never killed a release is an AnalysisTemplate you haven’t tested.

Rollouts or Flagger

We wrote about Flagger before — it’s the closest and fully comparable alternative, also mesh-free, also on Gateway API. The honest difference. Flagger wraps your existing Deployment without changing its kind — zero migration; it ships built-in webhook load tests and belongs to the Flux ecosystem. Rollouts replaces the Deployment with its own CR — the migration touches manifests, but steps with pauses and manual gates are explicit, the AnalysisRun is a separate reusable object, and you get blue-green, Experiment and a kubectl plugin with a live UI. Rule of thumb: Flux shop with a “don’t touch the Deployment” stance — take Flagger; Argo CD shop that needs pauses with manual promotion — Rollouts. Both deliver the same core thing: a rollback in seconds on metrics instead of a rollback in minutes on a pager.

Pitfalls

Bottom line

A service mesh is no longer a prerequisite for canaries: any cluster with Gateway API gets the full progressive delivery loop — steps, pauses, metrics, auto-rollback — for the price of one controller and a plugin. The cost: swapping Deployment for Rollout, RBAC for the plugin, and a careful ignoreDifferences for Argo CD. The payoff: a broken release is seen by 5% of users instead of all of them, and the rollback happens on a git-declared threshold in seconds — no on-call human with a pager. If you already run Gateway API, you can try this in one evening.


Share this post:

Next Post
DORA 2026: AI amplifies what you already have — an unstable pipeline gets worse, not better