Providers and Authentication
Configuring cloud providers with version constraints, and the real authentication patterns for AWS, Azure, and GCP — IAM roles, service principals, workload identity, and OIDC for CI/CD — plus multi-account and multi-provider setups.
- Chapter 1: Foundations & IaC Concepts
- Chapter 6: State Management
- Configure providers with correct version constraints
- Choose the right authentication pattern for AWS, Azure, and GCP
- Set up OIDC-based authentication for GitHub Actions/GitLab CI without long-lived credentials
- Configure multiple instances of the same provider for multi-account/multi-region setups
Providers and Authentication
1. Why does this exist?
Every resource block so far has assumed a provider "aws" { region = "us-east-1" } block just works — but where do the actual credentials authorizing those API calls come from? This chapter is about the layer beneath the provider block: how Terraform actually authenticates to AWS, Azure, and GCP, why long-lived credentials in CI are a real liability, and how to configure more than one instance of the same provider when your infrastructure spans multiple regions or accounts.
2. Concept
Provider Version Constraints
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # any 5.x, never 6.0
}
}
}
Constraint operators: = (exact), >=/<= (bounds), ~> (pessimistic — “this version or later within the same minor/major depending on precision”). Always constrain providers in shared/production configurations — an unconstrained provider can resolve to a version with breaking changes on a teammate’s next init.
Authentication Patterns by Cloud
| Cloud | Pattern | When to Use |
|---|---|---|
| AWS | IAM roles (EC2/ECS instance role) | Terraform running from within AWS infrastructure (a CI runner on EC2, a CodeBuild job) |
| AWS | Named profiles (~/.aws/credentials) |
Local developer machines |
| AWS | OIDC (GitHub Actions/GitLab CI → STS AssumeRoleWithWebIdentity) | CI/CD pipelines — no long-lived secrets stored anywhere |
| Azure | Service Principal (client ID/secret) | Traditional CI/CD, still requires secret rotation |
| Azure | Managed Identity | Terraform running on Azure infrastructure (an Azure VM, an Azure DevOps agent) |
| Azure | OIDC (Workload Identity Federation) | GitHub Actions/GitLab CI — the modern, secretless pattern |
| GCP | Service Account key file | Legacy pattern, a long-lived credential — avoid where possible |
| GCP | Workload Identity Federation | CI/CD — exchanges a CI-issued token for short-lived GCP credentials, no service account key needed |
The pattern across all three clouds: prefer short-lived, automatically-issued credentials (instance roles, managed identity, workload identity/OIDC) over long-lived secrets (access keys, service principal secrets, service account key files) wherever the running environment supports it.
3. Architecture Diagram
sequenceDiagram
participant GH as GitHub Actions
participant AWS as AWS STS
participant TF as Terraform
GH->>AWS: Present GitHub OIDC token (signed, short-lived)
AWS->>AWS: Verify token against trust policy (repo, branch, workflow)
AWS-->>GH: Issue temporary credentials (~1 hour)
GH->>TF: Inject temporary credentials as env vars
TF->>AWS: terraform plan/apply using temporary credentials
No long-lived AWS access key ever exists in GitHub’s secret store — the trust relationship is established once (an IAM role’s trust policy naming the GitHub org/repo/branch), and every workflow run gets its own short-lived credential.
4. Visual Explanation
AWS OIDC Trust Policy (What Makes This Secure)
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
"StringLike": { "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main" }
}
}
Hint: what does the "sub" condition actually protect against?
Without a scoped sub condition, ANY GitHub Actions workflow from ANY repository that can present a token to this OIDC provider could assume this role. The StringLike condition restricts assumption to workflows running specifically from my-org/my-repo on the main branch — this is the actual security boundary, not the OIDC mechanism itself.
5. Example
# GitHub Actions workflow using OIDC — no stored AWS secrets at all
permissions:
id-token: write # required for OIDC token issuance
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-terraform
aws-region: us-east-1
- run: terraform init && terraform plan
# Corresponding Terraform provider config — nothing special needed here;
# the credentials arrive via environment variables the AWS SDK reads automatically
provider "aws" {
region = "us-east-1"
}
The provider block itself stays simple — OIDC’s complexity lives entirely in the CI workflow configuration and the IAM role’s trust policy, not in Terraform’s provider configuration.
6. Production Example
Multi-account setup using provider aliases and assume-role:
provider "aws" {
region = "us-east-1"
alias = "shared_services"
}
provider "aws" {
region = "us-east-1"
alias = "production"
assume_role {
role_arn = "arn:aws:iam::999999999999:role/terraform-execution"
}
}
resource "aws_s3_bucket" "shared_logs" {
provider = aws.shared_services
bucket = "aetoso-shared-logs"
}
resource "aws_instance" "prod_web" {
provider = aws.production
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.large"
}
One base identity (whatever authenticates the un-aliased/default provider setup) assumes a role in the production account for anything using aws.production — this is the standard pattern for a central platform team managing resources across multiple AWS accounts from one Terraform configuration.
7. Code Walkthrough — Azure and GCP Equivalents
# Azure: OIDC / Workload Identity Federation (GitHub Actions)
provider "azurerm" {
features {}
use_oidc = true
client_id = var.azure_client_id
tenant_id = var.azure_tenant_id
subscription_id = var.azure_subscription_id
}
# GCP: Workload Identity Federation
provider "google" {
project = "my-gcp-project"
region = "us-central1"
# Credentials resolved automatically via GOOGLE_APPLICATION_CREDENTIALS
# pointing at a Workload Identity Federation config, injected by the CI provider
}
In both cases, notice what’s absent: no client secret, no service account key file path pointing at a checked-in JSON file. The federation setup does the work of exchanging a short-lived CI-issued token for cloud credentials, entirely outside Terraform’s own configuration.
8. Behind the Scenes
Terraform providers don’t implement authentication logic themselves — they typically delegate to each cloud’s own SDK (the AWS Go SDK, Azure SDK, Google Cloud SDK), which implements a standard credential resolution chain: environment variables → shared credentials file → instance/container metadata service → (for OIDC) a token exchange flow. This is why a provider "aws" {} block with zero explicit credentials still works when running on an EC2 instance with an attached IAM role — the AWS SDK’s default chain finds and uses the instance metadata service automatically, and Terraform’s AWS provider simply inherits that behavior rather than reimplementing it.
9. Common Mistakes
- Wrong: Storing long-lived cloud credentials as CI secrets when the CI provider supports OIDC. Why: a leaked long-lived credential grants standing access until manually rotated; a leaked short-lived OIDC-issued credential is often already expired by the time it could be misused. Correct: set up OIDC/workload identity federation for any CI/CD pipeline running Terraform.
- Wrong: Writing an overly broad OIDC trust policy (no
subcondition restricting which repo/branch can assume the role). Why: this defeats the security benefit entirely — any workflow from any repository trusted by the same OIDC provider could assume the role. Correct: always scope the trust policy’ssubcondition to the specific repository and branch (or usepull_requestconditions carefully, since PR-triggered workflows from forks need extra scrutiny). - Wrong: Hardcoding a second AWS account’s role ARN directly in a resource block instead of using a provider alias. Why: mixes authentication concerns into resource configuration, and doesn’t compose with
for_each/modules the way a properly aliased provider does. Correct: configure a distinct aliased provider per account/region, and passprovider = aws.alias_nameexplicitly on resources needing it.
10. Production Tips
- 🚀 Best Practice: Default to OIDC/workload identity everywhere your CI provider supports it — treat any remaining long-lived credential as a gap to close, not a permanent fixture.
- 💡 Tip: Name provider aliases after their purpose (
aws.production,aws.shared_services), not generic labels (aws.2) — six months later, the alias name is the only context a reader has. - ⚠️ Warning:
assume_roleblocks inherit whatever base credentials authenticate the “outer” identity — an overly permissive base role can assume into more accounts than intended if the target roles’ trust policies aren’t also scoped tightly.
11. Debugging
| Symptom | Likely Cause | Fix |
|---|---|---|
Error: error configuring Terraform AWS Provider: no valid credential sources found |
None of the SDK’s credential resolution chain found anything | Check environment variables, ~/.aws/credentials, or (in CI) confirm the OIDC role-assumption step actually ran before Terraform |
AccessDenied on sts:AssumeRoleWithWebIdentity |
Trust policy’s sub condition doesn’t match the actual workflow’s repo/branch |
Compare the exact sub claim (visible in AWS CloudTrail or the GitHub Actions OIDC token debug output) against the trust policy’s condition |
| Resources created in the wrong AWS account | Missing or incorrect provider = aws.alias on a resource, silently falling back to the default provider |
Audit every resource in a multi-account configuration for an explicit provider argument where more than one aliased provider exists |
Error: Invalid provider configuration alias |
Referenced an alias that was never declared, or a typo in the alias name | Confirm the alias string matches exactly between the provider block and every resource referencing it |
12. Interview Questions
Q — Walk through why OIDC-based CI authentication is considered a meaningful security improvement over storing access keys as CI secrets. Long-lived access keys, once stored as a secret, persist until someone manually rotates or revokes them — if ever exposed (a log line, a compromised dependency), they grant standing access for as long as they remain valid, often longer than anyone tracks. OIDC replaces this with a trust relationship (a cloud IAM role trusting a specific CI provider’s token issuer, scoped to a specific repo/branch) — every workflow run gets its own short-lived, automatically-expiring credential, with no long-lived secret ever stored anywhere to leak in the first place.
Q — What does a provider alias actually solve, and when do you need one?
It lets a single Terraform configuration define and reference multiple distinct instances of the same provider — most commonly for multi-region or multi-account setups. Without an alias, a configuration can only have one un-aliased configuration per provider type; any resource needing a second AWS region or account requires an aliased provider block and an explicit provider = aws.alias_name argument on that resource.
Q — Why doesn’t Terraform implement its own cloud authentication logic instead of delegating to each cloud’s SDK? Delegating to the official SDK means Terraform automatically benefits from that SDK’s standard credential resolution chain (environment variables, instance metadata, shared credentials files, SSO sessions) without reimplementing and maintaining it — and it means authentication behavior stays consistent with every other tool built on that same SDK, rather than Terraform inventing its own, potentially divergent, authentication conventions.
13. Hands-on Lab
Goal: configure two aliased AWS providers targeting two different regions, and confirm resources land in the correct one (no multi-account setup required for this exercise).
- Write a configuration with two
provider "aws"blocks — one default (us-east-1), one aliasedalias = "west"(us-west-2). - Create an
aws_s3_bucketusing the default provider, and another usingprovider = aws.west. - Run
terraform planand confirm the plan output (orterraform consolequerying each resource’s region-derived attributes) shows each bucket targeting its intended region.
Solution
This confirms the core mental model: an aliased provider block is a completely independent authentication/region context, and resources opt into a non-default one explicitly via the provider meta-argument — nothing implicit happens based on naming or file location.
14. Mini Project
Set up OIDC for the Chapter 6 mini project’s CI pipeline. Using a GitHub repository:
- Create an IAM OIDC identity provider for
token.actions.githubusercontent.com(one-time setup per AWS account). - Create an IAM role with a trust policy scoped to your specific repo and
mainbranch. - Update your GitHub Actions workflow to use
aws-actions/configure-aws-credentialswithrole-to-assume, removing any stored access key secrets. - Confirm
terraform planruns successfully in CI with zero long-lived AWS credentials stored in the repository’s secrets.
16. Summary
- Always pin provider versions with
required_providers— unconstrained providers can resolve to breaking versions unexpectedly. - Prefer short-lived, automatically-issued credentials (instance roles, managed identity, OIDC/workload identity) over long-lived secrets wherever the runtime environment supports it.
- OIDC’s real security boundary is the trust policy’s scoping condition (repo, branch) — the OIDC mechanism itself doesn’t protect you if that condition is too broad.
- Provider
alias+ theprovider = aws.alias_namemeta-argument is how one configuration manages multiple regions or accounts — Terraform doesn’t infer this from context. - Terraform providers delegate authentication to each cloud’s own SDK, inheriting its standard credential resolution chain rather than reinventing one.
17. Cheat Sheet
provider "aws" {
region = "us-east-1"
alias = "production"
assume_role {
role_arn = "arn:aws:iam::ACCOUNT:role/ROLE"
}
}
resource "aws_instance" "example" {
provider = aws.production
# ...
}
# GitHub Actions OIDC
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT:role/ROLE
aws-region: us-east-1
18. Related Topics
Next: Chapter 8 covers resource dependencies and lifecycle — including how Terraform orders operations across multiple aliased providers in the same apply. The CI/CD Learning Path course (in this platform’s Courses list) goes deeper on structuring the surrounding pipeline this authentication setup runs inside.