Table of contents
- What Providers Do
- The Basic Form of a provider Block
- source — The Provider’s Address
- version — Version Constraints
- provider Block Settings
- alias — Same Provider, Different Settings
- Passing Providers to Modules
- Official vs Partner vs Community
- Using Multiple Providers Simultaneously
- Cache and Network Considerations
- Common Mistakes
- What’s Next
What Providers Do
When Terraform creates resources in AWS or GCP, it’s not Terraform Core that actually calls the APIs. Dedicated plugins for each cloud/service handle that work. These plugins are called providers.
Here’s the structure in a diagram.
flowchart TB
CORE[Terraform Core<br/>Planning/State/Graph]
subgraph Providers
AWS[AWS Provider]
GCP[GCP Provider]
K8S[Kubernetes Provider]
GH[GitHub Provider]
end
AWS_API[(AWS API)]
GCP_API[(GCP API)]
K8S_API[(Kubernetes API)]
GH_API[(GitHub API)]
CORE --> AWS
CORE --> GCP
CORE --> K8S
CORE --> GH
AWS --> AWS_API
GCP --> GCP_API
K8S --> K8S_API
GH --> GH_API
Terraform Core acts as the “brain” — computing dependency graphs between resources, building execution plans, and managing state. The actual HTTP calls like “create an EC2 instance” are the provider’s job. Thanks to this division of labor, Terraform can manage platforms like AWS/GCP/Cloudflare/GitHub/Datadog with the same workflow.
As of 2025, over 3,000 providers are registered in the Terraform Registry. If “it has an API,” there’s probably a provider for it.
The Basic Form of a provider Block
Using a provider requires two steps. Declare the source and version in required_providers, then provide actual settings (region, profile, etc.) in the provider block.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-northeast-2"
profile = "terraform-demo"
}
The reason for the two-step split is separation of concerns. required_providers declares “what provider version this code requires,” while the provider block handles runtime configuration. When creating modules, it’s typical to only declare required_providers and have the provider block injected from the root module.
source — The Provider’s Address
source is the address that uniquely identifies a provider. It has three parts.
[HOST/]NAMESPACE/TYPE
- HOST: The registry where the provider is published (defaults to
registry.terraform.io, can be omitted) - NAMESPACE: The publisher name (
hashicorp,integrations,cloudflare, etc.) - TYPE: The provider name (
aws,kubernetes,github, etc.)
Real examples make it clear.
required_providers {
# HashiCorp official — namespace is hashicorp
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
# Partner provider — namespace is the service vendor
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
# Community provider — namespace is an individual/organization
cloudinit = {
source = "hashicorp/cloudinit"
}
}
Omitting the namespace and writing just aws is interpreted as hashicorp/aws for backward compatibility, but modern Terraform recommends being explicit.
version — Version Constraints
Not specifying a provider version might be fine at first, but months later when you run terraform init fresh, a different version gets installed and unexpected changes appear. Version constraints are essential.
Operators
version = "5.0.0" # Exactly this version
version = ">= 5.0" # This version or higher
version = "<= 5.10" # This version or lower
version = "~> 5.0" # Latest within 5.x (major pinned)
version = "~> 5.1" # Latest within 5.1.x (minor pinned)
version = ">= 5.0, < 6.0" # Range
~> 5.0 in particular is called a “pessimistic constraint.” Same as Ruby’s ~> operator. Allows 5.0, 5.1, up to 5.99, but rejects 6.0. Since major version upgrades typically contain breaking changes, blocking them this way is safe practice.
Lock File
When you run terraform init, the selected version is pinned in .terraform.lock.hcl.
# .terraform.lock.hcl (auto-generated)
provider "registry.terraform.io/hashicorp/aws" {
version = "5.70.0"
constraints = "~> 5.0"
hashes = [
"h1:abc123...",
...
]
}
With the lock file in place, subsequent init runs use the same version. Commit this file to Git. That way team members and CI use identical providers.
To upgrade a provider, you must explicitly allow it.
terraform init -upgrade
This option ignores the lock file version and installs the latest version within the constraint range. Review the plan carefully before applying to production.
provider Block Settings
The provider "<name>" block holds provider-specific arguments. Let’s look at AWS as an example.
provider "aws" {
region = "ap-northeast-2"
profile = "terraform-demo"
shared_credentials_files = ["~/.aws/credentials"]
default_tags {
tags = {
ManagedBy = "terraform"
Environment = var.environment
Project = var.project_name
}
}
}
default_tags is especially useful in practice. These tags are automatically applied to every AWS resource created with this provider. No need to repeat the same tags on every resource. If a resource needs different tags, specifying tags separately merges them.
Credentials can be mixed from multiple sources.
- CLI profile (
profile = "...") - Environment variables (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY) - IAM Role assumption (
assume_roleblock) - EC2 Instance Profile (automatic when running on EC2)
In practice, CI environment variables or IAM Role assume are typically used. For local labs, profiles are convenient. Never hardcode access keys in code.
alias — Same Provider, Different Settings
In practice, you often need to use two regions or two accounts simultaneously in one project. A classic example is “creating a CloudFront certificate in us-east-1 and deploying the rest in ap-northeast-2.” ACM certificates must be in us-east-1 to be attached to CloudFront.
This is where alias comes in.
# Default provider — Seoul region
provider "aws" {
region = "ap-northeast-2"
profile = "terraform-demo"
}
# Additional provider with alias — Virginia region
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
profile = "terraform-demo"
}
To select a specific provider for a resource, specify provider explicitly.
# Seoul region resource (default)
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.micro"
}
# Certificate for CloudFront goes in the Virginia region
resource "aws_acm_certificate" "cdn" {
provider = aws.us_east_1
domain_name = "www.example.com"
validation_method = "DNS"
}
# Data sources can also use alias
data "aws_ami" "us_east_amazon_linux" {
provider = aws.us_east_1
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
The structure of multi-region/account deployment can be visualized like this.
flowchart LR
subgraph TF[Terraform Project]
P1[provider aws<br/>default = Seoul]
P2[provider aws<br/>alias = us_east_1]
P3[provider aws<br/>alias = prod_account]
end
subgraph CLOUD[Cloud]
R1[(Seoul: EC2, RDS)]
R2[(N.Virginia: ACM Cert)]
R3[(Prod Account: S3)]
end
P1 --> R1
P2 --> R2
P3 --> R3
The same provider (aws) but with different regions and accounts can be managed within a single project. Without knowing this, you’d get stuck at “how do I create a certificate in us-east-1?”
Passing Providers to Modules
There’s an important caveat when creating modules. Aliased providers are not automatically passed to child modules. You must explicitly pass them.
# Module side — declare required providers
# modules/cdn/versions.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
configuration_aliases = [aws.us_east_1]
}
}
}
# Inside the module, use the alias explicitly
resource "aws_acm_certificate" "cdn" {
provider = aws.us_east_1
domain_name = var.domain_name
validation_method = "DNS"
}
# Caller side — pass mapping via providers
module "cdn" {
source = "./modules/cdn"
providers = {
aws.us_east_1 = aws.us_east_1
}
domain_name = "www.example.com"
}
configuration_aliases declares “this module needs this alias,” and the caller passes the actual mapping via the providers map. If this step is missing, you’ll get errors like “provider aws.us_east_1 is not defined.”
Official vs Partner vs Community
Providers in the Terraform Registry are divided into three tiers. Knowing which tier matters in practice.
Official
Maintained directly by HashiCorp. Namespace is hashicorp.
- Examples:
hashicorp/aws,hashicorp/google,hashicorp/kubernetes,hashicorp/random
Support and updates are stable. The most widely used providers belong here.
Partner
Managed directly by the service vendor. Certified by HashiCorp. Namespace is the vendor name.
- Examples:
cloudflare/cloudflare,datadog/datadog,mongodb/mongodbatlas
Since the vendor builds them directly, API updates are fast, and they can essentially be considered official.
Community
Providers contributed as open source by individuals or organizations. Namespace is a GitHub handle or organization name.
- Examples:
custom-user/some-provider
Before using, check maintenance status (recent commits, issue count), download count, and license. Sometimes the only provider for a specific SaaS is community-tier — get team consensus before adopting.
The priority for selection looks like this.
flowchart LR
NEED[Need a provider] -->|1st choice| OFF[Official<br/>hashicorp/*]
NEED -->|2nd choice| PART[Partner<br/>vendor/*]
NEED -->|3rd choice| COM[Community<br/>individual/*]
NEED -->|Last resort| CUSTOM[Build your own]
Community providers can be well-made, but be cautious before bringing them into production. If a repo has been abandoned for 2-3 years, you’re taking on that risk.
Using Multiple Providers Simultaneously
Real projects don’t use just one provider. You might create AWS resources while also deploying to Kubernetes, manage domains with Cloudflare, and handle monitoring with Datadog.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.0"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
}
provider "aws" {
region = "ap-northeast-2"
}
# Kubernetes auto-configures from EKS cluster info after creation
provider "kubernetes" {
host = aws_eks_cluster.main.endpoint
cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data)
exec {
api_version = "client.authentication.k8s.io/v1beta1"
command = "aws"
args = ["eks", "get-token", "--cluster-name", aws_eks_cluster.main.name]
}
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
An important note: when provider settings reference other resource attributes, a dependency is created. In the example above, the kubernetes provider depends on aws_eks_cluster.main, so Kubernetes resources can only be applied after the EKS cluster is created. Creating EKS and deploying resources inside it in a single apply can get tangled in practice. For production, it’s cleaner to separate “infrastructure (EKS)” and “applications (K8s resources)” into different Terraform projects.
Cache and Network Considerations
Provider binaries are large. The AWS provider easily exceeds 100MB. Repeatedly downloading the same provider across multiple projects wastes time and disk space.
The solution is plugin caching. Add the following to ~/.terraformrc (Linux/macOS) or %APPDATA%\terraform.rc (Windows).
plugin_cache_dir = "$HOME/.terraform.d/plugin-cache"
Create this directory and all Terraform projects will cache and share providers there. Essential for practical work switching between multiple projects.
Common Mistakes
- Not pinning versions. Versions diverge between team members and CI.
~>is the minimum defense - Not committing
.terraform.lock.hcl. Must be committed to share the same version - Duplicating the same provider without
alias. Leads to “provider block already defined” errors - Hardcoding secrets in provider blocks.
api_token = "abc123..."is absolutely forbidden. Separate into variables or environment variables - Duplicating provider blocks across files. Provider blocks are declared once in the root module. Use alias for multiple instances
What’s Next
Now that we’ve covered providers, it’s time to look closely at Terraform’s main stage: resources. We’ll examine the resource block structure, cross-resource references, implicit vs explicit dependencies, and lifecycle meta-arguments (create_before_destroy, prevent_destroy, ignore_changes) in detail.


Loading comments...