Command Palette

Search for a command to run...

Hectal

Mission 0.3 · Stage 0 — First Apply

Read a Plan Like a Reviewer

Goal: Versioning and public-access protection on the assets bucket, applied from a saved plan — and the ability to spot a destructive change in any plan before you approve it.

35 min Free 6 steps 3 break-it drills

By the end of this mission

  • Recognise all plan actions: +, ~, -, -/+, +/-
  • Find the # forces replacement line that turns an edit into a delete
  • Use references to create implicit dependencies between resources
  • Save a plan with -out and apply exactly what was reviewed

Part 1

Understand it first

The five actions a plan can show

+ create: a new object will exist. ~ update in-place: the same object, some settings change, no downtime in most cases. - destroy: the object will be deleted. -/+ destroy and then create replacement: the object will be DELETED and a new one created, because some argument can't be changed on an existing object. +/- create replacement and then destroy: the same, but the new one is created first (you'll use create_before_destroy for this in Stage 4).

For stateless things (a tag, a security-group description) replacement is harmless. For stateful things (a database, a bucket with objects, an EBS volume) replacement means data loss. That's the single most important thing to catch in a review.

Why some changes force replacement

Terraform doesn't choose this; the cloud API does. You can add tags to an existing bucket, but you can't rename a bucket, because S3 has no rename API. You can't change an RDS instance's encryption after creation, and you can't move a subnet to another AZ. The provider marks such arguments as 'ForceNew', and the plan labels the changed line # forces replacement so you can see exactly which edit caused it.

Review habit: when the summary shows any destroy, search the plan for forces replacement, check whether the resource holds data or serves traffic, and only then decide.

References build the dependency graph

When one resource uses another's attribute, such as bucket = aws_s3_bucket.assets.id, you've created an IMPLICIT DEPENDENCY. Terraform builds a graph from these references and uses it to order operations: the bucket is created before its versioning config, and on destroy the versioning config is removed first. Unrelated resources are handled in parallel.

References also carry values: if the bucket doesn't exist yet, the plan shows (known after apply) for the reference, and Terraform fills it in during apply. Only use depends_on (an explicit dependency) when there's a hidden ordering requirement that no attribute reference expresses. That's rare, and Stage 3 shows one example.

Saved plans: apply exactly what was reviewed

terraform apply with no arguments computes a NEW plan at that moment. If someone merged a change or the infrastructure drifted between your review and the apply, you could approve something you never saw. terraform plan -out=tfplan writes the reviewed plan to a file, and terraform apply tfplan executes precisely that plan with no second prompt.

If anything changed in state since the plan was saved, Terraform refuses with 'Saved plan is stale'. This is how CI pipelines work in Stage 7: plan on the pull request, review it, apply that exact artifact on merge.

ShopLite's first dependency graphdiagram
Rendering diagram…

Part 2

Your project after this mission · 1 file change

shoplite/
  • infra/
    • .terraform.lock.hcl
    • main.tfmodified
    • providers.tf
    • versions.tf
  • .gitignore

Part 3

Build it, step by step

  1. 1

    Add versioning and public-access protection

    In AWS provider 4 and later, bucket features are separate resources, one per concern, each pointing at the bucket by its ID. Versioning protects against overwrites and deletes (AWS course, Topic 4.1). The public access block guarantees that no policy or ACL can make the bucket public, even by mistake.

    Both use aws_s3_bucket.assets.id, which is the reference that creates the dependency.

    infra/main.tfadd to filehcl
    resource "aws_s3_bucket_versioning" "assets" {
      bucket = aws_s3_bucket.assets.id
    
      versioning_configuration {
        status = "Enabled"
      }
    }
    
    resource "aws_s3_bucket_public_access_block" "assets" {
      bucket = aws_s3_bucket.assets.id
    
      block_public_acls       = true
      block_public_policy     = true
      ignore_public_acls      = true
      restrict_public_buckets = true
    }
  2. 2

    Save the plan instead of applying straight away

    Write the plan to a file. Because the bucket already exists, the reference resolves to a real value in the plan (bucket = "shoplite-assets-ak-7x2q") rather than (known after apply).

    terminal
    $ terraform plan -out=tfplan
    ── expected output ──
    aws_s3_bucket.assets: Refreshing state... [id=shoplite-assets-ak-7x2q]
     
    Terraform will perform the following actions:
     
    # aws_s3_bucket_public_access_block.assets will be created
    + resource "aws_s3_bucket_public_access_block" "assets" {
    + block_public_acls = true
    + block_public_policy = true
    + bucket = "shoplite-assets-ak-7x2q"
    + id = (known after apply)
    + ignore_public_acls = true
    + restrict_public_buckets = true
    }
     
    # aws_s3_bucket_versioning.assets will be created
    + resource "aws_s3_bucket_versioning" "assets" {
    + bucket = "shoplite-assets-ak-7x2q"
    + id = (known after apply)
     
    + versioning_configuration {
    + mfa_delete = (known after apply)
    + status = "Enabled"
    }
    }
     
    Plan: 2 to add, 0 to change, 0 to destroy.
     
    Saved the plan to: tfplan
     
    To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"
  3. 3

    Review the saved plan, then apply exactly that

    terraform show tfplan re-renders a saved plan, which is how a reviewer inspects it later. -json gives machine-readable output that policy tools consume in Stage 7. Applying a saved plan doesn't prompt, because the review already happened.

    terminal
    $ terraform show tfplan | tail -n 3
    terraform apply tfplan
    ── expected output ──
    Plan: 2 to add, 0 to change, 0 to destroy.
     
    aws_s3_bucket_versioning.assets: Creating...
    aws_s3_bucket_public_access_block.assets: Creating...
    aws_s3_bucket_public_access_block.assets: Creation complete after 1s [id=shoplite-assets-ak-7x2q]
    aws_s3_bucket_versioning.assets: Creation complete after 2s [id=shoplite-assets-ak-7x2q]
     
    Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
    Both were created at the same time: they depend on the bucket, but not on each other.
  4. 4

    Provoke a replacement — and do NOT apply it

    Change the bucket name by one character (for example -7x2q to -7x2r) and plan. The bucket shows -/+, and the bucket line carries # forces replacement. Both dependent resources are replaced too, because their bucket argument is ForceNew as well.

    Read the summary: 3 to add, 0 to change, 3 to destroy. A one-character edit is about to delete a bucket along with every object in it. This is the review skill to learn in this mission.

    terminal
    $ terraform plan
    ── expected output ──
    # aws_s3_bucket.assets must be replaced
    -/+ resource "aws_s3_bucket" "assets" {
    ~ arn = "arn:aws:s3:::shoplite-assets-ak-7x2q" -> (known after apply)
    ~ bucket = "shoplite-assets-ak-7x2q" -> "shoplite-assets-ak-7x2r" # forces replacement
    ~ id = "shoplite-assets-ak-7x2q" -> (known after apply)
    # (8 unchanged attributes hidden)
    }
     
    # aws_s3_bucket_public_access_block.assets must be replaced
    -/+ resource "aws_s3_bucket_public_access_block" "assets" {
    ~ bucket = "shoplite-assets-ak-7x2q" -> (known after apply) # forces replacement
    # (4 unchanged attributes hidden)
    }
     
    # aws_s3_bucket_versioning.assets must be replaced
    -/+ resource "aws_s3_bucket_versioning" "assets" {
    ~ bucket = "shoplite-assets-ak-7x2q" -> (known after apply) # forces replacement
    # (1 unchanged block hidden)
    }
     
    Plan: 3 to add, 0 to change, 3 to destroy.
  5. 5

    Revert, and preview a destroy without doing one

    Put the original name back and confirm the plan is clean. Then look at plan -destroy, which shows what terraform destroy WOULD remove. It's useful before tearing down an environment, and it changes nothing.

    terminal
    $ terraform plan -destroy | tail -n 1
    ── expected output ──
    Plan: 0 to add, 0 to change, 3 to destroy.
  6. 6

    Delete the used plan file and commit

    A saved plan is single-use; once applied, it's stale. It's already git-ignored (the tfplan line from Mission 0.1's .gitignore), because saved plans can contain sensitive values. Delete it and commit the code.

    terminal
    $ rm tfplan
    git add -A && git commit -m "assets bucket: versioning + public access block"
    ── expected output ──
    [main 4b1c2d9] assets bucket: versioning + public access block
    1 file changed, 16 insertions(+)

Checkpoint — you should now have

  • ✓The bucket has versioning enabled and all four public-access-block settings on.
  • ✓You applied from a saved plan file, not an interactive apply.
  • ✓You saw a -/+ replacement with # forces replacement and did NOT apply it.
  • ✓terraform plan is clean again, and tfplan is git-ignored.

Part 4

Break it on purpose

Make each change, run the command, and read the error before revealing the diagnosis. Recognising these messages on sight is what makes you fast on a real team. Undo the change afterwards.

Break #1

Replace a bucket that has data in it

Upload a file, then change the bucket name and apply the replacement: echo hello > logo.txt && aws s3 cp logo.txt s3://shoplite-assets-ak-7x2q/ — then edit the name and run terraform apply, answering yes.

terminal
$ terraform apply
── what you'll see ──
aws_s3_bucket_versioning.assets: Destroying... [id=shoplite-assets-ak-7x2q]
aws_s3_bucket_public_access_block.assets: Destroying... [id=shoplite-assets-ak-7x2q]
aws_s3_bucket_versioning.assets: Destruction complete after 1s
aws_s3_bucket_public_access_block.assets: Destruction complete after 1s
aws_s3_bucket.assets: Destroying... [id=shoplite-assets-ak-7x2q]
╷
│ Error: deleting S3 Bucket (shoplite-assets-ak-7x2q): operation error S3:
│ DeleteBucket, https response error StatusCode: 409, BucketNotEmpty: The
│ bucket you tried to delete is not empty. You must delete all versions in
│ the bucket.
╵

Break #2

Apply a stale saved plan

Run terraform plan -out=tfplan. Then change the Owner tag and run a normal terraform apply. Finally run terraform apply tfplan.

terminal
$ terraform apply tfplan
── what you'll see ──
╷
│ Error: Saved plan is stale
│
│ The given plan file can no longer be applied because the state was changed
│ by another operation after the plan was created.
╵

Break #3

Reference a resource that doesn't exist

In aws_s3_bucket_versioning.assets, change the reference to aws_s3_bucket.asset.id (drop the s), then run terraform validate.

terminal
$ terraform validate
── what you'll see ──
╷
│ Error: Reference to undeclared resource
│
│ on main.tf line 10, in resource "aws_s3_bucket_versioning" "assets":
│ 10: bucket = aws_s3_bucket.asset.id
│
│ A managed resource "aws_s3_bucket" "asset" has not been declared in the
│ root module.
╵

Part 5

Interview questions from this mission

01

You're reviewing a teammate's plan. What do you check first, and what makes you stop?

02

Why does changing a bucket's name destroy the bucket, when changing its tags doesn't?

03

What's the difference between terraform apply and terraform apply tfplan?

04

How does Terraform decide the order in which to create resources? When would you use depends_on?

0/4 · 0%