Terraform Cheatsheet
Providers
Use this Terraform reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
What Providers Are
Providers are plugins that translate Terraform blocks into API calls for one platform. They ship independently of Terraform itself, on their own version numbers, and the Terraform Registry hosts thousands.
A provider supplies four things: resource types (aws_instance), data sources (data "aws_ami"), optionally functions (1.8+) and actions (1.14+), and optionally ephemeral resources (1.10+).
Declaring and Configuring
terraform { required_providers { aws = { source = "hashicorp/aws" # <namespace>/<type>, or <host>/<ns>/<type> version = "~> 6.0" } random = { source = "hashicorp/random" version = "~> 3.6" } } } provider "aws" { region = "us-east-1" profile = "myprofile" # ~/.aws/credentials profile }
The key in required_providers (aws) is the local name. It is what provider "aws", aws_instance, provider = aws.west and provider::aws:: all refer to. You can rename a provider locally, which is how you use two forks of the same provider side by side:
required_providers {
legacy_aws = { source = "acme/aws", version = "~> 4.0" }
}Never hardcode credentials in a
providerblock. They land in plan files and often in state. Use environment variables, an AWS profile, OIDC in CI, orassume_role.
export AWS_ACCESS_KEY_ID="AKIA..." export AWS_SECRET_ACCESS_KEY="..." export AWS_REGION="us-east-1"
Version Constraint Operators
| Operator | Meaning | Example allows |
|---|---|---|
= 6.2.0 | exact | 6.2.0 only |
!= 6.3.0 | exclude | anything but 6.3.0 |
>= 6.2.0 | at least | 6.2.0 and up, including 7.x |
<= 6.9.0 | at most | up to 6.9.0 |
~> 6.0 | pessimistic on minor | >= 6.0.0, < 7.0.0 |
~> 6.2.0 | pessimistic on patch | >= 6.2.0, < 6.3.0 |
>= 6.2, < 6.9 | both hold | 6.2.0 up to 6.8.x |
Use ~> MAJOR.0 for providers. Provider majors are where breaking changes land, so a major-pinned constraint gives you every bug fix and new resource type for free while never silently jumping a rewrite. A bare >= 6.0 lets a terraform init -upgrade pull 7.0 and break your config. An exact pin freezes you out of security fixes and forces a manual bump for every provider release.
Constraints from every module in the tree are intersected, so one child module pinned to = 6.1.0 narrows the whole configuration.
The Dependency Lock File
terraform init writes .terraform.lock.hcl recording the exact version and package checksums chosen. Commit it. It is the difference between "works on my machine" and a reproducible build.
# .terraform.lock.hcl (generated, do not hand-edit) provider "registry.terraform.io/hashicorp/aws" { version = "6.2.0" constraints = "~> 6.0" hashes = [ "h1:...", "zh:...", ] }
terraform init # respects the lock file terraform init -upgrade # re-resolve and rewrite the lock file terraform init -lockfile=readonly # fail rather than modify the lock
The multi-platform gotcha. A lock file written on an Apple Silicon laptop records only darwin_arm64 hashes. CI on linux_amd64 then fails with "checksums previously recorded in the lock file". Pre-record every platform you build on:
terraform providers lock \ -platform=linux_amd64 \ -platform=linux_arm64 \ -platform=darwin_arm64 \ -platform=windows_amd64
h1: hashes cover the whole package for the current platform. zh: hashes come from the registry and are what make cross-platform locking work.
Aliased Providers
Use alias for the same provider in more than one configuration, typically multiple regions or accounts.
provider "aws" { region = "us-east-1" # the default, no alias } provider "aws" { alias = "west" region = "us-west-2" } resource "aws_s3_bucket" "replica" { provider = aws.west # <local_name>.<alias>, unquoted bucket = "myapp-replica" }
Rules:
- A configuration may have at most one un-aliased ("default") configuration per provider.
- Aliased configurations are never inherited by child modules. Pass them with
providers = { aws = aws.west }on themoduleblock. - A
providerblock cannot itself takecount,for_eachordepends_on. To fan out over N accounts you need one block per account, or one module instance per account. - A
providerconfig may reference resource attributes, but doing so can create a chicken-and-egg problem where Terraform cannot plan. Prefer data sources or a two-stage apply.
Provider-Defined Functions (1.8+)
Providers can ship their own functions, namespaced so they cannot collide with built-ins.
# provider::<local_name>::<function> locals { parsed = provider::aws::arn_parse(aws_s3_bucket.app.arn) account = local.parsed.account_id tfvars = provider::terraform::encode_tfvars({ example = "Hello!" }) }
The terraform built-in provider ships encode_tfvars, decode_tfvars and encode_expr. Everything else is per provider, so check that provider's registry docs. Functions need the provider declared in required_providers and installed, so a bare terraform validate before init will not resolve them.
Common Providers
| Provider | Source | Credentials from |
|---|---|---|
| AWS | hashicorp/aws | AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY, profile, IRSA, OIDC |
| GCP | hashicorp/google | GOOGLE_CREDENTIALS, or Application Default Credentials |
| Azure | hashicorp/azurerm | ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID, ARM_SUBSCRIPTION_ID |
| Kubernetes | hashicorp/kubernetes | kubeconfig, or in-cluster service account |
| Helm | hashicorp/helm | same as kubernetes |
| GitHub | integrations/github | GITHUB_TOKEN |
| Cloudflare | cloudflare/cloudflare | CLOUDFLARE_API_TOKEN |
| Datadog | DataDog/datadog | DD_API_KEY, DD_APP_KEY |
| Random / Null / TLS / Local / Time | hashicorp/<name> | none |
AWS
provider "aws" { region = var.aws_region allowed_account_ids = ["123456789012"] # refuses to run elsewhere default_tags { tags = { ManagedBy = "terraform" Project = var.project_name } } assume_role { role_arn = "arn:aws:iam::123456789012:role/TerraformRole" session_name = "terraform" } }
allowed_account_ids is the cheapest guard against applying prod config to the wrong account. default_tags applies to every taggable resource, so you stop repeating the same tag map.
GCP and Azure
provider "google" { project = var.gcp_project region = "us-central1" zone = "us-central1-a" } provider "azurerm" { features {} # required, even when empty subscription_id = var.subscription_id tenant_id = var.tenant_id }
Kubernetes from an EKS cluster
provider "kubernetes" { host = data.aws_eks_cluster.main.endpoint cluster_ca_certificate = base64decode( data.aws_eks_cluster.main.certificate_authority[0].data ) token = data.aws_eks_cluster_auth.main.token }
This is the classic case of a provider configured from resource attributes. If the cluster does not exist yet the provider cannot be configured, so create the cluster in one apply and the Kubernetes objects in another.
Provider Commands
terraform providers # tree of providers per module terraform providers schema -json # full schema dump, huge terraform providers lock -platform=linux_amd64 terraform providers mirror ./mirror # local filesystem mirror for air-gapped CI terraform init -upgrade # re-resolve versions, rewrite the lock export TF_PLUGIN_CACHE_DIR="$HOME/.terraform.d/plugin-cache" # share downloads
terraform providers schema -json | jq '.provider_schemas["registry.terraform.io/hashicorp/aws"].resource_schemas.aws_instance' is the fastest way to answer "what arguments does this resource actually take" without leaving the terminal.