OutputValues icon
Fail icon
Labels icon
Parallel icon
Request icon
Get icon
Download icon
If icon
Log icon
Pause icon
Set icon
LoopUntil icon
Sleep icon
ArchiveDecompress icon
Delete icon
Schedule icon

Move a Kestra namespace to another instance with a drained cutover and rollback

Move a Kestra namespace to a new instance with target-side validation, dry run, approval, drained cutover, smoke test, rollback and a resumable KV ledger.

Categories
CoreInfrastructure

Moving a namespace to another Kestra instance, for example to a new 2.0 instance while the 1.3 one keeps running, is a manual sequence today: export, find what the new version rejects, import, then switch the triggers off on one side and on on the other without a double run or a lost KV write. This flow runs that sequence through the API of both instances. It writes nothing while dry_run is "true", stops before any write when the plan has a blocker, and rolls a failed cutover back.

Safety

Guard What it stops
dry_run defaults to "true" any write before you have read the plan
Every source flow goes through the target validator first importing a flow the target rejects, such as ForEach or pluginDefaults on 2.0
Ledger hashes plus on_conflict: STOP overwriting a flow, file or KV pair someone edited on the target since the last copy
Copies land with disabled: true a schedule, webhook or realtime trigger of the copy firing before the cutover
Instance uuid from /api/v1/configs moving a namespace onto itself through a second URL
Ledger bound to the target uuid a ledger written for one target driving another
Pause with a decision that defaults to ABORT a cutover nobody approved
Freeze, then drain with a deadline the same flow running on both instances at once
Read-back after the enable, then a smoke test on the target reporting success when the copies are not enabled or do not run
errors branch a failed cutover leaving the namespace disabled on both sides
Ledger phases FROZEN, DRAINED, CUT_OVER and concurrency.limit: 1 a killed run losing track of a half-done cutover, two runs interleaving
Checks on namespace file names two files landing on the same copy, such as a b.md and a_b.md

How it works

  1. check_inputs refuses identical URLs and a move of the namespace this flow runs in.
  2. discover reads /api/v1/configs on both instances and the ledger, the KV pair kestra-move.<namespace>. instances turns each version into the API calls it understands: KV listing, execution filters, unfinished states, tenant prefix and the file names it can store.
  3. read_state downloads the flow lists of both sides and the unfinished source executions.
  4. CUTOVER only: cutover_brief logs what will happen, approve_cutover (Pause) waits for the decision, freeze_source disables the enabled source flows and record_freeze writes the ledger. drain (LoopUntil) then polls until no execution of the namespace is unfinished on the source, and rolls back at drain_timeout_minutes. A CUTOVER that finds the ledger FROZEN or DRAINED skips the approval and the freeze and resumes at the drain.
  5. inventory exports both sides (flows by id, namespace files, KV list, triggers), unpacks the archives with io.kestra.plugin.compress.ArchiveDecompress, hashes the flows and files, sends the source flows to the target validator, and reads into internal storage the KV values whose update date changed since the last copy (kv_fingerprint). plan (jq) classifies each object as NEW, IN_SYNC, UPDATE, CONFLICT, INVALID, EXPIRING, UNSUPPORTED_NAME, STALE or TARGET_ONLY, report logs it and stop_on_blockers fails before any write. In CUTOVER this runs after the drain, so KV pairs written by the last source runs are copied too.
  6. write_target writes only what the plan marked: one request per flow with disabled: true, the namespace files, then each KV pair with its type and the TTL it has left. record_stage stores the hashes and update dates of everything now in sync.
  7. CUTOVER only: enable_target enables the flows that were enabled on the source, target_after reads them back and smoke_test runs smoke_test_flow on the target and waits for SUCCESS.
  8. ROLLBACK disables the target copies first, then enables the source flows again.
  9. If CUTOVER fails after the freeze, auto_rollback in errors does the same automatically. finally deletes the scratch KV pair.
  10. nightly_drift_report (disabled Schedule) runs a dry-run STAGE every morning while both instances coexist.

What you get

  • A plan in the report log and the plan flow output: counts per status, the exact writes, blockers, warnings, the new webhook URLs and the secrets the flows reference.
  • A repeatable STAGE. A second run writes nothing and reads no KV value; a run after a change writes only that change.
  • Faithful copies: the flow source text, comments included, with disabled: true as its last line; namespace files byte for byte; KV pairs with their type (STRING, NUMBER, BOOLEAN, JSON, DURATION, DATE, DATETIME) and the TTL they have left. A flow disabled on the source stays disabled on both sides.
  • KV values never appear in task outputs or logs: they are compared by hash and moved through internal storage.
  • An audit trail: the last 20 ledger events, plus kestra-move-namespace and kestra-move-mode labels on every run.

Who it's for

  • Platform teams who move to Kestra 2.0 side by side, one namespace at a time, instead of upgrading in place.
  • Teams that split an instance per business unit, merge instances, or move to another cluster, region or cloud.

Why orchestrate this with Kestra

A move is a state machine with a human gate, a wait and an undo. Kestra has each piece: Pause with a typed onResume input for the approval, LoopUntil with a deadline for the drain and the smoke test, the KV store as state that outlives a killed execution, errors and finally for rollback and cleanup, a concurrency limit, and retries with a time budget on every write. The flow also shows how to drive Kestra from Kestra through its API, with one secret per credential.

Tested end to end

Every version below ran these suites against a separate 2.0.8 instance (H2, local storage, basic auth): 853 checks, none failing. Each test namespace held 7 flows, among them a Schedule, a Webhook, a Subflow call, a 20-second job, a flow disabled on purpose and one with --- inside its text, plus 5 namespace files (one binary, one .zip, one name with a space) and 10 KV pairs covering every type, one with a TTL.

  • push: the flow runs on that version and moves a namespace to 2.0.8. Dry run, STAGE, repeated STAGE, update, conflict with STOP then OVERWRITE, ForEach blocker, ABORT at approval, CUTOVER with drain and smoke test, refused second CUTOVER, ROLLBACK, drain timeout, failed, unknown and refused smoke tests, kill during the drain then resume, and six guards (same instance, refused connection, wrong URL, own namespace, wrong credentials, ledger of another target).
  • pull and target: the flow runs on 2.0.8 and moves a namespace from that version, or to it. Dry run, STAGE twice, CUTOVER with drain and smoke test, ROLLBACK.
  • edge: a namespace without flows, plus file names that version cannot unpack or that collide once unpacked.
Kestra push pull target edge
0.21.20 59/59 14/14 15/15 9/9
0.22.46 59/59 14/14 15/15 9/9
0.23.33 59/59 14/14 15/15 9/9
0.24.20 59/59 14/14 15/15 9/9
1.0.60 59/59 14/14 15/15 9/9
1.1.20 59/59 14/14 15/15 9/9
1.2.24 59/59 14/14 15/15 9/9
1.3.45 59/59 14/14 14/14 9/9
2.0.8 57/57 14/14 n/a 7/7

One file absorbs these differences between versions, each measured on the versions above:

  • KV listing is /kv?filters from 1.1 and /namespaces/{ns}/kv before, which 2.0 no longer serves.
  • Execution search takes filters[...] from 0.22 and a prefix-matching namespace= before, so results are filtered again on the exact namespace.
  • BOOL inputs exist from 0.23, the retry attempt count is maxAttempt on 0.23 and maxAttempts on 2.0, and 2.0 rejects a top-level allowFailed: hence a SELECT for dry_run, maxDuration retries and options.allowFailed.
  • The namespace import endpoint of 0.21 to 1.3 splits its body on every ---, even inside a script, so flows are written one per request.
  • On 0.21 and on 1.0 to 1.3 the LoopUntil condition already sees the next iteration count, so each iteration computes its own last-item flag.
  • Kestra 1.0 to 1.2 keep the concurrency slot of an execution killed while LoopUntil waits, so the waits between polls are Sleep tasks.
  • Kestra 2.0 refuses queue messages over 1 MiB by default, which bounds task outputs, so large API answers go through internal storage with Download.
  • The KV API reads its body as Amazon Ion: an unquoted ISO date or UTC timestamp keeps the DATE or DATETIME type.
  • Every KV write moves the pair's updateDate, even with the same value, so matching dates on both sides show a pair unchanged without reading it.
  • Before 1.3 the namespace file API stores a space as + and refuses names with %, brackets and a few other characters; before 1.2 it also drops : and cuts a name at # or ?.

Prerequisites

  • Two Kestra instances, 0.21 or later. The flow can run on the source, on the target or on a third instance, in any namespace outside the one it moves, as long as it reaches both APIs.
  • plugin-compress on the instance that runs the flow (the full Kestra image includes it).
  • On both sides, read access to the flows, executions, namespace files and KV pairs of the namespace. On the source, the right to update flows (disable and enable). On the target, the right to write flows, namespace files and KV pairs and to start executions.

Secrets

  • KESTRA_SOURCE_AUTHORIZATION: the full Authorization header value for the source API, for example Basic <base64 of user:password> or Bearer <API token> (required).
  • KESTRA_TARGET_AUTHORIZATION: the same for the target API (required).

In open source Kestra a secret is an environment variable such as SECRET_KESTRA_SOURCE_AUTHORIZATION holding the base64 of the value.

Inputs

  • mode: STAGE, CUTOVER or ROLLBACK. dry_run: "true" (default) or "false".
  • namespace, source_url, target_url, and source_tenant and target_tenant for Enterprise Edition.
  • on_conflict: STOP (default) or OVERWRITE. drain_timeout_minutes (30), smoke_test_flow, smoke_test_timeout_minutes (10).

Quick start

  1. Create both secrets on the instance that runs the flow and save the flow there, in a namespace other than the one you move.
  2. Execute it with mode: STAGE and the default dry_run: "true". Read the report log and fix each blocker on the source, for example with kestra-migrate for a flow the 2.0 target rejects.
  3. Run STAGE with dry_run: "false". The copies land disabled on the target; repeat as often as you like.
  4. At a quiet moment, run STAGE once more so the cutover only has the last changes to copy. Then run CUTOVER with dry_run: "false" and smoke_test_flow set to a flow without required inputs, and resume the paused execution with decision CUTOVER.
  5. Point webhook callers to the URLs listed in webhooks. If anything looks wrong, run ROLLBACK.
  6. While both instances run, set the inputs of nightly_drift_report and enable it.

How to extend

  • Send cutover_brief to Slack or email before approve_cutover.
  • Move a list of namespaces from a parent flow that calls this one with a Subflow task per namespace.
  • Delete STALE objects on the target once the report has been reviewed.
  • Compare execution counts on both sides for a day after the cutover.

What it does not do

  • Executions, logs, metrics and secrets stay on the source. The plan lists the secrets the flows use, so you can create them on the target.
  • ROLLBACK switches the triggers back. KV pairs and files that changed on the target after the cutover are not copied back.
  • Schedules due between the freeze and the enable run on neither instance, and child namespaces are separate moves.
  • It handles up to 1,000 KV pairs per namespace on each side and blocks above that.
  • .zip namespace files are skipped because the upload API unpacks them, and so is a file name an older target would change or refuse, with a warning. If the instance that runs the flow cannot unpack a name (Kestra 1.3 and older, for %, brackets and a few other characters), the run stops before any write: run it on 2.0.
  • A binary namespace file is compared by its size and the hash of what read() returns, which decodes it as UTF-8, so an edit that keeps the size and only changes bytes outside valid UTF-8 goes unnoticed.
  • Each KV value is read and written in its own loop iteration. In a test on 2.0.8, a first STAGE of 40 flows, 60 files and 300 KV pairs took about 12 minutes; later runs only read the pairs whose update date changed.
  • The tenant inputs follow the Enterprise API paths but were only tested on open source Kestra.

Links

This blueprint was created by zkasuran.

See How

New to Kestra?

Use blueprints to kickstart your first workflows.