Skip to content

Move resources between roots

llms.txtlists every page for an agent
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`.

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.

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
  1. Move the resource blocks in code. Cut aws_sqs_queue.jobs from envs/dev/platform and paste it into envs/dev/queues along with any variables and outputs it needs.

  2. Write the migration file, migrations/split-queues.yml. Its name without .yml names the migration and its gate:

    moves:
    - from: envs/dev/platform
    to: envs/dev/queues
    addresses:
    - aws_sqs_queue.jobs

    Migration files lists what an address can name.

  3. 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/queues
    envs/dev/platform: no changes against its new state
    envs/dev/queues: no changes against its new state
    migration 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.

  4. 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.

  5. Approve it from a checkout. terragucci approve finds the migration, prints what it moves and approves its digest, with --sign under approval: sealed. With a wave waiting too, name it: terragucci approve split-queues. A pull request review does not approve a migration, under any approval: mode.

  6. Wave 1 runs again by itself: terragucci approve starts it with your forge token, or the resume job does on its next run with apply.resume set (Resume after an approval). init writes the resume job whenever apply.resume is set, under gate: never too. On that run it writes the states and goes on to plan and apply the wave:

    migration split-queues: approved by alice for this digest
    envs/dev/queues: wrote its new state
    envs/dev/platform: wrote its new state
    envs/dev/platform: version a7d36113-... before, 0dca9e05-... after
    envs/dev/queues: version none before, 3a695fbf-... after
    migration split-queues applied

    The root that gains resources is written first. A write that stops half way leaves a resource in both states, never in neither.

Point the root’s backend block at the new bucket or key, and add a migration naming where the state is now:

migrations/move-platform.yml
backends:
- root: envs/dev/platform
from:
backend: s3
config:
bucket: acme-old-state
key: envs/dev/platform.tfstate
region: us-east-1
use_lockfile: true

The 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.

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.

Terminal window
terragucci migrate revert split-queues

This 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.

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.

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

terragucci

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.