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.
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 initdownloads 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.
Part 2
Your project after this mission · 4 files change
- infra/
- .terraform.lock.hclnew
- providers.tfnew
- versions.tfnew
- .gitignorenew
Part 3
Build it, step by step
- 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, thenapt install terraform.terminal$ brew tap hashicorp/tap && brew install hashicorp/tap/terraformterraform version── expected output ──Terraform v1.13.3on darwin_arm64Any 1.10+ version works. Newer is fine — the pin in versions.tf is a minimum. - 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_PROFILEso both the CLI and Terraform pick it up, then confirm who you are before creating anything. Checkingget-caller-identityfirst 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-devexport AWS_PROFILE=shoplite-devaws 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
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
.gitignorematters more than it looks..terraform/is a local cache of downloaded providers (hundreds of MB).*.tfstatefiles can contain passwords and resource details in plain text and must never reach Git — Stage 1 moves state to S3.*.tfplanfiles are saved plans, which also contain sensitive values. Note what is NOT ignored:.terraform.lock.hclmust 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
Pin Terraform and provider versions
versions.tfholds theterraformsettings block.required_versionstops anyone running an older Terraform against this code.required_providerssays which provider to download (sourceis<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
Configure the AWS provider with default tags
The
providerblock 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
Run terraform init
initprepares the working directory: it readsrequired_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 infraterraform 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 providerselections it made above. Include this file in your version control repositoryso that Terraform can guarantee to make the same selections by default whenyou 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
Format and validate — the two commands to run before every commit
terraform fmtrewrites files into the canonical style (aligned=signs, two-space indents), so diffs in pull requests only show real changes.-recursivecovers subfolders.terraform validatechecks 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 -recursiveterraform validate── expected output ──Success! The configuration is valid. - 8
Commit the starting point
Check that
git statusshows 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 .gitignoreA infra/.terraform.lock.hclA infra/providers.tfA infra/versions.tf
Checkpoint — you should now have
- ✓
terraform versionprints 1.10 or newer. - ✓
aws sts get-caller-identityshows your dev/sandbox account, not production and not the root user. - ✓
infra/containsversions.tf,providers.tf, and a.terraform.lock.hclthat is committed to Git. - ✓
terraform validateprintsSuccess!.
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.
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.
Break #3
Demand a Terraform version you don't have
Set required_version = ">= 99.0" in versions.tf and run terraform validate.
Part 5
Interview questions from this mission
What problems does infrastructure as code solve compared with creating resources in the AWS console?
Terraform is described as declarative. What does that mean in practice, and why does it matter?
What is .terraform.lock.hcl, and should it be committed?
What does the version constraint ~> 6.0 allow? How is it different from ~> 6.0.0?