Clicking through a cloud console works until you need the same setup twice. Someone forgets a setting, a staging bucket is public while production is private, and nobody can say exactly what changed last Tuesday. Terraform fixes that by describing infrastructure in text files that you review, version and apply like code.

This is part 1 of Terraform from Zero, a five-part series. By the end of this post you will have installed Terraform, created a real S3 bucket on AWS, changed it safely and deleted it again. The later parts build on this same project.

What Terraform actually does

Terraform is an infrastructure as code tool from HashiCorp. You write configuration in HCL (HashiCorp Configuration Language) that says what should exist, for example "a private S3 bucket with versioning turned on". Terraform works out how to get there.

It does that with three ingredients:

  • Providers are plugins that talk to an API. The AWS provider knows how to create buckets, the GitHub provider knows how to create repositories, and there are thousands more on the Terraform Registry.
  • Configuration is your .tf files: the resources you want and how they connect.
  • State is a file where Terraform records what it created, so it can compare the real world with your configuration next time. Part 3 covers state in depth.

The workflow is always the same loop: write, plan, apply.

Install Terraform

Install the CLI from the official install page. The common options are:

bash
# macOS (Homebrew)
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

# Windows (winget)
winget install Hashicorp.Terraform

# Check it works
terraform -version

On Linux, HashiCorp publishes apt and yum repositories; follow the steps for your distribution on the install page. This series uses Terraform 1.11 or newer, because part 3 relies on S3 native state locking, which became generally available in 1.11.

If you work on several projects with different versions, a version manager such as tfenv keeps them apart, but it is optional for now.

Give Terraform access to AWS

Terraform uses the same credentials as the AWS CLI. The simplest safe setup for learning is an IAM Identity Center (SSO) profile:

bash
aws configure sso
aws sso login --profile learn
export AWS_PROFILE=learn

On Windows PowerShell, set the profile with $env:AWS_PROFILE = "learn". Avoid putting access keys in your .tf files. Anything in configuration ends up in Git, and keys in Git are how accounts get compromised.

Everything in this post fits in the AWS free tier, but you are responsible for what runs in your account. Run terraform destroy at the end.

Write your first configuration

Create an empty folder and a file called main.tf:

hcl
terraform {
  required_version = ">= 1.11"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "eu-west-2"
}

resource "aws_s3_bucket" "notes" {
  bucket_prefix = "field-notes-"

  tags = {
    Project   = "terraform-from-zero"
    ManagedBy = "terraform"
  }
}

Read it from top to bottom:

  • The terraform block pins the CLI version and the provider. ~> 6.0 means "any 6.x release, but not 7.0", so a future major version with breaking changes cannot sneak in.
  • The provider block configures the AWS provider. Change the region to one near you.
  • The resource block declares one S3 bucket. aws_s3_bucket is the resource type and notes is the local name you use to refer to it elsewhere. Bucket names are global across all AWS accounts, so bucket_prefix lets AWS add a unique suffix.

terraform init: download what you need

bash
terraform init

init reads the required_providers block, downloads the AWS provider into a hidden .terraform folder and writes .terraform.lock.hcl. The lock file records the exact provider version and checksums, so everyone on the team and your CI pipeline use the same build. Commit the lock file; do not commit .terraform/.

Run init again whenever you add a provider, add a module or change the backend.

terraform plan: preview before you touch anything

bash
terraform plan

The plan compares your configuration with the state (empty right now) and the real infrastructure, then prints what it would do:

text
Terraform will perform the following actions:

  # aws_s3_bucket.notes will be created
  + resource "aws_s3_bucket" "notes" {
      + bucket        = (known after apply)
      + bucket_prefix = "field-notes-"
      ...
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Learn the symbols now, because you will read plans every day:

  • + create a new resource
  • ~ update a resource in place
  • - destroy a resource
  • -/+ destroy and re-create it (a replacement, which can mean downtime or data loss)

Planning is read-only. It is always safe to run, and reading the plan before every apply is the single most important Terraform habit.

terraform apply: make it real

bash
terraform apply

apply shows the plan again and waits for you to type yes. Terraform then calls the AWS API, creates the bucket and writes the result to terraform.tfstate in your folder.

Check it worked:

bash
terraform state list
aws s3 ls | grep field-notes

Change something safely

Infrastructure changes over time. Add versioning, which is configured as its own resource in AWS provider v4 and later:

hcl
resource "aws_s3_bucket_versioning" "notes" {
  bucket = aws_s3_bucket.notes.id

  versioning_configuration {
    status = "Enabled"
  }
}

aws_s3_bucket.notes.id is a reference. It tells Terraform that versioning depends on the bucket, so Terraform creates them in the right order and passes the real bucket name across. You never hard-code IDs that Terraform already knows.

Run terraform plan and you will see 1 to add. Apply it, then run plan once more: Terraform reports No changes. Your configuration and the real world now match, which is exactly the state you want to be in.

Format and validate as you go

Two commands catch most mistakes before a plan does:

bash
terraform fmt        # rewrites files in the standard style
terraform validate   # checks syntax and references without calling AWS

Run both before every commit. Part 5 runs them automatically in CI.

Clean up with terraform destroy

bash
terraform destroy

destroy plans the removal of everything in the state, shows it with - symbols and asks for confirmation. AWS refuses to delete a bucket that still contains objects, so empty it first if you uploaded anything.

What to commit to Git

Add a .gitignore before your first commit:

text
.terraform/
*.tfstate
*.tfstate.*
crash.log
*.tfvars

State files can contain secrets and must never be committed. Right now your state lives on your laptop, which is fine for learning and a problem for a team. Part 3 moves it to S3.

Key takeaways

  • Terraform describes infrastructure in .tf files; providers translate that into API calls and state records what exists.
  • The daily loop is init once, then plan and apply for every change; read every plan before you apply it.
  • Pin the Terraform and provider versions and commit .terraform.lock.hcl.
  • Use references like aws_s3_bucket.notes.id instead of hard-coded IDs, so Terraform knows the dependency order.
  • Never commit state files or credentials.

Next in the series: part 2 makes this configuration reusable with variables, outputs and locals.