Operating Terraform
Drift, import, moved blocks and recovering from mistakes.
Part 1: The problem
At 2am, the orders API stopped answering. Noor was on call. An evening change had put the wrong port on a security group rule, in the code and in AWS. She fixed the rule in the console and went back to bed. The outage was over.
Nobody wrote the fix into the Terraform code.
Three weeks later, Leo added a tag to an unrelated resource and ran terraform apply. The plan had one more line than he expected, a ~ on that security group. He assumed it was noise. The apply put the wrong port back, and the outage came back with it, at lunchtime.
Nobody had changed the code, so nobody could see what had broken it.
AWS and the code had quietly disagreed for three weeks. That is drift, and dealing with it is part of running Terraform day to day, after the first build.
This lesson covers the tools for living with Terraform: spotting drift, adopting things made by hand, renaming without destroying, and getting out of trouble.
Part 2: The idea
Building with Terraform is the easy part. After that, the job is to keep three things agreeing: the code, the state and AWS. People click in the console. Code gets reorganised. Applies get interrupted. Providers release new versions.
| Situation | What goes wrong | The tool |
|---|---|---|
| Someone changes AWS by hand | The next apply quietly undoes it | Drift detection: terraform plan, -refresh-only |
| Something was built by hand, before Terraform | Terraform doesn’t know it exists | import blocks |
| You rename a resource or move it into a module | Terraform plans to destroy and recreate it | moved blocks |
| Something should no longer be managed by this code | Deleting it from the code destroys it | removed blocks, or terraform state rm |
| An apply went wrong | Half-built, stuck, or confused | A calm plan, and the recovery table below |
| A provider releases a new version | Behaviour changes under you | The lock file and terraform init -upgrade |
Drift: when AWS and the code disagree
Every terraform plan starts by refreshing: it reads the real settings of everything in the state from AWS. Then it compares them with the code. If someone changed a setting by hand, the plan offers to change it back. The code wins.
So when a plan shows a change you didn’t make, there are two honest answers:
| The hand-made change was… | What you do | Result |
|---|---|---|
| Wrong, or a leftover experiment | Apply the plan | AWS goes back to what the code says |
| Right, like Noor’s fix | Change the code to match, in a pull request | The next plan is clean, and the fix is safe for good |
The dishonest answer is to apply without reading, which is how Leo brought the outage back.
A refresh-only plan, made with terraform plan -refresh-only, is a different kind of plan. It shows only what changed outside Terraform, and it never plans to undo anything. It’s useful when you’re investigating. If you apply it, Terraform records the new values in the state and still changes nothing in AWS. It does not change your code, so on its own it fixes nothing. The next normal plan still wants to undo the change.
Catching drift on a schedule
A pipeline can run a plan every night and alert someone if anything differs. It uses -detailed-exitcode, which makes the plan’s exit code mean something:
| Exit code | Meaning |
|---|---|
0 | No changes. Code, state and AWS agree |
1 | Something went wrong: the plan failed |
2 | The plan succeeded and there are changes: drift, or code nobody has applied |
The pipeline project runs exactly this.
Adopting things made by hand: import
Lots of things are made before anyone writes Terraform: a repository clicked together in a hurry, a bucket from the early days. If you just write a resource block for one, Terraform plans to create it, and AWS refuses because it already exists.
An import block tells Terraform: this resource in the code is that thing which already exists.
import {
to = aws_ecr_repository.service["payments"]
id = "quickcart-payments"
}| Setting | What it does | If you leave it out | When you’d change it |
|---|---|---|---|
import | Adopt an existing object into the state during the next apply. Needs Terraform 1.5 or later | — | — |
to | The address in your code it becomes. A resource block for that address must exist | Required. Terraform doesn’t know where to put it | Once per thing you adopt |
id | How AWS identifies the object. It’s different for each resource type: a repository’s name, a VPC’s vpc- ID, a bucket’s name | Required | Look it up in the Import section at the bottom of that resource’s page in the AWS provider documentation |
Import blocks show up in the plan as will be imported, so they’re reviewed like any other change. The import only happens when you apply. Afterwards, delete the block. Leaving it in is harmless, but it’s clutter.
If the plan shows changes as well as the import, the code doesn’t match reality yet. Import doesn’t change the code. It only adopts the object, and after that Terraform makes it match the code. So either the code is right and you apply, or you edit the code until the only thing left is the import.
For a lot of resources at once, terraform plan -generate-config-out=generated.tf writes a first draft of the resource blocks for any import block that has none. Treat that file as a draft: tidy it and move the blocks into your own files.
The older way: the terraform import command
terraform import 'aws_ecr_repository.service["payments"]' quickcart-payments| Part | What it means |
|---|---|
terraform import | adopt an existing object into the state, immediately |
'aws_ecr_repository.service["payments"]' | the address in your code. The single quotes stop your shell treating the brackets and double quotes as special |
quickcart-payments | the object’s ID in AWS |
aws_ecr_repository.service["payments"]: Importing from ID "quickcart-payments"...
aws_ecr_repository.service["payments"]: Import prepared!
Prepared aws_ecr_repository for import
aws_ecr_repository.service["payments"]: Refreshing state... [id=quickcart-payments]
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.It changes the state straight away, with no plan and nothing to review. It still works, and you’ll see it in older guides. Prefer the block.
Renaming without destroying: moved
Terraform identifies each resource by its resource address: the type and the name you gave it in the code, like aws_ecr_repository.orders. Change the address, and as far as Terraform is concerned, one thing was deleted from the code and another was added.
A moved block says that the old address and the new one are the same thing:
moved {
from = aws_ecr_repository.orders
to = aws_ecr_repository.service["orders"]
}| Setting | What it does | If you leave it out | When you’d change it |
|---|---|---|---|
moved | During the next apply, rename the record in the state instead of destroying and creating. Needs Terraform 1.1 or later | Terraform plans a destroy and a create | Every time an address changes: a rename, a move into for_each or count, or a move into a module |
from | The old address, as it’s recorded in the state | Required | — |
to | The new address in your code | Required | — |
Examples of from and to pairs:
| Refactor | from | to |
|---|---|---|
| Rename | aws_ecr_repository.orders | aws_ecr_repository.orders_images |
Into a for_each | aws_ecr_repository.orders | aws_ecr_repository.service["orders"] |
| Into a module | aws_ecr_repository.orders | module.orders_repo.aws_ecr_repository.this |
| Rename a whole module call | module.repo | module.orders_repo |
The plan then says has moved to, and nothing in AWS changes. Keep the moved block for a while, until every copy of the state has been applied with it: every environment, and anyone’s branch. For a module that other teams use, keep it for good. It costs nothing.
terraform state mv does the same rename directly in the state, from the command line. Like terraform import, it changes the state at once with no plan and no review. Use a moved block instead.
Letting go without destroying: removed
Sometimes something should stay in AWS but leave this code. For example, another team takes it over. If you just delete the resource block, the next plan destroys it.
A removed block makes Terraform forget it instead:
removed {
from = aws_ecr_repository.reports
lifecycle {
destroy = false
}
}| Setting | What it does | If you leave it out | When you’d change it |
|---|---|---|---|
removed | Plan to drop a resource from the state. Needs Terraform 1.7 or later | Deleting the resource block plans a destroy | When a resource leaves this code |
from | The address to forget. It must be a whole resource or module, like aws_ecr_repository.reports, never one instance like service["payments"]. The resource block itself must be gone from the code | Required | — |
lifecycle → destroy | false means forget it, but leave it running in AWS | Defaults to true, which destroys it | Set true only if you really want it deleted, and want the plan to say so clearly |
The plan says so in plain words:
# aws_ecr_repository.reports will no longer be managed by Terraform, but will not be destroyed
# (destroy = false is set in the configuration)To forget one instance of a for_each or count, there’s no block. Use the command, which changes the state straight away:
terraform state rm 'aws_ecr_repository.service["payments"]'| Part | What it means |
|---|---|
terraform state rm | remove a record from the state. The object in AWS is not touched |
'aws_ecr_repository.service["payments"]' | the address to forget, in single quotes so your shell leaves the brackets alone |
Removed aws_ecr_repository.service["payments"]
Successfully removed 1 resource instance(s).Add -dry-run first to see what it would remove without removing it. Then take the item out of the code, or the next plan will offer to create it again.
Targeted plans, and why to avoid them
-target limits a plan or apply to one resource and whatever it depends on:
terraform plan -target='aws_ecr_repository.service["orders"]'| Part | What it means |
|---|---|
terraform plan | work out what would change |
-target= | but only for this address, and what it depends on |
'aws_ecr_repository.service["orders"]' | the address, in single quotes for your shell |
Terraform warns you every time:
Warning: Resource targeting is in effect
You are creating a plan with the -target option, which means that the result
of this plan may not represent all of the changes requested by the current
configuration.
The -target option is not for routine use, and is provided only for
exceptional situations such as recovering from errors or mistakes, or when
Terraform specifically suggests to use it as part of an error message.Take the warning literally. Everything outside the target goes unplanned, so drift and unapplied changes pile up out of sight. If a team reaches for -target every week, the configuration is too big. Split it into smaller parts with their own state, so each full plan is quick.
Recovering from mistakes
Before any repair, save a copy of the state:
terraform state pull > state-backup.json| Part | What it means |
|---|---|
terraform state pull | print the current state, wherever it’s stored |
> | your shell’s redirect: write what the command prints into a file instead of the screen |
state-backup.json | the file to write. Keep it out of git, because state can hold secrets |
Then find your situation:
| What happened | What you see | What to do |
|---|---|---|
| An apply was interrupted: Ctrl+C, a closed laptop, expired credentials | Some things were built, some weren’t | Run terraform plan. Terraform writes state as it goes, so the plan usually shows only what’s left. Apply it |
| The interrupted apply left its lock behind | Error acquiring the state lock | Check nobody is really applying, then terraform force-unlock with the lock’s ID. See Terraform state |
| Someone deleted a resource in the console | The plan shows + will be created for something that should exist | Decide whether it should exist. If yes, apply. If no, remove it from the code |
| A resource block was deleted from the code by mistake | The plan shows - will be destroyed | Don’t apply. Put the code back |
| The state was damaged or overwritten | The plan wants to create things that already exist | Restore an earlier version from the state bucket’s versioning. See Terraform state |
| An apply destroyed something that held data | It’s gone | Terraform can’t undo a destroy. Restore from a backup or snapshot. Then add prevent_destroy, see Loops and lifecycle |
| After a provider upgrade, the plan shows changes nobody made | Unexpected ~ lines | Read the provider’s changelog. If in doubt, go back to the previous .terraform.lock.hcl from git and run terraform init |
The pattern in every row: plan first, read it, and never apply a plan you can’t explain.
Upgrading providers
Two things decide which AWS provider you run:
| Where | What it says | Who changes it |
|---|---|---|
version = "~> 6.0" in required_providers | The range you allow: any 6.x, never 7.0 | You, on purpose, for a major upgrade |
.terraform.lock.hcl | The exact version chosen, like 6.66.0, with checksums | terraform init -upgrade, and you commit the result |
A plain terraform init installs exactly the version in the lock file, so everyone on the team runs the same one. terraform init -upgrade looks for the newest version your range allows and updates the lock file.
| Kind of upgrade | How to do it |
|---|---|
| Minor, like 6.60 to 6.66 | terraform init -upgrade, then terraform plan. Expect No changes. Commit the lock file in its own pull request |
| Major, like 6 to 7 | Read the provider’s upgrade guide first. Change the range to ~> 7.0, run init -upgrade, and fix whatever the plan and the guide point out. Do dev first, then stg, then production |
| Terraform itself | Upgrade everyone and the pipeline together, and raise required_version so an old copy refuses to run |
Part 3: See it
Maya reorganises the code: the orders repository moves into a for_each, or into a module. Nothing about the repository itself changes. Plan it without a moved block and then with one, and apply each to see what happens to the repository.
Your code
the address changedNo moved block. Terraform has no way to know the two addresses are the same repository.
State file
what Terraform remembersAWS
what really exists$ terraform plan # aws_ecr_repository.orders will be destroyed # (because aws_ecr_repository.orders is not in configuration) - resource "aws_ecr_repository" "orders" { # aws_ecr_repository.service["orders"] will be created + resource "aws_ecr_repository" "service" { Plan: 1 to add, 0 to change, 1 to destroy.
for_each. The repository in AWS is exactly the same one. But Terraform only compares addresses. The old one is gone from the code, so it plans to destroy it. The new one isn't in state, so it plans to create it.What to notice: the only difference between the two plans is four lines of code that never touch AWS. Without them, Terraform can only see an address that disappeared and a new one that appeared, so a harmless rename turns into a destroy.
Part 4: Do it
Free — two empty ECR repositories. ECR only charges for stored images, and these hold none.
You’ll build a repository with Terraform, drift it by hand, fix the drift, refactor the code with a moved block, adopt a repository made in the console, and upgrade the provider. About 40 minutes.
You need Terraform 1.10 or later and a working AWS login in your terminal. Set up your computer covers both.
Build the starting point
Make a new folder and move into it:
Terminalmkdir quickcart-opsPart What it means mkdirmake a directory quickcart-opsits name Terminalcd quickcart-opsPart What it means cdchange directory. Every later command runs in here quickcart-opsthe folder you just made Create
main.tf:main.tfterraform { required_version = ">= 1.10.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" } } } provider "aws" { region = "us-east-1" } resource "aws_ecr_repository" "orders" { name = "quickcart-orders" image_tag_mutability = "IMMUTABLE" force_delete = true tags = { Project = "quickcart" } }Setting What it does If you leave it out When you’d change it required_versionRefuse to run on a Terraform older than 1.10 An older Terraform might half-understand the files When you rely on a newer feature required_providers→awsUse the hashicorp/awsprovider, any 6.x versionTerraform picks the newest version, even a new major one For a deliberate major upgrade provider "aws"→regionBuild in us-east-1 Terraform uses the region from your AWS login settings To build somewhere else aws_ecr_repository"orders"A private store for container images. Its address in the code is aws_ecr_repository.orders— — nameThe repository’s name in AWS Required Never. Changing it replaces the repository image_tag_mutabilityIMMUTABLE: a tag likev1can never be moved to a different imageDefaults to MUTABLERarely. See Storing images: ECR force_deleteLet terraform destroydelete the repository even if it holds imagesDestroy fails while any image is inside Set it to falsefor real repositoriestagsLabels for finding and costing things No labels Add owner and environment in real projects Terminalterraform initPart What it means terraform initdownload the AWS provider and prepare the folder You should seeTerraform has been successfully initialized!Terminalterraform applyPart What it means terraform applyplan, show the plan, and carry it out once you type yesType
yeswhen asked.You should seeaws_ecr_repository.orders: Creating... aws_ecr_repository.orders: Creation complete after 1s [id=quickcart-orders] Apply complete! Resources: 1 added, 0 changed, 0 destroyed.Drift it by hand, and catch it
Play Noor at 2am. Change the repository in the console, not in the code:
In the AWS console: ECRPrivate registryRepositoriestick quickcart-ordersActionsEdit
Field Enter Image tag mutability Mutable Choose Save. Now ask Terraform what it thinks:
Terminalterraform planPart What it means terraform planread the real repository from AWS, compare it with the code, and show the difference You should seeTerraform will perform the following actions: # aws_ecr_repository.orders will be updated in-place ~ resource "aws_ecr_repository" "orders" { id = "quickcart-orders" ~ image_tag_mutability = "MUTABLE" -> "IMMUTABLE" name = "quickcart-orders" tags = { "Project" = "quickcart" } # (6 unchanged attributes hidden) # (2 unchanged blocks hidden) } Plan: 0 to add, 1 to change, 0 to destroy.Read it: the value in AWS is
MUTABLE, and the code wantsIMMUTABLE. Nobody changed the code, so this line is drift.Now look at the same drift the other way round:
Terminalterraform plan -refresh-onlyPart What it means terraform planmake a plan -refresh-onlyonly show what changed outside Terraform. Plan nothing to undo it You should seeNote: Objects have changed outside of Terraform Terraform detected the following changes made outside of Terraform since the last "terraform apply" which may have affected this plan: # aws_ecr_repository.orders has changed ~ resource "aws_ecr_repository" "orders" { id = "quickcart-orders" ~ image_tag_mutability = "IMMUTABLE" -> "MUTABLE" name = "quickcart-orders" tags = { "Project" = "quickcart" } # (6 unchanged attributes hidden) # (2 unchanged blocks hidden) } This is a refresh-only plan, so Terraform will not take any actions to undo these. If you were expecting these changes then you can apply this plan to record the updated values in the Terraform state without changing any remote objects.The arrow now points the other way: from what the state remembers to what AWS has.
This time the hand-made change was wrong, since immutable tags are what QuickCart wants. So put it back:
Terminalterraform applyPart What it means terraform applycarry out the plan: set the repository back to IMMUTABLEYou should seeApply complete! Resources: 0 added, 1 changed, 0 destroyed.Finally, the check a nightly pipeline runs:
Terminalterraform plan -detailed-exitcodePart What it means terraform planmake a plan -detailed-exitcodefinish with exit code 0 for no changes, 1 for an error, 2 for changes Terminalecho $?Part What it means echoprint something $?your shell’s record of the last command’s exit code You should see00: no drift. If you run the pair again after another console change, you’ll see2.Refactor into for_each, safely
QuickCart is about to have more than one service, so Maya wants one resource block that makes a repository per service. Replace
main.tfwith this version. Only the resource block changes:main.tfterraform { required_version = ">= 1.10.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" } } } provider "aws" { region = "us-east-1" } resource "aws_ecr_repository" "service" { for_each = toset(["orders"]) name = "quickcart-${each.key}" image_tag_mutability = "IMMUTABLE" force_delete = true tags = { Project = "quickcart" } }Setting What it does If you leave it out When you’d change it "service"The new name inside Terraform. The address becomes aws_ecr_repository.service["orders"]— — for_eachOne repository per item in the set. tosetturns the list into a set, whichfor_eachneeds. For now the set holds onlyordersOne repository only Add a service name to add a repository. You’ll add paymentsin the next stepnamequickcart-followed by the item, so the orders repository keeps the namequickcart-ordersRequired — the other settings Unchanged from step 1 — — Plan:
Terminalterraform planPart What it means terraform plancompare the reorganised code with the state The summary lines say# aws_ecr_repository.orders will be destroyed # (because aws_ecr_repository.orders is not in configuration) # aws_ecr_repository.service["orders"] will be created Plan: 1 to add, 0 to change, 1 to destroy.That’s the Case from the top of this lesson. Same repository, same name, but a new address. Applying would delete
quickcart-ordersand every image in it. Don’t apply.Create
moved.tfnext tomain.tf:moved.tfmoved { from = aws_ecr_repository.orders to = aws_ecr_repository.service["orders"] }Each setting is explained in the idea, above.
Terminalterraform planPart What it means terraform planplan again, now that Terraform knows the two addresses are the same You should seeTerraform will perform the following actions: # aws_ecr_repository.orders has moved to aws_ecr_repository.service["orders"] resource "aws_ecr_repository" "service" { id = "quickcart-orders" name = "quickcart-orders" tags = { "Project" = "quickcart" } # (7 unchanged attributes hidden) # (2 unchanged blocks hidden) } Plan: 0 to add, 0 to change, 0 to destroy.Terminalterraform applyPart What it means terraform applyrecord the new address in the state. Nothing in AWS changes You should seeApply complete! Resources: 0 added, 0 changed, 0 destroyed.Adopt a repository made in the console
Someone made the payments repository by hand before QuickCart used Terraform. Make it now, the same way:
In the AWS console: ECRPrivate registryRepositoriesCreate repository
Field Enter Repository name quickcart-paymentsImage tag mutability Mutable, the default. Leave it Encryption settings AES-256, the default. Leave it Choose Create.
Now add
paymentsto the set inmain.tf. The whole file now reads:main.tfterraform { required_version = ">= 1.10.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" } } } provider "aws" { region = "us-east-1" } resource "aws_ecr_repository" "service" { for_each = toset(["orders", "payments"]) name = "quickcart-${each.key}" image_tag_mutability = "IMMUTABLE" force_delete = true tags = { Project = "quickcart" } }Then create
import.tfnext to it:import.tfimport { to = aws_ecr_repository.service["payments"] id = "quickcart-payments" }Each setting is explained in the idea, above. For an ECR repository, the
idis its name.Terminalterraform planPart What it means terraform planplan the import, and compare the imported repository with the code You should see# aws_ecr_repository.service["payments"] will be updated in-place # (imported from "quickcart-payments") ~ resource "aws_ecr_repository" "service" { arn = "arn:aws:ecr:us-east-1:111122223333:repository/quickcart-payments" + force_delete = true id = "quickcart-payments" ~ image_tag_mutability = "MUTABLE" -> "IMMUTABLE" name = "quickcart-payments" region = "us-east-1" registry_id = "111122223333" repository_url = "111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-payments" ~ tags = { + "Project" = "quickcart" } ~ tags_all = { + "Project" = "quickcart" } encryption_configuration { encryption_type = "AES256" kms_key = null } image_scanning_configuration { scan_on_push = false } } Plan: 1 to import, 0 to add, 1 to change, 0 to destroy.Your account number will be your own, and there may be a few more lines. Read the
~lines: the repository made by hand is mutable and has no tags. The code wants immutable tags and theProjecttag. That’s QuickCart’s standard, so the code is right here. Apply:Terminalterraform applyPart What it means terraform applyimport the repository into the state, then bring it in line with the code You should seeaws_ecr_repository.service["payments"]: Importing... [id=quickcart-payments] aws_ecr_repository.service["payments"]: Import complete [id=quickcart-payments] aws_ecr_repository.service["payments"]: Modifying... [id=quickcart-payments] aws_ecr_repository.service["payments"]: Modifications complete after 0s [id=quickcart-payments] Apply complete! Resources: 1 imported, 0 added, 1 changed, 0 destroyed.Delete
import.tf: its job is done. Runterraform planonce more and you should seeNo changes. Your infrastructure matches the configuration.Check for provider upgrades
Terminalterraform init -upgradePart What it means terraform initprepare the folder -upgradelook for the newest provider allowed by ~> 6.0, instead of the version in the lock fileIf there’s nothing newer, you’ll see:
You should see- Finding hashicorp/aws versions matching "~> 6.0"... - Using previously-installed hashicorp/aws v6.66.0If there is, you’ll see it installed, and a note that the lock file changed:
You should see- Finding hashicorp/aws versions matching "~> 6.0"... - Installing hashicorp/aws v6.67.0... - Installed hashicorp/aws v6.67.0 (signed by HashiCorp) Terraform has made some changes to the provider dependency selections recorded in the .terraform.lock.hcl file. Review those changes and commit them to your version control system if they represent changes you intended to make.Your version numbers will be different. Either way, plan straight after:
Terminalterraform planPart What it means terraform plancheck the new provider sees everything the same way the old one did You should seeNo changes. Your infrastructure matches the configuration.A clean plan means the upgrade is safe to commit. Any
~here that nobody asked for comes from the upgrade, and is a reason to read the provider’s changelog before going further.Clean-up step: Clean up
Both repositories are now managed by Terraform, including the one you made by hand. So one command removes both:
Terminalterraform destroyPart What it means terraform destroyplan the removal of everything in the state, and carry it out once you type yesYou should seeDestroy complete! Resources: 2 destroyed.Check ECR → Private registry → Repositories. Neither
quickcart-ordersnorquickcart-paymentsshould be listed.
Part 5: Check yourself
At 2am Noor fixed an outage by changing a security group in the console. The next morning's plan shows a `~` change that would undo her fix. What should happen next?
Leo renames `aws_db_instance.main` to `aws_db_instance.orders` in the code. Nothing else changes. The plan says `1 to add, 0 to change, 1 to destroy`. What does he do?
Why do most teams prefer an import block to the terraform import command?
You've just imported a repository made by hand. The plan shows `~ image_tag_mutability = "MUTABLE" -> "IMMUTABLE"`. What does that mean?
A teammate uses -target on almost every apply, because a full plan takes 20 minutes. What does that tell you?
What does terraform init -upgrade do?
Part 6: Cheat sheet
| Command or block | What it does |
|---|---|
terraform plan | Always shows drift: any ~ you didn’t write is a question |
terraform plan -refresh-only | Show only what changed outside Terraform. Undo nothing |
terraform plan -detailed-exitcode | Exit 0 = no changes, 1 = error, 2 = changes. For nightly drift checks |
import { to = ADDRESS, id = "ID" } | Adopt something made by hand, through a reviewed plan |
terraform plan -generate-config-out=generated.tf | Draft resource blocks for your import blocks |
moved { from = OLD, to = NEW } | Rename or move a resource without destroying it |
removed { from = ADDRESS } with destroy = false | Stop managing a whole resource, and leave it in AWS |
terraform state rm 'ADDRESS' | Forget one instance. Changes state at once, so add -dry-run first |
terraform state pull > state-backup.json | Save a copy before any repair |
-target=ADDRESS | Emergencies only. Routine use means the state is too big |
terraform init -upgrade | Newest provider within your range. Plan, then commit .terraform.lock.hcl |