Providers & Resources
The two building blocks: providers + the resource graph.
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:
terraform— meta-config: which providers, which versions, which backend.provider— configure a provider instance (credentials, region, defaults).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 initnext month picks up a major-version bump and your plan looks weird. - Skipping
planbeforeapply— apply is destructive.planshows you what's about to happen. Read it.