Providers & Configuration
Terraform on its own knows nothing about AWS, Azure, GCP, Kubernetes, or any other platform. What makes Terraform useful is its provider ecosystem — a collection of plugins that translate your HCL declarations into real API calls against specific platforms. Understanding how providers work, how to configure them safely, and how to lock their versions is the foundation of every real Terraform project.
What Is a Provider?
A provider is a separate binary — a plugin — that Terraform downloads and executes alongside the core binary. Each provider implements a set of resource types and data sources for a specific platform or service. When you declare an aws_s3_bucket resource, it is the AWS provider that knows how to call the S3 API to create, read, update, or delete that bucket.
Providers are distributed through the Terraform Registry and come in three tiers:
| Tier | Maintained by | Examples |
|---|---|---|
| Official | HashiCorp | hashicorp/aws, hashicorp/google, hashicorp/azurerm, hashicorp/kubernetes |
| Partner | Technology partners | datadog/datadog, cloudflare/cloudflare, mongodb/mongodbatlas |
| Community | Community contributors | Thousands of providers on the registry |
The terraform binary you install is the core engine — it handles the DAG, state, and workflow. Provider binaries are downloaded separately by terraform init and cached locally. This separation allows providers to be versioned and updated independently of Terraform itself.
The Provider Registry
The Terraform Registry at registry.terraform.io is the default source for providers. Each provider page documents every resource type and data source it supports, including all arguments, attributes, and examples.
When you declare source = "hashicorp/aws", Terraform expands this to the full address registry.terraform.io/hashicorp/aws. The format is always <REGISTRY>/<NAMESPACE>/<TYPE>, with registry.terraform.io as the default registry.
You can also host a private registry (Terraform Enterprise, HCP Terraform) for internal providers, or reference providers from GitHub directly for development.
Declaring Providers
Providers are declared in the terraform block under required_providers. This is separate from configuring the provider:
# Declare what providers this configuration needs
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
}
# Configure the AWS provider
provider "aws" {
region = "us-east-1"
}
# Configure the Cloudflare provider
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
The required_providers block tells Terraform where to find the provider and what version to accept. The provider block configures how to connect — credentials, region, endpoint overrides, and so on.
providers.tf
By convention, keep the terraform {} block and all provider {} blocks in a file called providers.tf (or versions.tf for just the terraform block). This makes it easy to find provider configuration and version constraints without searching through main.tf.
Version Constraints
Provider APIs evolve — new resources are added, arguments change, breaking changes occasionally happen between major versions. Version constraints let you express which provider versions your configuration is compatible with:
| Constraint | Meaning | Example |
|---|---|---|
= 5.31.0 |
Exact version only | Pinned to one release |
!= 5.31.0 |
Exclude this version | Skip a known-buggy release |
>= 5.0 |
5.0 or any newer version | No upper bound |
~> 5.0 |
5.x only (patch and minor) | Allows 5.31, blocks 6.0 |
~> 5.31.0 |
5.31.x only (patch only) | Allows 5.31.1, blocks 5.32 |
>= 5.0, < 6.0 |
5.x range explicitly | Equivalent to ~> 5.0 |
The pessimistic constraint operator (~>) is the most common in practice. ~> 5.0 means "any 5.x version" — you get bug fixes and new features automatically within the major version, but a breaking major-version bump requires you to explicitly update the constraint.
Using version = ">= 5.0" with no upper bound means a future major release (e.g., v6.0 with breaking changes) could be selected on your next terraform init -upgrade. Use ~> 5.0 or commit your .terraform.lock.hcl to pin exact versions across environments.
Authentication
Every provider that connects to a real API needs credentials. How you supply them depends on the provider, but there are three universally recommended patterns — in order of preference:
1. Environment variables (recommended for CI/CD)
Most providers read credentials from standard environment variables. Never hard-code credentials in .tf files.
# AWS
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_DEFAULT_REGION="us-east-1"
# GCP
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
# Azure
export ARM_CLIENT_ID="..."
export ARM_CLIENT_SECRET="..."
export ARM_TENANT_ID="..."
export ARM_SUBSCRIPTION_ID="..."
With environment variables set, the provider configuration block can be empty — the provider picks up credentials automatically:
provider "aws" {
region = "us-east-1"
# No credentials here — read from environment
}
2. Shared credentials / config files (recommended for local dev)
AWS CLI stores credentials in ~/.aws/credentials and ~/.aws/config. Terraform's AWS provider reads these files automatically, respecting the active profile:
provider "aws" {
region = "us-east-1"
profile = "my-aws-profile" # optional, defaults to "default"
}
3. IAM roles / workload identity (recommended for cloud-native)
When Terraform runs on EC2, ECS, GitHub Actions, or GCP — attach an IAM role or service account. No static credentials needed. The provider detects the execution environment and uses its attached identity:
# On an EC2 instance with an IAM role attached:
provider "aws" {
region = "us-east-1"
# No credentials block — uses instance profile automatically
}
Hard-coding access_key = "AKIA..." in a provider block is a security vulnerability — especially if the file is committed to Git. Even in private repos, credentials in source code are a significant risk. Use environment variables, credential files, or IAM roles instead.
Common Providers
Here is how to configure the three major cloud providers and a few commonly used utilities:
AWS
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
ManagedBy = "terraform"
Environment = var.environment
}
}
}
The default_tags block applies tags to every resource managed by this provider — useful for cost allocation and resource tracking.
Google Cloud
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}
provider "google" {
project = var.gcp_project_id
region = "us-central1"
}
Azure
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
provider "azurerm" {
features {} # required empty block
subscription_id = var.azure_subscription_id
}
Kubernetes
provider "kubernetes" {
host = module.eks.cluster_endpoint
cluster_ca_certificate = base64decode(module.eks.cluster_ca_certificate)
token = module.eks.cluster_token
}
Null and Random (utility providers)
terraform {
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
null = {
source = "hashicorp/null"
version = "~> 3.2"
}
}
}
# Generate a unique suffix for globally unique names
resource "random_id" "bucket_suffix" {
byte_length = 4
}
resource "aws_s3_bucket" "data" {
bucket = "my-data-${random_id.bucket_suffix.hex}"
}
Provider Aliases
Sometimes you need multiple instances of the same provider — for example, deploying resources to two different AWS regions, or managing two different GCP projects from the same configuration. Provider aliases let you define named provider configurations and reference them explicitly in resources:
provider "aws" {
region = "us-east-1"
}
# Secondary region — give it an alias
provider "aws" {
alias = "eu-west"
region = "eu-west-1"
}
# This resource uses the default provider (us-east-1)
resource "aws_s3_bucket" "primary" {
bucket = "my-data-primary"
}
# This resource explicitly selects the aliased provider
resource "aws_s3_bucket" "replica" {
provider = aws.eu-west
bucket = "my-data-replica"
}
Aliases are also used when passing a provider to a module that needs to manage resources in a specific region:
module "replica" {
source = "./modules/replica"
providers = {
aws = aws.eu-west
}
}
You can define at most one provider block per provider type without an alias — that's the default. Every additional instance must have a unique alias. Resources that don't specify provider = ... use the default (un-aliased) provider.
The Lock File
When you run terraform init, Terraform creates or updates .terraform.lock.hcl — a file that records the exact provider versions selected and their checksums:
# .terraform.lock.hcl — commit this file to Git
provider "registry.terraform.io/hashicorp/aws" {
version = "5.31.0"
constraints = "~> 5.0"
hashes = [
"h1:abc123...",
"zh:def456...",
]
}
The lock file serves two critical purposes:
- Reproducibility — everyone on the team uses the exact same provider version, regardless of when they run
terraform init. - Integrity — checksums verify the downloaded binary matches what was selected when the lock file was created, protecting against supply chain attacks.
To upgrade providers to the newest version within your constraints, run:
# Upgrade all providers to latest within version constraints
terraform init -upgrade
# Or upgrade a specific provider
terraform init -upgrade -provider hashicorp/aws
Add .terraform/ to .gitignore — it contains large downloaded binaries. Commit .terraform.lock.hcl — it is a small text file that pins exact versions and should be reviewed in PRs just like any dependency lockfile (like package-lock.json or Cargo.lock).
With providers declared, versioned, and locked, you are ready to write your first real cloud resources. The next guide covers understanding state — the mechanism that makes Terraform's idempotent workflow possible.
Key Takeaways
- Providers are plugins that translate HCL into API calls for a specific platform. They are downloaded separately from Terraform core.
- Declare providers in
required_providerswith asourceandversionconstraint. - Use the pessimistic constraint operator (
~> 5.0) to accept minor/patch updates but block breaking major versions. - Authenticate via environment variables, shared credential files, or IAM roles — never hard-code credentials in
.tffiles. - Use provider aliases when you need multiple instances of the same provider (multi-region, multi-account).
- Commit
.terraform.lock.hclto Git. Add.terraform/to.gitignore.