Skip to content
shellhacks.dev
DevOps

How to Use Infrastructure as Code Basics With Terraform

Learn infrastructure as code basics with Terraform: write resources in HCL, run init, plan and apply, and understand state and variables.

6 min read
Summarize in AI
Illustration for "How to Use Infrastructure as Code Basics With Terraform": a terminal session, environment variables, a directory tree.

Servers created by clicking in a console cannot be reviewed, repeated or rebuilt. Writing them as code fixes all three, and Terraform (and its open-source fork OpenTofu, which uses the same files) is the common tool. The ideas fit in an afternoon. This post uses providers that need no cloud account, so you can try every command safely; swap in your cloud's provider when you are ready. It relates to Terraform basics and Ansible. Targets Terraform 1.10.

A first configuration

main.tfText
terraform {
  required_providers {
    local  = { source = "hashicorp/local",  version = "~> 2.5" }
    random = { source = "hashicorp/random", version = "~> 3.6" }
  }
}

variable "greeting" {
  type    = string
  default = "hello"
}

resource "random_pet" "name" {
  length = 2
}

resource "local_file" "motd" {
  filename = "${path.module}/motd.txt"
  content  = "${var.greeting} from ${random_pet.name.id}\n"
}

output "file" {
  value = local_file.motd.filename
}

A resource block names a type and a local label. References such as random_pet.name.id create dependencies, so Terraform creates the pet name before the file. The variable makes the greeting configurable and the output prints a value after apply. Fixed provider version constraints stop a new release from changing behavior unexpectedly.

The workflow

Shell
terraform fmt
terraform init
terraform validate
terraform plan
terraform apply
Text
Terraform will perform the following actions:

  # local_file.motd will be created
  + resource "local_file" "motd" {
      + content  = (known after apply)
      + filename = "./motd.txt"
    }

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

init downloads providers into .terraform/ and writes .terraform.lock.hcl, which records exact provider versions and hashes and should be committed. plan compares the code with the state and the real world and prints what will change: + create, ~ change, - destroy. Read it every time, especially lines marked "must be replaced", which mean destroy-then-create. apply asks for confirmation; in automation use terraform apply plan.out with a saved plan from plan -out=plan.out, so what ran is what was reviewed.

State

Shell
terraform state list
terraform show

The state file, terraform.tfstate, links each resource in your code to a real object and stores its attributes, including secrets in plain text for some resources. Do not commit it. For a team, use a remote backend with locking (an object store with a lock table, or Terraform's cloud service), so two people cannot apply at once and nobody works from a stale copy. Losing the state means Terraform no longer knows what it owns, and recovering it with terraform import is slow.

Change and destroy

Edit the greeting and plan again: only the file shows as changed, and the pet name stays. Pass values with -var greeting=hi or a terraform.tfvars file. terraform destroy removes everything the configuration manages, so it is a command to type only against a throwaway environment, with a plan reviewed first via terraform plan -destroy.

Where this does not apply

For one server, a cloud console, a CLI script or cloud-init (see cloud-init user data) may be less work than learning HCL. Terraform provisions infrastructure; it is not ideal for configuring the software inside servers, where configuration tools fit better. Manual changes to managed resources show up as drift in the next plan, which is useful but means the team must agree that the code is the only way to change things. Licensing changed for Terraform in 2023, and OpenTofu is the community fork that accepts the same configuration.

The model: desired state, recorded state and the plan

Terraform compares three things. The configuration says what you want. The state file records what Terraform believes it created, including identifiers and attributes that the provider returned. The real infrastructure is what exists in the cloud. A plan reads the real resources through the provider, compares them with the state and the configuration, and lists the actions needed to bring reality in line with the configuration. Apply carries out that list. This explains behaviors that confuse newcomers. A resource changed by hand in the console shows up in the next plan as drift, and Terraform will try to revert it. A resource removed from the configuration is destroyed, because the state says Terraform owns it. A resource that exists in the cloud but not in the state is invisible, and a new resource with the same name collides with it.

Always read the plan. The summary line of additions, changes and destructions is the first thing to check, and any "must be replaced" note deserves attention, since replacement means destroy and create, with downtime and data loss for some resource types.

State is the sensitive part

The state file contains resource attributes, and some of those are secrets: database passwords, access keys and private keys appear in it in plain text. Never commit state to version control, and never leave it only on one laptop. Use a remote backend with encryption, access control and locking, such as an object storage bucket with versioning plus a lock table, or a managed service, so two people running apply at once cannot corrupt it, and so there is a history to recover from. Restrict who can read the state to the people who may read the secrets in it. Mark sensitive outputs and variables as sensitive to keep them out of terminal output, and understand that this hides display and not the stored value. Losing the state is serious: Terraform no longer knows what it owns, and recovery means importing each resource by hand.

Split state by environment and by blast radius. One enormous state for everything makes every plan slow and every mistake large, and separate states for production and development stop a test from touching production.

Changes, drift and the discipline that keeps it safe

Review every plan before applying, and in a team use a pull request workflow where the plan output is attached and approved. Use terraform plan -out=file and apply that exact file, so what runs is what was reviewed and not a fresh plan that may differ. Pin the provider and Terraform versions in the configuration, and commit the lock file, since a provider upgrade can change behavior between a plan on Monday and an apply on Friday. Use prevent_destroy on resources whose loss would hurt, such as databases, and enable deletion protection in the provider where it exists. Import existing resources with terraform import or an import block, and check that the following plan shows no changes before trusting the configuration. Detect drift by running plan on a schedule and alerting on a non-empty result.

Treat destroy with the same care as production deploys. Run it only with an explicit target or in a disposable environment, and check the plan twice.

Working in a team and over time

Terraform in a team needs conventions. Keep configurations in version control with code review, use modules for patterns you repeat so changes are made once, and name resources consistently so the console and the code can be matched at a glance. Tag everything with owner and environment, which makes cost reports and cleanup possible. Run formatting and validation in CI with terraform fmt -check and terraform validate, and scan the configuration for risky settings with a policy tool. Plan in CI on every pull request and apply only from the main branch with approval. Keep credentials for the provider out of the repository, preferably short-lived and scoped to the job. Refactoring resource names or moving them between modules needs moved blocks, or Terraform will destroy and recreate them.

See the Terraform tutorials and the OpenTofu documentation.

Taggedterraform
Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.