Terraform's core knows nothing about AWS, Cloudflare, Kubernetes, or
any other real system. Every resource type you actually write comes from
a provider: a plugin that translates resource
blocks into real API calls against one specific system. This is why
Terraform can manage almost anything with the same workflow — the
providers do the system-specific work, Terraform just orchestrates
plan/apply/state.
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.52"
}
}
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
source is <namespace>/<name> in
the Terraform Registry — almost always the vendor's own namespace for an
official provider. version is a constraint, not an exact
pin by default: ~> 4.52 allows any 4.52.x release but not
4.53. That's a deliberate middle ground between "pin the exact version
and never get a bugfix without a manual bump" and "always take whatever
is newest and get surprised by a breaking change mid-apply." For
anything you actually depend on staying stable, pin tighter; a
`.terraform.lock.hcl` file (generated by init, meant to be
committed) additionally records the exact version and checksum actually
installed, so a second person running init gets the exact
same provider build, not just one that satisfies the constraint.
The example above reads a token from a variable, which is already
better than a literal string in the file — but where does that variable
get its value? Almost every provider also supports reading credentials
from an environment variable (for Cloudflare, CLOUDFLARE_API_TOKEN)
with no provider block configuration at all. That's usually
the better default: it means the actual secret never has to pass through
a .tfvars file, a CI variable that gets logged, or anywhere
else it could end up committed by accident. Reserve explicit
provider-block credentials for cases where you genuinely need more than
one identity active at once (see below).
Sometimes one configuration genuinely needs two accounts, two regions,
or two of anything the same provider talks to — most commonly, an AWS
config that needs both us-east-1 and a second region for
one global resource (ACM certificates for CloudFront famously have to
live in us-east-1 regardless of where everything else runs).
Provider aliases solve this without needing two separate provider
plugins or two separate Terraform runs:
provider "aws" {
region = "us-west-2"
}
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
}
resource "aws_acm_certificate" "cert" {
provider = aws.us_east_1
domain_name = "example.com"
validation_method = "DNS"
}
Every resource that doesn't set provider = ... explicitly
uses the default (unaliased) provider instance. Only the ones that
genuinely need the other one need the extra line.
Not every provider block is about creating something. A
data block reads existing information through the same
provider without taking ownership of it — useful for referencing
something that already exists (a DNS zone, an AMI, an existing VPC)
without Terraform trying to manage its full lifecycle:
data "cloudflare_zone" "this" {
name = "example.com"
}
resource "cloudflare_record" "www" {
zone_id = data.cloudflare_zone.this.id
name = "www"
type = "CNAME"
content = "example.com"
}
This is the same pattern that shows up constantly in real configurations: look something up with a data source, feed its output into the resources you're actually creating.