Move resources between roots
Optional: hand this page to your coding agentThe steps work by hand too.Show the whole prompt
Read https://intentius.io/terragucci/guides/move-resources-between-roots/.
In this repo, move the resource blocks I name from their root to the root I name, write the migration file the page describes for that move, and open a pull request.
Tell me which roots the migration touches and what the plan job must prove. Do not run state commands or write any state.
Never apply, approve (a pull request review or `terragucci approve`), override a policy denial (`terragucci override`), use `--mode apply`, or merge; never touch `.chant/allowed_signers` or `chant/lifecycle`.Result
Section titled “Result”A resource moves between two roots’ states through the same gate as an apply, and nothing is destroyed or created. moved, import and removed blocks work only inside one root; a migration file covers a move across roots.
| Step | Who | What happens |
|---|---|---|
| the pull request’s plan job | terragucci | builds each affected root’s new state in the job, plans every root against it, requires no change, and prints the digest |
| the apply job’s wave 1, after the merge | terragucci | proves the move again and waits for an approval of that digest; no wave plans until it applies |
terragucci approve <name> --plan <digest> |
you | approves that digest and nothing else |
wave 1 again, started by terragucci approve or the resume job |
terragucci | takes each state’s lock, refuses if a state moved since the plan, writes the new states, plans every root again and requires no change, and records each state’s version id before and after |
The digest covers the file plus each root’s state version and digest before the move and its new state’s digest. A state written by anyone after the approval changes the digest, and wave 1 refuses with exit 4, naming the root.
State contents stay in your backend and the job’s temporary directory. The record holds version ids and digests.
Prerequisites
Section titled “Prerequisites”| You need | Why |
|---|---|
Terraform or OpenTofu roots, CDK Terrain stacks, whose roots are cdktf.out/stacks/<stack> holding cdk.tf.json, or Terragrunt units |
a unit is prepared by Terragrunt (terragrunt run -- init), and the job then reads and writes its state in the directory Terragrunt runs it in, with the inputs Terragrunt gives it; a move can go between two units, or between a unit and a plain root |
An s3 backend with use_lockfile = true, a gcs, azurerm or local backend, or a pg, kubernetes, consul or http backend, in every root the move touches |
each state’s lock is held while it is written; a root whose code has a cloud block, or another backend, is refused; a backend move also reads from a workspace or a state file |
Versions kept: S3 bucket versioning, GCS object versioning, Azure blob versioning or snapshot = true |
each version id before is the version to go back to; see Find the state version an apply left. pg, kubernetes, consul and http keep none: the digest binds each state’s contents, and the migration cannot be reverted |
| An apply identity that can write the states and take their locks | the rights the backend already needs |
Each backend’s lock is taken as the backend takes it:
| Backend | Lock | Who writes the new state |
|---|---|---|
s3, gcs |
the lock object beside the state | the binary, with state push -lock=false while the job holds the lock |
azurerm |
a lease on the state blob | the job, under its lease, which keeps the binary out; a snapshot first when the backend sets snapshot = true |
pg, kubernetes, consul, http |
the backend’s own: an advisory lock, a Lease, a session, the lock address | the binary, with state push under that lock, which refuses a state of another lineage or an older serial |
A pipeline terragucci init wrote after the first migration file was added |
on GitHub under gate: never, the apply jobs get contents: write to record the migration’s gate on chant/lifecycle only then; config check and the plan job refuse a repo whose pipeline lacks it |
-
Move the resource blocks in code. Cut
aws_sqs_queue.jobsfromenvs/dev/platformand paste it intoenvs/dev/queuesalong with any variables and outputs it needs. -
Write the migration file,
migrations/split-queues.yml. Its name without.ymlnames the migration and its gate:moves:- from: envs/dev/platformto: envs/dev/queuesaddresses:- aws_sqs_queue.jobsMigration files lists what an address can name.
-
Open the pull request. The plan job proves the migration and prints its digest:
migration split-queues: moving aws_sqs_queue.jobs from envs/dev/platform to envs/dev/queuesenvs/dev/platform: no changes against its new stateenvs/dev/queues: no changes against its new statemigration split-queues: every root plans with no change against its new state; digest jcs1-sha256:b298de39...A root that would change against its new state fails the job, which names each change. The roots’ own plans in the same note still show the move as a destroy in one root and a create in the other: they plan against the states as they are, before the move is written.
-
Merge. Wave 1 proves it again and waits, exit 3:
migration split-queues waits for an approval of digest jcs1-sha256:b298de39.... Read the proof above, then approve it with:terragucci approve split-queues --plan jcs1-sha256:b298de39...Every later wave waits on wave 1, so no root applies against a state about to be rewritten.
-
Approve it from a checkout.
terragucci approvefinds the migration, prints what it moves and approves its digest, with--signunderapproval: sealed. With a wave waiting too, name it:terragucci approve split-queues. A pull request review does not approve a migration, under anyapproval:mode. -
Wave 1 runs again by itself:
terragucci approvestarts it with your forge token, or the resume job does on its next run withapply.resumeset (Resume after an approval).initwrites the resume job wheneverapply.resumeis set, undergate: nevertoo. On that run it writes the states and goes on to plan and apply the wave:migration split-queues: approved by alice for this digestenvs/dev/queues: wrote its new stateenvs/dev/platform: wrote its new stateenvs/dev/platform: version a7d36113-... before, 0dca9e05-... afterenvs/dev/queues: version none before, 3a695fbf-... aftermigration split-queues appliedThe root that gains resources is written first. A write that stops half way leaves a resource in both states, never in neither.
Move a state to a new backend
Section titled “Move a state to a new backend”Point the root’s backend block at the new bucket or key, and add a migration naming where the state is now:
backends:
- root: envs/dev/platform
from:
backend: s3
config:
bucket: acme-old-state
key: envs/dev/platform.tfstate
region: us-east-1
use_lockfile: trueThe plan job plans the root against the old backend’s state with no change; the digest covers that state’s version and digest. Wave 1 waits for the approval as for any migration. Once approved, it takes the lock files of the old and new state, checks that neither moved, then writes the state to the backend the code names, which must hold none for the root. The old state stays where it was; delete it once the move is verified.
A root on HCP Terraform, Scalr, OTF or env zero’s backend moves the same way, with backend: cloud (or remote) naming the workspace, and the token in TF_TOKEN_<host>. The job holds the workspace’s lock while it writes. State exported from another platform moves with backend: file. A pg, kubernetes, consul or http state moves with backend naming its type; the binary reads it with state pull. Migration files lists the keys.
Estates
Section titled “Estates”With choudoufu as the binary, a root with a live block is an estate: it keeps no state file, and each resource carries its owner in two tags, tofu-estate and tofu-address. The same migration files move resources between estates and bring a state into one, through the same gate.
| File | Roots | The write |
|---|---|---|
moves |
both are estates | choudoufu live-mv -from-estate rewrites the tags of each resource, run in the root it moves to |
backends |
the root’s code now has a live block in place of its backend block |
choudoufu live-import reads the old state once and stamps the tags on each resource it verifies |
The proof reads the live system and writes nothing. Each root plans as its code stands, and every change it plans must be one the write removes: a destroy in the estate a resource leaves and a create in the one it joins, or a create of a resource the old state holds and the live system verifies. live-mv -dry-run must find each resource, and live-import without -approve must verify each one. The digest covers each root’s planned changes, the live id of each resource and, for an adoption, the old state’s version id and digest.
migration carve-jobs: retagging aws_sqs_queue.jobs from the estate of mono to the estate of team
aws_sqs_queue.jobs: https://sqs.us-east-1.amazonaws.com/123456789012/jobs moves from estate mono to estate team
mono: 1 planned change, each one the retag removes
team: 1 planned change, each one the retag removes
migration carve-jobs: every change each root plans is one the retag removes; digest jcs1-sha256:5c0e91d2...Once approved, wave 1 writes the tags, plans every root again and requires no change. An adoption holds the old state’s lock file while it stamps, refuses if that state moved since the plan, and leaves it where it was, at the version the record names. A move from a root with a state to an estate is refused: adopt the state first, then move. terragucci migrate revert refuses both; move the resources back with a moves file the other way.
Put a migration back
Section titled “Put a migration back”terragucci migrate revert split-queuesThis writes migrations/split-queues-revert.yml from the record of split-queues on chant/lifecycle. The file names each root’s version before the migration and the version it left. Open one pull request with that file and the reverted code of split-queues. The revert goes through the same proof and approval as any migration:
| Root | After the revert |
|---|---|
| one whose state the migration changed | the version it recorded before, read by its id, with the root’s lineage and the next serial |
| one that had no state before | an empty state |
| one whose state moved past the version the migration left | refused: putting the older version back would undo that later change too |
A revert needs versions kept: a root with no version recorded before, or a local backend, has nothing to put back. An azurerm state with no blob versions is named by the snapshots the migration took before and after its write. A backend move is put back with a backend move the other way.
Records
Section titled “Records”| Where | What it holds |
|---|---|
terragucci-report/migrations/<name>.json in the job, and <prefix>/<project>/migrations/<name>.json in the reports bucket |
the moves, each root’s version id and digest before and after, the proof and the plan after the write, the approver and the outcome; for estates, each resource’s live id |
_gates/tf-migrate/done.jsonl on chant/lifecycle |
one line per migration that wrote its states: the digest, the approver, the result and each root’s version before and after |
| the audit trail | a migration entry from that line, and the gate’s request and approval |
A migration that applied never runs again. Its file can stay in the repo; changing it after it applied fails wave 1, so write a new file for a new move.
Stop conditions
Section titled “Stop conditions”| Wave 1 says | Exit | What to do |
|---|---|---|
| a root would change against its new state | 1 | fix the code or the file so the move is exact, and push again |
| the states moved since the approval | 4 | read the new proof, and approve the new digest it prints |
| under the lock, a state is not the one planned | 4 | run wave 1 again: it plans from the states as they are |
| a state is locked | 1 | wait for the run that holds it, then run wave 1 again |
| after the write, a root plans changes | 1 | the states were written; the record names each version before, which Find the state version an apply left restores |
These docs count page views and clicks with PostHog. They set no cookies, store nothing in your browser, and send nothing when your browser asks not to be tracked.