When working with IAM roles in Terraform, you’ll often need to give them certain permissions. Instead of putting everything directly into the role, you can simply attach policies that define what the role is allowed to do. This keeps things cleaner, easier to manage, and lets you reuse the same policies across different roles.
The aws_iam_role_policy_attachment
resource is part of the Terraform AWS provider that attaches a managed IAM policy to an IAM role. It creates a link between an existing IAM role and a standalone IAM policy (AWS-managed or customer-managed).
resource "aws_iam_role_policy_attachment" "example" {
role = aws_iam_role.example.name
policy_arn = aws_iam_policy.example.arn
}
role
– (Required) The name of the IAM role to attach the policy to.policy_arn
– (Required) The ARN of the IAM policy you want to attach.
This resource is useful when separating IAM role and policy definitions to maintain modular Terraform configurations. Instead of embedding inline policies or defining them directly within the role, you can manage policies independently and attach them as needed. It only supports managed policies, not inline ones.
Additionally, avoid managing the same role–policy attachment in two places: don’t use both aws_iam_role_policy_attachment
and the managed_policy_arns
argument on aws_iam_role
for the same attachment, or Terraform will keep showing a diff.
In this example, we create an IAM role (s3_access_role
) that can be assumed by EC2 instances. This role itself doesn’t have any permissions until we attach a policy. The aws_iam_role_policy_attachment
resource is then used to attach the AWS-managed AmazonS3ReadOnlyAccess policy.
resource "aws_iam_role" "s3_access_role" {
name = "s3-access-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = "sts:AssumeRole",
Effect = "Allow",
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "s3_readonly_attach" {
role = aws_iam_role.s3_access_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
Using aws_iam_role_policy_attachment
makes it clear which managed policy is associated with which role, helping with visibility and modular Terraform code.
Here, instead of using a pre-defined AWS-managed policy, we define a custom policy (DynamoDBReadWrite
) that grants full read/write access to all DynamoDB tables. Then, we create an IAM role (lambda-dynamodb-role
) intended for AWS Lambda functions and use aws_iam_role_policy_attachment
to connect this role with the custom policy.
resource "aws_iam_policy" "dynamodb_rw_policy" {
name = "DynamoDBReadWrite"
description = "Custom policy for read/write access to DynamoDB"
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Scan",
"dynamodb:Query"
],
Resource = "*"
}
]
})
}
resource "aws_iam_role" "lambda_role" {
name = "lambda-dynamodb-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = "sts:AssumeRole",
Effect = "Allow",
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "lambda_dynamodb_attach" {
role = aws_iam_role.lambda_role.name
policy_arn = aws_iam_policy.dynamodb_rw_policy.arn
}
This approach is ideal when you need fine-grained permissions tailored to your application’s needs while keeping the policy separate so it can be reused across roles. It also helps security audits since permissions are centralized in managed policies instead of scattered across inline role definitions.
When building microservices or containerized apps, you often need roles with multiple, distinct capabilities. Attaching multiple policies via separate aws_iam_role_policy_attachment
resources gives you modularity, reusability, and fine-grained Terraform control.
ECS tasks often need access to multiple AWS services simultaneously, such as storing logs in CloudWatch and reading data from S3.
resource "aws_iam_role" "ecs_task_role" {
name = "ecs-task-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = "sts:AssumeRole",
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role" "ecs_execution_role" {
name = "ecs-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = "sts:AssumeRole",
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "ecs_exec_attach" {
role = aws_iam_role.ecs_execution_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
# Attach AWS-managed S3 Read-only policy to the TASK role
resource "aws_iam_role_policy_attachment" "ecs_s3_attach" {
role = aws_iam_role.ecs_task_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
In this example, we define an IAM role for ECS tasks (ecs-task-role). We attach S3 read-only to the task role, and move logging/image-pull permissions to the separate execution role using AmazonECSTaskExecutionRolePolicy
(instead of attaching a broad CloudWatchLogsFullAccess
policy to the task role).
AmazonECSTaskExecutionRolePolicy
→ for the execution role to pull images and write logs (least-privilege for logging and runtime needs).AmazonS3ReadOnlyAccess
→ allows the tasks to safely fetch objects from S3 without modification
By separating attachments into distinct resources, Terraform tracks and manages each policy attachment individually. This ensures clean separation of concerns, avoids confusion in audits, and makes it easy to add or remove policies later without touching the others.
Note: Avoid *FullAccess
policies unless you truly need them; prefer narrower managed or custom policies.
Managed policies are a better choice when you want permissions that can be reused across multiple roles, and the aws_iam_role_policy_attachment
resource makes that easy to manage. It works with both AWS-managed and custom-managed policies, and helps keep your Terraform code modular and clean by avoiding inline policies scattered across different roles.
We encourage you to explore how Spacelift makes it easy to work with Terraform. If you need help managing your Terraform infrastructure, building more complex workflows based on Terraform, and managing AWS credentials per run, instead of using a static pair on your local machine, Spacelift is a fantastic tool for this.
If you want to learn more about Spacelift, create a free account today or book a demo with one of our engineers.
Note: New versions of Terraform are placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that expands on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6.
Manage Terraform better with Spacelift
Build more complex workflows based on Terraform using policy as code, programmatic configuration, context sharing, drift detection, resource visualization and many more.