Skip to content

Intermediate · Infrastructure as code with Terraform · topic 31 of 53 · 35 min

Terraform basics: HCL, providers, resources and the workflow

Explanation

Terraform reads .tf files written in HCL and talks to cloud APIs through plugins called providers. You declare resources (things to create), data sources (things to look up), variables (inputs), outputs (results) and locals (named values). The workflow is init, fmt, validate, plan and apply. Always read the plan: + means create, ~ means update in place, -/+ means replace, and - means destroy.

Files and HCL syntax

Terraform reads every file ending in .tf in the current folder. Together they are one configuration. The language is HCL (HashiCorp Configuration Language). It has three building pieces.

  • Blocks: a type, zero or more labels, and a body in curly braces, like resource "aws_s3_bucket" "logs" { ... }.
  • Arguments: name = value lines inside a block, like bucket = "college-logs".
  • Expressions: the values, such as strings, numbers, lists, maps, references to other things and function calls.
output
terraform {
  required_version = ">= 1.6"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"   # any 6.x, never 7.0; check the registry for the current major version
    }
  }
}

provider "aws" {
  region = "ap-south-1"   # Mumbai
}
Terraform (HCL): the terraform block pins versions; the provider block configures the AWS plugin.

Providers

A provider is a plugin that knows how to talk to one API: AWS, Azure, Google Cloud, Kubernetes, GitHub, Cloudflare and many more. required_providers says which providers you need and which versions are allowed. The constraint "~> 6.0" means "6.0 or newer, but below 7.0", so a breaking major release is not picked up by surprise. terraform init downloads the providers.

Resources and references

A resource block declares one thing Terraform should create and manage. The first label is the resource type (from the provider), the second is your local name. Together they form the address, such as aws_s3_bucket.logs. You refer to an attribute of a resource with address.attribute, for example aws_s3_bucket.logs.id. Terraform uses these references to work out the order: a resource that refers to another is created after it.

output
resource "aws_s3_bucket" "logs" {
  bucket = "${var.project}-logs-${var.environment}"
  tags   = local.common_tags
}

resource "aws_s3_bucket_versioning" "logs" {
  bucket = aws_s3_bucket.logs.id   # reference: created after the bucket
  versioning_configuration {
    status = "Enabled"
  }
}
Terraform (HCL): "${...}" inside a string inserts a value (interpolation).

Data sources

A data block reads something that already exists, without managing it. For example, look up the newest Ubuntu image ID or an existing VPC created by another team. You refer to it as data.aws_ami.ubuntu.id.

Variables, outputs and locals

Input variables are the parameters of your configuration. Each has a type, an optional default, an optional description and optional validation rules. Mark secrets with sensitive = true so Terraform hides them in its output. Outputs print useful results after apply (and let other code read them). Locals are named values you compute once inside the configuration to avoid repeating yourself.

output
variable "environment" {
  type        = string
  description = "dev, staging or prod"
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "environment must be dev, staging or prod."
  }
}

variable "db_password" {
  type      = string
  sensitive = true   # hidden in plan output, but still stored in state
}

locals {
  common_tags = { project = var.project, environment = var.environment, owner = "platform-team" }
}

output "logs_bucket_name" {
  value = aws_s3_bucket.logs.bucket
}
Terraform (HCL): set variables in a terraform.tfvars file, with -var on the command line, or with TF_VAR_ environment variables.

Many copies: count and for_each

count = 3 makes three copies addressed as aws_instance.web[0], [1] and [2]. for_each takes a map or a set of strings and makes one copy per key, addressed by key, like aws_s3_bucket.site["dev"]. Prefer for_each when items have names: if you remove the middle item of a count list, every later index shifts and Terraform wants to destroy and recreate them. With for_each only the removed key is affected.

output
resource "aws_s3_bucket" "site" {
  for_each = toset(["dev", "staging", "prod"])
  bucket   = "college-site-${each.key}"
}
Terraform (HCL): each.key is "dev", "staging" or "prod" in turn.

The workflow

bash
terraform init        # download providers, set up the backend (run once, and after changes to them)
terraform fmt         # format files in the standard style
terraform validate    # check syntax and internal consistency
terraform plan        # show what would change; changes nothing
terraform apply       # show the plan again, ask "yes", then make the changes
terraform destroy     # delete everything this configuration manages (careful!)
The everyday Terraform commands, in order.

Reading a plan

SymbolMeaningCounts in the summary as
+create a new resource1 to add
~update in place (the resource stays)1 to change
-/+destroy, then create a replacement (some arguments cannot change in place)1 to add and 1 to destroy
+/-create the replacement first, then destroy the old one (when create_before_destroy is set)1 to add and 1 to destroy
-destroy1 to destroy
<=read a data sourcenot counted

The plan ends with a line such as "Plan: 2 to add, 1 to change, 1 to destroy." Look for "forces replacement" next to an argument: it means that change will delete and recreate the resource. For a database, that can mean data loss.

Common mistakes

Careful: Running apply without reading the plan. Read every -/+ and - line. A renamed bucket or a changed database engine can destroy data. In CI, save the plan with terraform plan -out=tfplan and apply exactly that file.
Careful: Hard-coding values and secrets. Put changing values in variables and pass secrets from a secret manager or CI secrets. sensitive = true only hides a value on screen; it does not encrypt it in state.
Tip: Run terraform fmt before every commit. Reviewers then see only real changes, not spacing.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Terraform basics: HCL, providers, resources and the workflow · 1/8

Terraform basics

In this lesson you will write your first Terraform configuration and learn the commands that run it.

Prefer a video by a teacher? Find free videos on “terraform tutorial for beginners aws providers resources plan apply” on YouTube ↗

In real life: A startup's static website bucket

A Bengaluru food-delivery startup keeps its marketing site in an S3 bucket. The configuration has a variable for the environment, one bucket per environment with for_each, and outputs for the bucket names. A developer changes a tag and runs plan: it shows "~ update in place" for three buckets and "Plan: 0 to add, 3 to change, 0 to destroy." Only then does she apply.

output
  # aws_s3_bucket.site["dev"] will be updated in-place
  ~ resource "aws_s3_bucket" "site" {
      ~ tags = {
          ~ "team" = "web" -> "growth"
        }
    }

Plan: 0 to add, 3 to change, 0 to destroy.
Part of a terraform plan output: ~ means update in place.

Practice (0/4 done)

Task 1 · Question

Which command shows what Terraform would change, without changing anything?

Task 2 · Code · Python

Write summarise_plan(actions). actions is a list of strings, each one of "create", "update", "replace", "delete" or "no-op". Return the Terraform summary line "Plan: X to add, Y to change, Z to destroy." A replace counts as 1 to add AND 1 to destroy. no-op counts as nothing.

Task 3 · Question

A plan line says: -/+ resource "aws_db_instance" "main" ... # forces replacement. What will apply do?

Task 4 · Practice

Write a variable block "instance_count" (a number, default 2) with a validation rule that allows only 1 to 5, and an output "bucket_arn" that shows the arn of aws_s3_bucket.logs. If you have an AWS free-tier account, run init, validate and plan (not apply) on it.

Quick quiz

Question 1

1.What does the version constraint "~> 6.0" allow?

Question 2

2.How do you refer to the id of resource "aws_s3_bucket" "logs"?

Question 3

3.What is a data block used for?

Question 4

4.Why is for_each often safer than count?

Question 5

5.A variable has sensitive = true. Which is true?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does the version constraint "~> 6.0" allow?

Answer: 6.0 or newer, but below 7.0

The pessimistic constraint ~> 6.0 allows 6.x versions but not the next major version.

How do you refer to the id of resource "aws_s3_bucket" "logs"?

Answer: aws_s3_bucket.logs.id

A resource address is TYPE.NAME, and you add the attribute after another dot.

What is a data block used for?

Answer: Reading an existing object without managing it

Data sources look things up, such as an AMI ID or a VPC made elsewhere.

Why is for_each often safer than count?

Answer: Removing one item does not shift the addresses of the others

count uses numbered indexes that shift; for_each uses stable keys.

A variable has sensitive = true. Which is true?

Answer: It is hidden in plan and apply output but can still appear in state

sensitive only redacts the value on screen. State can still hold it in plain text.

Which command checks the syntax and references of your .tf files without contacting the cloud to plan changes?Related

Answer: terraform validate

validate checks the configuration itself. plan is the step that compares it with real infrastructure.

From Terraform for the cloud

What should happen to .terraform.lock.hcl?Related

Answer: Commit it to Git

The lock file pins exact provider versions and checksums for everyone, so it belongs in Git.

From Modules, environments and Terraform in CI

What does terraform state rm aws_s3_bucket.legacy do to the real bucket?Related

Answer: Nothing: Terraform just stops tracking it

state rm only removes the record. The real resource keeps existing.

From Terraform state, backends and drift

Your company has 200 resources that were created in the console. What is the usual way to bring them under Terraform?Related

Answer: Import them (import blocks or terraform import), then plan until it shows no changes

Importing adds existing resources to the state; a clean plan proves the code matches them.

From IaC in a team: reviews, pipelines and policy

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Infrastructure as code with Terraform” before its exam: Terraform state, backends and drift, Modules, environments and Terraform in CI.
  2. Module 7 exam: Infrastructure as code with Terraform · 13 questions · 20 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Configuration management with Ansible, and YAML. 53 topics left in the course before the final exam.

← Previous: Why infrastructure as code