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
.tffiles: 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:
# macOS (Homebrew)
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
# Windows (winget)
winget install Hashicorp.Terraform
# Check it works
terraform -versionOn 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:
aws configure sso
aws sso login --profile learn
export AWS_PROFILE=learnOn 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:
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
terraformblock pins the CLI version and the provider.~> 6.0means "any 6.x release, but not 7.0", so a future major version with breaking changes cannot sneak in. - The
providerblock configures the AWS provider. Change the region to one near you. - The
resourceblock declares one S3 bucket.aws_s3_bucketis the resource type andnotesis the local name you use to refer to it elsewhere. Bucket names are global across all AWS accounts, sobucket_prefixlets AWS add a unique suffix.
terraform init: download what you need
terraform initinit 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
terraform planThe plan compares your configuration with the state (empty right now) and the real infrastructure, then prints what it would do:
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
terraform applyapply 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:
terraform state list
aws s3 ls | grep field-notesChange something safely
Infrastructure changes over time. Add versioning, which is configured as its own resource in AWS provider v4 and later:
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:
terraform fmt # rewrites files in the standard style
terraform validate # checks syntax and references without calling AWSRun both before every commit. Part 5 runs them automatically in CI.
Clean up with terraform destroy
terraform destroydestroy 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:
.terraform/
*.tfstate
*.tfstate.*
crash.log
*.tfvarsState 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
.tffiles; providers translate that into API calls and state records what exists. - The daily loop is
initonce, thenplanandapplyfor 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.idinstead 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.

Ansible for Beginners: Automate an Ubuntu Server with Your First Playbook4 min read
Terraform and Ansible Together: Provision with One, Configure with the Other4 min read
Vercel Fluid Compute Explained: Concurrency, Active CPU Pricing and waitUntil4 min read