Directions
1Install Terraform (or OpenTofu)
Install Terraform from HashiCorp's repos, or OpenTofu — the open-source fork with compatible syntax — if licensing matters to you; everything below works the same. Verify with terraform version.
Start with a low-stakes, high-value target: DNS records or a storage bucket are ideal first resources. Something you can destroy and recreate without fear beats importing your production database on day one.
2Configure a provider with credentials in env vars
A provider is the plugin that talks to a platform — Cloudflare, AWS, GitHub, hundreds more. Declare it with a version constraint in main.tf, and feed it credentials through environment variables, never hardcoded in .tf files that go to git.
Run terraform init to download the provider and create the lock file (commit that — it pins exact provider builds for everyone).
terraform {
required_providers {
cloudflare = { source = "cloudflare/cloudflare", version = "~> 4.0" }
}
}
provider "cloudflare" {} # reads CLOUDFLARE_API_TOKEN from env 3Declare a resource
Describe what should exist — not the steps to create it. A DNS record is a perfect first resource: small, visible, instantly verifiable.
Each resource has a type and a local name; together they're the address Terraform uses to track it. Variables and outputs come later; one honest resource teaches the loop.
resource "cloudflare_record" "app" {
zone_id = var.zone_id
name = "app"
type = "CNAME"
content = "my-project.pages.dev"
proxied = true
} 4Plan, read the diff, then apply
terraform plan shows exactly what would change — create, update, destroy — without touching anything. Read it like a code review; the habit of actually reading plans is the discipline that makes Terraform safe.
terraform apply executes after a confirmation. Now change a value in the file and plan again: the diff shows an in-place update or a destroy-and-recreate, and learning which changes force replacement is where the real skill lives. Destructive plans say so plainly — that '-/+' marker deserves respect.
5Move state somewhere shared
Terraform records what it manages in terraform.tfstate — and that file contains resource details and often secrets, so it must not live in git or solely on a laptop. Configure a remote backend: an S3/R2 bucket with locking, or HCP Terraform's free tier.
Remote state with locking is the line between a personal experiment and a team tool: two people can now plan and apply without corrupting each other. From here, running plan in CI on pull requests is the natural next step — the pipeline shows the infra diff next to the code diff.
Common issues & fixes
Plan wants to destroy something it shouldn't.
Renaming a resource's local name looks like delete+create to Terraform. Use a moved block (or terraform state mv) to rename without touching the real thing.
Resource already exists error on apply.
It was created by hand before Terraform knew it. Import it (terraform import or an import block) so state matches reality, then re-plan.
State file conflicts between teammates.
Two people applied with local state. Adopt the remote backend with locking now — merging state files by hand is misery.
Secrets visible in state.
That's inherent — state stores attribute values. Encrypt the backend, restrict who can read it, and pass secrets via env/secret stores so they're at least not in .tf files too.
Provider version bump broke plan.
The lock file wasn't committed, so CI picked a newer provider. Commit .terraform.lock.hcl and upgrade deliberately with terraform init -upgrade.
Honesty note: articles are drafted with AI assistance, then a human
checks each one against the vendor's current setup flow before it
publishes — nothing goes live unreviewed. Vendor interfaces still
change; if a step looks different, the underlying record is what
matters.