Skip to main content

Module 3, Providers and Resources

Providers

A provider is a plugin that lets Terraform communicate with an API. Without a provider, Terraform cannot create anything.

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # any 5.x version
}
github = {
source = "integrations/github"
version = "~> 6.0"
}
}
}

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

Version Constraints

ConstraintMeaning
= 5.0.0Exactly 5.0.0
!= 5.0.0Anything except 5.0.0
> 5.0Greater than 5.0
>= 5.0, < 6.0Between 5.0 and 6.0
~> 5.05.x only (pessimistic constraint)
warning

Always pin to a major version with ~>. Never use no constraint, a provider update can break your infrastructure silently.

Multiple Provider Configurations

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

# Named provider for a second region
provider "aws" {
alias = "stockholm"
region = "eu-north-1"
}

# Use the named provider on a specific resource
resource "aws_s3_bucket" "eu_bucket" {
provider = aws.stockholm
bucket = "finpay-eu-north-1-backups"
}

Resources

resource "<PROVIDER>_<TYPE>" "<LOCAL_NAME>" {
# arguments
}

Resource References and Dependencies

Resources can reference each other. Terraform uses references to determine creation order:

resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id # Terraform creates VPC first
cidr_block = "10.0.1.0/24"
}

Explicit Dependencies

When Terraform cannot detect a dependency automatically:

resource "aws_s3_bucket_policy" "allow_access" {
bucket = aws_s3_bucket.example.id
policy = data.aws_iam_policy_document.allow_access.json

depends_on = [
aws_s3_bucket_public_access_block.example
]
}

Meta-Arguments

count, multiple copies

resource "aws_instance" "server" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = { Name = "server-${count.index}" }
}

for_each, create from a map or set

resource "aws_iam_user" "team" {
for_each = toset(["alice", "bob", "charlie"])
name = each.value
}
tip

Prefer for_each over count for most cases. count is index-based, removing an item from the middle triggers unexpected destroys. for_each is key-based and only affects the removed item.

lifecycle, control create/destroy behaviour

resource "aws_db_instance" "main" {
lifecycle {
prevent_destroy = true # block terraform destroy
create_before_destroy = true # zero-downtime replacement
ignore_changes = [password] # ignore console changes
}
}

Lab 2, Multi-Resource Configuration

resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = { Name = "lab-vpc" }
}

resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
tags = { Name = "lab-public-subnet" }
}

resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = { Name = "lab-igw" }
}

Run terraform plan and observe the dependency graph. Visualise it:

terraform graph | dot -Tsvg > graph.svg

Knowledge Check

  1. What command downloads providers after writing the required_providers block?
  2. What does ~> 5.0 mean as a version constraint?
  3. What is the difference between count and for_each?
  4. When would you use depends_on explicitly?
  5. What does prevent_destroy = true do?
  6. If resource B references aws_vpc.main.id, which resource does Terraform create first?
Exam Mapping
  • HashiCorp Associate Obj 3b: Describe plugin-based architecture
  • HashiCorp Associate Obj 5: Use Terraform outside of core workflow
  • HashiCorp Associate Obj 8: Read, generate, and modify configuration