How to Use AWS Assume Role with Terraform Apply: Best Practices and Common Pitfalls
When executing terraform apply with an AWS assume role configuration, always use the nested assume_role block with a valid role_arn, ensure your source credentials have sts:AssumeRole permissions, and align your backend and provider configurations to avoid permission mismatches during state operations.
Terraform’s AWS provider and remote-state backends support assuming an IAM role via the assume_role block. According to the hashicorp/terraform source code, the provider constructs an STS AssumeRole request using your initial credentials—whether from environment variables, shared credentials files, or EC2 instance profiles—and then uses the returned temporary credentials for all subsequent AWS API calls.
How AWS Assume Role Works in Terraform
Provider Schema and Validation
In internal/backend/remote-state/s3/backend.go (lines 298‑324), Terraform defines the assumeRoleSchema as a single-nested attribute. The only required field is role_arn; optional fields include duration, session_name, policy, policy_arns, tags, transitive_tag_keys, and external_id.
Before calling STS, Terraform validates the configuration. The validation logic (lines 306‑311) enforces that the ARN conforms to IAM role ARN syntax and that the session duration falls within the AWS-mandated 15‑minute to 12‑hour window.
The Assume Role Request Flow
When you run terraform apply, the provider initializes an STS client using your source credentials. It then calls AssumeRole (or AssumeRoleWithWebIdentity for OIDC scenarios), passing the parameters from your assume_role block. The AWS SDK returns temporary credentials—an access key, secret key, and session token—which Terraform caches and uses for every subsequent resource API call.
Remote State Backend Considerations
The S3 backend also supports role assumption for state operations. In internal/backend/remote-state/s3/backend.go (lines 250‑253), the backend schema includes the same assume_role block. A common mistake is configuring the provider to assume a deployment role while leaving the backend to use default credentials, causing permission errors during state locking or writing.
Common Pitfalls When Running Terraform Apply with Assume Role
-
Missing or malformed
role_arn: The schema validator throws an error ifrole_arnis empty or does not match the IAM ARN format. Always use a fully-qualified ARN such asarn:aws:iam::123456789012:role/MyTerraformRole. -
Session duration outside the allowed range: AWS STS rejects durations below 15 minutes or above 12 hours. Specify a value like
1hor3600swithin this window. -
Using deprecated flat
assume_role_*keys: Legacy versions used top-level arguments likeassume_role_arn. These are deprecated in favor of the nested block and may be removed in future releases. Migrate to theassume_role { ... }syntax. -
Insufficient permissions on source credentials: The initial principal (user or instance role) must have
sts:AssumeRolepermission on the target role’s trust policy. Without this, Terraform cannot obtain temporary credentials. -
Incorrect
external_idor missing sessionpolicy: If the target role’s trust policy requires anExternalId(common for third-party access), you must setexternal_idin the block. Similarly, if the role requires a session policy, provide thepolicyorpolicy_arnsfields. -
Tag or transitive-tag misconfiguration: Tags must be a map of strings. Supplying complex types or incorrect syntax causes schema validation errors before any AWS API call is made.
-
Backend and provider role mismatch: Configuring
assume_rolein the provider but not in the S3 backend leads to scenarios where Terraform can create resources but fails to write state. Align both configurations or ensure the backend uses credentials with sufficient permissions.
Best Practices for AWS Assume Role Configuration
-
Define the role ARN explicitly in the nested
assume_roleblock rather than relying on variables that might evaluate to empty strings. -
Keep session duration within limits—use values between
15mand12h(e.g.,duration = "1h"). -
Prefer the nested block syntax over deprecated flat arguments:
provider "aws" { region = "us-east-1" assume_role { role_arn = "arn:aws:iam::123456789012:role/TerraformDeploy" session_name = "terraform" external_id = "my-external-id" duration = "1h" policy = data.aws_iam_policy_document.session.json tags = { Owner = "infra-team" } transitive_tag_keys = ["Owner"] } } -
Configure the S3 backend with the same role if your state bucket resides in a different account or requires specific permissions:
terraform { backend "s3" { bucket = "my-terraform-state" key = "prod/terraform.tfstate" region = "us-east-1" assume_role { role_arn = "arn:aws:iam::123456789012:role/TerraformState" } } } -
Validate source credential permissions—ensure the initial principal has
sts:AssumeRolein the target role’s trust policy. -
Use IAM session policies to scope down permissions for the Terraform run rather than granting overly permissive roles.
-
Avoid mixing deprecated
assume_role_*keys—migrate to the nested block to prevent configuration drift. -
Pin the AWS provider version to a known-good release (e.g.,
>= 5.0, < 6.0) to ensure consistent behavior with theassume_roleschema.
Code Examples
Simple Provider Assume Role Block
provider "aws" {
region = "us-west-2"
assume_role {
role_arn = "arn:aws:iam::111122223333:role/terraform-ci"
session_name = "ci-run"
duration = "30m"
}
}
Provider with Inline Session Policy
data "aws_iam_policy_document" "session" {
statement {
actions = ["s3:*"]
resources = ["arn:aws:s3:::my-bucket/*"]
}
}
provider "aws" {
assume_role {
role_arn = "arn:aws:iam::111122223333:role/terraform-ci"
policy = data.aws_iam_policy_document.session.json
}
}
Remote State Backend Using the Same Role
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-west-2"
assume_role {
role_arn = "arn:aws:iam::111122223333:role/terraform-state"
}
}
}
Assume Role with Web Identity (OIDC)
provider "aws" {
assume_role_with_web_identity {
role_arn = "arn:aws:iam::111122223333:role/oidc-terraform"
web_identity_token_file = "/var/run/secrets/eks.amazonaws.com/serviceaccount/token"
session_name = "k8s-terraform"
}
}
Key Source Files in the Terraform Repository
| File | Relevant Content |
|---|---|
internal/backend/remote-state/s3/backend.go |
Defines the assumeRoleSchema (lines 298‑324) including all nested attributes like role_arn, duration, and external_id. |
internal/backend/remote-state/s3/backend.go |
Contains validation logic (lines 306‑311) that enforces ARN format and session duration limits before calling STS. |
internal/backend/remote-state/s3/backend.go |
Backend schema definition (lines 250‑253) showing how the S3 backend accepts the same assume_role block for state operations. |
internal/backend/remote-state/oss/backend.go |
Contains deprecated flat assume_role_* arguments (lines 229‑236) kept for backward compatibility but slated for removal. |
internal/backend/remote-state/cos/backend.go |
Example implementation showing how other backends parse the assume_role block using similar schema patterns. |
internal/backend/remote-state/s3/backend_test.go |
Test cases validating that the schema correctly enforces required fields and rejects malformed ARNs. |
These files demonstrate exactly how Terraform parses your assume_role configuration, validates parameters, and passes them to the AWS SDK, explaining why certain errors appear before any infrastructure is provisioned.
Summary
- Use the nested
assume_roleblock (not deprecated flat keys) in both provider and backend configurations to ensure consistent role assumption. - Validate the
role_arnformat and keepdurationbetween 15 minutes and 12 hours to avoid STS validation errors. - Align provider and S3 backend roles so that state operations use the same credentials as resource provisioning, preventing "access denied" errors during state locking.
- Grant
sts:AssumeRolepermissions to your source credentials and match anyexternal_idor session policy requirements defined in the target role’s trust policy. - Scope permissions with inline session policies rather than granting overly permissive roles, and pin your AWS provider version to avoid schema drift.
Frequently Asked Questions
What is the difference between assume_role and assume_role_with_web_identity in Terraform?
The assume_role block uses standard IAM user or role credentials to call STS AssumeRole, requiring the source principal to have sts:AssumeRole permission on the target role. The assume_role_with_web_identity block is designed for OIDC providers (such as EKS service accounts or GitHub Actions) and calls AssumeRoleWithWebIdentity, using a web identity token file instead of long-term AWS credentials. Both blocks are defined in the provider schema within internal/backend/remote-state/s3/backend.go.
Why does terraform apply fail with "AccessDenied" even though my IAM role has full permissions?
This typically occurs when the source credentials—the ones Terraform uses before assuming the role—lack permission to call sts:AssumeRole on the target role. Check the trust policy of the assumed role to ensure it allows the source principal (user or instance role) to assume it. Additionally, verify that you have specified any required external_id or session policy parameters that the trust policy enforces.
Can I use different assume role configurations for the provider and the S3 backend?
Yes, you can configure distinct assume_role blocks in the provider configuration (for resource provisioning) and the S3 backend configuration (for state storage). However, doing so requires that both roles have appropriate permissions: the provider role needs access to create and modify resources, while the backend role needs s3:GetObject, s3:PutObject, and dynamodb:* permissions for state locking. Misalignment between these roles is a common source of "access denied" errors during state operations.
How do I troubleshoot session duration errors when assuming a role in Terraform?
If you encounter errors stating the session duration is invalid, ensure your duration value is between 15 minutes (15m) and 12 hours (12h). The validation logic in internal/backend/remote-state/s3/backend.go enforces these AWS STS limits before making the API call. If you need sessions longer than 12 hours, you must implement credential refreshing outside of Terraform or use a different authentication mechanism, as STS does not support longer durations for standard role assumption.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →