Laptop to Production

Operating Terraform

Drift, import, moved blocks and recovering from mistakes.

Time
60 minutes
Level
Advanced

Part 1: The problem

QuickCart, three weeks after an outage

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.

SituationWhat goes wrongThe tool
Someone changes AWS by handThe next apply quietly undoes itDrift detection: terraform plan, -refresh-only
Something was built by hand, before TerraformTerraform doesn’t know it existsimport blocks
You rename a resource or move it into a moduleTerraform plans to destroy and recreate itmoved blocks
Something should no longer be managed by this codeDeleting it from the code destroys itremoved blocks, or terraform state rm
An apply went wrongHalf-built, stuck, or confusedA calm plan, and the recovery table below
A provider releases a new versionBehaviour changes under youThe lock file and terraform init -upgrade
Key point: Every plan must be read. Anything in it that you didn’t intend is a question to answer before you apply, never noise.

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 doResult
Wrong, or a leftover experimentApply the planAWS goes back to what the code says
Right, like Noor’s fixChange the code to match, in a pull requestThe 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 codeMeaning
0No changes. Code, state and AWS agree
1Something went wrong: the plan failed
2The 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.tf
import {
  to = aws_ecr_repository.service["payments"]
  id = "quickcart-payments"
}
SettingWhat it doesIf you leave it outWhen you’d change it
importAdopt an existing object into the state during the next apply. Needs Terraform 1.5 or later——
toThe address in your code it becomes. A resource block for that address must existRequired. Terraform doesn’t know where to put itOnce per thing you adopt
idHow AWS identifies the object. It’s different for each resource type: a repository’s name, a VPC’s vpc- ID, a bucket’s nameRequiredLook 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

Terminal
terraform import 'aws_ecr_repository.service["payments"]' quickcart-payments
PartWhat it means
terraform importadopt 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-paymentsthe object’s ID in AWS
You should see
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.tf
moved {
  from = aws_ecr_repository.orders
  to   = aws_ecr_repository.service["orders"]
}
SettingWhat it doesIf you leave it outWhen you’d change it
movedDuring the next apply, rename the record in the state instead of destroying and creating. Needs Terraform 1.1 or laterTerraform plans a destroy and a createEvery time an address changes: a rename, a move into for_each or count, or a move into a module
fromThe old address, as it’s recorded in the stateRequired—
toThe new address in your codeRequired—

Examples of from and to pairs:

Refactorfromto
Renameaws_ecr_repository.ordersaws_ecr_repository.orders_images
Into a for_eachaws_ecr_repository.ordersaws_ecr_repository.service["orders"]
Into a moduleaws_ecr_repository.ordersmodule.orders_repo.aws_ecr_repository.this
Rename a whole module callmodule.repomodule.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.tf
removed {
  from = aws_ecr_repository.reports

  lifecycle {
    destroy = false
  }
}
SettingWhat it doesIf you leave it outWhen you’d change it
removedPlan to drop a resource from the state. Needs Terraform 1.7 or laterDeleting the resource block plans a destroyWhen a resource leaves this code
fromThe 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 codeRequired—
lifecycle → destroyfalse means forget it, but leave it running in AWSDefaults to true, which destroys itSet true only if you really want it deleted, and want the plan to say so clearly

The plan says so in plain words:

You should see
 # 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:

Terminal
terraform state rm 'aws_ecr_repository.service["payments"]'
PartWhat it means
terraform state rmremove 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
You should see
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:

Terminal
terraform plan -target='aws_ecr_repository.service["orders"]'
PartWhat it means
terraform planwork 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:

You should see
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:

Terminal
terraform state pull > state-backup.json
PartWhat it means
terraform state pullprint 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.jsonthe file to write. Keep it out of git, because state can hold secrets

Then find your situation:

What happenedWhat you seeWhat to do
An apply was interrupted: Ctrl+C, a closed laptop, expired credentialsSome things were built, some weren’tRun 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 behindError acquiring the state lockCheck nobody is really applying, then terraform force-unlock with the lock’s ID. See Terraform state
Someone deleted a resource in the consoleThe plan shows + will be created for something that should existDecide whether it should exist. If yes, apply. If no, remove it from the code
A resource block was deleted from the code by mistakeThe plan shows - will be destroyedDon’t apply. Put the code back
The state was damaged or overwrittenThe plan wants to create things that already existRestore an earlier version from the state bucket’s versioning. See Terraform state
An apply destroyed something that held dataIt’s goneTerraform 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 madeUnexpected ~ linesRead 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:

WhereWhat it saysWho changes it
version = "~> 6.0" in required_providersThe range you allow: any 6.x, never 7.0You, on purpose, for a major upgrade
.terraform.lock.hclThe exact version chosen, like 6.66.0, with checksumsterraform 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 upgradeHow to do it
Minor, like 6.60 to 6.66terraform init -upgrade, then terraform plan. Expect No changes. Commit the lock file in its own pull request
Major, like 6 to 7Read 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 itselfUpgrade 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.

The change
moved block

Your code

the address changed
aws_ecr_repository.orders
aws_ecr_repository.service["orders"]
moved.tf

No moved block. Terraform has no way to know the two addresses are the same repository.

State file

what Terraform remembers
aws_ecr_repository.orders

AWS

what really exists
quickcart-ordersevery image of the orders service
$ 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.
Plan, without a moved blockMaya changed the resource's address to fit a 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.

  1. Build the starting point

    Make a new folder and move into it:

    Terminal
    mkdir quickcart-ops
    PartWhat it means
    mkdirmake a directory
    quickcart-opsits name
    Terminal
    cd quickcart-ops
    PartWhat it means
    cdchange directory. Every later command runs in here
    quickcart-opsthe folder you just made

    Create main.tf:

    main.tf
    terraform {
      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"
      }
    }
    SettingWhat it doesIf you leave it outWhen you’d change it
    required_versionRefuse to run on a Terraform older than 1.10An older Terraform might half-understand the filesWhen you rely on a newer feature
    required_providers → awsUse the hashicorp/aws provider, any 6.x versionTerraform picks the newest version, even a new major oneFor a deliberate major upgrade
    provider "aws" → regionBuild in us-east-1Terraform uses the region from your AWS login settingsTo 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 AWSRequiredNever. Changing it replaces the repository
    image_tag_mutabilityIMMUTABLE: a tag like v1 can never be moved to a different imageDefaults to MUTABLERarely. See Storing images: ECR
    force_deleteLet terraform destroy delete the repository even if it holds imagesDestroy fails while any image is insideSet it to false for real repositories
    tagsLabels for finding and costing thingsNo labelsAdd owner and environment in real projects
    Terminal
    terraform init
    PartWhat it means
    terraform initdownload the AWS provider and prepare the folder
    You should see
    Terraform has been successfully initialized!
    Terminal
    terraform apply
    PartWhat it means
    terraform applyplan, show the plan, and carry it out once you type yes

    Type yes when asked.

    You should see
    aws_ecr_repository.orders: Creating...
    aws_ecr_repository.orders: Creation complete after 1s [id=quickcart-orders]
    
    Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
  2. 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

    FieldEnter
    Image tag mutabilityMutable

    Choose Save. Now ask Terraform what it thinks:

    Terminal
    terraform plan
    PartWhat it means
    terraform planread the real repository from AWS, compare it with the code, and show the difference
    You should see
    Terraform 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 wants IMMUTABLE. Nobody changed the code, so this line is drift.

    Now look at the same drift the other way round:

    Terminal
    terraform plan -refresh-only
    PartWhat it means
    terraform planmake a plan
    -refresh-onlyonly show what changed outside Terraform. Plan nothing to undo it
    You should see
    Note: 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:

    Terminal
    terraform apply
    PartWhat it means
    terraform applycarry out the plan: set the repository back to IMMUTABLE
    You should see
    Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

    Finally, the check a nightly pipeline runs:

    Terminal
    terraform plan -detailed-exitcode
    PartWhat it means
    terraform planmake a plan
    -detailed-exitcodefinish with exit code 0 for no changes, 1 for an error, 2 for changes
    Terminal
    echo $?
    PartWhat it means
    echoprint something
    $?your shell’s record of the last command’s exit code
    You should see
    0

    0: no drift. If you run the pair again after another console change, you’ll see 2.

  3. 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.tf with this version. Only the resource block changes:

    main.tf
    terraform {
      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"
      }
    }
    SettingWhat it doesIf you leave it outWhen 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. toset turns the list into a set, which for_each needs. For now the set holds only ordersOne repository onlyAdd a service name to add a repository. You’ll add payments in the next step
    namequickcart- followed by the item, so the orders repository keeps the name quickcart-ordersRequired—
    the other settingsUnchanged from step 1——

    Plan:

    Terminal
    terraform plan
    PartWhat 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-orders and every image in it. Don’t apply.

    Create moved.tf next to main.tf:

    moved.tf
    moved {
      from = aws_ecr_repository.orders
      to   = aws_ecr_repository.service["orders"]
    }

    Each setting is explained in the idea, above.

    Terminal
    terraform plan
    PartWhat it means
    terraform planplan again, now that Terraform knows the two addresses are the same
    You should see
    Terraform 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.
    Terminal
    terraform apply
    PartWhat it means
    terraform applyrecord the new address in the state. Nothing in AWS changes
    You should see
    Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
  4. 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

    FieldEnter
    Repository namequickcart-payments
    Image tag mutabilityMutable, the default. Leave it
    Encryption settingsAES-256, the default. Leave it

    Choose Create.

    Now add payments to the set in main.tf. The whole file now reads:

    main.tf
    terraform {
      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.tf next to it:

    import.tf
    import {
      to = aws_ecr_repository.service["payments"]
      id = "quickcart-payments"
    }

    Each setting is explained in the idea, above. For an ECR repository, the id is its name.

    Terminal
    terraform plan
    PartWhat 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 the Project tag. That’s QuickCart’s standard, so the code is right here. Apply:

    Terminal
    terraform apply
    PartWhat it means
    terraform applyimport the repository into the state, then bring it in line with the code
    You should see
    aws_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. Run terraform plan once more and you should see No changes. Your infrastructure matches the configuration.

  5. Check for provider upgrades

    Terminal
    terraform init -upgrade
    PartWhat it means
    terraform initprepare the folder
    -upgradelook for the newest provider allowed by ~> 6.0, instead of the version in the lock file

    If there’s nothing newer, you’ll see:

    You should see
    - Finding hashicorp/aws versions matching "~> 6.0"...
    - Using previously-installed hashicorp/aws v6.66.0

    If 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:

    Terminal
    terraform plan
    PartWhat it means
    terraform plancheck the new provider sees everything the same way the old one did
    You should see
    No 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.

  6. Clean-up step: Clean up

    Both repositories are now managed by Terraform, including the one you made by hand. So one command removes both:

    Terminal
    terraform destroy
    PartWhat it means
    terraform destroyplan the removal of everything in the state, and carry it out once you type yes
    You should see
    Destroy complete! Resources: 2 destroyed.

    Check ECR → Private registry → Repositories. Neither quickcart-orders nor quickcart-payments should 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

On one card
Command or blockWhat it does
terraform planAlways shows drift: any ~ you didn’t write is a question
terraform plan -refresh-onlyShow only what changed outside Terraform. Undo nothing
terraform plan -detailed-exitcodeExit 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.tfDraft resource blocks for your import blocks
moved { from = OLD, to = NEW }Rename or move a resource without destroying it
removed { from = ADDRESS } with destroy = falseStop 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.jsonSave a copy before any repair
-target=ADDRESSEmergencies only. Routine use means the state is too big
terraform init -upgradeNewest provider within your range. Plan, then commit .terraform.lock.hcl

Finished the build and the quiz? Mark it done to fill in its stop on the route map.