Argo CD Lesson 30 of 45

Metrics-Driven Promotion: AnalysisTemplates, Prometheus Queries & Automatic Rollback

A canary deploy is only as good as the thing deciding whether the canary is healthy. In most shops that “thing” is a human: someone pushes a new version to 10% of traffic, opens Grafana, squints at an error-rate panel for ten minutes, and either clicks promote or clicks rollback. That works right up until it doesn’t — the deploy goes out at 2 a.m., or the reviewer gets pulled into a meeting, or the panel they’re watching is the wrong one. The whole promise of progressive delivery is that a bad version should never reach 100% of users. A human with a dashboard is a fragile way to keep that promise.

This lesson is about removing the human from the promotion decision — not the human judgement (you still write the rules), but the human staring. You will teach the canary to query its own vital signs, evaluate them against a threshold you defined in Git, and act: promote when the numbers are good, automatically abort and revert to stable when they are not. Argo Rollouts calls this analysis, and it is the single feature that turns “canary” from a traffic trick into a real safety system.

We assume you already know the Rollout resource and the canary/blue-green strategies from Argo Rollouts: Canary & Blue-Green, and that you have Prometheus scraping your workloads (if not, Observability: Metrics with Prometheus & Grafana gets you there). Everything here targets Argo Rollouts 1.7+ on Kubernetes 1.29+. There is no live cluster behind this page, so every command output below is labelled representative — the shapes, states, and field names are exactly what a real run produces; only the hashes and timestamps will differ on yours.


Why this matters

The core problem is a mismatch of speed. A deployment can go from “10% of traffic” to “100% of traffic” in the time it takes to click a button, but knowing whether that 10% is healthy takes minutes of observation and a clear definition of “healthy.” When the two are done by the same tired person under deadline pressure, you get one of two failure modes: you promote too fast (and ship the outage), or you promote too slow (and every deploy becomes an afternoon of babysitting). Neither scales past a handful of services.

Metric-driven promotion fixes the mismatch by writing the definition of “healthy” down, once, as code, and letting the controller enforce it on every rollout forever. The definition lives in an AnalysisTemplate — a small CRD that says “query this metric, this often, and the new version passes only if the number clears this bar.” The Rollout references the template at a specific point in the canary, and the controller creates an AnalysisRun that does the watching for you. If the numbers hold, the rollout proceeds. If they don’t, the rollout aborts itself and shifts all traffic back to the stable version — no page, no click, no Grafana.

The mental model to hold for the rest of this lesson is a control loop, not a pipeline: observe → decide → act, repeated on an interval. Observe the metric. Decide against the condition. Act by continuing or aborting. That loop is what an AnalysisRun is.

Human-gated promotion Metric-gated promotion (analysis)
Who decides A person watching a dashboard The Rollout controller evaluating an AnalysisRun
Definition of “healthy” In someone’s head, varies by reviewer In Git, one AnalysisTemplate, identical every time
Reaction time Minutes; depends who’s awake Seconds after the measurement interval
At 2 a.m. Nobody watching → bad version promotes Same rules run; bad version auto-aborts
Rollback Manual: notice, decide, kubectl/click Automatic: analysis Failed → abort → revert to stable
Scales to N services No — one dashboard per person Yes — one template per SLO, reused everywhere
Auditability Chat message “looked fine to me” An AnalysisRun object with every measurement recorded

The right way to read that table: analysis does not replace your judgement, it encodes it. You still decide that 95% success rate is the line. You just stop being the sensor and the switch.


The idea: a canary that watches its own vitals

Picture the canary as a patient in an ICU and the AnalysisRun as the monitor wired to it. The monitor takes a reading every so often (the interval), compares each reading to an alarm threshold (the successCondition / failureCondition), and tolerates a set number of bad readings before it pulls the alarm (the failureLimit). While the readings are good, the surgery continues — traffic keeps shifting to the new version. The instant too many readings go bad, the alarm fires and the controller rolls the patient back to the last known-good state — the stable ReplicaSet.

Three properties make this trustworthy rather than a gimmick:

Read the loop left to right below. The Rollout is mid-canary; the controller spawns an AnalysisRun; the run queries Prometheus on an interval; each result is checked against the successCondition; if the condition holds the step promotes, and if failures pile past the limit the Rollout aborts and traffic snaps back to stable. Every numbered badge is a decision point or a failure mode — the places where this either saves you or, misconfigured, bites you.

Metric-gated promotion loop: Argo CD syncs a Rollout, the canary step spawns an AnalysisRun that queries Prometheus for success-rate, the successCondition and failureLimit form the gate, and the outcome branches to promote-to-stable or auto-abort-and-revert

The two nodes worth tattooing on your brain are the amber gate and the red outcome. The gate (badges 3-4) is where your SLO becomes an executable expression — result[0] >= 0.95 — with a tolerance (failureLimit) that decides how jumpy it is. The red outcome (badge 6) is the payoff of the entire feature: when analysis fails, you do nothing and the right thing happens. That is the difference between “we have canaries” and “we are actually protected by them.”


AnalysisTemplate and ClusterAnalysisTemplate, field by field

Analysis is defined by two nearly identical CRDs. An AnalysisTemplate is namespaced — it lives next to the Rollout that uses it. A ClusterAnalysisTemplate is cluster-scoped — define your organisation’s standard SLO checks once and reference them from Rollouts in any namespace. The spec schema is identical between the two; only the scope and how you reference them differ.

Here is a complete, schema-correct AnalysisTemplate with every commonly used field present. Read it once, then we’ll dissect the metric block.

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
  namespace: demo
spec:
  args:
  - name: service-name                     # required at use-time (no default)
  - name: prometheus-address               # has a default, can be overridden
    value: http://prometheus-operated.monitoring.svc.cluster.local:9090
  metrics:
  - name: success-rate                     # identifier for this measurement
    interval: 30s                          # measure every 30s...
    count: 5                               # ...five times, then finish
    initialDelay: 60s                      # wait 60s before the first measurement
    successCondition: result[0] >= 0.95    # pass when success rate >= 95%
    failureLimit: 3                        # tolerate 3 bad readings; the 4th fails the run
    consecutiveErrorLimit: 4               # 4 provider errors in a row = Error (default)
    provider:
      prometheus:
        address: "{{args.prometheus-address}}"
        timeout: 20                        # seconds to wait for the query
        query: |
          sum(rate(
            http_requests_total{app="{{args.service-name}}", code!~"5.."}[2m]
          ))
          /
          sum(rate(
            http_requests_total{app="{{args.service-name}}"}[2m]
          ))

The metric fields, exactly

Every field under spec.metrics[] is confirmed against the Argo Rollouts Metric type. These are the knobs that decide how jumpy, how patient, and how strict your gate is.

Field Type Default What it controls
name string — (required) Identifier for the measurement; shows up in the AnalysisRun status
interval duration once How often to take a measurement (30s, 2m, 1h). Omit it and the metric runs a single time
count int/string How many measurements to take. With interval, the run finishes after count measurements. 0 = run continuously (used for background analysis)
initialDelay duration 0 Wait this long after the run starts before the first measurement — the warm-up window
successCondition expr Expression that, when true, marks the measurement Successful
failureCondition expr Expression that, when true, marks the measurement Failed
failureLimit int/string 0 Max Failed measurements tolerated before the metric (and run) is Failed
inconclusiveLimit int/string 0 Max Inconclusive measurements tolerated before the run is Inconclusive
consecutiveErrorLimit int/string 4 Max consecutive provider errors (e.g. Prometheus unreachable) before the run Errors
consecutiveSuccessLimit int/string 0 Require N consecutive successes to pass early (Argo Rollouts 1.8+); 0 disables
provider object — (required) Exactly one metric source: prometheus, datadog, web, job, etc.

Two subtleties beginners miss. First, failureLimit counts total failed measurements across the run, while consecutiveErrorLimit counts consecutive provider errors — a failed measurement (your app is genuinely returning 500s) and an errored measurement (Prometheus timed out) are different things with different budgets. Second, count: 0 is not “measure zero times” — it means “measure forever,” which is exactly what you want for a background analysis that should run for the whole duration of the canary.

Namespaced vs cluster-scoped

AnalysisTemplate ClusterAnalysisTemplate
Scope One namespace Whole cluster
Referenced by templateName (same namespace) templateName + clusterScope: true
Good for App-specific checks (this service’s own metric) Org-wide SLO standards reused everywhere
Ownership App team, lives in the app’s GitOps path Platform team, lives in the platform repo
spec schema Identical Identical

A healthy pattern for a platform team: publish ClusterAnalysisTemplates named success-rate, latency-p95, and error-budget that encode the house SLOs, and have every app Rollout reference them with clusterScope: true, passing only the app-specific args (the service name, the namespace). One definition of “healthy,” enforced fleet-wide, owned by the people who own the SLOs.

Arguments: parameterising a template

spec.args turns a template into a reusable function. Each arg is either declared with no value (the caller must supply it), given a default value, or sourced from a Secret with valueFrom.secretKeyRef — the right place for an API token so it never appears in the manifest.

spec:
  args:
  - name: service-name                     # caller must provide this
  - name: prometheus-address
    value: http://prometheus-operated.monitoring.svc:9090   # default
  - name: api-token
    valueFrom:
      secretKeyRef:
        name: datadog-creds
        key: api-token                     # secret, never inline

Inside metrics[], reference an arg anywhere a string is expected with the double-brace syntax: {{args.service-name}}. The controller substitutes the value before sending the query. Miss a required arg at use-time and the AnalysisRun never starts — you’ll see it in the Rollout events, covered in troubleshooting.

Arg declaration Where the value comes from Use it for
- name: x (no value) Caller must pass it in the Rollout’s analysis.args Per-app values: service name, namespace
- name: x + value: The default in the template Shared defaults: Prometheus address
- name: x + valueFrom.secretKeyRef A Kubernetes Secret Credentials: API keys, tokens

The metric providers

A provider is the source the analysis queries. Argo Rollouts ships twelve built-in providers, and you pick exactly one per metric. There is deliberately no generic “Kubernetes” provider — when people ask for one they mean the job provider, which runs an arbitrary Kubernetes Job as the check (run any container, any script, any in-cluster probe, and let its exit code be the verdict). For anything truly custom there is the plugin provider, which loads a metric plugin you supply. Everything else is a specific observability backend.

Provider (provider.<key>) Category Reach for it when…
prometheus Time-series You self-host Prometheus (or a Prometheus-compatible store); the default for most teams
datadog SaaS APM/metrics Datadog is your system of record; apiVersion: v2 for Metrics Query
newRelic SaaS APM You run New Relic; query is NRQL
cloudWatch Cloud-native (AWS) Your signals are already in CloudWatch on EKS
wavefront SaaS time-series You use Wavefront (Tanzu Observability)
graphite Time-series Legacy Graphite is your metric store
influxdb Time-series Metrics live in InfluxDB; query is Flux
skywalking APM (open source) Apache SkyWalking traces your services
kayenta Automated canary You already run Spinnaker’s Kayenta for ACA (advanced statistical scoring)
web HTTP endpoint You want to hit any REST endpoint and parse JSON (a gate service, a custom SLO API)
job Kubernetes Job You want to run a check — an integration test, a smoke test, a load probe — not query a metric
plugin Custom None of the above fit; you ship a Go metric plugin

Prometheus — the one you’ll use most

The Prometheus provider runs a PromQL query and exposes the outcome to your condition as result. For an instant query returning a vector, result is an array of the scalar values, and result[0] is the first (usually only) series’ value. The whole art is writing a query that collapses to a single meaningful number — a ratio, a quantile, a rate.

provider:
  prometheus:
    address: http://prometheus-operated.monitoring.svc.cluster.local:9090
    timeout: 20                  # seconds; a slow query shouldn't hang the run
    insecure: false              # set true only to skip TLS verification (dev)
    query: |
      sum(rate(
        http_requests_total{app="checkout", code!~"5.."}[2m]
      ))
      /
      sum(rate(
        http_requests_total{app="checkout"}[2m]
      ))
Field Meaning
address Prometheus server URL (in-cluster Service DNS or external)
query The PromQL; templated with {{args.*}}
timeout Seconds to wait for the query before erroring
insecure Skip TLS verification (dev only)
headers Extra HTTP headers (e.g. X-Scope-OrgID for a Thanos/Mimir tenant)
rangeQuery start/end/step for a range query instead of an instant query
authentication sigv4 (AWS SigV4) or oauth2 (tokenUrl, clientId, clientSecret, scopes)

The result shape is the number-one source of broken conditions, so be precise:

Query returns result is Reference it as Watch out
One series (instant vector) [value] result[0] The normal, intended case
Multiple series [v0, v1, …] result[0], result[1] You probably meant one number — collapse with sum(...)
A scalar the scalar result Some functions (scalar()) return this
Nothing matched [] (empty) result[0] errors The no-data trap → Error measurement

A bare instant vector gives you result[0] as a float; multiple series give you result[0], result[1], …; and a query that matches nothing gives an empty result, where result[0] is an error — the dreaded no-data trap we handle below.

Web — hit any HTTP endpoint

When your source of truth is a REST API (a home-grown gate service, a third-party SLO tool), the web provider fetches a URL and evaluates a jsonPath from the response.

provider:
  web:
    url: https://gate.example.com/api/v1/canary?svc={{args.service-name}}
    method: GET                  # or POST/PUT with a body
    timeoutSeconds: 20
    headers:
    - key: Authorization
      value: "Bearer {{args.api-token}}"
    jsonPath: "{$.approved}"     # pull one field out of the JSON response
# successCondition: "result == true"
web field Meaning
url Endpoint to hit (templated with {{args.*}})
method GET, POST, or PUT
body Request body for POST/PUT
headers[] key/value HTTP headers — where auth tokens go
jsonPath JSONPath extracting one value from the response into result
timeoutSeconds Per-request timeout

Job — run a check instead of querying one

The job provider runs a Kubernetes Job. The measurement is Successful if the Job completes with exit code 0 and Failed otherwise — no successCondition needed. This is your escape hatch for anything you can script: integration tests, synthetic transactions, a load-test-then-check.

provider:
  job:
    metadata:
      labels:
        role: canary-check
    spec:
      backoffLimit: 1
      template:
        spec:
          containers:
          - name: test
            image: ghcr.io/acme/canary-tests:1.4.0
            command: ["pytest", "-q", "--target", "{{args.service-name}}"]
          restartPolicy: Never
job provider Meaning
metadata labels/annotations stamped onto the created Job
spec A standard Kubernetes Job spec (backoffLimit, template, containers)
Success Job completes with exit code 0 → measurement Successful
Failure Job fails or exits non-zero → measurement Failed

Because the verdict is just an exit code, the job provider is how you run any Kubernetes-native check — the closest thing to the “Kubernetes provider” people expect, and far more flexible.

Cloud-managed metric backends — where the multi-cloud edge appears

The analysis mechanics are entirely cloud-neutral: the same AnalysisTemplate, successCondition, and abort behaviour run identically on AKS, EKS, and GKE. The one place a cloud edge appears is which metric store you query and how the Rollouts controller authenticates to it. If you run a plain in-cluster Prometheus, there is no cloud edge at all — skip this table. The moment you point analysis at a managed metric service, each cloud authenticates its own way, and you grant that identity to the argo-rollouts controller ServiceAccount (namespace argo-rollouts), because the controller is the process making the query.

Cloud Managed metric backend Provider to use How the controller authenticates Permission to grant
AKS (Azure) Azure Monitor managed Prometheus prometheus with authentication.oauth2 Microsoft Entra Workload Identity federated to the argo-rollouts SA; client-credentials to the query endpoint (scope https://prometheus.monitor.azure.com/.default) Monitoring Data Reader on the workspace
AKS (Azure) Azure Monitor metrics (non-Prometheus) web against the Azure Monitor REST API Entra Workload Identity → bearer token in a header Monitoring Reader on the resource
EKS (AWS) Amazon Managed Prometheus (AMP) prometheus with authentication.sigv4 (region, roleArn) IRSA or EKS Pod Identity on the argo-rollouts SA assumes a role; SigV4-signs the query aps:QueryMetrics, aps:GetSeries on the workspace
EKS (AWS) CloudWatch cloudWatch IRSA or EKS Pod Identity on the argo-rollouts SA cloudwatch:GetMetricData
GKE (GCP) Google Cloud Managed Service for Prometheus prometheus against the in-cluster GMP frontend proxy Workload Identity: the argo-rollouts KSA bound to a GSA; the frontend adds Google auth roles/monitoring.viewer
GKE (GCP) Cloud Monitoring (non-Prometheus) web against the Cloud Monitoring API Workload Identity → OAuth token roles/monitoring.viewer

Two paired examples make the difference concrete. On EKS with Amazon Managed Prometheus, SigV4 signing is the whole story — no address-embedded credentials:

# EKS + Amazon Managed Prometheus
provider:
  prometheus:
    address: https://aps-workspaces.us-east-1.amazonaws.com/workspaces/ws-abc123/
    authentication:
      sigv4:
        region: us-east-1
        roleArn: arn:aws:iam::111122223333:role/argo-rollouts-amp-query
    query: |
      sum(rate(http_requests_total{app="checkout",code!~"5.."}[2m]))
      / sum(rate(http_requests_total{app="checkout"}[2m]))

On AKS with Azure Monitor managed Prometheus, OAuth2 client-credentials against the query endpoint, with the client secret pulled from a Kubernetes Secret via an arg:

# AKS + Azure Monitor managed Prometheus
provider:
  prometheus:
    address: https://my-workspace.eastus.prometheus.monitor.azure.com
    authentication:
      oauth2:
        tokenUrl: https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
        clientId: "{{args.entra-client-id}}"
        clientSecret: "{{args.entra-client-secret}}"
        scopes:
        - https://prometheus.monitor.azure.com/.default
    query: |
      sum(rate(http_requests_total{app="checkout",code!~"5.."}[2m]))
      / sum(rate(http_requests_total{app="checkout"}[2m]))

On GKE, the cleanest path avoids auth in the template entirely: deploy the Managed Service for Prometheus frontend in-cluster (it runs under Workload Identity and adds Google credentials for you), and point the analysis at it like any ordinary Prometheus — address: http://frontend.gmp-system.svc:9090. The cloud edge collapses back into a plain Prometheus query.

For deeper coverage of registering these identities — IRSA vs EKS Pod Identity, Entra Workload Identity, GKE Workload Identity — the per-cloud lessons on running Argo CD on each platform go field by field; here the only new fact is that the Rollouts controller SA is the identity that needs the read permission, not Argo CD’s.


Writing the success and failure conditions

The condition is where your SLO becomes executable. Conditions are expressions in expr evaluated against result. The rules that decide a measurement’s phase are simple but easy to get backwards:

You specify Expression true Expression false
Only successCondition measurement Successful measurement Failed
Only failureCondition measurement Failed measurement Successful
Both (success true →) Successful; else (failure true →) Failed; else Inconclusive

So successCondition: result[0] >= 0.95 alone means “anything under 95% is a failure.” If you’d rather express the bad case directly — often clearer for error budgets — use failureCondition: result[0] > 0.05 (“more than 5% errors is a failure”). Using both creates a deliberate dead-band in the middle that is neither pass nor fail — an Inconclusive zone that pauses the rollout for a human instead of promoting or aborting. That’s the tool for “I’m not confident enough to auto-decide in this range.”

The condition language gives you a small, reliable toolkit — learn these five pieces and you can express almost any gate:

Expression piece Example What it does
Index result[0] The first series’ scalar value
Comparison result[0] >= 0.95 Threshold test → bool
Boolean combine result[0] < 0.05 && result[1] > 100 Gate on multiple signals at once
Length guard len(result) > 0 “Did the query return any data?”
Default fallback default(result, 0) Substitute a value when the result is missing (choose the fail-safe fallback)
Named field result.successRate Pull a field from a JSON/NRQL result (web, newRelic)

The limit fields decide how jumpy the gate is

A single measurement almost never decides a run — the limits do. Each phase has its own budget:

Limit Counts Run outcome when exceeded Default
failureLimit Total Failed measurements Run Failed → Rollout aborts 0
inconclusiveLimit Total Inconclusive measurements Run Inconclusive → Rollout pauses 0
consecutiveErrorLimit Consecutive provider errors Run Error → Rollout aborts 4
consecutiveSuccessLimit Consecutive successes (1.8+) Run Successful early (short-circuit pass) 0 (off)

The interaction to internalise: with the defaults (failureLimit: 0), the very first failed measurement fails the whole run. That is far too jumpy for real metrics — one slow scrape and your good deploy aborts. Production templates almost always raise failureLimit to tolerate a few blips over a count of several measurements. A common, sane shape is count: 5, interval: 1m, failureLimit: 2 — “measure five times a minute apart; abort only if three of them are bad.”

Timing: count, interval, initialDelay

Field Question it answers Typical value
initialDelay How long to let the new version warm up before judging it? 30s5m
interval How often to re-measure? 30s5m (match your metric’s scrape/aggregation)
count How many measurements before deciding? 310 (inline); 0 = forever (background)

initialDelay is the field people forget and then blame the tool. A brand-new pod has cold caches, an unwarmed JVM, and near-zero traffic; measured immediately, its “success rate” is noise. Give it a warm-up. And interval should never be shorter than your metric’s freshness — if Prometheus aggregates over a 2-minute window and scrapes every 30s, an interval: 10s just re-reads the same stale number four times.

The no-data trap — the failure mode that fools everyone

Here is the gotcha that costs teams a weekend. Your successCondition: result[0] >= 0.95 looks airtight. Then a canary goes out during a quiet hour, receives almost no traffic, and your success-rate query — a ratio with near-zero denominator — returns empty or NaN. Now result[0] either throws (an Error measurement, eating your consecutiveErrorLimit) or is NaN (the comparison is false, a Failed measurement). Either way a perfectly healthy deploy gets aborted because there wasn’t enough traffic to judge it — or, if you “fixed” it the wrong way with a permissive default, a genuinely broken deploy sails through because no-data was treated as pass.

There is no free lunch here; you choose a risk posture:

The instinct to reach for default(..., 1) “so my quiet-hour deploys stop failing” is exactly how a real outage promotes itself. Prefer guaranteeing traffic. If you must default, default to the fail-safe value and accept the occasional false abort.


AnalysisRun: the instance

An AnalysisTemplate is a class; an AnalysisRun is the object instantiated from it when analysis actually runs. The controller creates it, records every measurement in its status, and drives it to a terminal phase. You rarely write one by hand — you read them to understand what a rollout decided and why.

There are two levels of “phase,” and confusing them is a common source of muddled debugging. Each individual measurement gets a phase; the accumulated measurements, weighed against the limits, decide the run’s phase. First, the per-measurement phases:

Measurement phase Set when Counts toward
Successful successCondition true (or failureCondition false) consecutiveSuccessLimit
Failed failureCondition true (or successCondition false) failureLimit
Inconclusive Both conditions set, neither matched inconclusiveLimit
Error The provider call itself failed (timeout, unreachable, auth) consecutiveErrorLimit

And then the run’s overall status.phase, which is what the Rollout actually reacts to:

status.phase Meaning Effect on the Rollout
Pending Created, first measurement not yet taken (e.g. in initialDelay) Rollout waits
Running Actively measuring Rollout proceeds/paused per strategy
Successful Passed the conditions within limits Rollout continues / promotes
Failed failureLimit exceeded (or failureCondition decisive) Rollout aborts → revert to stable
Error consecutiveErrorLimit of provider errors Rollout aborts → revert to stable
Inconclusive inconclusiveLimit exceeded Rollout pauses for human decision
Terminated Externally stopped (rollout aborted/retried) Run halts

The distinction that matters operationally: Failed and Error both abort, but they mean opposite things. Failed = “your app is genuinely bad” (the metric cleared the failure bar). Error = “I couldn’t measure your app” (Prometheus was down, the query timed out, credentials expired). Chasing a code bug when the real problem was an unreachable Prometheus is a classic wasted afternoon — always read the run’s message to see which it was.

AnalysisRuns are named after the Rollout, the ReplicaSet’s pod-template-hash, and a counter (e.g. rollouts-demo-687d76d795-2). Inspect them with standard kubectl:

# List the runs for a namespace
kubectl -n demo get analysisrun
# NAME                          STATUS      AGE
# rollouts-demo-687d76d795-2    Running     45s

# The full story: every measurement, its value, its phase
kubectl -n demo get analysisrun rollouts-demo-687d76d795-2 -o yaml

The status.metricResults[].measurements[] array is the receipt — each entry has a phase, a value (the actual number the query returned), and timestamps. When someone asks “why did the deploy roll back?”, that array is the answer, and it’s in the cluster, not in anyone’s memory.


Wiring analysis into a Rollout

A template that no Rollout references never runs. There are four placements, and choosing the right one is a real design decision. The table first, then each in code.

Placement Field When it runs On failure Use it for
Inline (per-step) strategy.canary.steps[].analysis At that exact step, blocks until it finishes Abort → revert to stable A gate between weight increases: “prove 20% is healthy before going to 50%”
Background strategy.canary.analysis Throughout the rollout, in parallel with steps Abort at any point Continuous guard for the whole canary duration
Pre-promotion (BG) strategy.blueGreen.prePromotionAnalysis After preview is up, before traffic cutover Cancel promotion; active stays on stable Smoke-test the green stack before sending users to it
Post-promotion (BG) strategy.blueGreen.postPromotionAnalysis After traffic cutover to green Roll back to blue Confirm the live cutover is healthy, revert if not

Inline: a gate between canary steps

The most common shape. analysis is a step, so the rollout stops at it and won’t advance until the run reaches Successful.

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: rollouts-demo
  namespace: demo
spec:
  replicas: 5
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: rollouts-demo
  template:
    metadata:
      labels:
        app: rollouts-demo
    spec:
      containers:
      - name: rollouts-demo
        image: argoproj/rollouts-demo:blue
        ports:
        - name: http
          containerPort: 8080
        resources:
          requests: { cpu: 5m, memory: 32Mi }
  strategy:
    canary:
      steps:
      - setWeight: 20
      - pause: { duration: 30s }
      - analysis:                      # <-- the gate
          templates:
          - templateName: success-rate
          args:
          - name: service-name
            value: rollouts-demo
      - setWeight: 50
      - pause: { duration: 30s }
      - setWeight: 80
      - pause: { duration: 30s }

Background: watch the whole rollout

strategy.canary.analysis starts a run that lives for the entire rollout and can abort it at any step, not just at a gate. Pair it with a template using count: 0 (continuous). startingStep delays the watch until a given step index, so you don’t judge the version before any traffic reaches it.

  strategy:
    canary:
      analysis:
        templates:
        - templateName: success-rate
        args:
        - name: service-name
          value: rollouts-demo
        startingStep: 2               # don't start analysing until step index 2
      steps:
      - setWeight: 20
      - pause: { duration: 1m }
      - setWeight: 50                  # from here, background analysis is watching
      - pause: { duration: 1m }
      - setWeight: 80
      - pause: { duration: 1m }

Inline and background are complementary, not exclusive: inline gates make discrete promotion decisions (“is 20% healthy enough to earn 50%?”), while background analysis is a continuous safety net that catches a regression that only shows up at higher weight. Many production Rollouts use both.

Inline (steps[].analysis) Background (canary.analysis)
Blocks a step? Yes — the rollout waits at that step until it passes No — runs in parallel with the steps
Typical count Finite (310) so the step can advance 0 (continuous, runs the whole rollout)
Can abort at That step only Any point during the rollout
startingStep n/a Delays when the watch begins
Best for A hard gate between weight increases A continuous safety net for the full canary

Blue-green: pre- and post-promotion

For blue-green, the two hooks bracket the cutover. prePromotionAnalysis runs against the preview Service before flipping traffic; postPromotionAnalysis runs against the active Service after the flip and can roll the cutover back.

  strategy:
    blueGreen:
      activeService: rollouts-demo-active
      previewService: rollouts-demo-preview
      autoPromotionEnabled: false
      prePromotionAnalysis:            # gate BEFORE traffic cutover
        templates:
        - templateName: smoke-tests
        args:
        - name: service-name
          value: rollouts-demo-preview.demo.svc.cluster.local
      postPromotionAnalysis:           # verify AFTER cutover; revert if bad
        templates:
        - templateName: success-rate
        args:
        - name: service-name
          value: rollouts-demo-active.demo.svc.cluster.local

Passing args at use-time, including the canary’s own hash

Notice the args under each analysis reference — that’s where the caller fills in the template’s declared arguments. Beyond static values, valueFrom can inject dynamic facts about the rollout itself, which is how you scope a query to only the new pods:

Arg source Syntax Gives you
Static value value: rollouts-demo A literal
Canary pod hash valueFrom: { podTemplateHashValue: Latest } The new ReplicaSet’s rollouts-pod-template-hash
Stable pod hash valueFrom: { podTemplateHashValue: Stable } The stable ReplicaSet’s hash
A field on the Rollout valueFrom: { fieldRef: { fieldPath: metadata.labels['region'] } } Any label/field, e.g. region for a per-region query
        args:
        - name: service-name
          value: rollouts-demo
        - name: canary-hash
          valueFrom:
            podTemplateHashValue: Latest     # measure ONLY the new pods

Then your PromQL filters on that hash — ...{rollouts_pod_template_hash="{{args.canary-hash}}"}... — so the success rate reflects the canary alone, not a blend of canary and stable that hides the regression. (This assumes your metrics carry the pod-template-hash as a label, e.g. via kube-state-metrics or pod relabeling; without traffic routing that splits canary and stable, scoping the query is the only way to isolate the new version’s health.)


Automatic rollback: the payoff

Everything so far exists to make one sentence true: when analysis fails, you do nothing and traffic returns to the last good version. Here is exactly what the controller does the moment an inline or background AnalysisRun reaches Failed (or Error):

  1. It sets the Rollout’s status.abort: true and stamps status.abortedAt.
  2. It scales the stable ReplicaSet back to full and the canary to zero — traffic snaps back to the known-good version, immediately, because stable was never torn down.
  3. The Rollout’s phase becomes Degraded with a message naming the metric that failed.
  4. It stays aborted. It does not thrash — it won’t retry the bad revision on its own.

To move forward from an aborted rollout you have two levers. Push a new image (a fix), and the new revision clears the abort and starts a fresh canary. Or, if you believe the abort was a fluke (a flaky metric, a transient Prometheus outage), retry the same revision:

# Re-run the aborted rollout from step 0 (same revision)
kubectl argo rollouts retry rollout rollouts-demo -n demo

# Manually abort a rollout yourself (e.g. you spotted trouble first)
kubectl argo rollouts abort rollouts-demo -n demo

# Manually promote past the current step / to full
kubectl argo rollouts promote rollouts-demo -n demo

The metric that matters — tie the gate to an SLO

A rollback system is only as trustworthy as the metric it watches. Gate on user-facing SLIs, not on infrastructure vanity metrics. CPU being high doesn’t mean users are unhappy; a rising 5xx rate does. The three that belong in almost every gate:

SLI What it protects PromQL shape Condition
Success rate Availability — are requests succeeding? sum(rate(reqs{code!~"5.."}[w])) / sum(rate(reqs[w])) result[0] >= 0.99
p95/p99 latency Speed — is it fast enough? histogram_quantile(0.95, sum(rate(bucket[w])) by (le)) result[0] <= 0.3 (seconds)
Error budget burn The SLO itself — burning too fast? error-rate ÷ (1 − SLO target) over a window failureCondition: result[0] > 14.4 (fast burn)

The error-budget framing is the SRE-grade version: instead of a static “95% is fine,” you gate on how fast the new version is spending this month’s allowed downtime. A fast-burn condition (e.g. burn rate > 14.4× over an hour, the multiwindow-multiburn convention) aborts a version that would exhaust the budget in hours, even if its instantaneous success rate still looks “okay-ish.” If your org runs formal SLOs, gate the canary on burn rate and the promotion decision and the reliability target become the same number.


Best practices

Analysis is powerful enough to hurt you if you deploy it aggressively on day one. The path that works:

Practice Why How
Start observe-only Learn what your metric does during a real rollout before it can abort anything Set a generous failureLimit (or a permissive condition) so the run reports but rarely fails; watch a few deploys, then tighten
Warm up before judging Cold pods produce noise metrics Always set initialDelay (≥ 30s, often more)
Match interval to metric freshness Sampling faster than the data updates just re-reads stale numbers interval ≥ scrape/aggregation window
Tolerate blips Real metrics jitter; failureLimit: 0 is hair-trigger count several measurements, failureLimit 2–3
Avoid flaky metrics A gate on a noisy signal aborts good deploys and erodes trust Prefer smooth rate/ratio SLIs; widen rate windows; don’t gate on raw single-request latency
Guarantee traffic The no-data trap aborts quiet-hour deploys Run a load generator against the canary during analysis
Scope to the canary A blended canary+stable metric hides the regression Pass podTemplateHashValue: Latest and filter the query, or measure a canary-only Service
Gate on SLIs, not vanity Users feel errors and latency, not CPU Success rate, latency percentile, error-budget burn
Use ClusterAnalysisTemplates for standards One definition of “healthy” beats fifty copies Platform-owned ClusterAnalysisTemplates, apps pass only args
Page on Failed, not just abort The rollback is silent by design; someone should still know Wire an alert on AnalysisRun Failed (see notifications)

The last one deserves a pointer: the auto-abort is deliberately quiet — that’s the feature — but a version that rolled itself back is still a signal a human should see. Route it with Argo CD Notifications: Slack, Teams & Webhooks so a failed analysis posts to the channel that owns the service. And if your canary uses real traffic shifting (not just replica-weighting) to route that 20%, the mechanics are in Rollouts Traffic Management: Istio, NGINX, ALB & Gateway API — analysis and traffic routing are the two halves of a real canary.


Hands-on lab

You’ll build a metric-gated canary end to end on a free local cluster, then watch a healthy version promote and a bad version auto-abort and revert to stable. Everything runs on kind — no cloud, no bill. The two rollout outcomes are shown as representative output (this page has no live cluster), but every manifest is real and applies cleanly, and the states/fields are exactly what you’ll see.

⚠️ Nothing in this lab bills. It is entirely local (kind + in-cluster Prometheus). The cloud-managed-Prometheus variants from the providers section would incur cost (AMP query units, Azure Monitor ingestion) — don’t point this lab at them.

Step 1 — Cluster, controller, and the kubectl plugin

# A throwaway local cluster
kind create cluster --name analysis-lab

# Install the Argo Rollouts controller
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts \
  -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml

# Install the kubectl plugin (macOS/Linux via krew, or grab the binary)
kubectl krew install argo-rollouts
kubectl argo rollouts version
# kubectl-argo-rollouts: v1.7.x

What just happened: you have the controller that reconciles Rollout and AnalysisRun objects, plus the kubectl argo rollouts plugin that renders them far more readably than raw kubectl get.

Step 2 — A minimal Prometheus

Install a small Prometheus (the community chart is fine; kube-prometheus-stack works too). The only requirement is that it scrapes our demo app’s /metrics.

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prom prometheus-community/prometheus \
  --namespace monitoring --create-namespace
# Its in-cluster address will be:
#   http://prom-prometheus-server.monitoring.svc.cluster.local

What just happened: you now have a metric store the AnalysisRun can query. In a real cluster this is your existing Prometheus; here it’s a throwaway.

Step 3 — The AnalysisTemplate

Save and apply the success-rate template. Note the guarded, canary-tolerant settings: a warm-up, five measurements, and room for two blips.

# analysis-template.yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
  namespace: demo
spec:
  args:
  - name: service-name
  - name: prometheus-address
    value: http://prom-prometheus-server.monitoring.svc.cluster.local
  metrics:
  - name: success-rate
    interval: 20s
    count: 5
    initialDelay: 30s
    successCondition: result[0] >= 0.95
    failureLimit: 2
    provider:
      prometheus:
        address: "{{args.prometheus-address}}"
        timeout: 15
        query: |
          sum(rate(
            http_requests_total{app="{{args.service-name}}", code!~"5.."}[1m]
          ))
          /
          sum(rate(
            http_requests_total{app="{{args.service-name}}"}[1m]
          ))
kubectl create namespace demo
kubectl apply -f analysis-template.yaml

What just happened: the definition of “healthy” for this service now lives in the cluster (and, in real life, in Git). It measures success rate every 20s, five times, after a 30s warm-up, and fails only if three of five readings drop below 95%.

Step 4 — The Rollout that references it, plus traffic

# rollout.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: rollouts-demo
  namespace: demo
spec:
  replicas: 5
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: rollouts-demo
  template:
    metadata:
      labels:
        app: rollouts-demo
      annotations:
        prometheus.io/scrape: "true"     # let the community Prometheus scrape it
        prometheus.io/port: "8080"
    spec:
      containers:
      - name: rollouts-demo
        image: argoproj/rollouts-demo:blue
        ports:
        - name: http
          containerPort: 8080
        resources:
          requests: { cpu: 5m, memory: 32Mi }
  strategy:
    canary:
      steps:
      - setWeight: 20
      - pause: { duration: 20s }
      - analysis:
          templates:
          - templateName: success-rate
          args:
          - name: service-name
            value: rollouts-demo
      - setWeight: 60
      - pause: { duration: 20s }
      - setWeight: 100
---
apiVersion: v1
kind: Service
metadata:
  name: rollouts-demo
  namespace: demo
spec:
  selector:
    app: rollouts-demo
  ports:
  - port: 80
    targetPort: 8080
kubectl apply -f rollout.yaml
# Generate steady traffic so the query always has data (dodges the no-data trap):
kubectl -n demo run loadgen --image=busybox --restart=Never -- \
  /bin/sh -c 'while true; do wget -q -O- http://rollouts-demo/ >/dev/null 2>&1; done'

What just happened: the initial revision comes up as stable, the Service gives the loadgen something to hit, and steady traffic keeps the success-rate query populated. The gate sits at step 3. (The PromQL here assumes your app exports http_requests_total{app,code}; adapt the metric name in the template to whatever your workload actually exposes — the mechanism is identical regardless of the metric’s name.)

Step 5 — A healthy update (analysis passes → promotes)

Trigger a canary by changing the image to another good build, then watch:

kubectl argo rollouts set image rollouts-demo \
  rollouts-demo=argoproj/rollouts-demo:green -n demo

kubectl argo rollouts get rollout rollouts-demo -n demo --watch

Representative output while the gate is evaluating, then after it passes:

Name:            rollouts-demo
Namespace:       demo
Status:          ॥ Paused
Message:         CanaryPauseStep
Strategy:        Canary
  Step:          3/6
  SetWeight:     20
  ActualWeight:  20
Images:          argoproj/rollouts-demo:blue (stable)
                 argoproj/rollouts-demo:green (canary)

NAME                                        KIND         STATUS        AGE  INFO
⟳ rollouts-demo                             Rollout      ॥ Paused      6m
├──# revision:2
│  ├──⧉ rollouts-demo-687d76d795            ReplicaSet   ✔ Healthy     40s  canary
│  │  └──□ rollouts-demo-687d76d795-abc     Pod          ✔ Running     40s  ready:1/1
│  └──α rollouts-demo-687d76d795-2          AnalysisRun  ◌ Running     20s  ✔ 2
└──# revision:1
   └──⧉ rollouts-demo-6cf78c96b5            ReplicaSet   ✔ Healthy     6m   stable

The AnalysisRun line (α) shows ◌ Running ✔ 2 — two successful measurements so far. Once it reaches five successes, the run flips to ✔ Successful, the gate opens, and the rollout walks the remaining steps to setWeight: 100, at which point revision 2 becomes stable:

Status:          ✔ Healthy
  Step:          6/6
  SetWeight:     100
Images:          argoproj/rollouts-demo:green (stable)
   └──α rollouts-demo-687d76d795-2          AnalysisRun  ✔ Successful  3m   ✔ 5

What just happened: nobody watched Grafana. The canary measured itself five times, cleared 95% every time, and promoted on its own.

Step 6 — A bad update (analysis Failed → auto-abort → revert to stable)

Now ship a “bad” version — one that returns 5xx. The rollouts-demo app has a built-in error slider in its web UI you can crank up to make the new pods return errors; in your own service this is simply a build with a regression. We change the image tag to trigger a fresh canary revision, then drive its error rate up. The success-rate query collapses below 0.95, three of five readings fail, and the run fails.

# New tag = new revision = new canary; the ':red' tag is just a distinct build to roll out
kubectl argo rollouts set image rollouts-demo \
  rollouts-demo=argoproj/rollouts-demo:red -n demo
# ...then raise the error rate on the canary pods (demo UI slider, or deploy a genuinely bad build)

kubectl argo rollouts get rollout rollouts-demo -n demo --watch

Representative output as the gate fails:

Name:            rollouts-demo
Status:          ✖ Degraded
Message:         RolloutAborted: Rollout aborted update to revision 3: metric "success-rate" assessed Failed: failed (3) > failureLimit (2)
Strategy:        Canary
  Step:          3/6
  ActualWeight:  0
Images:          argoproj/rollouts-demo:green (stable)
                 argoproj/rollouts-demo:red   (canary, scaled down)

NAME                                        KIND         STATUS        AGE  INFO
⟳ rollouts-demo                             Rollout      ✖ Degraded    9m
├──# revision:3
│  ├──⧉ rollouts-demo-7c9fbb6d84            ReplicaSet   •             30s  canary
│  └──α rollouts-demo-7c9fbb6d84-3          AnalysisRun  ✖ Failed      25s  ✖ 3
└──# revision:2
   └──⧉ rollouts-demo-6b5c8d7f9c            ReplicaSet   ✔ Healthy     6m   stable

Inspect the run to see the receipt:

kubectl -n demo get analysisrun rollouts-demo-7c9fbb6d84-3 \
  -o jsonpath='{.status.phase} {.status.metricResults[0].failed}{"\n"}'
# Failed 3

kubectl -n demo describe analysisrun rollouts-demo-7c9fbb6d84-3 | grep -A2 Message
# Message: metric "success-rate" assessed Failed:
#          failed (3) > failureLimit (2)

What just happened — the payoff: three of five measurements came back under 95%, the run hit Failed, and the controller aborted the rollout and scaled the bad canary to zero — traffic is entirely back on the green stable version. ActualWeight: 0, revision 3 canary scaled down, revision 2 still Healthy and serving. No human touched it. To move on you’d set image to a fix (new revision) or retry if you thought the metric lied.

Step 7 — Teardown

kubectl -n demo delete pod loadgen --ignore-not-found
kind delete cluster --name analysis-lab
# One command removes the cluster, the controller, Prometheus, and everything above.

Common mistakes and troubleshooting

Symptom Likely cause Fix
AnalysisRun stuck Inconclusive / always inconclusive No data (quiet hours), still in initialDelay, or you set both conditions and results land in the dead-band Guarantee traffic (load gen); confirm the query returns data in Prometheus directly; widen the band or set only successCondition
Good deploy aborts immediately failureLimit: 0 (default) is hair-trigger; one blip fails the run Raise failureLimit (2–3) over a count of several; add initialDelay for warm-up
successCondition never true even when app is fine Wrong result shape — query returns a vector of many series, or you compared result (array) instead of result[0] (scalar) Run the query in Prometheus; collapse it to one series with sum(...); index with result[0]
Run phase Error, message “connection refused”/timeout provider.prometheus.address wrong, Prometheus unreachable from the argo-rollouts namespace, or timeout too low Fix the in-cluster DNS address; test with a debug pod wget; raise timeout
Scrape flaps → run Error and aborts Transient provider errors eating consecutiveErrorLimit (default 4) Stabilise scraping; raise consecutiveErrorLimit if flaps are expected; distinguish Error (can’t measure) from Failed (bad app)
Bad version promotes anyway (no rollback) The Rollout never references the template — analysis block missing or misplaced Add steps[].analysis (inline) or canary.analysis (background); kubectl get analysisrun should show a run during the canary
{{args.service-name}} appears literally in the query Arg not templated: missing at use-time, or a typo in the arg name Pass every declared arg in the Rollout’s analysis.args; names must match exactly
Run measures a blend, regression hidden Query matches both canary and stable pods Scope with podTemplateHashValue: Latest + a hash label filter, or measure a canary-only Service
Analysis reads the same stale number repeatedly interval shorter than the metric’s aggregation/scrape window Set interval ≥ the rate window / scrape interval
Cloud query returns 403/Unauthenticated The argo-rollouts controller SA lacks read permission on the managed metric store Grant IRSA/Pod Identity (AWS), Entra Workload Identity (Azure), or GKE Workload Identity to the controller SA with the reader role
Background analysis “confusingly never gates a step” Background runs alongside steps; it doesn’t block a step like inline does Use inline steps[].analysis when you want a hard gate between weights; background is a parallel safety net

Three gotchas cost the most hours:

1. Confusing Failed with Error. Both abort, so people treat them the same and go hunting for an application bug — when half the time the app was fine and Prometheus was the problem. Failed means the metric cleared the failure bar (real regression). Error means the controller couldn’t get a measurement (unreachable provider, timeout, bad auth). Always read status.metricResults[].phase and the message. If it’s Error, fix your observability plumbing, not your code.

2. The no-data trap, again. It is the single most common reason a healthy deploy aborts. A ratio query with no traffic returns empty/NaN, which becomes Error or Failed, and your good canary rolls back at 3 a.m. The instinct to “just default no-data to pass” (default(result[0], 1)) is worse than the disease — now a broken build with no traffic promotes itself. Guarantee traffic during analysis; if you must default, default to fail-safe and raise your tolerance.

3. Forgetting the reference. The most anticlimactic failure: you wrote a beautiful AnalysisTemplate, watched it apply cleanly, and shipped a bad version that promoted straight to 100% — because the Rollout never mentioned the template. An AnalysisTemplate on its own does nothing. Prove the wiring on every new Rollout: during a canary, kubectl -n <ns> get analysisrun must show a run. No run, no gate, no protection.


Cheat-sheet

AnalysisTemplate metric fields

Field What it does
metrics[].name Identifier for the measurement
interval How often to measure (30s, 2m)
count How many measurements (0 = continuous, for background)
initialDelay Warm-up before the first measurement
successCondition Expr true → measurement Successful
failureCondition Expr true → measurement Failed
failureLimit Failed measurements tolerated (default 0)
inconclusiveLimit Inconclusive measurements tolerated (default 0)
consecutiveErrorLimit Consecutive provider errors tolerated (default 4)
consecutiveSuccessLimit Consecutive successes to pass early (1.8+)
args[] Declared inputs; value, valueFrom.secretKeyRef, or caller-supplied
provider The metric source (one of twelve)

Metric providers (pick one per metric)

Key Source Key Source
prometheus PromQL query cloudWatch AWS CloudWatch
datadog Datadog query wavefront Wavefront
newRelic NRQL graphite Graphite
web HTTP + jsonPath influxdb Flux
job Kubernetes Job (exit 0 = pass) skywalking SkyWalking
kayenta Spinnaker Kayenta ACA plugin Custom Go plugin

PromQL patterns for gates (collapse to one number, use result[0])

Signal Query
Success rate sum(rate(http_requests_total{app="X",code!~"5.."}[2m])) / sum(rate(http_requests_total{app="X"}[2m]))
Error rate sum(rate(http_requests_total{app="X",code=~"5.."}[2m])) / sum(rate(http_requests_total{app="X"}[2m]))
p95 latency (s) histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{app="X"}[5m])) by (le))
Saturation avg(rate(container_cpu_usage_seconds_total{pod=~"X-.*"}[5m]))

kubectl argo rollouts commands

Command What it does
get rollout <n> --watch Live tree of the rollout + its AnalysisRuns
status <n> Wait for / print the rollout status
promote <n> Advance past the current step (or --full to skip all)
abort <n> Manually abort → revert to stable
retry rollout <n> Re-run an aborted rollout from step 0
set image <n> c=img Trigger a new canary
kubectl get analysisrun List the run instances
kubectl describe analysisrun <n> Every measurement, value, and the failure message

Interview and exam questions

Q: What is the difference between an AnalysisTemplate and an AnalysisRun? A: The AnalysisTemplate (or ClusterAnalysisTemplate) is the reusable definition — which metric to query, how often, and the pass/fail condition. The AnalysisRun is the instance the controller creates when analysis actually runs during a rollout; it records every measurement and drives to a terminal phase (Successful, Failed, Error, Inconclusive). Template is the class; run is the object.

Q: A canary’s inline analysis reaches Failed. Walk through what happens next. A: The controller sets status.abort: true on the Rollout, scales the stable ReplicaSet back to full and the canary to zero (traffic returns to the known-good version instantly, because stable was never removed), and marks the Rollout Degraded with a message naming the failed metric. It stays aborted — no auto-retry. You move forward by pushing a fixed image (new revision) or kubectl argo rollouts retry rollout (same revision).

Q: Explain failureLimit vs consecutiveErrorLimit. Why are they separate? A: failureLimit budgets Failed measurements — the app genuinely missed the SLO (query returned a bad number). consecutiveErrorLimit budgets consecutive provider Errors — the controller couldn’t get a measurement at all (Prometheus unreachable, timeout, bad auth). They’re separate because “my app is bad” and “I can’t measure my app” are different problems: one is your code, the other is your observability plumbing, and you want different tolerances for each.

Q: Your success-rate condition is result[0] >= 0.95. A deploy during a quiet hour aborts even though the app is fine. Why, and how do you fix it? A: The no-data trap. With almost no traffic, the ratio query returns empty or NaN; result[0] then errors (an Error measurement) or the comparison is false (a Failed measurement), so a healthy deploy aborts. Fix by guaranteeing traffic during analysis (a load generator against the canary), widening the rate window, or guarding with a fail-safe default — never default no-data to “pass,” which would let a broken low-traffic build promote.

Q: When would you use background analysis over inline (per-step) analysis? A: Inline analysis is a hard gate at a specific step — the rollout blocks until it passes, good for “prove 20% is healthy before earning 50%.” Background analysis (strategy.canary.analysis, usually count: 0) runs continuously for the whole rollout and can abort at any point, catching a regression that only appears at higher weight. They’re complementary; many Rollouts use inline gates plus a background safety net.

Q: How do you make an analysis measure only the new (canary) pods rather than a blend of canary and stable? A: Pass the canary’s pod-template-hash as an arg with valueFrom.podTemplateHashValue: Latest, then filter the PromQL on that hash label (rollouts_pod_template_hash="{{args.canary-hash}}"). Alternatively, when using traffic routing, point the query at a canary-only Service. A blended metric dilutes the canary’s errors with stable’s health and can hide the regression.

Q: What does count: 0 mean in a metric, and where is it appropriate? A: count: 0 means measure continuously (not zero times). It’s used for background analysis, which should keep watching for the entire duration of the rollout rather than finishing after a fixed number of measurements. Inline gates, by contrast, use a finite count so the run completes and the step advances.

Q: Which metric providers exist, and how do you run a check that isn’t a metric query at all? A: Twelve built-ins: prometheus, datadog, newRelic, cloudWatch, wavefront, graphite, influxdb, skywalking, kayenta, web, job, and plugin. There is no generic “Kubernetes” provider — to run a check (integration test, smoke test, synthetic probe) you use the job provider, which runs a Kubernetes Job and treats exit code 0 as success. For anything custom, plugin loads a metric plugin you supply.

Q (multi-cloud): You point analysis at Amazon Managed Prometheus on EKS and every run is Error with 403. What’s wrong? A: The identity making the query — the argo-rollouts controller ServiceAccount — lacks permission on the AMP workspace. AMP requires SigV4-signed requests; configure provider.prometheus.authentication.sigv4 with the region and a role ARN, and give the controller SA that role via IRSA or EKS Pod Identity with aps:QueryMetrics. The equivalent on AKS is Entra Workload Identity + oauth2 to the Azure Monitor endpoint; on GKE, Workload Identity to the managed-Prometheus frontend.

Q: How do you introduce analysis to a nervous team without risking aborts on day one? A: Start observe-only: a permissive condition or a high failureLimit so the AnalysisRun records measurements but rarely fails. Watch several real rollouts to learn how the metric behaves (warm-up noise, quiet-hour data gaps), then tighten the condition and lower the tolerance once you trust it. It turns analysis into a monitoring dashboard first and a gate second.

Q: You set both successCondition and failureCondition. What happens when a measurement satisfies neither? A: It’s Inconclusive. If inconclusiveLimit is exceeded, the run is Inconclusive and the Rollout pauses for a human decision rather than promoting or aborting. Using both conditions deliberately creates a dead-band — a “not confident enough to auto-decide” zone — which is exactly when you want a person in the loop instead of an automatic promote or rollback.

Q: Why gate on error-budget burn rate instead of a static success-rate threshold? A: A static “95%” is disconnected from your reliability target. Burn rate expresses how fast the new version is spending the month’s allowed error budget; a fast-burn condition aborts a version that would exhaust the budget in hours even if its instantaneous success rate looks marginally acceptable. It makes the promotion decision and the SLO the same number, which is the SRE-grade way to gate.


Key takeaways

argocdgitopskubernetesargo-rolloutsprogressive-deliverycanaryanalysistemplateanalysisrunprometheuspromqlmetricsauto-rollbacksredatadogcloudwatchakseksgke
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments