Finalizeยป
Step 11 of the migration walkthrough: close out a batch โ push in what couldn't travel as code (secrets, state, module versions), then mark the batch migrated so the next batch starts clean.
Non-sensitive variables never pass through here: generate emits each one as a spacelift_environment_variable in its entity's file, readable in Spacelift rather than write-only, so only what can't be expressed as HCL is left to push.
Run the pushers before finalize staged.
finalize sensitive, finalize state, and finalize modules act on staged units only, and finalize staged is the transition that flips the batch out of staged.
Flip first and the pushers find nothing to move โ the stacks come up marked migrated holding no secrets and no state.
So the order is: the pushers (below), then finalize staged last.
The last step โ finalize the batchยป
1 | |
Run this after the pushers below.
It flips every staged unit staged โ migrated โ the explicit "this batch is live in Spacelift now" transition, and the batch-closing one: it declares the batch done.
It's what lets the pipeline be iterative and additive: once a batch is migrated, the next discover preserves it, generate keeps its files (and never regenerates them โ they're the customer's now), and batch list shows it as done.
Treat this as a one-way door. Once it flips, for that batch:
generatestops rewriting its entity files (they're yours to hand-edit โ see generate),- the batch drops out of the staged set, so it no longer appears in
batch listas stageable, and - the pushers (
finalize sensitive,finalize state,finalize modules) no longer act on it โ they touch staged units only.
That last point is why the ordering above is not optional: run every pusher you need first, because after finalize staged there is no pusher left that will move this batch's secrets, state, or module versions. Re-staging a migrated unit is possible but takes a person's explicit agreement and names the full blast radius (see batch); it is the deliberate exception, not the undo button.
Because flipping the batch out of staged is what strands unpushed secrets and state, finalize staged refuses until every captured secret and state in the batch has been pushed.
There is no override โ pushing the data is the only way through, so the batch cannot be marked done while anything is still stranded.
Run the pushers first (below); once each captured value has been pushed, finalize staged proceeds.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
The database โ not git โ is the authority on what's migrated.
After this, you're back at the top: run liftoff discover to refresh the picture and liftoff batch to stage the next batch.
Repeat until the estate is migrated.
The other finalize stepsยป
finalize sensitive pushes the sensitive values mutate captured into the live Spacelift stacks and contexts as write-only environment variables and mounted files โ the one moment those values leave the local store, and unreadable once set.
It resolves each target by the Spacelift id derived from its name โ the same derivation Spacelift itself performs when the entity is created (lowercased, transliterated to ASCII, spaces and punctuation collapsed to dashes) โ so it needs the stacks already standing in Spacelift (the publish step) and the Spacelift destination credentials in your config:
1 2 3 4 5 | |
Any sensitive value still empty in the store is reported as skipped rather than pushed โ set those in Spacelift directly.
Variable-set (context) secrets need mutate --allow-mutation context-secrets to have run; without it they arrive empty and are skipped here.
Every skip is named: the report lists each skipped value (kind, id, name) and why it was skipped, so "N skipped" is never a number you have to bisect.
When captured state is still unpushed, Next names liftoff finalize state โ not finalize staged โ so the ordering the page warns about is also what the hint says.
finalize state pushes each staged stack's Terraform state โ captured locally by mutate --allow-mutation state at cutover โ into its live Spacelift stack: the raw state is uploaded to Spacelift storage, then imported onto the stack (briefly locked for the import, as Spacelift requires), addressed by the same name-derived id.
A skipped stack is named with why, and the two reasons are kept apart: a stack recorded unresolvable (say, one the source holds no state for) is nothing to push โ its skip carries the recorded reason, not a gap, and liftoff status doesn't count it as one โ while a stack whose state is simply not captured yet names the mutate run that fixes it.
It needs the same Spacelift credentials as above:
1 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | |
finalize modules backfills each staged module's published versions into Spacelift โ Spacelift won't recreate a module's version history on its own.
It's a pure push from the local store: for each version whose commit SHA was resolved earlier by liftoff mutate --allow-mutation module-git-versions, it calls Spacelift's versionCreate at that commit.
Like finalize sensitive and finalize state, it never reaches the VCS itself โ that resolution happened in mutate โ so it needs only the Spacelift credentials:
1 | |
1 2 3 4 5 | |
A version is Skipped when its commit SHA was never resolved, and the report keeps the two cases apart: a version the mutation recorded as unresolvable (the module has no VCS connection, or its tag no longer resolves) is counted under Unresolvable โ an explained dead end, not work outstanding โ while the rest simply haven't been resolved yet, and the note names the module-git-versions run that fixes them.
liftoff model list --kind module_version shows each recorded reason, and liftoff status counts the unresolvable ones apart from a real shortfall.
The unrecoverable ones are also surfaced by liftoff audit as module-version-unmigratable; create those in Spacelift by hand, pointing each at its tag's commit.
finalize staged above is the lifecycle transition.
What comes afterยป
After state lands, the migration is complete: Spacelift runs plans against the same state the source last held, and the source can be retired on your schedule.
Migration is lift-and-shift by design: everything lands where it lived at the source.
The phase that follows is adoption: organizing spaces around how your teams actually work, tightening policies, and adopting Spacelift-native workflows.
The liftoff space command group (create, move, reshape; a move relocates a stack's file into the target space's directory and emits a moved block, so the admin stack re-parents it in place) is the tooling for that phase, and its shape is still being worked out with early users.
Expect this page to grow.
Dispose of the workspace when you're doneยป
When the last batch is migrated and the estate is fully on Spacelift, one thing is left: the ./.liftoff/ workspace.
By now it is the most sensitive artifact the migration produced, all of it unencrypted on your disk:
config.yamlโ the source and Spacelift settings, including any API token pasted in rather than kept as an environment reference.liftoff.dbโ the SQLite store. It holds every captured sensitive variable value (frommutate --allow-mutation secrets/context-secrets) and every captured Terraform state blob (frommutate --allow-mutation state) โ the same production secrets and state yourfinalizesteps just pushed into Spacelift, now sitting in a plain file with no password on it.data/generated/โ the rendered OpenTofu. Not secret (secrets are never inlined), but it describes your whole estate.
Nothing about the kit protects this for you: it isn't encrypted, and the .gitignore line from setup only keeps it out of git โ not off backups, cloud sync, or a machine someone else can reach.
So while a migration is in flight, keep the directory somewhere only you can read; once it's finished and everything is verified in Spacelift, delete it:
1 | |
There is nothing to keep โ Spacelift is now the system of record, and re-running the kit from scratch would rebuild the workspace anyway. If your platform has a secure-erase tool, prefer it, since the file held live secrets and state. Never hand this directory to anyone as-is for support or sharing: it carries live production secrets and state.