Schedule icon
Get icon
List icon
OutputValues icon
If icon
Fail icon
Log icon
Loop icon
Download icon
Export icon
Upload icon
Set icon
Write icon
Labels icon
SlackIncomingWebhook icon
Delete icon

Nightly Kestra flows backup to S3 with a SHA-256 drift manifest per namespace

Nightly Kestra flows backup with namespaces.List, flows.Export and s3.Upload. A SHA-256 manifest in KV separates real edits from re-saves and names what changed since yesterday; ZIPs and manifest land in S3 or MinIO.

Categories
CloudInfrastructure

A Kestra instance changes every day: someone edits a flow in the UI, a deployment adds ten flows, one quietly disappears. This flow takes a nightly snapshot of the flows of the instance it runs on, through the public API, and compares it with the night before: a ZIP of the flow sources per namespace, a SHA-256 hash per definition in a KV manifest (added, removed, changed and revision-only flows the next night), and a copy of the ZIPs and the manifest in an S3 bucket, one folder per night. Nothing on the instance is modified. Platform teams that do not run their flows from Git get a daily, restorable copy that lives outside the instance, and "what changed since yesterday" in Slack.

The ERROR lines of recent failed executions are a separate concern with a separate schedule, so they live in the companion blueprint kestra-failed-execution-error-digest.

How it works

  1. The nightly Schedule (02:15) or a manual run starts the flow. checks block the execution before it exists when exclude_namespaces is not a JSON array of strings, namespace_prefix has invalid characters, or kestra_api_url or s3_endpoint is not a clean URL.
  2. previous_manifest (kv.Get, errorOnMissing: false) loads last night's manifest, missing on the first run. discover_namespaces (namespaces.List, existingOnly: false) lists every namespace under the prefix; scope removes the excluded ones and fixes the snapshot date on trigger.date, so a recovered run is filed under its own night. no_offsite_copy logs a WARN when s3_bucket is empty.
  3. snapshot is a Loop over the namespaces, two at a time, transmitFailed: false. Each iteration downloads the definitions of that namespace (http.Download), hashes them (flow_hashes), exports the ZIP (flows.Export, skipped when the namespace owns no flow) and uploads it as <prefix>/<day>/<namespace>.zip (s3.Upload, skipped with the bucket empty). The Loop outputs give one row per namespace with an ok flag.
  4. drift compares both manifests with one jq program from variables; a failed namespace keeps last night's entry. save_manifest stores tonight's manifest in KV with a renewed TTL and offsite_manifest writes the same manifest, tonight's drift and the ZIP keys to <prefix>/<day>/manifest.json.
  5. label sets three snapshot_* execution labels, log_summary prints the report, drift_alert posts to Slack on changes, and dead_letter stores failed namespaces in KV for 7 days and fails the run.

What this blueprint teaches

  • The Kestra plugin against its own instance: namespaces.List and flows.Export share kestraUrl, tenantId and an auth object (username and password, or apiToken). On 2.0 pluginDefaults is no longer a flow property, so they repeat per task.
  • A download instead of flows.List: flows.List returns every definition as a task output; a namespace with 140 flows produced a 2.5 MB output, over the 1 MB queue message limit, and the iteration failed. A file in internal storage has no such limit.
  • Content hashing in templates: the sha256 filter on each definition's JSON with revision and updated removed separates real edits (changed) from re-saves (revision_only); the comparison is one jq program kept in variables (vars.drift_jq).
  • One s3.Upload for AWS and S3-compatible stores: endpointOverride, forcePathStyle and compatibilityMode are rendered from s3_endpoint, empty means AWS, a URL means MinIO or any compatible endpoint.
  • Loop rows that survive failures: transmitFailed: false, an ok flag per row and a keep list for the manifest, so one broken namespace does not stop the others.
  • State in KV with ttl: manifest (P90D, renewed nightly), dead-letter list and last failure (P7D); a stopped flow leaves no state behind.
  • sla plus afterExecution: a 30-minute MAX_DURATION with behavior: FAIL labels the run snapshot_sla: breached; on 2.0.5 the breach kills pending tasks, errors included, so notify_outcome reports it.

What you get

  • In the bucket: <prefix>/<yyyy-MM-dd>/<namespace>.zip with <namespace>-<flow>.yml sources you can re-import from the Flows page or with the /flows/import API, and manifest.json naming every flow's hash and revision, the night's drift and which ZIP holds which namespace.
  • Outputs: snapshot (one row per namespace with ZIP URI and S3 key), manifest (also in KV), drift (added, removed, changed and revision-only flow ids) and s3_objects.

How it differs from the Git-based blueprints

  • git-backup-flows-to-github pushes the flow sources of one namespace to a GitHub repository on a schedule. It is version control: no hashing, no night-over-night comparison, no alert, no ZIP artifact.
  • kestra-ui-edit-backport-to-git-guard treats Git as the source of truth, diffs the live flows against the deployed Git baseline and, after an approval, writes UI edits back to Git. It needs a Git repository and a human in the loop.
  • This flow needs neither Git nor approvals. It covers every namespace of the instance, tells a real edit from a re-save with a content hash, copies a restorable ZIP per namespace off the instance every night, and alerts only when something actually changed.

Prerequisites

  • Kestra 2.0 with the Kestra, AWS and Slack plugins; an API user that can read namespaces and flows (vars.tenant is main on OSS); kestra_api_url reachable from the worker.
  • Secrets KESTRA_API_USERNAME and KESTRA_API_PASSWORD (required), AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY with PutObject on the bucket (required when s3_bucket is set) and SLACK_WEBHOOK_URL (optional, every message is also in the logs and the Slack tasks have allowFailure: true).

Quick start

  1. Add the secrets and run with s3_bucket set (and s3_endpoint for MinIO): every namespace except system is snapshotted and the first run records a baseline. Open the bucket: one folder for tonight with a ZIP per namespace and manifest.json.
  2. Edit any flow and run again: log_summary and the Slack message name it under changed; a plain re-save appears under revision_only.
  3. Leave the nightly trigger on; recoverMissedSchedules: LAST takes one snapshot after an outage, not one per missed night.

Error handling

  • Bad inputs never create an execution (checks); a prefix that matches nothing fails the run in refuse_empty_scope with the namespaces that do exist. An unreachable API or a wrong password fails discover_namespaces; the errors branch reads errorLogs() once into first_error, logs task and message with the execution link, stores them in KV and sends a Slack alert.
  • A failing namespace (export or upload, after three attempts) keeps its previous manifest entry, remember_failed dead-letters it for 7 days and report_partial_failure fails the run naming it. The other namespaces are snapshotted and uploaded; snapshot_offsite: partial marks the run.
  • concurrency limit: 1 with CANCEL stops a manual run during the nightly one; finally always writes kestra_snapshot_last_run with the counts reached and the keys written.

How to adapt

  • Set namespace_prefix and exclude_namespaces per environment, or run one copy per team with its own state_key and s3_prefix.
  • Add a bucket lifecycle rule to expire folders older than your retention; replace notify_drift with a Subflow that opens a ticket or runs your Git backport flow.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.