Learning PathsTerraform
TerraformChapter 7·INTERMEDIATE·11 min read

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.

Prerequisites
  • Chapter 1: Foundations & IaC Concepts
  • Chapter 6: State Management
You'll learn to
  • 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 sub condition 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’s sub condition to the specific repository and branch (or use pull_request conditions 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 pass provider = aws.alias_name explicitly 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_role blocks 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).

  1. Write a configuration with two provider "aws" blocks — one default (us-east-1), one aliased alias = "west" (us-west-2).
  2. Create an aws_s3_bucket using the default provider, and another using provider = aws.west.
  3. Run terraform plan and confirm the plan output (or terraform console querying 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:

  1. Create an IAM OIDC identity provider for token.actions.githubusercontent.com (one-time setup per AWS account).
  2. Create an IAM role with a trust policy scoped to your specific repo and main branch.
  3. Update your GitHub Actions workflow to use aws-actions/configure-aws-credentials with role-to-assume, removing any stored access key secrets.
  4. Confirm terraform plan runs 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 + the provider = aws.alias_name meta-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

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.

🔒 Locked
Sign in and unlock the full syllabus to keep reading.
Sign in to continue
← State Management