# Argo Rollouts canary: traffic weights on an HTTPRoute and analysis from Prometheus

> How Argo Rollouts runs a canary: a Rollout owns two ReplicaSets, shifts traffic by editing HTTPRoute weights through the Gateway API plugin, and an AnalysisTemplate queries Prometheus to promote or abort.

Interactive graph: https://k8wiz.com/graphs/argo-rollouts-canary-gateway-api-analysis

A Deployment's rolling update replaces Pods at a fixed pace and stops only when Pods fail their probes. Argo Rollouts replaces the Deployment with a `Rollout` that moves traffic in steps, measures the new version while it takes real requests, and goes back by itself when the numbers are bad.

Here the traffic split lives in a Gateway API HTTPRoute and the measurement is a Prometheus query. Argo CD syncs the Rollout from Git like any other object.

## Resources

### Rollout checkout

you write it, argoproj.io/v1alpha1, namespace shop

A Deployment-shaped object with a `strategy.canary`. The steps set a traffic weight, pause, and run an analysis. It names the two Services and the HTTPRoute it may edit.

### ReplicaSet checkout-6d5f8c7b9

created by a controller, apps/v1, namespace shop

The current version, 2.4.1. It keeps serving most of the traffic during the canary and is scaled down only after the new version is promoted.

### ReplicaSet checkout-79c4d6f5b

created by a controller, apps/v1, namespace shop

The new version, 2.5.0, created when the Pod template changes. The controller sizes it to match the traffic weight.

### Service checkout-stable

you write it, v1, namespace shop

You write it with the plain `app: checkout` selector. The controller adds the stable ReplicaSet's `rollouts-pod-template-hash` to the selector, so it only ever reaches the old Pods.

### Service checkout-canary

you write it, v1, namespace shop

The same manifest under another name. Its selector gets the canary ReplicaSet's hash, so it reaches only the new Pods.

### Gateway public

comes with an add-on, gateway.networking.k8s.io/v1, namespace infra

The platform team's Gateway. Requests for `shop.example.com` arrive here and follow the HTTPRoute attached to it.

### HTTPRoute checkout

you write it, gateway.networking.k8s.io/v1, namespace shop

Two backends with weights. You write 100 and 0; the Gateway API plugin rewrites the two numbers at each `setWeight` step, and the Gateway splits real requests accordingly.

### AnalysisTemplate success-rate

you write it, argoproj.io/v1alpha1, namespace shop

How to judge a version: a query, how often to run it, and what counts as success. Here the canary's share of non-5xx responses must stay at 99% or more, and two failed measurements abort.

### AnalysisRun checkout-79c4d6f5b-2

created by a controller, argoproj.io/v1alpha1, namespace shop

One execution of the template for one rollout step. It stores each measurement and ends as `Successful`, `Failed` or `Inconclusive`.

### Service prometheus-k8s

comes with an add-on, v1, namespace monitoring

Answers the analysis query with the canary's success rate over the last two minutes.

## How the resources connect

- Rollout checkout → ReplicaSet checkout-6d5f8c7b9: owns (creates)
- Rollout checkout → ReplicaSet checkout-79c4d6f5b: owns (creates)
- Rollout checkout → Service checkout-stable: stableService (references)
- Rollout checkout → Service checkout-canary: canaryService (references)
- Service checkout-stable → ReplicaSet checkout-6d5f8c7b9: selector + hash (selects)
- Service checkout-canary → ReplicaSet checkout-79c4d6f5b: selector + hash (selects)
- Rollout checkout → HTTPRoute checkout: plugin sets weights (references)
- Gateway public → HTTPRoute checkout: parentRefs (references)
- HTTPRoute checkout → Service checkout-stable: 90% (traffic)
- HTTPRoute checkout → Service checkout-canary: 10% (traffic)
- Rollout checkout → AnalysisTemplate success-rate: analysis step (references)
- Rollout checkout → AnalysisRun checkout-79c4d6f5b-2: creates (creates)
- AnalysisRun checkout-79c4d6f5b-2 → Service prometheus-k8s: queries (traffic)

## How it works

1. **A new image starts a canary.** Git changes the image to 2.5.0 and Argo CD syncs the Rollout. The controller creates a second ReplicaSet for the new template and leaves the old one at full size.
2. **Services split the Pods by version.** The controller writes each ReplicaSet's template hash into one Service's selector. `checkout-stable` now reaches 2.4.1 only and `checkout-canary` reaches 2.5.0 only.
3. **setWeight edits the HTTPRoute.** At `setWeight: 10` the plugin patches the route's two `backendRefs` to 90 and 10. The Gateway sends one request in ten to the canary. The share of traffic no longer depends on the share of Pods.
4. **Analysis measures the canary.** After the pause, the analysis step creates an AnalysisRun. It queries Prometheus once a minute, five times, and compares each answer with `successCondition`.
5. **Promote or abort.** A successful run moves on to 50% and then to 100%, after which the new ReplicaSet becomes the stable one. A failed run sets the weights back to 100 and 0, scales the canary down and marks the Rollout `Degraded`.

## Common mistakes

- Argo CD sees the edited weights as drift and puts 100 and 0 back in the middle of a canary. Add `ignoreDifferences` for the route's `backendRefs` weights, or leave the weights out of Git.
- The Gateway API support is a plugin. The controller only loads it when the `argo-rollouts-config` ConfigMap lists it under `trafficRouterPlugins`.
- With too little traffic on the canary the query returns no data, and the measurement is an error, not a pass. Give the step real traffic, or handle the empty result in the condition.
- An aborted Rollout stays aborted with the new image still in its spec. Fix forward with a new image, or revert the commit; `kubectl argo rollouts retry rollout checkout` starts the same version again.
- A HorizontalPodAutoscaler must target the Rollout, not a Deployment. `scaleTargetRef` takes `apiVersion: argoproj.io/v1alpha1` and `kind: Rollout`.

## The manifests you write

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: checkout
  namespace: shop
spec:
  replicas: 6
  selector:
    matchLabels:
      app: checkout
  template:
    metadata:
      labels:
        app: checkout
    spec:
      containers:
        - name: checkout
          image: ghcr.io/acme/checkout:2.5.0
          ports:
            - containerPort: 8080
  strategy:
    canary:
      stableService: checkout-stable
      canaryService: checkout-canary
      trafficRouting:
        plugins:
          argoproj-labs/gatewayAPI:
            httpRoute: checkout
            namespace: shop
      steps:
        - setWeight: 10
        - pause: { duration: 5m }
        - analysis:
            templates:
              - templateName: success-rate
        - setWeight: 50
        - pause: { duration: 10m }
---
apiVersion: v1
kind: Service
metadata:
  name: checkout-stable
  namespace: shop
spec:
  selector:
    app: checkout
  ports:
    - port: 80
      targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: checkout-canary
  namespace: shop
spec:
  selector:
    app: checkout
  ports:
    - port: 80
      targetPort: 8080
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: checkout
  namespace: shop
spec:
  parentRefs:
    - name: public
      namespace: infra
  hostnames:
    - shop.example.com
  rules:
    - backendRefs:
        - name: checkout-stable
          port: 80
          weight: 100
        - name: checkout-canary
          port: 80
          weight: 0
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
  namespace: shop
spec:
  metrics:
    - name: success-rate
      interval: 1m
      count: 5
      failureLimit: 1
      successCondition: result[0] >= 0.99
      provider:
        prometheus:
          address: http://prometheus-k8s.monitoring.svc:9090
          query: |
            sum(rate(http_requests_total{service="checkout-canary",code!~"5.."}[2m]))
              / sum(rate(http_requests_total{service="checkout-canary"}[2m]))
```
