Kahibaro
Discord Login Register

Providers and resources

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:

In code, you typically:

Example of a basic provider block:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
provider "aws" {
  region = "us-east-1"
}

Here:

Provider Configuration: The `terraform` Block

The terraform block is where you tell Terraform:

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:

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:

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:

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:

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:

General syntax:

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

Where:

Example:

resource "aws_instance" "web" {
  ami           = "ami-12345678"
  instance_type = "t3.micro"
}

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_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:

`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:

Use this when:

`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:

Advantages over count:

`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]
  }
}

Other options include:

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"
}

Remember:

Provider Credentials and Security Basics

Provider configuration often needs sensitive values (keys, secrets). Common approaches:

Avoid:

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:

To manage this:

If a provider change affects a resource:

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

Mastering how providers and resources interact is essential to using Terraform effectively in real-world DevOps and cloud environments.

Views: 25

Comments

Please login to add a comment.

Don't have an account? Register now!