You inherit a Terraform project managing about 60 live resources whose state is a local terraform.tfstate file. How do you move it onto an S3 backend without destroying or recreating anything?
answer
- bucket must exist first
- backend block, then re-init
- the flag that copies existing state
- plan must report no changes
- keep the old file as backup
basics
~20 sCreate the state bucket first with versioning enabled, add a backend "s3" block to the terraform block, then run terraform init -migrate-state to copy the existing state up. Confirm success by running terraform plan and seeing no changes.
solid answer
~50 sFirst solve the chicken-and-egg problem: the S3 bucket has to exist before Terraform can store state in it, so I create it out of band — by hand, by CLI, or in a small bootstrap root module that keeps its own state locally — with versioning turned on. Then I add a `backend "s3"` block naming the bucket, key and region, and run `terraform init -migrate-state`. Terraform notices the backend changed, offers to copy the existing local state into the new location, and I say yes. The proof it worked is `terraform plan` reporting no changes: same 60 resources, same IDs, nothing to create. I keep the old `terraform.tfstate` around as a belt-and-braces backup for a while, confirm nobody else is mid-apply during the switch, and only then delete it and make sure state files are gitignored.
code
bash · 11 lines# 1. Bucket exists first, with versioning enabled
aws s3api create-bucket --bucket acme-tfstate --region eu-west-1 \
--create-bucket-configuration LocationConstraint=eu-west-1
aws s3api put-bucket-versioning --bucket acme-tfstate \
--versioning-configuration Status=Enabled
# 2. After adding the backend "s3" block to the terraform block:
terraform init -migrate-state
# 3. The only proof that matters
terraform plango deeper
Recall the sequence: bucket first, add the backend block, run terraform init, accept the prompt to copy state. Know that migrating state does not touch the real resources at all.
Explain what -migrate-state does versus -reconfigure, why the bucket cannot be created by the module that uses it, and why terraform plan reporting no changes is the only real proof of success.
Talk about doing it safely on a live estate: announcing the window so nobody applies mid-switch, versioning on the bucket before the first write, keeping the local file as a rollback, and refusing to apply on a plan full of creates.
Frame the bootstrap question at organisation scale — who provisions state buckets, what the key naming convention is across dozens of root modules, and how a team is onboarded onto the standard backend without a bespoke migration each time.
## The order of operations Migration is mechanically simple and unforgiving about sequence, so it is worth being explicit about each step. **1. Create the backing store first.** Terraform cannot create the bucket that holds its own state in the same run that starts using it. The standard resolutions are: create the bucket manually or with the cloud CLI; or keep a tiny separate "bootstrap" root module that creates the bucket and deliberately keeps its own state local and committed nowhere important. Whichever you choose, enable object versioning on the bucket at creation time — that is your only recovery path if state is later corrupted — and enable encryption at rest. **2. Declare the backend.** Add the block to the `terraform` block in the root module: ```hcl terraform { backend "s3" { bucket = "acme-tfstate" key = "prod/network/terraform.tfstate" region = "eu-west-1" encrypt = true } } ``` The `key` is the object path inside the bucket, and it is worth choosing a convention now — one key per root module, namespaced by environment — because moving it later means another migration. **3. Re-initialise with migration.** Run `terraform init -migrate-state`. Terraform detects that the previously recorded backend (cached in `.terraform/terraform.tfstate`, which stores the *backend configuration*, not your resources) differs from the one in the configuration, and prompts: ``` Do you want to copy existing state to the new backend? ``` Answering yes uploads the local state to the new location. In automation, `-force-copy` accepts that prompt non-interactively. Without `-migrate-state`, newer Terraform versions error out rather than guess what you meant. **4. Verify with a plan, not with a feeling.** `terraform plan` must report "No changes. Your infrastructure matches the configuration." That single line proves the 60 resources are still mapped to their real IDs at the new location. If instead you see 60 creates, the remote state is empty — stop immediately and do not apply, or you will build a duplicate estate alongside the real one. **5. Clean up deliberately.** Keep the local `terraform.tfstate` and `terraform.tfstate.backup` files until you have had a successful apply through the new backend, then remove them, and make sure `*.tfstate*` and `.terraform/` are gitignored so nothing drifts back. ## The failure modes worth naming **Answering "no" to the copy prompt.** You get a fresh, empty state in the bucket. Every subsequent plan proposes creating the whole estate. The recovery is to re-run init pointed back at the local state and migrate again properly — but if someone applied in between, you now have duplicated resources to reconcile by hand. **Someone else applying during the window.** Migration is not atomic across humans. Announce it, and make sure no other engineer is mid-apply with their own copy of the local state, or their write lands nowhere anyone will look again. **Wrong key, silently.** If the `key` points at an object another root module already uses, migration will happily overwrite it. One key per root module is a rule, not a style preference. **Confusing `-migrate-state` with `-reconfigure`.** `-reconfigure` tells Terraform to discard the recorded backend configuration and initialise fresh *without* copying state. Running it here gives you an empty remote state and a local file Terraform no longer looks at. Use `-migrate-state` when you want the data to move; use `-reconfigure` only when you are deliberately re-pointing at a state that already exists. ## What migration is not Migration copies the state document byte-for-byte to a new home. It does not touch real infrastructure, does not renumber resource addresses, and does not need `import` — the resource-to-ID mappings are already in the file. If you also want to *reorganise* addresses, that is a separate concern and a separate change; doing both at once makes it impossible to tell which step caused a surprise diff. The same procedure works in reverse and sideways: removing the backend block and re-running `terraform init -migrate-state` pulls state back down to local, and pointing the block at a different bucket moves it again. It is one mechanism, not a one-way door. ## How to say it in an interview "Bucket first with versioning on; add the backend block; `terraform init -migrate-state` and accept the copy; then `terraform plan` must say no changes. If it says sixty creates, the state did not come with me and I stop."
- Who creates the state bucket, given Terraform cannot create the bucket it stores its own state in?Either out of band — console or cloud CLI, often one-time and documented — or with a small bootstrap root module that creates the bucket and keeps its own state local. Some teams then migrate the bootstrap module into the bucket it just created, which works fine but adds a step. What you must not do is have the main root module manage the bucket its own backend points at.
- You run terraform plan after migrating and it wants to create all 60 resources. What happened?The remote state is empty — almost always because the copy prompt was declined, `-reconfigure` was used instead of `-migrate-state`, or the backend `key` points at a different object than the one holding the state. Do not apply. Re-point at the original local state file, confirm a clean plan there, and redo the migration properly.
- How do you do this migration inside a CI pipeline with no interactive prompt?Add `-force-copy` to `terraform init -migrate-state`, which accepts the copy confirmation non-interactively. In practice migration is a one-off change best done by a human at a terminal with the local state in hand; automating the prompt away is convenient but removes the last chance to notice you are copying the wrong state over an existing one.
saying these in an interview costs you the question
- Thinks resources must be re-imported after moving state
- Deletes the local state file before verifying the plan
- Uses -reconfigure when the intent was to copy state
- Manages the state bucket in the same root module it backs
- Declares migration successful because init exited zero