Progressive Delivery on Kubernetes with Argo Rollouts, ArgoCD, and Helm

Progressive Delivery on Kubernetes with Argo Rollouts, ArgoCD, and Helm

Traditional Kubernetes deployments replace an old application version with a new one and declare success when the new Pods become ready. That works—until a release is technically “ready” but is returning more errors, timing out under real traffic, or breaking a critical user journey.

Progressive delivery adds controlled exposure, measurement, and automated decision-making to the release process. Instead of sending every request to a new version at once, we introduce it gradually, validate its behavior, and either promote it or restore the known-good version.

In this guide, we will build that workflow using:

  • Helm to package the Kubernetes resources.
  • ArgoCD to reconcile the cluster with Git.
  • Argo Rollouts to manage the canary and promotion process.
  • Prometheus to determine whether the canary is healthy.

The result is a GitOps-driven release with explicit traffic steps, measurable acceptance criteria, and an automatic rollback path.

The delivery model

The workflow has a clear separation of responsibilities:

  1. CI builds and tests the application image.
  2. CI writes the immutable image tag to the Helm values stored in Git.
  3. ArgoCD detects the commit and synchronizes the desired state.
  4. Argo Rollouts creates a canary ReplicaSet alongside the stable one.
  5. Traffic moves to the canary in controlled increments.
  6. Prometheus evaluates the canary during the rollout.
  7. Argo Rollouts promotes a healthy revision or aborts a failed one.

Git remains the source of truth, while the rollout controller makes runtime safety decisions inside boundaries defined in Git.

Prerequisites

You need a Kubernetes cluster and the following command-line tools:

kubectl version --client
helm version
argocd version --client
kubectl argo rollouts version

The examples use the demo-api namespace and assume Prometheus is reachable at http://prometheus-server.monitoring.svc.cluster.local.

Use immutable image tags such as a Git SHA. Reusing latest makes auditing and rollback unnecessarily difficult.

1. Install Argo Rollouts

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

kubectl wait --for=condition=Available \
  deployment/argo-rollouts \
  -n argo-rollouts \
  --timeout=180s

Install the dashboard locally when you want a visual view of the rollout:

kubectl argo rollouts dashboard

This exposes a local dashboard without making it publicly reachable from the cluster.

2. Structure the Git repository

Keep the chart and environment-specific configuration easy to find:

platform-config/
├── applications/
│   └── demo-api.yaml
└── charts/
    └── demo-api/
        ├── Chart.yaml
        ├── values.yaml
        └── templates/
            ├── rollout.yaml
            ├── services.yaml
            └── analysis-template.yaml

The minimal Chart.yaml is straightforward:

apiVersion: v2
name: demo-api
description: Progressive delivery example with Argo Rollouts
type: application
version: 0.1.0
appVersion: "1.0.0"

Define the image, rollout steps, and analysis settings in values.yaml:

image:
  repository: ghcr.io/example/demo-api
  tag: "a1b2c3d"
  pullPolicy: IfNotPresent

replicaCount: 6

service:
  port: 80
  targetPort: 3000

rollout:
  maxSurge: 1
  maxUnavailable: 0
  steps:
    - setWeight: 10
    - pause: { duration: 2m }
    - setWeight: 25
    - pause: { duration: 5m }
    - setWeight: 50
    - pause: { duration: 10m }

These pauses are observation windows, not arbitrary delays. Tune them to your traffic volume and the time required to produce trustworthy metrics.

3. Create stable and canary Services

Argo Rollouts switches the selectors on these Services as revisions change:

apiVersion: v1
kind: Service
metadata:
  name: demo-api-stable
spec:
  ports:
    - port: 80
      targetPort: 3000
  selector:
    app.kubernetes.io/name: demo-api
---
apiVersion: v1
kind: Service
metadata:
  name: demo-api-canary
spec:
  ports:
    - port: 80
      targetPort: 3000
  selector:
    app.kubernetes.io/name: demo-api

For precise request-level traffic splitting, connect the rollout to a supported traffic router such as Istio, NGINX, AWS ALB, or another provider supported by Argo Rollouts. Without a traffic router, setWeight approximates the percentage by scaling ReplicaSets.

4. Replace Deployment with Rollout

A Rollout resembles a Deployment but adds a delivery strategy:

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: demo-api
spec:
  replicas: {{ .Values.replicaCount }}
  revisionHistoryLimit: 3
  strategy:
    canary:
      stableService: demo-api-stable
      canaryService: demo-api-canary
      maxSurge: {{ .Values.rollout.maxSurge }}
      maxUnavailable: {{ .Values.rollout.maxUnavailable }}
      analysis:
        templates:
          - templateName: demo-api-success-rate
        startingStep: 1
      steps:
{{ toYaml .Values.rollout.steps | indent 8 }}
  selector:
    matchLabels:
      app.kubernetes.io/name: demo-api
  template:
    metadata:
      labels:
        app.kubernetes.io/name: demo-api
    spec:
      containers:
        - name: api
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: 3000
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            periodSeconds: 5
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            periodSeconds: 10
            failureThreshold: 3
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 512Mi

maxUnavailable: 0 prevents the rollout from deliberately reducing available capacity during the transition. The readiness probe prevents unready Pods from receiving traffic, but it does not prove that a release is safe. That is the job of the analysis.

5. Define a Prometheus analysis

The following AnalysisTemplate checks the proportion of successful requests. Adapt the metric names and labels to your instrumentation:

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: demo-api-success-rate
spec:
  metrics:
    - name: success-rate
      interval: 1m
      count: 5
      failureLimit: 1
      successCondition: result[0] >= 0.99
      provider:
        prometheus:
          address: http://prometheus-server.monitoring.svc.cluster.local
          query: |
            sum(
              rate(http_requests_total{
                app="demo-api",
                status!~"5.."
              }[2m])
            )
            /
            sum(
              rate(http_requests_total{
                app="demo-api"
              }[2m])
            )

The metric succeeds when at least 99% of requests are not server errors. A production policy usually combines multiple signals—for example success rate, latency, saturation, and a business-critical operation.

Guard against missing data. A query returning no series should not accidentally be interpreted as a healthy result. Test the exact query before attaching it to an automated rollback decision.

6. Let ArgoCD manage the release

Create an ArgoCD Application pointing to the chart:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: demo-api
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/example/platform-config.git
    targetRevision: main
    path: charts/demo-api
  destination:
    server: https://kubernetes.default.svc
    namespace: demo-api
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Apply it once:

kubectl apply -f applications/demo-api.yaml
argocd app wait demo-api --health --sync --timeout 300

The Application Details Tree brings the entire release into one view: the Git revision, sync result, application health, Services, Rollout, ReplicaSets, Pods, and AnalysisRun.

ArgoCD Application Details Tree for demo-api after a successful GitOps sync
Screenshot: ArgoCD Application Details Tree for demo-api after a successful GitOps sync.

From this point onward, change the image tag in Git instead of modifying the Rollout directly with kubectl. Direct cluster edits create drift and weaken the audit trail.

7. Trigger and observe a canary

Update the image tag, commit, and push:

git checkout -b release/demo-api-a7f9e21
sed -i 's/tag: "a1b2c3d"/tag: "a7f9e21"/' charts/demo-api/values.yaml
git add charts/demo-api/values.yaml
git commit -m "deploy demo-api a7f9e21"
git push origin release/demo-api-a7f9e21

After the change reaches the tracked branch, watch the rollout:

kubectl argo rollouts get rollout demo-api \
  -n demo-api \
  --watch
Argo Rollouts dashboard with the canary paused at 20 percent traffic
Screenshot: Argo Rollouts dashboard with the canary paused at 20% traffic.

At the first step, most traffic remains on the stable revision while the new revision receives enough traffic to produce a meaningful signal. The analysis runs during the configured observation window.

Useful inspection commands include:

kubectl get analysisruns -n demo-api
kubectl describe analysisrun -n demo-api <analysis-run-name>
kubectl argo rollouts status demo-api -n demo-api --timeout 10m

For a manual gate, use an indefinite pause in the steps and promote only after review:

kubectl argo rollouts promote demo-api -n demo-api

Use manual promotion sparingly. A repeatable, well-instrumented service should gradually move toward automated acceptance criteria.

8. Test the failure and rollback path

A delivery system is not trustworthy until its failure behavior has been exercised. Deploy a test revision that produces controlled HTTP 500 responses or deliberately tighten the analysis threshold in a non-production environment.

When the error rate violates the policy, the AnalysisRun fails and the Rollout enters a degraded state:

Argo Rollouts dashboard after a failed success-rate analysis and aborted canary
Screenshot: Argo Rollouts dashboard after the success-rate analysis failed and the canary was aborted.

Abort a rollout manually when human judgment identifies a problem not covered by the automated metrics:

kubectl argo rollouts abort demo-api -n demo-api

Verify the outcome:

kubectl argo rollouts get rollout demo-api -n demo-api
kubectl get rs -n demo-api -l app.kubernetes.io/name=demo-api
kubectl get analysisruns -n demo-api

The important measure is not merely whether rollback exists. Measure the time from the first bad signal to restored stable traffic. That is the practical recovery time experienced by users.

Production hardening checklist

Before using this pattern for critical workloads, confirm the following:

  • Images use immutable tags or digests.
  • Readiness and liveness probes represent different failure modes.
  • PodDisruptionBudgets preserve capacity during node disruption.
  • CPU and memory requests are based on measurements.
  • Analysis queries have been tested with healthy, unhealthy, and missing data.
  • Canary traffic is large enough to produce statistically useful signals.
  • Rollout notifications reach the incident and engineering channels.
  • ArgoCD projects restrict repositories, destinations, and resource types.
  • ArgoCD and Argo Rollouts permissions follow least privilege.
  • Rollback behavior is tested regularly outside production.
  • Dashboards and runbooks link directly to the Rollout and AnalysisRun.
  • CI updates Git; it does not need broad write access to the cluster.

Common mistakes

Treating readiness as a release-quality signal

Readiness only answers whether Kubernetes may route traffic to a Pod. It does not measure user-visible error rates, latency, or correctness.

Using pauses without defining what will be evaluated

A ten-minute pause adds no safety if nobody and nothing examines the release. Pair every observation window with a specific automated analysis or human decision.

Evaluating only cluster health

CPU, memory, and Pod restarts matter, but a healthy cluster can still serve a broken product. Include request and business-level indicators.

Allowing ArgoCD and the rollout controller to fight

Keep desired rollout structure in Git and let Argo Rollouts manage the live ReplicaSets and traffic progression. Avoid CI jobs that issue competing imperative changes.

Calling every regression a rollback problem

Rollback restores the earlier application version; it does not reverse incompatible database migrations or external side effects. Use backward-compatible schema changes and expand-and-contract migrations.

Final thoughts

Helm, ArgoCD, and Argo Rollouts solve different parts of the same delivery problem. Helm describes the package, ArgoCD continuously reconciles Git with the cluster, and Argo Rollouts controls exposure of the new revision.

The value is not a more elaborate deployment animation. The value is a release process that makes risk explicit: small exposure, measurable evidence, fast promotion, and a tested path back to safety.

Start with one service and one dependable metric. Exercise a failed release deliberately, measure recovery time, and improve the process from what you observe. Progressive delivery becomes valuable when teams trust it enough for deployments to become routine—and deliberately boring.

Posts Carousel

Leave a Comment

Your email address will not be published. Required fields are marked with *

Latest Posts

Most Commented

Featured Videos