# How to Use AWS Assume Role with Terraform Apply: Best Practices and Common Pitfalls

> Master AWS Assume Role with Terraform Apply. Learn best practices for role assumption and avoid common pitfalls to ensure successful resource provisioning and state operations.

- Repository: [HashiCorp/terraform](https://github.com/hashicorp/terraform)
- Tags: best-practices
- Published: 2026-02-19

---

**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`](https://github.com/hashicorp/terraform/blob/main/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`](https://github.com/hashicorp/terraform/blob/main/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 if `role_arn` is empty or does not match the IAM ARN format. Always use a fully-qualified ARN such as `arn: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 `1h` or `3600s` within this window.

- **Using deprecated flat `assume_role_*` keys**: Legacy versions used top-level arguments like `assume_role_arn`. These are deprecated in favor of the nested block and may be removed in future releases. Migrate to the `assume_role { ... }` syntax.

- **Insufficient permissions on source credentials**: The initial principal (user or instance role) must have `sts:AssumeRole` permission on the target role’s trust policy. Without this, Terraform cannot obtain temporary credentials.

- **Incorrect `external_id` or missing session `policy`**: If the target role’s trust policy requires an `ExternalId` (common for third-party access), you must set `external_id` in the block. Similarly, if the role requires a session policy, provide the `policy` or `policy_arns` fields.

- **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_role` in 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

1. **Define the role ARN explicitly** in the nested `assume_role` block rather than relying on variables that might evaluate to empty strings.

2. **Keep session duration within limits**—use values between `15m` and `12h` (e.g., `duration = "1h"`).

3. **Prefer the nested block syntax** over deprecated flat arguments:

   ```hcl
   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"]
     }
   }
   ```

4. **Configure the S3 backend with the same role** if your state bucket resides in a different account or requires specific permissions:

   ```hcl
   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"
       }
     }
   }
   ```

5. **Validate source credential permissions**—ensure the initial principal has `sts:AssumeRole` in the target role’s trust policy.

6. **Use IAM session policies** to scope down permissions for the Terraform run rather than granting overly permissive roles.

7. **Avoid mixing deprecated `assume_role_*` keys**—migrate to the nested block to prevent configuration drift.

8. **Pin the AWS provider version** to a known-good release (e.g., `>= 5.0, < 6.0`) to ensure consistent behavior with the `assume_role` schema.

## Code Examples

### Simple Provider Assume Role Block

```hcl
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

```hcl
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

```hcl
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)

```hcl
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`](https://github.com/hashicorp/terraform/blob/main/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`](https://github.com/hashicorp/terraform/blob/main/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`](https://github.com/hashicorp/terraform/blob/main/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`](https://github.com/hashicorp/terraform/blob/main/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`](https://github.com/hashicorp/terraform/blob/main/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`](https://github.com/hashicorp/terraform/blob/main/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_role` block** (not deprecated flat keys) in both provider and backend configurations to ensure consistent role assumption.
- **Validate the `role_arn` format** and keep `duration` between 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:AssumeRole` permissions** to your source credentials and match any `external_id` or 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`](https://github.com/hashicorp/terraform/blob/main/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`](https://github.com/hashicorp/terraform/blob/main/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.