RizTech Academy logo
RizTech Academy
Infrastructure as CodeLesson 2 of 440 min

Terraform: providers, resources and state

Now the tool itself. Terraform configurations are made of a few building blocks — providers, resources, variables, outputs — and one concept that trips everyone up until it clicks: state. This lesson builds a small, real, runnable Terraform configuration (verified end to end) and explains each piece, so the workflow in the next lesson makes sense.

HCL: the language

Terraform configurations are written in HCL (HashiCorp Configuration Language) in .tf files. It is declarative — you describe what you want, in blocks. A configuration is a directory of .tf files (Terraform reads them all together); by convention you split them into main.tf, variables.tf, outputs.tf, but that is organisation, not requirement.

The examples here use the local and random providers so the whole thing runs with no cloud account — but every concept (providers, resources, variables, outputs, state) is identical to provisioning real AWS servers.

Providers: the plugins for each platform

A provider is a plugin that teaches Terraform how to manage one kind of thing — AWS, Google Cloud, a DNS service, the local filesystem. You declare which providers you need, and Terraform downloads them:

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

provider "local" {}
provider "random" {}

For real cloud work you would declare the aws provider here (with a region), and Terraform would then know how to create AWS resources. Pinning provider versions (~> 2.5) is the same reproducibility discipline as pinning a Docker base image or a dependency — so a later provider update does not change your infrastructure unexpectedly.

Resources: the things you create

A resource is one piece of infrastructure Terraform creates and manages. Each resource block has a type (what kind of thing, e.g. aws_instance, or here local_file) and a name (a label you choose to refer to it), then its configuration:

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

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

Two things to notice:

  • References between resources. local_file.greeting uses random_pet.name.id — it refers to another resource's attribute. Terraform sees this and knows the random_pet must be created first; it builds a dependency graph automatically from these references and creates things in the right order. You do not sequence steps; you reference, and Terraform orders. (This is the declarative idea again.)
  • var.greeting — this pulls in a variable (below).

For real infrastructure, a resource might be aws_instance (a server), aws_db_instance (a database), aws_security_group (a firewall rule) — but the shape is identical: type, name, configuration, references.

Variables and outputs: inputs and results

Variables are the inputs that change between environments (a region, a size, a name), keeping the config reusable rather than hard-coded:

variable "greeting" {
  description = "The greeting written into the generated file"
  type        = string
  default     = "Hello"
}

You reference it as var.greeting, and you can override it per environment (a terraform.tfvars file, a -var flag, or an environment variable) — so the same configuration provisions staging and production with different inputs.

Outputs expose values after Terraform runs — a server's IP, a database endpoint, or here the generated name and file path — for you or other tooling to read:

output "pet_name" {
  value = random_pet.name.id
}
output "file_path" {
  value = local_file.greeting.filename
}

The verified run produced pet_name = "nearby-tapir" and wrote Hello from nearby-tapir — outputs are how you get real, resulting values (like an assigned IP) back out of Terraform.

State: the thing you must understand

Here is the concept that confuses newcomers and matters most. Terraform keeps a state file — a record of what it has created and the mapping between your configuration and the real resources. When you change your config and run Terraform, it compares three things: your configuration (desired), the state (what it thinks exists), and the real infrastructure — and works out what to add, change or destroy.

Why state matters, practically:

  • It is how Terraform knows what it manages. Without state, Terraform could not tell "create a new server" from "this server already exists". The state maps local_file.greeting (verified in terraform state list) to the actual created file.
  • It is precious and sometimes sensitive. The state can contain sensitive values (it may hold secrets a resource generated), and losing it means Terraform loses track of your infrastructure. So you protect it.
  • On a team, state must be shared and locked. If two people run Terraform against local state files, they will conflict and corrupt things. Real teams store state in a remote backend (an S3 bucket with locking, or Terraform Cloud) so everyone shares one state and Terraform locks it during changes to prevent two people applying at once. This is the single most important operational fact about Terraform on a team.

For learning, local state (a terraform.tfstate file) is fine; for a team, remote state with locking is essential, and setting it up is one of the first things you do on a real project.

Check your work

HCL in .tf files, declarative; a config is a directory of .tf files (split into main/variables/outputs by convention). Examples use local/random providers so they run with no cloud — concepts identical to AWS.

Provider = a plugin for one platform (aws, google, local); declare + pin versions (reproducibility), then Terraform downloads it.

Resource = one managed piece of infrastructure: resource "TYPE" "NAME" { ... }. References between resources (random_pet.name.id) build a dependency graph so Terraform orders creation automatically — you reference, not sequence.

Variables = inputs that vary per environment (var.greeting, overridable) → reusable config; outputs = resulting values exposed after a run (IP, endpoint; verified pet_name = "nearby-tapir").

State (the key concept): a file mapping config → real resources; Terraform diffs config (desired) vs state vs reality to decide add/change/destroy. It is how Terraform knows what it manages, is precious/sensitive, and on a team must be a shared remote backend with locking (S3+lock, Terraform Cloud) so people don't conflict.

Practice

  1. Write a terraform block declaring the local and random providers with pinned versions.
  2. Write two resources where one references the other, and explain how Terraform orders their creation.
  3. Add a variable with a type and default, use it in a resource, and explain how you would override it per environment.
  4. Add outputs for two resource attributes and explain what outputs are for.
  5. Explain what the state file is, and how Terraform uses it to decide what to add/change/destroy.
  6. Explain why a team must use remote state with locking, and what goes wrong with local state files shared across people.

Official documentation

Next: plan, apply and reading a diff carefully.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship