terraform · level 1

Providers & Resources

The two building blocks: providers + the resource graph.

100 XP

Providers & Resources

Terraform is a tool for declaring infrastructure in code and reconciling that declaration with what actually exists. Two concepts sit at the heart of it: providers (the plugins that talk to clouds) and resources (the things providers create).

A minimal config

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

resource "aws_s3_bucket" "logs" {
  bucket = "my-app-logs"
}

Three blocks:

  1. terraform — meta-config: which providers, which versions, which backend.
  2. provider — configure a provider instance (credentials, region, defaults).
  3. resource — declare a thing the provider should create / update / destroy.

The resource address

Every resource has an address: <type>.<name>. Above, the bucket's address is aws_s3_bucket.logs. That address is how you:

  • Reference the resource elsewhere: aws_s3_bucket.logs.arn.
  • Target it in CLI commands: terraform apply -target=aws_s3_bucket.logs.
  • Look it up in state: terraform state show aws_s3_bucket.logs.

The type (aws_s3_bucket) is decided by the provider — it's the API resource being modelled. The name (logs) is yours, chosen for readability inside the config.

Providers

A provider is a plugin Terraform downloads on terraform init. It maps Terraform resources to API calls. Most-used:

Provider Source
AWS hashicorp/aws
Google Cloud hashicorp/google
Azure hashicorp/azurerm
Kubernetes hashicorp/kubernetes
GitHub integrations/github
Cloudflare cloudflare/cloudflare

The version constraint (~> 5.0 means "5.x but not 6.0") protects you from breaking changes when the provider updates. Pin tightly in production.

Resources are declarative

You don't say "create the bucket" — you describe the bucket you want. Terraform compares your declaration against the state (more on that next lesson) and decides what to do:

  • Doesn't exist → create.
  • Exists, matches → no-op.
  • Exists, differs → update (or destroy + recreate if the change can't be applied in place).
  • Declared elsewhere, removed from config → destroy.
terraform init       # download providers
terraform plan       # show what would change
terraform apply      # do it
terraform destroy    # remove everything

plan is the safety net. Always read it before you apply.

HCL basics

The config language is HCL (HashiCorp Configuration Language). It's mostly JSON-shaped with a friendlier syntax:

resource "aws_instance" "web" {
  ami           = "ami-0abc123"
  instance_type = "t3.micro"

  tags = {
    Name        = "web-server"
    Environment = "production"
  }

  vpc_security_group_ids = [
    aws_security_group.web.id,
    aws_security_group.shared.id,
  ]
}

Strings, numbers, lists, and maps. References (aws_security_group.web.id) get resolved at plan time to actual values from state.

Multi-resource references build the graph

Terraform reads every reference and builds a dependency graph. Resources without dependencies on each other create in parallel; dependent resources wait for their inputs:

resource "aws_security_group" "web" { ... }

resource "aws_instance" "web" {
  vpc_security_group_ids = [aws_security_group.web.id]   # implicit dependency
}

The instance won't be created until the security group exists, because the instance config references it. You almost never need explicit depends_on — the graph builds itself from references.

Where it goes wrong

Three first-day mistakes:

  • Editing infrastructure in the cloud console — Terraform won't know, and the next plan will try to "fix" it back. Make changes in code, not in the UI.
  • Not pinning provider versions — a terraform init next month picks up a major-version bump and your plan looks weird.
  • Skipping plan before apply — apply is destructive. plan shows you what's about to happen. Read it.