Running Jobs with ResourceSet Steps
A common requirement when deploying applications is to run one-off tasks in sequence with the deployment itself: a database migration must complete before the new version rolls out, and a cache warmup or smoke test should run only after the rollout has finished.
The ResourceSet API supports this workflow through the
.spec.steps field: an ordered list of named steps
that combine Kubernetes Jobs with Flux appliers (Kustomization, HelmRelease).
Each step’s resources are applied and health-checked before the next step starts,
and a failed step blocks the rest of the sequence.
Staged Deployment Workflow
The upstream Flux running jobs use case
implements this workflow with three Kustomization objects chained with dependsOn
(app-pre-deploy → app-deploy → app-post-deploy), three repository directories,
and force: true/wait: true set on each Kustomization.
With ResourceSet steps, the equivalent workflow is expressed as a single object, with the same execution flow: the migration runs first, the app deployment is blocked on its completion, and the cache warmup Job runs only after the rollout has finished.
Combined with a ResourceSetInputProvider that scans the container registry, the workflow becomes Gitless GitOps image automation: when a new image version is published, the whole sequence runs for it without any Git commit.
First, define an input provider that picks the latest stable version of the
podinfo image according to semver:
apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSetInputProvider
metadata:
name: podinfo-image
namespace: apps
annotations:
fluxcd.controlplane.io/reconcileEvery: "10m"
spec:
type: OCIArtifactTag
url: oci://ghcr.io/stefanprodan/podinfo
filter:
semver: "*"
limit: 1
Then define the staged workflow, with the exported tag templated into the Jobs and set in the Flux Kustomization using an image patch:
apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSet
metadata:
name: podinfo
namespace: apps
annotations:
fluxcd.controlplane.io/reconcileEvery: "10m"
spec:
inputsFrom:
- kind: ResourceSetInputProvider
name: podinfo-image
wait: true
steps:
- name: pre-deploy
timeout: 5m
resources:
- apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
namespace: apps
annotations:
fluxcd.controlplane.io/force: enabled
fluxcd.controlplane.io/recreateOnFailure: enabled
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migration
image: ghcr.io/stefanprodan/podinfo:<< inputs.tag >>
command: ["sh", "-c", "echo running db migration"]
- name: deploy
timeout: 10m
resources:
- apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: podinfo
namespace: apps
spec:
targetNamespace: apps
sourceRef:
kind: GitRepository
name: apps
path: deploy/podinfo
interval: 60m
prune: true
wait: true
timeout: 9m
images:
- name: ghcr.io/stefanprodan/podinfo
newTag: << inputs.tag | quote >>
- name: post-deploy
timeout: 5m
resources:
- apiVersion: batch/v1
kind: Job
metadata:
name: cache-warmup
namespace: apps
annotations:
fluxcd.controlplane.io/force: enabled
fluxcd.controlplane.io/recreateOnFailure: enabled
spec:
template:
spec:
restartPolicy: Never
containers:
- name: cache
image: ghcr.io/stefanprodan/podinfo:<< inputs.tag >>
command: ["sh", "-c", "echo refreshing cache"]
On reconciliation, the operator applies the pre-deploy step and waits up to 5m
for the db-migration Job to complete. It then applies the deploy step and waits
up to 10m for the Flux Kustomization to become ready, before applying the
post-deploy step. Because .spec.wait is set to true, the final step is also
health-checked.
When the input provider detects a new image version, the force annotation on the
Jobs makes the operator recreate them, so the migration and cache warmup run again
for the new version. Because the tag is set in the Kustomization spec through
the .spec.images patch, the version bump changes its metadata.generation and
the deploy step waits for the actual rollout of the new pods (see
Triggering Rollouts on Data-Only Changes).
Comparison with the Flux Kustomization Pattern
The ResourceSet steps map onto the upstream pattern as follows:
- The
dependsOnchain between Kustomizations → the step order. - The per-Kustomization
wait: true→ the implied wait between steps (the final step is gated by the ResourceSet.spec.waitfield). - The per-Job Kustomization
force: true→ the per-Jobfluxcd.controlplane.io/force: enabledannotation (the Job is recreated when the image tag changes). - The per-Job Kustomization
timeout→ the per-Jobtimeout. - The per-Job Kustomization
prune: true→ the ResourceSet inventory-based garbage collection.
Beyond replicating the upstream pattern, the single-object form adds:
one status, inventory and history to inspect instead of three; no dependsOn
requeue polling latency between stages; the opt-in
recreateOnFailure annotation to retry a permanently
failed migration; and version bumps that flow automatically from the registry
scan through a single input, instead of Git edits in three places.
Re-running Jobs
Kubernetes Job specs are immutable, so the supported re-run mechanism is the
fluxcd.controlplane.io/force: enabled annotation: when the rendered Job spec
changes (e.g. the image tag from an input bump), the operator deletes and
recreates the Job, and it runs again. With an unchanged spec, re-applying a
completed Job is a no-op, which makes retrying a failed sequence idempotent.
The same no-op behavior means a Job that has failed permanently (e.g. its
backoffLimit is exhausted) stays failed: the ResourceSet remains Ready=False
until the Job spec or an input changes. To automatically retry failed Jobs,
add the fluxcd.controlplane.io/recreateOnFailure: enabled annotation:
metadata:
annotations:
fluxcd.controlplane.io/force: enabled
fluxcd.controlplane.io/recreateOnFailure: enabled
Before applying a step, the operator deletes any Job in that step that carries
this annotation and has the Failed=True condition, then recreates it when
applying the step.
Warning: only use recreateOnFailure for idempotent Jobs. A non-idempotent
migration would be re-run on every reconciliation until it succeeds, repeating
any partial changes it made before failing. The annotation works for any Job
managed by a ResourceSet, with or without steps.
Triggering Rollouts on Data-Only Changes
To trigger a rolling update of the app workloads when the config inputs
change, hash the inputs with the sha256sum template function and inject the
result into the pod template using a Kustomization patch:
- apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
spec:
patches:
- target:
kind: Deployment
patch: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: all
spec:
template:
metadata:
annotations:
config-checksum: << inputs.config | toYaml | sha256sum | quote >>
When the workload is defined directly in a step (without an applier in
between), the
checksumFrom
annotation on its pod template works as documented and the step health check
waits on the workload rollout itself.
Job Lifecycle Caveats
Do not set ttlSecondsAfterFinished on Jobs managed by a ResourceSet.
When the TTL controller deletes a completed Job, the operator detects the
missing resource as drift on the next reconciliation and re-applies it,
causing the migration to run again unexpectedly.
To explicitly re-run a Job for every new revision while keeping a record of
past runs, template the Job name from an input revision, e.g.
name: db-migration-<< inputs.tag >>. Each version bump creates a new Job,
and the previous one is removed by garbage collection after the sequence succeeds.
Copying Data Between Steps
The copyFrom annotation reads from the cluster before the first step runs,
it is not step-aware. A resource in a later step cannot copy data from a Secret created by
an earlier step in the same reconciliation.
The only step-safe cross-reference is checksumFrom pointing at ConfigMaps or
Secrets generated by the same ResourceSet: those references are resolved
in-memory from the pending apply, regardless of which step defines them.