Command Palette

Search for a command to run...

Hectal

Mission 0.1 · Stage 0 — First Apply

Set Up the ShopLite Repo and Run terraform init

Goal: A ShopLite repo with a pinned Terraform and AWS provider version, working AWS credentials, and a successful terraform init — nothing created in AWS yet.

25 min Free — nothing is created 8 steps 3 break-it drills

By the end of this mission

  • Explain why teams use infrastructure as code instead of the console
  • Describe Terraform's three parts: core, providers, and state
  • Pin Terraform and provider versions and explain ~> 6.0
  • Know what terraform init downloads and which files to commit

Part 1

Understand it first

Click-ops vs infrastructure as code

Imagine setting up ShopLite's production network by clicking through the AWS console: a VPC, six subnets, route tables, a NAT gateway, security groups. It works. Three months later you need an identical staging environment, and nobody remembers the exact settings. A teammate changed a security group last week and nobody knows why. An auditor asks who opened port 5432, and there's no record.

Infrastructure as code (IaC) fixes this by describing infrastructure in text files that live in Git. The files are the documentation: they're reviewed in pull requests like application code, every change has an author and a reason, and running the same files against a new account produces the same environment. Recovering from a deleted resource becomes 'run it again', not 'try to remember what we clicked'.

Declarative: you describe the end state, Terraform works out the steps

A bash script of aws CLI commands is IMPERATIVE: it lists actions. Run it twice and it either creates a second bucket or fails because the first one exists. You have to write every 'check if it exists, then update, else create' branch yourself.

Terraform is DECLARATIVE: you write 'there should be a bucket named X with versioning on'. On every run, Terraform compares that desired state with what actually exists and computes only the difference — create it if it's missing, update it if a setting drifted, do nothing if it already matches. Running apply ten times in a row is safe; the last nine do nothing. This property, idempotency, is the foundation everything else in the course builds on.

Terraform's three moving parts: core, providers, and state

Terraform CORE is the terraform binary. It reads your .tf files, builds a dependency graph of resources, compares it with state, and decides what to create, change, or destroy. Core knows nothing about AWS.

PROVIDERS are plugins that translate Terraform resources into real API calls. The hashicorp/aws provider knows that aws_s3_bucket means calling S3's CreateBucket. There are providers for GCP, Azure, Kubernetes, GitHub, Cloudflare, Datadog and thousands more, all downloaded from the Terraform Registry. The same Terraform workflow works for all of them.

STATE is Terraform's memory: a JSON file mapping each resource in your code (like aws_s3_bucket.assets) to the real object it created (the bucket named shoplite-dev-assets-3f9a). Without state, Terraform can't tell 'this bucket is mine' from 'this bucket was made by someone else'. State gets all of Stage 1, because it causes most real Terraform incidents.

Version pinning and the lock file

Terraform and its providers ship often, and major versions contain breaking changes (AWS provider 6.0 changed how several S3 and region settings work). If your laptop runs one version and CI runs another, the same code can produce different plans. So you pin versions in code: required_version for Terraform itself, and a version constraint for each provider.

~> 6.0 is the 'pessimistic' operator: it allows 6.1, 6.14 and so on, but never 7.0. You get bug fixes and new resources automatically, but no major-version breakage. terraform init then records the EXACT provider version it chose, plus checksums, in .terraform.lock.hcl. Commit that file: it makes every machine and every CI run use byte-for-byte the same provider until someone deliberately runs terraform init -upgrade.

How Terraform turns files into AWS resourcesdiagram
Rendering diagram…

Part 2

Your project after this mission · 4 files change

shoplite/
  • infra/
    • .terraform.lock.hclnew
    • providers.tfnew
    • versions.tfnew
  • .gitignorenew

Part 3

Build it, step by step

  1. 1

    Install Terraform and check the version

    Install Terraform 1.10 or newer; this course uses features from 1.10 onward, such as S3-native state locking. Use your OS package manager so updates are easy, and avoid random binaries from the internet.

    On Windows, run the commands in Git Bash or PowerShell after installing with winget install Hashicorp.Terraform. On Linux, add HashiCorp's apt/yum repository as described on developer.hashicorp.com, then apt install terraform.

    terminal
    $ brew tap hashicorp/tap && brew install hashicorp/tap/terraform
    terraform version
    ── expected output ──
    Terraform v1.13.3
    on darwin_arm64
    Any 1.10+ version works. Newer is fine — the pin in versions.tf is a minimum.
  2. 2

    Give Terraform AWS credentials (not root, not long-lived keys)

    Terraform's AWS provider finds credentials the same way the AWS CLI does. Use the named profile you set up in the AWS course (Topic 0.4 / 1.4), ideally an IAM Identity Center (SSO) profile, which gives short-lived credentials. Never use root user keys.

    Export AWS_PROFILE so both the CLI and Terraform pick it up, then confirm who you are before creating anything. Checking get-caller-identity first is a habit worth keeping, because it's the only reliable way to know which account you're about to change.

    terminal
    $ aws sso login --profile shoplite-dev
    export AWS_PROFILE=shoplite-dev
    aws sts get-caller-identity
    ── expected output ──
    {
    "UserId": "AROAXXXXXXXXXXXXXXXXX:priya",
    "Account": "123456789012",
    "Arn": "arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_AdministratorAccess_abc123/priya"
    }
    Use a sandbox or dev account for this course, never a production account.
  3. 3

    Create the repo and a .gitignore that protects state and secrets

    All infrastructure lives in infra/ inside the ShopLite repo. Later stages split it into modules and environments, but a single folder is the right place to start.

    The .gitignore matters more than it looks. .terraform/ is a local cache of downloaded providers (hundreds of MB). *.tfstate files can contain passwords and resource details in plain text and must never reach Git — Stage 1 moves state to S3. *.tfplan files are saved plans, which also contain sensitive values. Note what is NOT ignored: .terraform.lock.hcl must be committed.

    .gitignorewhole filebash
    # Local provider cache — recreated by terraform init
    **/.terraform/*
    
    # State: may contain secrets in plain text. Never commit.
    *.tfstate
    *.tfstate.*
    
    # Saved plans also contain sensitive values
    *.tfplan
    tfplan
    
    # Crash logs
    crash.log
    crash.*.log
    
    # Local-only variable overrides (secrets, personal settings)
    *.local.tfvars
    override.tf
    override.tf.json
    
    # NOTE: .terraform.lock.hcl is intentionally NOT ignored — commit it.
    terminal
    $ mkdir -p shoplite/infra && cd shoplite && git init
    ── expected output ──
    Initialized empty Git repository in /home/priya/shoplite/.git/
  4. 4

    Pin Terraform and provider versions

    versions.tf holds the terraform settings block. required_version stops anyone running an older Terraform against this code. required_providers says which provider to download (source is <namespace>/<name> on the Registry) and which versions are allowed.

    infra/versions.tfwhole filehcl
    terraform {
      required_version = ">= 1.10"
    
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 6.0"
        }
      }
    }
  5. 5

    Configure the AWS provider with default tags

    The provider block configures the plugin: which region to use and, very usefully, default_tags. Every taggable resource this configuration creates automatically gets these tags, so you never forget them. In the AWS course's cost topic (7.4), tags are how you find out what ShopLite actually costs.

    We hardcode the region for now; Mission 0.4 turns it into a variable.

    infra/providers.tfwhole filehcl
    provider "aws" {
      region = "ap-south-1"
    
      default_tags {
        tags = {
          Project   = "shoplite"
          ManagedBy = "terraform"
          Repo      = "github.com/you/shoplite"
        }
      }
    }
  6. 6

    Run terraform init

    init prepares the working directory: it reads required_providers, downloads matching providers from the Registry into .terraform/, writes .terraform.lock.hcl, and sets up the backend where state is stored (local for now). Run it the first time, and again whenever you add a provider or module or change the backend.

    terminal
    $ cd infra
    terraform init
    ── expected output ──
    Initializing the backend...
    Initializing provider plugins...
    - Finding hashicorp/aws versions matching "~> 6.0"...
    - Installing hashicorp/aws v6.14.0...
    - Installed hashicorp/aws v6.14.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 provider version may be newer than 6.14.0 — anything 6.x is fine.
  7. 7

    Format and validate — the two commands to run before every commit

    terraform fmt rewrites files into the canonical style (aligned = signs, two-space indents), so diffs in pull requests only show real changes. -recursive covers subfolders. terraform validate checks syntax and internal consistency, such as unknown arguments or bad references, without calling AWS. In Stage 7 both run in CI on every pull request.

    terminal
    $ terraform fmt -recursive
    terraform validate
    ── expected output ──
    Success! The configuration is valid.
  8. 8

    Commit the starting point

    Check that git status shows the lock file but not .terraform/, then commit. Every mission in this course ends with a commit, so your Git history becomes the ShopLite build log.

    terminal
    $ cd .. && git add . && git status --short
    ── expected output ──
    A .gitignore
    A infra/.terraform.lock.hcl
    A infra/providers.tf
    A infra/versions.tf

Checkpoint — you should now have

  • ✓terraform version prints 1.10 or newer.
  • ✓aws sts get-caller-identity shows your dev/sandbox account, not production and not the root user.
  • ✓infra/ contains versions.tf, providers.tf, and a .terraform.lock.hcl that is committed to Git.
  • ✓terraform validate prints Success!.

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

Misspell the provider source

In versions.tf, change source = "hashicorp/aws" to source = "hashicorp/awss", then run terraform init.

terminal
$ terraform init
── what you'll see ──
Initializing provider plugins...
- Finding hashicorp/awss versions matching "~> 6.0"...
╷
│ Error: Failed to query available provider packages
│
│ Could not retrieve the list of available versions for provider
│ hashicorp/awss: provider registry registry.terraform.io does not have a
│ provider named registry.terraform.io/hashicorp/awss
╵

Break #2

Change the provider constraint after init

After a successful init, change the AWS provider constraint in versions.tf to version = "~> 5.0" and run terraform init again.

terminal
$ terraform init
── what you'll see ──
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
╷
│ Error: Failed to query available provider packages
│
│ Could not retrieve the list of available versions for provider
│ hashicorp/aws: locked provider registry.terraform.io/hashicorp/aws 6.14.0
│ does not match configured version constraint ~> 5.0; must use terraform
│ init -upgrade to allow selection of new versions
╵

Break #3

Demand a Terraform version you don't have

Set required_version = ">= 99.0" in versions.tf and run terraform validate.

terminal
$ terraform validate
── what you'll see ──
╷
│ Error: Unsupported Terraform Core version
│
│ on versions.tf line 2, in terraform:
│ 2: required_version = ">= 99.0"
│
│ This configuration does not support Terraform version 1.13.3. To proceed,
│ either choose another supported Terraform version or update this version
│ constraint. Version constraints are normally set for good reason, so
│ updating the constraint may lead to other errors or unexpected behavior.
╵

Part 5

Interview questions from this mission

01

What problems does infrastructure as code solve compared with creating resources in the AWS console?

02

Terraform is described as declarative. What does that mean in practice, and why does it matter?

03

What is .terraform.lock.hcl, and should it be committed?

04

What does the version constraint ~> 6.0 allow? How is it different from ~> 6.0.0?

0/4 · 0%