






















A discussion on Reddit recently highlighted a challenge that many engineers and teams face: how to handle automated deployments when each release requires different dynamic steps.
The question was like: "How do you automate deployments in Kubernetes when each release is unique and needs its own workflow?" The scenarios are: pre-deployment schema changes, controlled rollouts across services, and post-deployment verification checks.
The tricky part is, not every deployment follows the same pattern, and some steps are one-time tasks that don't fit into reusable templates.
In this article, we'll look into how the ordering can be configured and controlled in ArgoCD.
"ArgoCD gives us Sync Waves, Hooks, and Progressive Syncs. But when do we use which one?"
Now the question is when I should use which approach. Sync Waves order resources within a single application (except of App of Apps pattern). Hooks run tasks at specific lifecycle points. Progressive Syncs control the order of applications across environments when using ApplicationSet. Each mechanism serves a purpose so let's look into it.
Consider a common deployment: a PostgreSQL database, a backend API service, and a frontend application. In the simplest case, you might deploy all three simultaneously and let Kubernetes sort out the startup order. The frontend will retry connections until the backend responds, and the backend will wait for the database to accept connections. This works, but it creates noise in logs and delays in readiness checks.

The reality becomes more complex once you add requirements: the database needs a schema migration before the backend starts, the backend needs health verification before the frontend deploys, and certain configuration changes require a specific sequence.
Sync Waves provide the most basic ordering mechanism.
You annotate resources with argocd.argoproj.io/sync-wave, and ArgoCD processes them in numerical order, waiting for each wave to become healthy before proceeding to the next.
apiVersion: v1
kind: ConfigMap
metadata:
name: database-config
annotations:
argocd.argoproj.io/sync-wave: "-1"
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
annotations:
argocd.argoproj.io/sync-wave: "0"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-api
annotations:
argocd.argoproj.io/sync-wave: "1"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
annotations:
argocd.argoproj.io/sync-wave: "2"
Please be aware that hooks and resources are assigned to wave 0 by default. If you want something to have higher priority, you should use negative numbers.
This approach establishes a clear dependency chain. The ConfigMap appears first (wave -1), then the database (wave 0), followed by the backend (wave 1), and finally the frontend (wave 2).

The delay between sync waves is 2 seconds by default. This can be configured by setting the ARGOCD_SYNC_WAVE_DELAY environment variable.
You can verify the order by watching the sync operation:
# Watch the sync progress
kubectl get applications -n argocd -w
# Check which resources are in which wave
kubectl get all -n production -o json | \
jq -r '.items[] | select(.metadata.annotations."argocd.argoproj.io/sync-wave" != null) |
"\(.metadata.annotations."argocd.argoproj.io/sync-wave"): \(.kind)/\(.metadata.name)"'
Sync Waves only order resources within a single Application. To use sync waves between Applications, you need the App of Apps pattern along with custom health assessment configuration (required since ArgoCD v1.8). ref here
Example of the health assessment configuration that needs to be added (as from the official doc):
---
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
labels:
app.kubernetes.io/name: argocd-cm
app.kubernetes.io/part-of: argocd
data:
resource.customizations.health.argoproj.io_Application: |
hs = {}
hs.status = "Progressing"
hs.message = ""
if obj.status ~= nil then
if obj.status.health ~= nil then
hs.status = obj.status.health.status
if obj.status.health.message ~= nil then
hs.message = obj.status.health.message
end
end
end
return hs
They establish ordering but don't address the scenario from the Reddit discussion: what about one-time migration scripts or verification checks that shouldn't remain after deployment?
"Sync Waves order your long-lived resources, but they don't handle the ephemeral tasks that appear once and then disappear."
This is where Hooks enter the picture. A Hook runs a Kubernetes resource (typically a Job) at a specific point in the sync lifecycle and then removes it according to a deletion policy. The most common hooks are PreSync and PostSync.
Also, Kubernetes Job objects are fundamentally immutable after creation, meaning they generally cannot be "overwritten" in place. So if you want to re-run a job via ArgoCD, a great way is to have a hook-delete-policy in place. This will make sure that the Job will be deleted which then provides possibility to run the Job again on next Sync. Technically it's a new Job.
Consider the database migration requirement. The schema needs updating before the backend deploys, but the migration Job shouldn't remain in the cluster afterward:
apiVersion: batch/v1
kind: Job
metadata:
name: database-migration
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
argocd.argoproj.io/sync-wave: "0"
spec:
template:
spec:
containers:
- name: migrate
image: migrate/migrate:v4
command:
- migrate
- -path=/migrations
- -database=postgres://postgres:5432/appdb
- up
restartPolicy: Never
The PreSync hook ensures this Job completes before ArgoCD applies the main application resources. The deletion policy removes it once successful.

For verification tasks after deployment, PostSync hooks serve the same purpose:

We need to specify the argocd.argoproj.io/hook: PostSync annotation:
apiVersion: batch/v1
kind: Job
metadata:
name: smoke-test
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
template:
spec:
containers:
- name: test
image: curlimages/curl:latest
command:
- sh
- -c
- |
curl -f http://backend-api:8080/health || exit 1
curl -f http://frontend:80/health || exit 1
restartPolicy: Never
The combination of Hooks and Sync Waves addresses the Reddit scenario. Migrations run before deployment, verification runs after, and each step can be unique to that specific release. Your CI pipeline generates the appropriate Job manifest with specific parameters, commits it to Git, and ArgoCD syncs it once.
"Hooks let you inject imperative steps into a declarative workflow, but they only work within a single ArgoCD Application."
During resource creation and updates, ArgoCD processes waves from lowest to highest. However, during pruning (deletion), the order is reversed—resources are deleted from highest wave to lowest. This ensures dependencies are cleaned up in the correct order.
Note: Progressive Syncs is an Beta feature that must be explicitly enabled and may change in future releases.
The mechanisms discussed so far operate within a single ArgoCD Application. But what happens when your architecture spans multiple Applications? A microservices platform split across five Applications. A multi-environment pipeline where dev, staging, and production are separate Application instances. A multi-region deployment where each region has its own Application.
When you have multiple Applications with dependencies between them, simultaneous syncing breaks ordering guarantees. This is where Progressive Syncs provides control.
You might structure deployments as multiple Applications for various reasons:
Separation of concerns:
Environment progression:
Scale and complexity:
Geographic distribution:
Whatever the reason, once you have multiple Applications with dependencies, you need a way to control their sync order. Without Progressive Syncs, all Applications sync simultaneously when changes hit Git. The backend Application syncs before the database migration completes. The production Application deploys while staging is still failing.
Progressive Syncs extends sync control across multiple Applications managed by an ApplicationSet. It defines stages—groups of Applications that sync together—and ensures each stage reaches "Healthy" before the next stage begins.
The ApplicationSet generates multiple Application resources and controls when each syncs based on matching criteria. Applications are grouped by labels, and sync operations follow the defined sequence.
Note: Progressive Syncs is a beta feature that must be explicitly enabled.
Progressive Syncs is not enabled by default. Enable it using one of these methods:
# Method 1: Controller argument
--enable-progressive-syncs
# Method 2: Environment variable
ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS=true
# Method 3: ConfigMap
kubectl patch cm argocd-cmd-params-cm -n argocd \
--patch '{"data":{"applicationsetcontroller.enable.progressive.syncs":"true"}}'
If we go back to our original story, with the ApplicationSet we want to generate the three Applications: one for the database layer, one for the backend services, and one for the frontend. Progressive Syncs ensures they deploy in sequence, as shown in the following diagram:

The ApplicationSet acts as the orchestrator, defining which Applications exist and in what order they sync. It uses a list generator to create three separate Applications and a RollingSync strategy to control their sync sequence.
Generators define what Applications to create. In the config below, we're creating three Applications from a list, each with a different component name and priority label. The priority label (1, 2, 3) determines sync order.
Strategy defines how Applications sync. The RollingSync type enables Progressive Syncs. Each step in the rollingSync.steps array defines a group of Applications that sync together. Applications matching step 1 sync first, then step 2, then step 3. The maxUpdate: 100% means all Applications in a step sync simultaneously (you could use 50% to sync half at a time, or 1 to sync one at a time).
Template defines the Application spec for each generated Application. The {{.component}} and {{.priority}} variables get replaced with values from the generator. So we end up with three Applications: platform-database, platform-backend, and platform-frontend, each labeled with its priority.
When this ApplicationSet is applied, ArgoCD creates three Application resources. When changes occur, the ApplicationSet controller syncs them in priority order: database first (must reach healthy), then backend (must reach healthy), then frontend.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: platform-stack
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- list:
elements:
- component: database
priority: "1"
- component: backend
priority: "2"
- component: frontend
priority: "3"
strategy:
type: RollingSync
rollingSync:
steps:
- matchExpressions:
- key: priority
operator: In
values: ["1"]
maxUpdate: 100%
- matchExpressions:
- key: priority
operator: In
values: ["2"]
maxUpdate: 100%
- matchExpressions:
- key: priority
operator: In
values: ["3"]
maxUpdate: 100%
template:
metadata:
name: 'platform-{{.component}}'
labels:
priority: '{{.priority}}'
spec:
project: platform
source:
repoURL: https://github.com/org/manifests
targetRevision: main
path: '{{.component}}'
destination:
server: https://kubernetes.default.svc
namespace: production
The maxUpdate parameter controls how many Applications in each step can update simultaneously. Setting it to `100%` means all Applications in a step update together. Setting it to `50%` updates half at a time. Setting it to `1` updates one Application at a time.
RollingSync monitors the OutOfSync status of managed Applications. When Applications become out of sync (due to Git changes, ApplicationSet updates, or manual modifications), RollingSync triggers sync operations following the defined strategy.
You'll see warnings in the `applicationset-controller` logs if Application specs try to enable automated sync policies:
WARN Application has automated sync enabled, but RollingSync requires manual sync control
You can monitor the progressive rollout:
# Watch ApplicationSet status
kubectl get applicationset -n argocd production-stack -o yaml | \
yq '.status'
# Check individual application sync status
argocd app list --selector priority=1
argocd app list --selector priority=2
argocd app list --selector priority=3
# View the rollout progression
argocd appset get production-stack
read more about the commands here
As mentioned, RollingSync forces all generated Applications to have auto-sync disabled.
The ApplicationSet controller manages when syncs happen according to your defined steps. Applications won't automatically sync on every Git commit—they sync when their step is active and previous steps are healthy.
When you commit a change to Git with RollingSync enabled:
The question isn't "Should I use Progressive Syncs?" The question is "Do I have multiple Applications that need ordering?"
If you have one Application, use Sync Waves to order resources within it. If you have multiple Applications without dependencies, let them sync independently. If you have multiple Applications with dependencies, Progressive Syncs provides the coordination mechanism.
The ApplicationSet generates Applications and controls their sync sequence. Each Application can have its own source repository, its own sync waves, its own hooks. Progressive Syncs coordinates at the Application level, not the resource level.
Consider the Reddit scenario about unique release steps. That requirement alone doesn't mandate multiple Applications or Progressive Syncs. Hooks handle unique steps within a single Application. Progressive Syncs only adds value when those steps span multiple Applications that need coordination.
Understanding these mechanisms conceptually is one thing; verifying their behavior in a running cluster is another. When troubleshooting ordering issues, specific kubectl commands reveal what's happening:
# Check resource order within an Application
kubectl get all -n production -o json | \
jq -r '.items[] |
select(.metadata.annotations != null) |
{
name: "\(.kind)/\(.metadata.name)",
wave: (.metadata.annotations."argocd.argoproj.io/sync-wave" // "0"),
hook: (.metadata.annotations."argocd.argoproj.io/hook" // "none")
}' | jq -s 'sort_by(.wave | tonumber)'
# View current Application health and sync status
argocd app get production-app --refresh
# Check sync operation details including hook execution
argocd app get production-app --show-operation
# Watch live sync progress
argocd app sync production-app --async
kubectl get pods -n production -w
# Verify Progressive Sync step execution
kubectl get applications -n argocd -l priority=1 -o jsonpath='{.items[*].status.sync.status}'
These commands expose the current state and help diagnose when ordering isn't working as expected. The most common issue I've encountered is forgetting that Sync Waves only order resources within the same sync operation which also means within the same application. If you sync one resource outside of application, the wave ordering no longer applies.
Each mechanism operates at a different level. Understanding how they layer together provides the full control over deployment ordering:

The Hierarchy:
ApplicationSet Level (Progressive Syncs):
Within Each Application (Phases + Waves):
Within Each Wave (Resource Ordering):
Start simple, add complexity only when needed:
An ApplicationSet with Progressive Syncs can generate Applications that use Sync Waves, which can include Hooks. Each level addresses a different aspect of deployment control: which Applications deploy when (Progressive Syncs), what order resources deploy within Applications (Sync Waves), and what ephemeral tasks run at specific lifecycle points (Hooks).
You can find more posts and tutorials under the ArgoCD and Kubernetes categories.
This post is part of the Debate Series on devoriales.com, where I share my perspective on topics that often spark strong opinions in the tech community. My view is not the final answer, and it shouldn't be treated as such. The goal is to exchange ideas, question assumptions, and learn from each other. If you see something differently or have experience that adds another angle, I welcome your thoughts. These discussions shape how we grow, and my own opinions may shift as the conversations continue.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。