Skip to main content

How to Do Canary Releases Without a Service Mesh

5 min read
Set up canary releases

We split traffic to new versions and roll back automatically on a failed Prometheus check.

You can send five percent of production traffic at a new version, watch the error rate, and roll back in seconds without installing a service mesh. This walks through replica weighted canaries with a progressive delivery controller, real percentage splitting at the edge, an automated pass or fail check against Prometheus, and the rollback path. It also covers what changed when Kubernetes retired Ingress NGINX in March 2026.

A canary release puts a small share of real traffic on a new version, watches a signal that matters, and reverts automatically when the signal goes bad. It is the cheapest insurance available for a deploy, and it does not require a service mesh, a sidecar in every pod or a platform team to run one.

Three pieces make it work. Something that splits traffic, something that decides whether the new version is healthy, and a rollback that takes seconds rather than a rebuild.

Start with what needs no traffic router at all

Argo Rollouts replaces a Deployment with a Rollout object that understands stepped releases. Without any traffic router configured, it approximates the weight with replica counts, scaling the new ReplicaSet up and the stable one down. At ten replicas and a ten percent step, that is one new pod and nine old ones, and Service load balancing does the rest.

The approximation is rough at low replica counts and completely adequate at twenty, and it works on any cluster.

kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
# k8s/rollout-checkout.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: checkout
spec:
  replicas: 10
  selector:
    matchLabels:
      app: checkout
  template:
    metadata:
      labels:
        app: checkout
    spec:
      containers:
        - name: checkout
          image: registry.example.com/checkout:2.7.0
  strategy:
    canary:
      maxSurge: "25%"
      maxUnavailable: 0
      steps:
        - setWeight: 10
        - pause: { duration: 10m }
        - setWeight: 25
        - pause: { duration: 10m }
        - setWeight: 50
        - pause: {}

A pause with a duration waits that long and continues, supporting s, m and h units. A pause with no duration pauses indefinitely until it is manually resumed, which is the gate to put in front of the final step while a team gets used to the process.

Migrating an existing Deployment does not require deleting it. A Rollout can reference one through spec.workloadRef with a scaleDown choice of never, onsuccess or progressively, so the old object is drained rather than removed on day one.

Real percentages at the edge

Replica weighting cannot give you 2 percent, and some releases deserve 2 percent. For that you need something that splits requests rather than pods.

The landscape here changed in 2026. Kubernetes retired Ingress NGINX in March 2026, with no more releases for bug fixes, security patches or any updates of any kind after retirement. Existing deployments keep working, which is the dangerous part, since the statement notes that unless you proactively check you may not know you are affected until you are compromised. The recommended direction is Gateway API or one of the other ingress controllers, and none of the alternatives are drop-in replacements.

So for new work, build the canary on Gateway API, where weighted splitting is a first class field.

# k8s/httproute-checkout.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: checkout
spec:
  rules:
    - backendRefs:
        - name: checkout-stable
          port: 8080
          weight: 90
        - name: checkout-canary
          port: 8080
          weight: 10

Weights are a proportional split rather than a percentage, with the sum of the weights in a rule acting as the denominator, and an omitted weight defaults to 1. Ninety and ten happen to read as percentages here because they add up to a hundred.

Argo Rollouts drives that split for you rather than leaving you to patch YAML by hand. It supports AWS ALB, Ambassador, Apache APISIX, Google Cloud, Istio, Kong, NGINX Ingress Controller, SMI and Traefik as traffic providers, with Gateway API available through its plugin mechanism.

If you are still running Ingress NGINX while a migration is planned, its canary annotations do work today. nginx.ingress.kubernetes.io/canary: "true" marks the second Ingress, and the split is driven by canary-weight with canary-weight-total defaulting to 100. Header and cookie targeting exist through canary-by-header, canary-by-header-value, canary-by-header-pattern and canary-by-cookie, evaluated in the order header, then cookie, then weight. A maximum of one canary Ingress can be applied per Ingress rule. Treat all of that as a bridge, not a destination.

Let the metrics decide

A canary that a human has to watch is a canary that gets promoted at 18:00 because everyone wants to go home. An AnalysisTemplate moves the decision to the data.

# k8s/analysis-success-rate.yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
spec:
  args:
    - name: service-name
  metrics:
    - name: success-rate
      interval: 5m
      successCondition: result[0] >= 0.95
      failureLimit: 3
      provider:
        prometheus:
          address: http://prometheus.example.com:9090
          query: |
            sum(rate(http_requests_total{service="{{args.service-name}}", code!~"5.."}[5m]))
            /
            sum(rate(http_requests_total{service="{{args.service-name}}"}[5m]))

Reference it from a step, where it runs inline and blocks the rollout until it completes.

strategy:
  canary:
    steps:
      - setWeight: 10
      - pause: { duration: 5m }
      - analysis:
          templates:
            - templateName: success-rate

Pick a threshold from your own baseline rather than from an example. A service that normally runs at 99.9 percent success is already broken at 99, and a success condition of 0.95 would sail straight past it.

The rollback path, which is the point

kubectl argo rollouts get rollout checkout --watch
kubectl argo rollouts promote checkout
kubectl argo rollouts abort checkout
kubectl argo rollouts undo checkout --to-revision=3

abort stops progressing the current rollout and reverts all steps, leaving the previous ReplicaSet active. There is a subtlety worth internalising before you need it at 03:00. The spec template still points at the new version, so if the Rollout leaves the aborted state it will try to progress to that version again. To fully revert, set the template back to the previous version, which is what undo does. promote --full goes the other way, skipping analysis, pauses and steps for a change you need out immediately.

Set progressDeadlineSeconds too. It bounds how long a rollout may go without making progress before it is considered failed, and it defaults to 600 seconds.

What a canary cannot save you from

Traffic splitting says nothing about the database. If the new version needs a schema change, that change has to be backward compatible with the old version, because both are serving at once. Expand the schema, deploy, migrate the data, then contract in a later release.

Low traffic services are the other limit. Five percent of forty requests an hour is not a sample, it is an anecdote, so weight the canary higher or hold it longer on quiet services.

Progressive delivery is part of how we build pipelines in CI/CD pipeline setup, and the analysis side of it belongs with monitoring and observability. For the deployment mechanics underneath, see Zero-Downtime Kubernetes Deployments.

Frequently asked questions

Do I need a service mesh to do canary releases on Kubernetes?

No. A canary release needs something that splits traffic, something that decides whether the new version is healthy, and a rollback that takes seconds, with no sidecar in every pod. Argo Rollouts can approximate the split with replica counts on any cluster, and a Gateway API HTTPRoute gives real weighted splitting at the edge.

How does Argo Rollouts run a canary without a traffic router?

Without a traffic router, Argo Rollouts approximates the canary weight with replica counts, scaling the new ReplicaSet up and the stable one down while Service load balancing spreads the requests. At ten replicas a ten percent step means one new pod and nine old ones. The approximation is rough at low replica counts and adequate at twenty, and it cannot produce a share as small as 2 percent, which needs request level splitting.

Can I still use Ingress NGINX for canary deployments after its retirement?

Kubernetes retired Ingress NGINX in March 2026, with no more releases for bug fixes, security patches or anything else. Existing deployments keep working, and the canary annotations still function, with nginx.ingress.kubernetes.io/canary set to true on a second Ingress and the split driven by canary-weight against a canary-weight-total that defaults to 100. Treat that as a bridge during a migration to Gateway API or another ingress controller, none of which are drop-in replacements.

How do weights work in a Gateway API HTTPRoute canary?

Gateway API backendRefs weights are a proportional split, with the sum of the weights in a rule acting as the denominator, and an omitted weight defaults to 1. Weights of 90 and 10 read as percentages only because they add up to a hundred. Argo Rollouts can drive the split instead of hand-patched YAML, with Gateway API available through its plugin mechanism.

How can Argo Rollouts roll back a canary automatically based on Prometheus metrics?

An AnalysisTemplate with a Prometheus provider moves the decision to the data. Define a query such as the share of non-5xx requests, an interval, a successCondition and a failureLimit, then reference the template from an analysis step in the canary strategy, where it runs inline and blocks the rollout until it completes. Pick the threshold from the service's own baseline. A service that normally runs at 99.9 percent success is already broken at 99, and a successCondition of 0.95 would let it straight through.

What is the difference between abort and undo in Argo Rollouts?

Running kubectl argo rollouts abort stops the current rollout and reverts all steps, leaving the previous ReplicaSet active. The spec template still points at the new version, though, so if the Rollout leaves the aborted state it will try to progress to that version again. A full revert means setting the template back to the previous version, which is what undo does, as in kubectl argo rollouts undo checkout --to-revision=3. Going the other way, promote --full skips analysis, pauses and steps.

How do I migrate an existing Deployment to Argo Rollouts without deleting it?

A Rollout can reference the existing Deployment through spec.workloadRef with a scaleDown choice of never, onsuccess or progressively, so the old object is drained instead of removed on day one. Argo Rollouts itself installs into an argo-rollouts namespace from the project's install.yaml manifest. While a team gets used to the process, a pause step with no duration in front of the final step waits until someone resumes it by hand.

What can a canary release not protect against?

Traffic splitting does nothing for the database. Both versions serve at once during a canary, so any schema change has to stay backward compatible with the old version, which means expanding the schema, deploying, migrating the data and contracting in a later release. Low traffic is the other limit. Five percent of forty requests an hour is an anecdote and not a sample, so weight the canary higher or hold it longer on quiet services.

Read how we handle it in Infrastructure Setup.