Command Palette

Search for a command to run...

Hectal

Mission 0.2 · Stage 0 — First Apply

Your First Resource: the ShopLite Assets Bucket

Goal: A real S3 bucket for ShopLite's static assets, created, updated, and inspected entirely through Terraform — and a clear picture of what plan and apply actually do.

30 min Free — an empty S3 bucket costs nothing 7 steps 4 break-it drills

By the end of this mission

  • Read the anatomy of a resource block: type, name, address, arguments
  • Run plan and apply and explain every line of their output
  • See an in-place update and confirm idempotency with a no-op plan
  • Inspect what Terraform recorded with terraform state list/show

Part 1

Understand it first

Anatomy of a resource block

resource "aws_s3_bucket" "assets" { ... } has three parts. aws_s3_bucket is the RESOURCE TYPE; its prefix (aws_) tells Terraform which provider owns it. assets is the LOCAL NAME, your label for this particular bucket, used only inside Terraform. Together they form the resource ADDRESS aws_s3_bucket.assets, which is how you refer to it from other resources, in state commands, and in plan output.

Inside the braces are ARGUMENTS, the settings you choose (bucket, tags). After creation the resource also has ATTRIBUTES that AWS decides, such as arn, id, and bucket_regional_domain_name. Some values are both: you set bucket, and it's also readable as an attribute. Every resource's arguments and attributes are listed on its Registry documentation page; keep that page open while writing.

What plan really does

terraform plan doesn't just read your files. It (1) loads the state, (2) REFRESHES by asking AWS for the current settings of every resource in state, (3) compares the refreshed reality with your configuration, and (4) prints the actions needed to make reality match the code. It changes nothing.

Because of the refresh step, a plan also reveals DRIFT, meaning changes someone made outside Terraform, such as in the console. It also means plans need working credentials, and on a large configuration they take time, because every resource is read from AWS.

What apply does — and why it shows the plan again

terraform apply computes a fresh plan, shows it, and waits for you to type exactly yes. Then it calls the provider for each action in dependency order, in parallel where possible (10 at a time by default), and writes the results to state as each resource finishes. If one resource fails, the ones that already succeeded stay recorded, and the next apply continues from there. Terraform doesn't roll back.

The confirmation is your last review. On real teams, the plan is saved and reviewed in a pull request and applied unchanged by CI (Mission 0.3 and Stage 7). Treat yes as a signature, not a formality.

S3 bucket names are global

Almost every AWS resource name is unique only within your account and region. S3 bucket names are unique across ALL AWS accounts worldwide, because they form part of public DNS names (<bucket>.s3.amazonaws.com). shoplite-assets was almost certainly taken years ago. For this mission you'll add your initials and a few random characters by hand; Mission 0.4 generates a suffix automatically.

plan → apply, step by stepdiagram
Rendering diagram…

Part 2

Your project after this mission · 2 files change

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

Part 3

Build it, step by step

  1. 1

    Declare the bucket

    Create main.tf. Terraform loads every .tf file in the folder as one configuration, so file names are only for humans; the convention is main.tf for resources, variables.tf, outputs.tf and so on.

    Replace ak-7x2q with your own initials and four random characters so the name is globally unique. The Name tag is merged with the default_tags from the provider.

    infra/main.tfwhole filehcl
    resource "aws_s3_bucket" "assets" {
      bucket = "shoplite-assets-ak-7x2q"
    
      tags = {
        Name = "shoplite-assets"
      }
    }
  2. 2

    Plan, and read it top to bottom

    The + symbol means create. (known after apply) marks values AWS decides during creation, such as the ARN. Look at tags_all: it's the merge of this resource's tags and the provider's default_tags, so you can confirm your tags before anything exists.

    The last line is the one to check first on every plan: 1 to add, 0 to change, 0 to destroy. Any non-zero destroy deserves a slow, careful read.

    terminal
    $ cd infra
    terraform plan
    ── expected output ──
    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_s3_bucket.assets will be created
    + resource "aws_s3_bucket" "assets" {
    + acceleration_status = (known after apply)
    + arn = (known after apply)
    + bucket = "shoplite-assets-ak-7x2q"
    + bucket_domain_name = (known after apply)
    + bucket_regional_domain_name = (known after apply)
    + force_destroy = false
    + id = (known after apply)
    + region = "ap-south-1"
    + tags = {
    + "Name" = "shoplite-assets"
    }
    + tags_all = {
    + "ManagedBy" = "terraform"
    + "Name" = "shoplite-assets"
    + "Project" = "shoplite"
    + "Repo" = "github.com/you/shoplite"
    }
    # (several more attributes known after apply)
    }
     
    Plan: 1 to add, 0 to change, 0 to destroy.
  3. 3

    Apply it

    Terraform shows the same plan again and waits. Type yes (exactly; y is treated as no). Each resource logs Creating... and then Creation complete with its real ID.

    terminal
    $ terraform apply
    ── expected output ──
    # aws_s3_bucket.assets will be created
    + resource "aws_s3_bucket" "assets" { ... }
     
    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_s3_bucket.assets: Creating...
    aws_s3_bucket.assets: Creation complete after 2s [id=shoplite-assets-ak-7x2q]
     
    Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
  4. 4

    Verify in AWS — don't just trust the output

    Check the result independently with the AWS CLI. The bucket exists and carries all four tags, including the three default tags you never wrote on the resource itself.

    terminal
    $ aws s3api get-bucket-tagging --bucket shoplite-assets-ak-7x2q
    ── expected output ──
    {
    "TagSet": [
    { "Key": "Project", "Value": "shoplite" },
    { "Key": "ManagedBy", "Value": "terraform" },
    { "Key": "Repo", "Value": "github.com/you/shoplite" },
    { "Key": "Name", "Value": "shoplite-assets" }
    ]
    }
  5. 5

    Change something and watch an in-place update

    Add an Owner tag to the resource. Tags can be changed on an existing bucket, so Terraform plans ~ update in-place: the same bucket, one setting changed. Inside the diff, + marks the new map entry, and the comment # (N unchanged attributes hidden) means Terraform only shows what's changing.

    infra/main.tfwhole filehcl
    resource "aws_s3_bucket" "assets" {
      bucket = "shoplite-assets-ak-7x2q"
    
      tags = {
        Name  = "shoplite-assets"
        Owner = "platform-team"
      }
    }
    terminal
    $ terraform apply
    ── expected output ──
    aws_s3_bucket.assets: Refreshing state... [id=shoplite-assets-ak-7x2q]
     
    Terraform will perform the following actions:
     
    # aws_s3_bucket.assets will be updated in-place
    ~ resource "aws_s3_bucket" "assets" {
    id = "shoplite-assets-ak-7x2q"
    ~ tags = {
    "Name" = "shoplite-assets"
    + "Owner" = "platform-team"
    }
    ~ tags_all = {
    + "Owner" = "platform-team"
    # (4 unchanged elements hidden)
    }
    # (11 unchanged attributes hidden)
    }
     
    Plan: 0 to add, 1 to change, 0 to destroy.
    ...
    aws_s3_bucket.assets: Modifying... [id=shoplite-assets-ak-7x2q]
    aws_s3_bucket.assets: Modifications complete after 1s [id=shoplite-assets-ak-7x2q]
     
    Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
  6. 6

    Plan again: nothing to do

    Run plan with no changes. Reality matches the code, so Terraform proposes nothing. This is the idempotency from Mission 0.1, now visible. Seeing No changes after an apply confirms that your code fully describes what exists.

    terminal
    $ terraform plan
    ── expected output ──
    aws_s3_bucket.assets: Refreshing state... [id=shoplite-assets-ak-7x2q]
     
    No changes. Your infrastructure matches the configuration.
     
    Terraform has compared your real infrastructure against your configuration
    and found no differences, so no changes are needed.
  7. 7

    Look at what Terraform remembered

    The apply created terraform.tfstate next to your code. Don't edit it by hand; use the state subcommands. state list shows every address Terraform manages; state show prints the recorded attributes for one of them. Stage 1 explains state in depth and moves this file somewhere safer than your laptop.

    terminal
    $ terraform state list
    terraform state show aws_s3_bucket.assets
    ── expected output ──
    aws_s3_bucket.assets
     
    # aws_s3_bucket.assets:
    resource "aws_s3_bucket" "assets" {
    arn = "arn:aws:s3:::shoplite-assets-ak-7x2q"
    bucket = "shoplite-assets-ak-7x2q"
    bucket_domain_name = "shoplite-assets-ak-7x2q.s3.amazonaws.com"
    bucket_regional_domain_name = "shoplite-assets-ak-7x2q.s3.ap-south-1.amazonaws.com"
    force_destroy = false
    id = "shoplite-assets-ak-7x2q"
    region = "ap-south-1"
    tags = {
    "Name" = "shoplite-assets"
    "Owner" = "platform-team"
    }
    ...
    }

Checkpoint — you should now have

  • ✓An S3 bucket named shoplite-assets-<yours> exists in ap-south-1 with four tags.
  • ✓terraform plan prints No changes.
  • ✓terraform state list prints aws_s3_bucket.assets.
  • ✓You can explain what +, ~, and (known after apply) mean.
  • ✓main.tf is committed; terraform.tfstate is NOT (the .gitignore from 0.1 handles it).

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

Use a bucket name someone else already owns

Change bucket to plain "shoplite-assets" and run terraform apply. (Terraform will plan a replacement; approve it. You'll see why that's dangerous in Mission 0.3.)

terminal
$ terraform apply
── what you'll see ──
aws_s3_bucket.assets: Destroying... [id=shoplite-assets-ak-7x2q]
aws_s3_bucket.assets: Destruction complete after 1s
aws_s3_bucket.assets: Creating...
╷
│ Error: creating S3 Bucket (shoplite-assets): operation error S3: CreateBucket,
│ https response error StatusCode: 409, RequestID: 8Q2..., BucketAlreadyExists:
│
│ with aws_s3_bucket.assets,
│ on main.tf line 1, in resource "aws_s3_bucket" "assets":
│ 1: resource "aws_s3_bucket" "assets" {
│
╵

Break #2

Change the bucket outside Terraform (drift)

Add a tag directly with the CLI, as a teammate might in the console, then run terraform plan: aws s3api put-bucket-tagging --bucket shoplite-assets-ak-7x2q --tagging 'TagSet=[{Key=Name,Value=shoplite-assets},{Key=Owner,Value=platform-team},{Key=Temp,Value=yes}]'

terminal
$ terraform plan
── what you'll see ──
aws_s3_bucket.assets: Refreshing state... [id=shoplite-assets-ak-7x2q]
 
# aws_s3_bucket.assets will be updated in-place
~ resource "aws_s3_bucket" "assets" {
id = "shoplite-assets-ak-7x2q"
~ tags = {
- "Temp" = "yes" -> null
# (2 unchanged elements hidden)
}
~ tags_all = {
- "Temp" = "yes" -> null
+ "ManagedBy" = "terraform"
+ "Project" = "shoplite"
+ "Repo" = "github.com/you/shoplite"
# (2 unchanged elements hidden)
}
}
 
Plan: 0 to add, 1 to change, 0 to destroy.

Break #3

Use an argument that doesn't exist

Rename bucket to bucket_name in main.tf and run terraform validate.

terminal
$ terraform validate
── what you'll see ──
╷
│ Error: Unsupported argument
│
│ on main.tf line 2, in resource "aws_s3_bucket" "assets":
│ 2: bucket_name = "shoplite-assets-ak-7x2q"
│
│ An argument named "bucket_name" is not expected here.
╵

Break #4

Plan with a profile that doesn't exist

Run a plan with a non-existent profile: AWS_PROFILE=nope terraform plan.

terminal
$ AWS_PROFILE=nope terraform plan
── what you'll see ──
╷
│ Error: failed to get shared config profile, nope
│
│ with provider["registry.terraform.io/hashicorp/aws"],
│ on providers.tf line 1, in provider "aws":
│ 1: provider "aws" {
│
╵

Part 5

Interview questions from this mission

01

What's the difference between an argument and an attribute in Terraform?

02

Walk me through what happens when you run terraform plan.

03

An apply fails halfway through creating ten resources. What state are you in, and how do you recover?

04

What are default_tags, and how do they show up in a plan?

Before you stop

Clean up

terminal
$ # Nothing to clean up — keep the bucket, Mission 0.3 builds on it.
terraform state list
── expected output ──
aws_s3_bucket.assets
0/4 · 0%