title: Provider configuration description: Production defaults for the AWS Terraform / OpenTofu provider: version pinning, default_tags, assume_role, retries, multi-region aliases, and OIDC for GitHub Actions. tags: - terraform - aws
Provider configuration¶
Sensible, production-grade defaults for the AWS provider. Drop these into your root module to get consistent tagging, friendly retries, and federated credentials for CI/CD.
Root module owns the provider
Reusable child modules declare provider requirements in versions.tf
but never instantiate provider "aws" { ... }. Provider configuration
lives in the root module so the same module can be reused across
accounts, regions, and partitions.
required_providers pinning¶
Pin the AWS provider to a major version with the pessimistic constraint so new minors flow in but breaking changes don't:
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.60, < 6.0"
}
}
}
Commit the lockfile
terraform init writes .terraform.lock.hcl with checksums for every
provider. Always commit it, it's the only thing keeping you from a
silent supply-chain change.
default_tags¶
Tags applied here are merged onto every taggable resource managed by this
provider instance. Stop sprinkling tags = { ... } across hundreds of
resources.
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Owner = "platform"
Environment = var.environment
ManagedBy = "terraform"
CostCenter = var.cost_center
Repository = "github.com/acme-co/platform"
}
}
}
Tag drift on aws_autoscaling_group
A handful of resources (notably aws_autoscaling_group and
aws_eks_node_group) propagate tags through a different mechanism and
can show up as drift on plan. Use lifecycle { ignore_changes = [tag] }
or set the tags explicitly on those resources.
assume_role with external_id¶
Run plans as a deploy role instead of a long-lived user. The external_id
is required when the trust policy on the target role enforces it (recommended
for any cross-account scenario).
provider "aws" {
region = "us-east-1"
assume_role {
role_arn = "arn:aws:iam::111122223333:role/terraform-deploy"
session_name = "terraform-${var.environment}"
external_id = var.external_id
# Optional: cap the maximum permissions of the session, even if the
# underlying role has more. Useful for plan-only sessions.
# policy_arns = ["arn:aws:iam::aws:policy/ReadOnlyAccess"]
}
}
Retry configuration¶
The default retry behaviour is conservative. For long applies that touch hundreds of resources, the adaptive retry mode backs off intelligently when AWS starts throttling.
provider "aws" {
region = "us-east-1"
max_retries = 10
retry_mode = "adaptive" # one of: "standard" | "adaptive" | "legacy"
}
Pair with -parallelism
Adaptive retries help, but if you're hitting throttling regularly,
consider lowering terraform apply -parallelism=10 (default 10, but
sometimes worth dropping further on heavy IAM/EC2 modules).
Multi-region with provider aliases¶
Some resources are global (CloudFront, ACM certs for CloudFront, IAM) and
must be created in us-east-1. Define an aliased provider and pass it
explicitly to those modules.
provider "aws" {
region = var.region # e.g. eu-west-1
}
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
}
# ACM cert for a CloudFront distribution must live in us-east-1.
module "cdn_cert" {
source = "./modules/acm-cert"
domain = "www.example.com"
providers = {
aws = aws.us_east_1
}
}
A child module that needs more than one provider declares the aliases it
expects in its own versions.tf:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.60, < 6.0"
configuration_aliases = [aws.us_east_1]
}
}
}
OIDC + GitHub Actions (assume_role_with_web_identity)¶
Inside a GitHub Actions runner, exchange the workflow's OIDC token for AWS
credentials: no static keys, no aws-actions/configure-aws-credentials
env-var dance required by Terraform itself.
provider "aws" {
region = "us-east-1"
assume_role_with_web_identity {
role_arn = "arn:aws:iam::111122223333:role/github-actions-deploy"
session_name = "gha-${var.github_run_id}"
web_identity_token_file = "/var/run/secrets/github/token"
}
}
In practice you'll keep using aws-actions/configure-aws-credentials to
fetch the token and write it to the env, then Terraform will pick it up
automatically because it reads the standard
AWS_ROLE_ARN / AWS_WEB_IDENTITY_TOKEN_FILE variables. The explicit block
above is useful when you need to assume a different role than the one the
action configured (for example, a per-env role).
A typical workflow step:
# .github/workflows/deploy.yml
permissions:
id-token: write # required for OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/github-actions-deploy
aws-region: us-east-1
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.10.0
- run: terraform init
- run: terraform apply -auto-approve
See the matching trust policy in IAM policy patterns → GitHub Actions OIDC.
Putting it all together¶
A complete root-module provider block for a CI-driven, multi-region deployment:
terraform {
required_version = ">= 1.10.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.60, < 6.0"
}
}
}
provider "aws" {
region = var.region
max_retries = 10
retry_mode = "adaptive"
assume_role {
role_arn = var.deploy_role_arn
session_name = "terraform-${var.environment}"
external_id = var.external_id
}
default_tags {
tags = {
Owner = "platform"
Environment = var.environment
ManagedBy = "terraform"
Repository = "github.com/acme-co/platform"
}
}
}
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
max_retries = 10
retry_mode = "adaptive"
assume_role {
role_arn = var.deploy_role_arn
session_name = "terraform-${var.environment}-use1"
external_id = var.external_id
}
default_tags {
tags = {
Owner = "platform"
Environment = var.environment
ManagedBy = "terraform"
Repository = "github.com/acme-co/platform"
}
}
}