Provider #
Providers are the components that let Terraform talk to the outside world. Without a provider, Terraform only knows how to read HCL files — it has no idea how to create an EC2 instance, create a DNS record, or configure a Kubernetes cluster. Providers translate the instructions in your .tf files into API calls to the actual services. Understanding how providers work behind the scenes, how to configure them correctly, and multi-provider usage patterns will help you build robust, maintainable Terraform configurations.
What Is a Provider #
A provider is a binary plugin that extends Terraform’s capabilities to interact with a specific external service. Each provider provides a set of resource types and data sources you can use in your configuration. When you write resource "aws_instance" "web", the aws prefix refers to the AWS provider — that provider knows how to call the AWS EC2 API to create, modify, and delete instances.
flowchart TD
A["Configuration File (.tf)"] --> B["Terraform Core"]
B --> C["Provider Plugin\n(AWS / GCP / Azure / Cloudflare)"]
C --> D["External Service API"]
D --> E["Resources created /\nmodified / deleted"]
F["terraform init"] -->|"Download & install"| C
G[".terraform.lock.hcl"] -->|"Pin version"| C
style A fill:#e3f2fd,stroke:#1565c0
style B fill:#f3e5f5,stroke:#7b1fa2
style C fill:#e8f5e9,stroke:#2e7d32
style D fill:#fff3e0,stroke:#e65100
style E fill:#fff3e0,stroke:#e65100Providers aren’t just for clouds. There are providers for DNS (Cloudflare, Route53), monitoring (Datadog, Grafana), SaaS (GitHub, PagerDuty), databases (PostgreSQL, MySQL), and even local utilities (null, local, random). Any service with an API can have a Terraform provider.
Configuring Providers #
Every provider needs to be declared in the required_providers block and configured in the provider block. The declaration tells Terraform which providers are needed and where to download them from. The configuration provides the credentials and parameters the provider needs to connect to the service.
# Step 1: Declare the required providers in the terraform block
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
}
# Step 2: Configure the providers
# Credentials come from environment variables, not hardcoding
provider "aws" {
region = "ap-southeast-1"
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
# Step 3: Download the providers (only needed once or when the version changes)
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding cloudflare/cloudflare versions matching "~> 4.0"...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.31.0...
- Installing cloudflare/cloudflare v4.22.0...
Terraform has been successfully initialized!
Never hardcode credentials (access keys, secrets, tokens) directly inside theproviderblock. Terraform configuration files go into version control — hardcoded credentials will leak into the repository. Use environment variables,.tfvarsfiles that are gitignored, or a secrets manager.
Declaration and Configuration Order #
Terraform doesn’t care about the order of blocks inside .tf files. You can write provider before terraform {} or after it — the result is the same. What matters is that both exist and are syntactically correct.
# Any order is valid — write in whatever order is most readable
# Option 1: terraform block first (more common)
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
provider "aws" {
region = "ap-southeast-1"
}
# Option 2: provider block first (also valid)
provider "aws" {
region = "ap-southeast-1"
}
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
Provider Versions and Why They Matter #
Pinning provider versions is a mandatory practice, not optional. Providers can release breaking changes that break a working configuration. Without a version constraint, terraform init will download the latest version — which might not be compatible with your configuration.
Version Constraint Operators #
HCL provides several operators for version constraints:
| Operator | Meaning | Example | Fit |
|---|---|---|---|
= 5.31.0 | Exactly this version | version = "= 5.31.0" | Rarely used, too strict |
>= 5.0 | Greater than or equal | version = ">= 5.0" | Too loose, unsafe |
~> 5.31 | >= 5.31.0, < 6.0.0 | version = "~> 5.31" | Most common and recommended |
>= 5.0, < 6.0 | Explicit range | version = ">= 5.0, < 6.0" | Same as ~> 5.0 |
The ~> (pessimistic constraint) operator is the most commonly used. It allows patch updates but prevents potentially breaking major version upgrades.
# ANTI-PATTERN: Not pinning a version
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
# No version constraint
# → terraform init downloads the latest version
# → Provider v6 could break changes from v5
# → A working configuration could break
}
}
}
# ANTI-PATTERN: Pinning too strictly
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 5.31.0" # Only this exact version
# → No patch updates
# → Security fixes in 5.31.1 don't come in
# → Other teams must pin the exact same version
}
}
}
# CORRECT: Pin to a safe version range
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.31"
# → >= 5.31.0 and < 6.0.0
# → Patch updates (5.31.1, 5.32.0) accepted
# → Major updates (6.0.0) rejected
}
}
}
The Lock File #
After terraform init, Terraform creates a .terraform.lock.hcl file recording the installed provider versions along with their hashes. This file should be committed to version control.
# .terraform.lock.hcl — commit this file!
provider "registry.terraform.io/hashicorp/aws" {
version = "5.31.0"
constraints = "~> 5.31"
hashes = [
"h1:abcdef1234567890...",
]
}
provider "registry.terraform.io/cloudflare/cloudflare" {
version = "4.22.0"
constraints = "~> 4.0"
hashes = [
"h1:fedcba0987654321...",
]
}
The lock file guarantees that all team members use identical provider versions, even if a new release appears between when developer A and developer B run terraform init.
# Update providers to the latest version within the constraint
$ terraform init -upgrade
# Update only a specific provider
$ terraform init -upgrade=hashicorp/aws
flowchart TD
A["terraform init"] --> B{"Is there a\n.terraform.lock.hcl?"}
B -->|"Yes"| C["Use the version in the lock file"]
B -->|"No"| D["Download the latest version\n(within the constraint)"]
C --> E["Lock file committed\nto version control"]
D --> E
E --> F["Everyone uses the\nsame provider versions"]
style C fill:#e8f5e9,stroke:#2e7d32
style D fill:#e3f2fd,stroke:#1565c0
style F fill:#e8f5e9,stroke:#2e7d32Multi-Provider #
Terraform can use more than one provider at once. This is very common for managing resources across multiple regions, multiple clouds, or multiple different services from a single configuration.
Multi-Region with Aliases #
When you need to use the same provider with different configurations (like AWS in different regions), use alias.
# Default provider — no alias
provider "aws" {
region = "ap-southeast-1" # Singapore
}
# Aliased provider — for another region
provider "aws" {
alias = "us_east"
region = "us-east-1" # Virginia
}
provider "aws" {
alias = "eu_west"
region = "eu-west-1" # Ireland
profile = "production" # Can use a different profile
}
# Resources use the default provider (no provider argument)
resource "aws_s3_bucket" "asia" {
bucket = "app-assets-asia"
}
# Resources use an aliased provider (explicit)
resource "aws_s3_bucket" "us" {
provider = aws.us_east
bucket = "app-assets-us"
}
resource "aws_s3_bucket" "eu" {
provider = aws.eu_west
bucket = "app-assets-eu"
}
Multi-Cloud #
Terraform can manage resources from several cloud providers at once. This is useful for multi-cloud architectures or when you use services from different vendors.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
}
# Each provider is configured separately
provider "aws" {
region = "ap-southeast-1"
}
provider "google" {
project = var.gcp_project
region = "asia-southeast1"
}
provider "cloudflare" {
api_token = var.cloudflare_token
}
# Resources from various providers in a single configuration
resource "aws_instance" "app" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.medium"
}
resource "google_dns_managed_zone" "main" {
name = "example-zone"
dns_name = "example.com."
}
resource "cloudflare_record" "app" {
zone_id = var.cloudflare_zone_id
name = "app"
value = aws_instance.app.public_ip
type = "A"
proxied = true
}
default_tags and Additional Provider Configuration
#
The AWS provider supports default_tags, which automatically adds tags to all resources it manages. This is very useful for cost allocation and resource tracking.
provider "aws" {
region = "ap-southeast-1"
default_tags {
tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "terraform"
Team = "platform-engineering"
}
}
}
# All resources below automatically get the tags above
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
tags = {
Name = "web-server"
# The tags above are merged with default_tags
# Result: Name, Environment, Project, ManagedBy, Team
}
}
Finding and Choosing Providers #
All providers are available on the Terraform Registry at registry.terraform.io. The source address always follows the namespace/name pattern.
| Namespace | Type | Example |
|---|---|---|
hashicorp/ | Official HashiCorp providers | hashicorp/aws, hashicorp/google |
integrations/ | Official vendor providers | integrations/github, integrations/gitlab |
| Direct vendor | Providers from the vendor | cloudflare/cloudflare, datadog/datadog |
| Community | Providers from the community | cyrilgdn/postgresql, aidan-melen/eksa |
terraform {
required_providers {
# Official HashiCorp providers
aws = { source = "hashicorp/aws", version = "~> 5.0" }
azurerm = { source = "hashicorp/azurerm", version = "~> 3.0" }
google = { source = "hashicorp/google", version = "~> 5.0" }
kubernetes = { source = "hashicorp/kubernetes", version = "~> 2.0" }
# Providers from vendors
cloudflare = { source = "cloudflare/cloudflare", version = "~> 4.0" }
datadog = { source = "datadog/datadog", version = "~> 3.0" }
github = { source = "integrations/github", version = "~> 6.0" }
# Utility providers (built-in, no download needed)
null = { source = "hashicorp/null" }
local = { source = "hashicorp/local" }
random = { source = "hashicorp/random" }
}
}
Utility Providers #
Some providers don’t connect Terraform to an external service — they provide local utilities that are useful in configurations.
# null_resource — for running provisioners or triggers
resource "null_resource" "setup" {
triggers = {
always_run = timestamp() # Always runs
}
provisioner "local-exec" {
command = "echo 'Setup complete'"
}
}
# random_string — for generating random values
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
resource "aws_s3_bucket" "logs" {
bucket = "app-logs-${random_string.suffix.result}"
# Result: app-logs-a1b2c3d4
}
# local_file — for creating local files
resource "local_file" "config" {
content = jsonencode({ endpoint = aws_instance.web.public_ip })
filename = "${path.module}/generated/config.json"
}
Authentication Patterns #
Providers need credentials to connect to their services. There are several authentication patterns you can use, each with its own strengths and trade-offs.
Environment Variables (Most Common) #
# Set environment variables before terraform plan/apply
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_DEFAULT_REGION="ap-southeast-1"
# For other providers
export CLOUDFLARE_API_TOKEN="your-api-token"
export DATADOG_API_KEY="your-api-key"
export DATADOG_APP_KEY="your-app-key"
AWS Profile #
# Using an AWS profile from ~/.aws/credentials
provider "aws" {
region = "ap-southeast-1"
profile = "production" # Profile name
}
Assume Role (AWS) #
# Assume a role in another account
provider "aws" {
region = "ap-southeast-1"
assume_role {
role_arn = "arn:aws:iam::123456789012:role/TerraformRole"
session_name = "terraform-session"
}
}
OIDC Token (Modern CI/CD) #
# GitHub Actions OIDC — no need to store AWS keys
provider "aws" {
region = "ap-southeast-1"
assume_role_with_web_identity {
role_arn = var.github_actions_role_arn
web_identity_token_file = "/tmp/oidc-token"
}
}
flowchart TD
A["Choose Authentication"] --> B{"Where is it running?"}
B -->|"Local (development)"| C["AWS Profile\n~/.aws/credentials"]
B -->|"CI/CD Pipeline"| D{"Cloud Provider CI?"}
D -->|"GitHub Actions"| E["OIDC Token\n(no stored credentials)"]
D -->|"GitLab CI"| F["CI/CD Variables\n(environment variables)"]
D -->|"Jenkins / Self-hosted"| G["Secrets Manager\n(Vault, AWS SM)"]
B -->|"Production Server"| H["IAM Role\n(instance profile)"]
style C fill:#e3f2fd,stroke:#1565c0
style E fill:#e8f5e9,stroke:#2e7d32
style F fill:#e3f2fd,stroke:#1565c0
style G fill:#fff3e0,stroke:#e65100
style H fill:#e8f5e9,stroke:#2e7d32Provider Troubleshooting #
Provider problems are among the most common issues in Terraform. Here are some common problems and their solutions.
Provider Not Found #
# Error: provider registry.terraform.io/hashicorp/xyz not found
# Cause: typo in the source address or the provider doesn't exist in the registry
# Solution: check the provider name at registry.terraform.io
Version Conflict #
# Error: provider constraints conflict
# Module A needs aws ~> 5.0, module B needs aws ~> 4.0
# Solution: update module B to be compatible with v5
# or use a single version constraint in the root module
Authentication Failed #
# Error: InvalidClientTokenId: The security token included is invalid
# Cause: wrong or expired credentials
# Solution: check the credentials
$ aws sts get-caller-identity # For AWS
$ terraform providers mirror ./mirror # Cache providers locally
Summary #
- A provider is a plugin that connects Terraform to an external service API — without providers, Terraform can’t create any resources.
- Providers are installed automatically during
terraform init— you only need to declare them in therequired_providersblock withsourceandversion.- Always pin provider versions with
version = "~> X.Y"to prevent unexpected breaking changes from major version upgrades.- Commit
.terraform.lock.hclto version control so all team members use identical provider versions.- Multi-provider with aliases lets you manage resources across different regions, accounts, or clouds from a single Terraform configuration.
- Don’t hardcode credentials — use environment variables, AWS profiles, assume role, or OIDC tokens depending on the environment.
default_tagsin the AWS provider automatically adds tags to all resources — very useful for cost allocation.