Table of Contents
Understanding Providers and Resources in Terraform
In Terraform, providers and resources are the core building blocks of how you describe and manage infrastructure. This chapter focuses specifically on how they work, how to configure them, and how to use them safely and effectively.
What Is a Provider (In Practice)?
A provider is a plugin that lets Terraform talk to an external API (for example, AWS, Azure, GCP, Cloudflare, GitHub, etc.).
Conceptually:
- Provider = “driver” to a specific platform
- Each provider defines:
- Resource types (what you can create/manage)
- Data sources (what you can read/look up)
In code, you typically:
- Declare which providers you use
- Configure each provider (region, credentials, etc.)
Example of a basic provider block:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}Here:
terraformblock: states which provider plugin is required and which version.provider "aws": configures that provider instance (region, credentials).
Provider Configuration: The `terraform` Block
The terraform block is where you tell Terraform:
- Which providers you need
- Which versions you expect
- Optionally, required Terraform version
Example:
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
google = {
source = "hashicorp/google"
version = "4.84.0"
}
}
}Key points:
source: usually"hashicorp/<name>"from the Terraform Registry.version: use a constraint (like~> 5.0) to avoid unexpected breaking changes.
Basic Provider Blocks
A provider block configures how Terraform authenticates and interacts with that provider.
Example: AWS Provider
provider "aws" {
region = "us-east-1"
shared_credentials_files = ["~/.aws/credentials"]
profile = "dev"
}Typical provider inputs:
- Location / region (e.g.,
regionfor AWS,locationfor Azure) - Credentials:
- Explicit variables (
access_key,client_id, etc.) - Shared config files
- Environment variables
- Instance/VM roles (IAM roles, managed identities, etc.)
Best practice: prefer environment variables or platform-native identity (like IAM roles) over hardcoding secrets in .tf files.
Example: Azure Provider
provider "azurerm" {
features {}
subscription_id = var.subscription_id
tenant_id = var.tenant_id
}
The features {} block is required for the AzureRM provider, even if empty.
Multiple Providers and Aliases
You can:
- Use more than one provider type (AWS + Cloudflare, etc.)
- Use multiple instances of the same provider (e.g., multiple regions or accounts)
To have multiple configurations of the same provider, you use aliases.
Example: Multiple AWS Regions
provider "aws" {
region = "us-east-1"
}
provider "aws" {
alias = "eu"
region = "eu-west-1"
}Then, when defining a resource, specify which provider to use:
resource "aws_s3_bucket" "us_logs" {
bucket = "my-logs-us"
# uses default aws provider (us-east-1)
}
resource "aws_s3_bucket" "eu_logs" {
provider = aws.eu
bucket = "my-logs-eu"
}Notes:
- The
providerargument inside a resource picks a specific provider instance. aws.eurefers to the provider withalias = "eu".
Example: Multiple AWS Accounts
provider "aws" {
alias = "prod"
region = "us-east-1"
profile = "prod"
}
provider "aws" {
alias = "dev"
region = "us-east-1"
profile = "dev"
}
resource "aws_s3_bucket" "prod_bucket" {
provider = aws.prod
bucket = "my-prod-bucket"
}
resource "aws_s3_bucket" "dev_bucket" {
provider = aws.dev
bucket = "my-dev-bucket"
}This pattern is common when one Terraform configuration manages multiple accounts or projects.
What Is a Resource?
A resource is a single piece of infrastructure that Terraform manages via a provider.
Examples:
- AWS:
aws_instance,aws_s3_bucket - Azure:
azurerm_linux_virtual_machine - GCP:
google_compute_instance - DNS providers:
cloudflare_record - Others:
github_repository,kubernetes_deployment, etc.
General syntax:
resource "<PROVIDER>_<TYPE>" "<NAME>" {
# arguments
}Where:
<PROVIDER>_<TYPE>is the resource type, defined by the provider.<NAME>is a local name you choose, unique in your project.
Example:
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t3.micro"
}aws_instanceis the resource type.webis the local name.- Terraform tracks and manages this instance as
aws_instance.web.
Resource Arguments and Meta-Arguments
Resource arguments (like ami, instance_type, name) are specific to that resource type and defined by the provider.
Terraform also supports some meta-arguments that behave the same across all resources:
depends_oncountfor_eachprovider(to pick a specific provider instance)lifecycleblock
`depends_on`
For special cases where Terraform cannot infer dependencies automatically, you can explicitly set them:
resource "aws_iam_role" "app_role" {
# ...
}
resource "aws_iam_instance_profile" "app_profile" {
role = aws_iam_role.app_role.name
depends_on = [
aws_iam_role.app_role
]
}
Usually unnecessary, because reference aws_iam_role.app_role.name already implies a dependency, but it’s useful when:
- No explicit attribute is referenced
- You want to force ordering (e.g., with modules or null resources)
`count`
count lets you create multiple instances of the same resource using an index:
resource "aws_instance" "web" {
count = 3
ami = var.web_ami
instance_type = var.web_instance_type
tags = {
Name = "web-${count.index}"
}
}Terraform creates:
aws_instance.web[0]aws_instance.web[1]aws_instance.web[2]
Use this when:
- You want a simple numeric set of identical or nearly identical resources.
`for_each`
for_each lets you create resources from a map or set, with each key becoming an index:
variable "buckets" {
type = map(string)
default = {
logs = "my-logs-bucket"
backup = "my-backup-bucket"
}
}
resource "aws_s3_bucket" "buckets" {
for_each = var.buckets
bucket = each.value
tags = {
Name = each.key
}
}Terraform creates:
aws_s3_bucket.buckets["logs"]aws_s3_bucket.buckets["backup"]
Advantages over count:
- You can address resources by meaningful keys.
- Adding/removing items causes less disruption than renumbering indexes.
`lifecycle` Block
Controls how Terraform treats changes to a resource.
Common options:
resource "aws_s3_bucket" "data" {
bucket = "my-important-data"
lifecycle {
prevent_destroy = true
ignore_changes = [tags]
}
}prevent_destroy = true:- Terraform will refuse to destroy this resource unless you override it.
ignore_changes = [tags]:- Terraform will not modify the resource if only
tagsdiffer from the config.
Other options include:
create_before_destroy(useful when changing immutable properties)replace_triggered_by(force replacement when some condition changes)
Use lifecycle rules carefully; they can hide drift or block intended changes.
Data Sources vs Resources (Brief Distinction)
Providers also define data sources, which are similar in structure to resources but are read-only.
Example:
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
owners = ["099720109477"] # Canonical
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
}data "aws_ami" "ubuntu"looks up an existing AMI.resource "aws_instance" "web"creates a new instance using that AMI.
Remember:
- Resource: create/modify/destroy
- Data source: read/lookup only
Provider Credentials and Security Basics
Provider configuration often needs sensitive values (keys, secrets). Common approaches:
- Environment variables (recommended)
- AWS:
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_PROFILE - Azure:
ARM_CLIENT_ID,ARM_CLIENT_SECRET, etc. - Instance/VM profiles (IAM roles, managed identities)
- Separate files (e.g.,
~/.aws/credentials)
Avoid:
- Hardcoding secrets in
.tffiles - Committing
terraform.tfvarswith secrets to git
Example using environment variables (no secrets in code):
provider "aws" {
region = var.region
}Terraform picks up credentials from your environment or role automatically.
Provider and Resource Version Drift
Providers change over time; new versions may:
- Add new resource types
- Deprecate or change arguments
To manage this:
- Use
versionconstraints inrequired_providers. - Periodically run
terraform init -upgradein a controlled environment. - Test changes (e.g.,
terraform plan) before applying.
If a provider change affects a resource:
terraform planwill show what changed.- You may need to adjust arguments or resources to match the new schema.
Common Patterns with Providers and Resources
Pattern 1: One Provider, Many Resources
Basic setup for a small project:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_vpc" "main" {
# ...
}
resource "aws_subnet" "public" {
# ...
}
resource "aws_instance" "web" {
# ...
}Pattern 2: Multi-Cloud or Multi-Service
One configuration manages several platforms:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
resource "aws_s3_bucket" "website" {
bucket = "my-static-site"
}
resource "cloudflare_record" "www" {
zone_id = var.cloudflare_zone_id
name = "www"
type = "CNAME"
value = aws_s3_bucket.website.bucket_regional_domain_name
}Here Terraform manages both S3 and DNS records via two providers.
Pattern 3: Provider Aliases Inside Modules (Conceptual)
Modules often accept a provider configuration from the root module so they can work in a specific account, region, or project. At the module call site you might see:
module "network" {
source = "./modules/network"
providers = {
aws = aws.eu
}
vpc_cidr = "10.0.0.0/16"
}
Inside the module, resources just use provider "aws" { ... } configuration passed from the caller. You’ll see this more as you begin to structure larger Terraform projects.
Recap
- Providers:
- Connect Terraform to external platforms/APIs.
- Are configured via
terraformandproviderblocks. - Can have multiple instances using
alias. - Resources:
- Represent actual infrastructure objects to be created/managed.
- Use provider-defined arguments and common meta-arguments (
count,for_each,depends_on,lifecycle). - Data sources:
- Read information from providers without creating objects.
Mastering how providers and resources interact is essential to using Terraform effectively in real-world DevOps and cloud environments.