Installing Terraform & Your First Configuration

Terraform / Infrastructure as Code

Chapter 2 · Installing Terraform & Your First Configuration

Chapter 1's resource block was a preview. This chapter installs the real CLI and runs an actual configuration end to end — deliberately using a provider that needs no cloud account or credentials, so the workflow itself is the entire focus.

Installing Terraform

Terraform ships as a single binary — download it from HashiCorp's releases, or install via a package manager (brew install terraform on macOS, choco install terraform on Windows, or an apt/dnf repository on Linux). Verify the install:

$ terraform version Terraform v1.9.0

The terraform and provider Blocks

Every configuration starts with a terraform block declaring which providers it needs, and a provider block configuring one of them. A provider is a plugin that knows how to talk to a specific system — AWS, Azure, a local filesystem, even things like GitHub or Cloudflare all have providers.

terraform { required_providers { local = { source = "hashicorp/local" version = "~> 2.5" } } } provider "local" {}

terraform init

init reads the required_providers block, downloads the matching provider plugin(s) into a local .terraform/ directory, and initializes the backend (by default, a plain local state file — remote backends are Chapter 6's own topic).

$ terraform init Initializing the backend... Initializing provider plugins... - Finding hashicorp/local versions matching "~> 2.5"... - Installing hashicorp/local v2.5.1... Terraform has been successfully initialized!

The Dependency Lock File

init also writes .terraform.lock.hcl, recording the exact provider version and checksum actually installed. This is the same idea as node1-2's package-lock.json or ruby2-7's Gemfile.lock: it guarantees everyone running init against this configuration gets the identical provider build, not just "something matching the version constraint."

  • Commit .terraform.lock.hcl to git — it's what makes provider versions reproducible across machines and teammates
  • Never commit .terraform/ — it's a local download cache, regenerated by init on any machine, exactly like node_modules/ or a Rust target/ directory

Provider Version Pinning

The version = "~> 2.5" constraint means "any 2.5.x release, but not 2.6.0 or higher." Pinning matters for the same reason the lock file does — an un-pinned provider could silently pick up a newer version with different behavior, producing configuration drift of its own, this time in the tool itself rather than the infrastructure it manages.

The resource Block In Depth

A resource has the shape resource "<type>" "<local name>" { ... }. The type (local_file) is defined by the provider; the local name (hello) is how this configuration refers to it. Together they form a unique resource address, local_file.hello, used throughout the rest of the configuration and in CLI commands.

resource "local_file" "hello" { content = "Hello from Terraform!" filename = "${path.module}/hello.txt" }

terraform plan

Run plan before ever applying — it shows exactly what will change, using + for create, - for destroy, and ~ for update-in-place.

$ terraform plan Terraform will perform the following actions: # local_file.hello will be created + resource "local_file" "hello" { + content = "Hello from Terraform!" + filename = "./hello.txt" } Plan: 1 to add, 0 to change, 0 to destroy.

terraform apply

apply re-runs the same plan and, after an interactive confirmation, executes it.

$ terraform apply ... 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 local_file.hello: Creating... local_file.hello: Creation complete after 0s Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

hello.txt now exists on disk. Run terraform plan again with nothing changed, and it reports "No changes" — the same idempotency property from Chapter 1's Exercise 1, now demonstrated for real.

terraform destroy

Tears down everything this configuration manages, in dependency order, after its own interactive confirmation.

$ terraform destroy ... local_file.hello: Destroying... local_file.hello: Destruction complete after 0s Destroy complete! Resources: 0 added, 0 changed, 1 destroyed.
A safe first provider
The local provider needs no account, no credentials, and no cost — deliberately chosen here so the workflow itself (init → plan → apply → destroy) is what gets practiced first, before real cloud providers enter the picture.
Never hardcode credentials in a provider block
A real cloud provider block (e.g. provider "aws" {}) should read credentials from environment variables or a credentials file — never write an access key directly into a .tf file. Exactly the rule pipelines1-5 and crypto1-11 already established: a committed credential is compromised the moment it's pushed, permanently, even if later removed.

Hands-On Exercises

Exercise 1

After applying the hello.txt example, running terraform plan again with nothing changed prints "No changes." Explain what this output confirms about the workflow, connecting it back to Chapter 1's idempotency exercise.

📄 View solution
Exercise 2

Explain why .terraform.lock.hcl should be committed to git while the .terraform/ directory should not, drawing the parallel to another ecosystem's own lock-file convention.

📄 View solution
Exercise 3

A colleague writes provider "aws" { access_key = "AKIA..." secret_key = "..." } directly in a committed .tf file. Explain why this is dangerous and what to do instead.

📄 View solution

Chapter 2 Quick Reference

  • terraform init — downloads providers, initializes the backend, writes the lock file
  • terraform plan — shows what would change (+/-/~), changes nothing
  • terraform apply — executes the plan after confirmation
  • terraform destroy — tears down every managed resource
  • Commit .terraform.lock.hclnever commit .terraform/
  • Never hardcode credentials in a provider block — use environment variables or a credentials file