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.
Talk to the engineer who will own your stack.
No account managers, no offshore handoff. Senior DevOps, direct. Tell us what you are dealing with and you get a straight answer.
Related Articles
The Ultimate Guide to Linux Server Management in 2025
A comprehensive guide to modern Linux server management covering automation, containerization, cloud integration, AI-driven operations, security best practices, and essential tooling for 2025.
Server & DevOpsFixing "421 Misdirected Request" for Plesk Sites on Ubuntu 22.04 After Apache Update
Resolve the 421 Misdirected Request error affecting all HTTPS sites on Plesk for Ubuntu 22.04 after an Apache update, caused by changed SNI requirements in the nginx-to-Apache proxy chain.
Server & DevOpsHow to Set Up GlusterFS on Ubuntu
A complete guide to setting up a distributed, replicated GlusterFS filesystem across multiple Ubuntu 22.04 nodes, including installation, volume creation, client mounting, maintenance, and troubleshooting.