Laptop to Production

Your first resource

Providers, plan, apply, destroy — and reading a plan.

Time
45 minutes
Level
Beginner

Part 1: The problem

QuickCart, a small tidy-up

Theo was new to Terraform. QuickCart’s image repository was called quickcart-orders, and he wanted it to match a new naming scheme: quickcart-orders-api. One word, in one file.

He ran terraform apply. A long plan scrolled past, ending in Plan: 1 to add, 0 to change, 1 to destroy. That looked like a rename, so he typed yes.

It wasn’t a rename. AWS can’t rename an image repository, so Terraform deleted the old one — with every image the team had ever pushed — and made a new, empty one. The next deployment couldn’t find the image it needed. The plan had said so, in one short comment: # forces replacement.

Terraform told Theo exactly what it would do. He just couldn’t read it yet.

This lesson builds one real resource and teaches you to read what Terraform says it will do with it.

Part 2: The idea

A Terraform project is a folder of .tf files. Terraform reads all of them together, so how you split them into files is up to you. A few names are usual:

FileWhat usually goes in it
main.tfThe resources
variables.tfInputs. Covered in Variables, locals and outputs
outputs.tfValues to print or share
backend.tfWhere state lives. Covered in Terraform state

Providers: how Terraform talks to AWS

Terraform itself knows nothing about AWS. A provider is a plugin that teaches it one platform: every resource type, and how to create, change and delete each one. There are providers for AWS, Azure, Google Cloud, GitHub and hundreds more.

You say which provider you need, and which versions are allowed, in a terraform block:

Version constraintWhat it allowsWhen you’d use it
"~> 6.0"Any 6.x, never 7.0The usual choice: fixes and new features, no breaking changes
"6.66.0"Exactly that versionRarely. You’d miss every fix
">= 6.0"6.0 or anything newer, including 7Avoid: a new major version can break your code without warning

A major version is the first number. When it goes up, things you rely on may change or disappear. That’s why ~> stops before it.

Resources: the things you build

A resource is one thing in AWS that Terraform creates and looks after. Its first line has two names:

PartExampleWhat it is
resourceCreate and manage something
Typeaws_ecr_repositoryWhat kind of thing. The provider defines the types
NameordersYour code’s name for it. Other code refers to it as aws_ecr_repository.orders
name = inside"quickcart-orders"What AWS calls it

orders and quickcart-orders are different names for different readers. One is for your code; the other is what you see in the console.

Four commands

CommandWhat it doesHow often
terraform initDownload the providers and set up the folderOnce, and again when you add a provider or module
terraform planShow what would change. Changes nothingBefore every apply
terraform applyShow the plan, ask for yes, then make the changesWhenever you change the code
terraform destroyDelete everything this folder managesPractice environments only

Reading a plan

Each resource in a plan starts with a symbol. It tells you how careful to be.

SymbolMeaningHow careful to be
+CreateNormal
~Change in place. The resource stays; some settings changeRead which settings
-/+Destroy, then create a replacementStop and read. For a database, this is data loss
+/-Create the replacement first, then destroy the old oneRead carefully. Only happens with a lifecycle setting, covered in Loops and lifecycle
-DestroyStop and read
<=Read something that already existsHarmless

Inside a resource, each line has a symbol too, and a few phrases matter:

You seeWhat it means
"old" -> "new"This setting changes from old to new
# forces replacementThis line is why the resource must be destroyed and rebuilt. AWS can’t change it in place
(known after apply)AWS decides this value when it creates the resource, so Terraform can’t show it yet
-> nullThis value will be gone
# (6 unchanged attributes hidden)Terraform hides settings that don’t change, so the real change stands out

Which settings force replacement is decided by AWS, not by you. You can’t memorise them all, and you don’t need to. The plan always tells you.

Key point: Read every plan before you type yes. The first symbol tells you how careful to be.

What init leaves in the folder

ItemWhat it isCommit to git?
.terraform/The downloaded provider plugin. Hundreds of megabytesNo. init downloads it again anywhere
.terraform.lock.hclThe exact provider version chosen, with checksums of the downloadYes
terraform.tfstateAppears after apply: Terraform’s record of what it builtNo. It can hold secrets. It moves to S3 in Terraform state

Part 3: See it

Here is QuickCart’s repository in code. Pick a change, and see the plan Terraform makes for it.

Change main.tf
  • +create
  • ~update in place
  • -/+destroy, then create
  • -destroy
main.tf

resource "aws_ecr_repository" "orders" {
  name                 = "quickcart-orders"
  image_tag_mutability = "IMMUTABLE"
  force_delete         = true

  tags = {
    Project = "quickcart"
    Team    = "platform"
  }
}
$ terraform plan

  # aws_ecr_repository.orders will be updated in-place
  ~ resource "aws_ecr_repository" "orders" {
        id                   = "quickcart-orders"
        name                 = "quickcart-orders"
      ~ tags                 = {
            "Project" = "quickcart"
          + "Team"    = "platform"
        }
        # (6 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Real Terraform output, trimmed to the lines that matter.

What it means: Read the fields, then carry on~ update in-place. The same repository stays; only its tags change. The images inside are untouched. Read which fields change, then carry on.

What to notice: adding a tag and renaming the repository are both one-line changes. One is a harmless ~. The other is a -/+ that deletes every image inside. The size of the change in your code tells you nothing; only the plan does.

Part 4: Do it

Free — one empty ECR repository, which costs nothing, created and destroyed in the same lab.

You’ll build QuickCart’s image repository with Terraform, change it in place, see a change that would force replacement, and destroy it. About 30 minutes.

You need Terraform (see Why Terraform) and a working AWS login in your terminal (Set up your computer covers it). Terraform uses the same login as the AWS CLI. Check it:

Terminal
aws sts get-caller-identity
PartWhat it means
aws sts get-caller-identityask AWS “who am I?” — it answers with your account and user
You should see
{
    "UserId": "AIDAEXAMPLEUSERID1234",
    "Account": "111122223333",
    "Arn": "arn:aws:iam::111122223333:user/quickcart-admin"
}

Your account number and user name will be your own. If this command fails, fix your login first: Terraform will fail the same way.

  1. Make the project folder

    Terminal
    mkdir quickcart-tf
    PartWhat it means
    mkdirmake a directory
    quickcart-tfits name. Later lessons build on this folder
    Terminal
    cd quickcart-tf
    PartWhat it means
    cdchange directory. Every later command runs in here
    quickcart-tfthe folder you just made
  2. Write the code

    Create main.tf in that folder:

    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
    terraform blockSettings for Terraform itself, not for AWSTerraform runs with no version rules at all—
    required_versionRefuse to run on a Terraform older than 1.10An older Terraform may fail later, with a confusing errorWhen you start using a newer Terraform feature
    required_providers → awsWhich provider this code needs, under the local name awsTerraform guesses hashicorp/aws and takes the newest version, even a new major oneWhen you add another provider
    sourceWhere to download it: hashicorp/aws on the public Terraform RegistryTerraform assumes hashicorp/aws. Saying it is clearerAlmost never
    versionWhich versions are allowed: any 6.xAlways the newest, which one day brings breaking changesWhen you deliberately move to a new major version
    provider "aws"Settings for the AWS providerTerraform still uses the provider, with default settings—
    regionWhere the resources are builtTerraform uses the region from your AWS login settings, which may not be what you expectTo build somewhere else
    resource "aws_ecr_repository" "orders"One ECR repository. orders is your code’s name for it——
    nameThe repository’s name in AWSRequired: plan stops with an errorNever casually. Changing it replaces the repository, as you’ll see
    image_tag_mutabilityIMMUTABLE: once v1 is pushed, no other image can take that tagDefaults to MUTABLE: someone can push a different image under an old tagRarely. Storing images: ECR explains why immutable is safer
    force_deleteLet terraform destroy delete the repository even if it holds imagesDestroy fails while any image is insideSet it to false for production, so images can’t be deleted by accident
    tagsLabels to find and cost everything that belongs to QuickCartNo labels. Harder to find laterAdd owner and environment tags in real projects

    Now tidy the formatting:

    Terminal
    terraform fmt
    PartWhat it means
    terraform fmtline up the = signs and fix the spacing in every .tf file here

    It prints the name of any file it changed, or nothing if the file was already tidy. Everyone running fmt means code reviews show real changes, not spacing.

  3. Initialise the folder

    Terminal
    terraform init
    PartWhat it means
    terraform initdownload the AWS provider and prepare this folder
    You should see
    Initializing the backend...
    
    Initializing provider plugins...
    - Finding hashicorp/aws versions matching "~> 6.0"...
    - Installing hashicorp/aws v6.66.0...
    - Installed hashicorp/aws v6.66.0 (signed by HashiCorp)
    
    Terraform has created a lock file .terraform.lock.hcl to record the provider
    selections it made above. Include this file in your version control repository
    so that Terraform can guarantee to make the same selections by default when
    you run "terraform init" in the future.
    
    Terraform has been successfully initialized!

    Your version number will probably be newer. Now check the code makes sense:

    Terminal
    terraform validate
    PartWhat it means
    terraform validatecheck the code for mistakes — wrong types, unknown settings — without contacting AWS
    You should see
    Success! The configuration is valid.

    Open .terraform.lock.hcl in your editor. It starts like this:

    The start of .terraform.lock.hcl
    # This file is maintained automatically by "terraform init".
    # Manual edits may be lost in future updates.
    
    provider "registry.terraform.io/hashicorp/aws" {
      version     = "6.66.0"
      constraints = "~> 6.0"
      hashes = [

    version is what was chosen; constraints is the range you allowed. The hashes are checksums, so a tampered download is refused. When a colleague runs init, they get exactly this version, not whatever is newest that day.

  4. Keep the right files out of git

    If this folder will go into git, create a file called .gitignore next to main.tf:

    .gitignore
    .terraform/
    *.tfstate
    *.tfstate.*
    LineWhat it keeps out of git
    .terraform/the downloaded plugin: large, and init fetches it again
    *.tfstatethe state file, which can hold secrets
    *.tfstate.*the backup copy Terraform keeps, terraform.tfstate.backup

    .terraform.lock.hcl isn’t listed, so it is committed. That’s what you want.

  5. Read the plan

    Terminal
    terraform plan
    PartWhat it means
    terraform plancompare the code, the state and AWS, and show what would change. Changes nothing
    You should see
    Terraform used the selected providers to generate the following execution
    plan. Resource actions are indicated with the following symbols:
      + create
    
    Terraform will perform the following actions:
    
      # aws_ecr_repository.orders will be created
      + resource "aws_ecr_repository" "orders" {
          + arn                  = (known after apply)
          + force_delete         = true
          + id                   = (known after apply)
          + image_tag_mutability = "IMMUTABLE"
          + name                 = "quickcart-orders"
          + region               = "us-east-1"
          + registry_id          = (known after apply)
          + repository_url       = (known after apply)
          + tags                 = {
              + "Project" = "quickcart"
            }
          + tags_all             = {
              + "Project" = "quickcart"
            }
        }
    
    Plan: 1 to add, 0 to change, 0 to destroy.

    Every line starts with +: all new. arn, id and repository_url are (known after apply), because AWS makes them up when it creates the repository. tags_all is your tags plus any the provider adds for every resource; here they’re the same.

  6. Apply it

    Terminal
    terraform apply
    PartWhat it means
    terraform applymake the plan again, show it, and carry it out once you type yes

    It shows the same plan. Read the summary line, then type yes:

    You should see
    Plan: 1 to add, 0 to change, 0 to destroy.
    
    Do you want to perform these actions?
      Terraform will perform the actions described above.
      Only 'yes' will be accepted to approve.
    
      Enter a value: yes
    
    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.

    Check it in the console:

    In the AWS console: Amazon ECRPrivate registryRepositories

    You should see

    quickcart-orders in the list, with Tag immutability shown as immutable. Built from one short file, and you can build it again identically whenever you like.

  7. Change it in place

    Add a second tag, Team. 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" "orders" {
      name                 = "quickcart-orders"
      image_tag_mutability = "IMMUTABLE"
      force_delete         = true
    
      tags = {
        Project = "quickcart"
        Team    = "platform"
      }
    }
    SettingWhat changed
    tags → Teama second label, saying which team owns the repository. Every other setting is as before
    Terminal
    terraform apply
    PartWhat it means
    terraform applyplan the tag change, show it, and carry it out once you type yes
    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"
            name                 = "quickcart-orders"
          ~ tags                 = {
                "Project" = "quickcart"
              + "Team"    = "platform"
            }
          ~ tags_all             = {
              + "Team"    = "platform"
                # (1 unchanged element hidden)
            }
            # (6 unchanged attributes hidden)
    
            # (2 unchanged blocks hidden)
        }
    
    Plan: 0 to add, 1 to change, 0 to destroy.

    ~: the same repository, updated. Type yes:

    You should see
    aws_ecr_repository.orders: Modifying... [id=quickcart-orders]
    aws_ecr_repository.orders: Modifications complete after 1s [id=quickcart-orders]
    
    Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
  8. See a change that forces replacement — and don't apply it

    Now do what Theo did. Change the name to quickcart-orders-api:

    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-api"
      image_tag_mutability = "IMMUTABLE"
      force_delete         = true
    
      tags = {
        Project = "quickcart"
        Team    = "platform"
      }
    }
    SettingWhat changed
    namequickcart-orders becomes quickcart-orders-api

    This time, only plan:

    Terminal
    terraform plan
    PartWhat it means
    terraform planshow what the rename would do, without doing it
    You should see
    Terraform will perform the following actions:
    
      # aws_ecr_repository.orders must be replaced
    -/+ resource "aws_ecr_repository" "orders" {
          ~ arn                  = "arn:aws:ecr:us-east-1:111122223333:repository/quickcart-orders" -> (known after apply)
          ~ id                   = "quickcart-orders" -> (known after apply)
          ~ name                 = "quickcart-orders" -> "quickcart-orders-api" # forces replacement
          ~ registry_id          = "111122223333" -> (known after apply)
          ~ repository_url       = "111122223333.dkr.ecr.us-east-1.amazonaws.com/quickcart-orders" -> (known after apply)
            tags                 = {
                "Project" = "quickcart"
                "Team"    = "platform"
            }
            # (4 unchanged attributes hidden)
        }
    
    Plan: 1 to add, 0 to change, 1 to destroy.

    There are a few more lines about the repository’s settings blocks; they don’t matter here. Read the rest:

    LineWhat it tells you
    must be replaced and -/+the repository will be destroyed, then a new one created
    # forces replacementthe name change is the cause
    repository_url ... -> (known after apply)the address changes, so anything pushing or pulling images breaks until it’s updated
    1 to add, 1 to destroynot a rename. A delete and a create

    Don’t apply it. Change the name back to quickcart-orders, then plan again:

    Terminal
    terraform plan
    PartWhat it means
    terraform plancheck the code matches what’s built again
    You should see
    No changes. Your infrastructure matches the configuration.
  9. Clean-up step: Clean up

    Terminal
    terraform destroy
    PartWhat it means
    terraform destroyplan the removal of everything this folder built, and carry it out once you type yes

    It shows the plan, with every line starting -. Check the summary says 1 to destroy, then type yes:

    You should see
    Plan: 0 to add, 0 to change, 1 to destroy.
    
    Do you really want to destroy all resources?
      Terraform will destroy all your managed infrastructure, as shown above.
      There is no undo. Only 'yes' will be accepted to confirm.
    
      Enter a value: yes
    
    aws_ecr_repository.orders: Destroying... [id=quickcart-orders]
    aws_ecr_repository.orders: Destruction complete after 1s
    
    Destroy complete! Resources: 1 destroyed.

    Check Amazon ECR → Private registry → Repositories: quickcart-orders has gone.

    Keep the quickcart-tf folder. Terraform state starts from it.

Part 5: Check yourself

A plan line starts with -/+ and ends with "# forces replacement". What will happen?

Which file should you commit to git?

What does version = "~> 6.0" allow?

You haven't changed anything, and plan says "Plan 0 to add, 1 to change, 0 to destroy". What's the most likely cause?

What is a provider?

Part 6: Cheat sheet

On one card
Command or symbolWhat it means
terraform initDownload providers, create .terraform.lock.hcl. Again after adding a provider or module
terraform fmtTidy the formatting
terraform validateCheck the code without contacting AWS
terraform planShow what would change. Changes nothing
terraform applyShow the plan, ask for yes, make the changes
terraform destroyDelete everything this folder manages
+ / ~ / -Create / change in place / destroy
-/+ and # forces replacementDestroy then create. Stop and read
(known after apply)AWS decides the value when it creates the resource
version = "~> 6.0"Any 6.x provider, never 7.0
Commit.tf files and .terraform.lock.hcl
Never commit.terraform/ and *.tfstate

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