IAM Role, Policy Document, and Instance Profile
Scenario
Your EC2 instances need to read objects from a specific S3 bucket and send messages to an SQS queue, but you do not want to store AWS access keys on the servers. The correct AWS approach is to use an IAM Role attached to the instance via an Instance Profile.
Define the role, write a least-privilege policy that grants only the specific permissions needed, and create an instance profile so EC2 can assume the role automatically.
- Create an aws_iam_role with a trust policy allowing ec2.amazonaws.com to assume it.
- Use aws_iam_policy_document data source to write a least-privilege policy granting s3:GetObject and sqs:SendMessage.
- Create an aws_iam_policy from the policy document and attach it to the role.
- Create an aws_iam_instance_profile wrapping the role.
- Output the role ARN and instance profile name.
Additional Context
The aws_iam_policy_document data source generates a properly structured JSON IAM policy as a Terraform value — you never write raw JSON strings. This is safer (Terraform validates the structure) and more readable. The resulting JSON string is passed to aws_iam_policy.policy.
Use variables for the S3 bucket ARN and SQS queue ARN so this role definition can be reused across environments by changing only the .tfvars file.
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}variables.tf
variable "aws_region" { type = string; default = "us-east-1" }
variable "role_name" { type = string; default = "app-server-role" }
variable "s3_bucket_arn" { type = string; description = "ARN of the S3 bucket to grant read access to." }
variable "sqs_queue_arn" { type = string; description = "ARN of the SQS queue to grant send access to." }
variable "environment" { type = string; default = "dev" }main.tf
# main.tf
provider "aws" {
region = var.aws_region
}
# Trust policy — defines WHO can assume this role.
# In this case, the EC2 service is allowed to call sts:AssumeRole.
data "aws_iam_policy_document" "assume_role" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
}
}
# The IAM role itself — an identity that AWS services (EC2 here) can assume.
resource "aws_iam_role" "app_server" {
name = var.role_name
assume_role_policy = data.aws_iam_policy_document.assume_role.json
tags = { Environment = var.environment, ManagedBy = "terraform" }
}
# Permissions policy — defines WHAT actions the role can perform.
# Always follow least privilege: only grant what the application needs.
data "aws_iam_policy_document" "app_permissions" {
# Allow reading objects from a specific S3 bucket only.
statement {
sid = "AllowS3Read"
effect = "Allow"
actions = ["s3:GetObject", "s3:ListBucket"]
resources = [
var.s3_bucket_arn,
"${var.s3_bucket_arn}/*" # bucket-level and object-level permissions required
]
}
# Allow sending messages to a specific SQS queue only.
statement {
sid = "AllowSQSSend"
effect = "Allow"
actions = ["sqs:SendMessage", "sqs:GetQueueAttributes"]
resources = [var.sqs_queue_arn]
}
}
# Create the named policy from the document.
resource "aws_iam_policy" "app_permissions" {
name = "${var.role_name}-permissions"
description = "Least-privilege policy for app server: S3 read + SQS send"
policy = data.aws_iam_policy_document.app_permissions.json
}
# Attach the policy to the role.
resource "aws_iam_role_policy_attachment" "app_permissions" {
role = aws_iam_role.app_server.name
policy_arn = aws_iam_policy.app_permissions.arn
}
# Instance Profile — the wrapper that lets EC2 use an IAM role.
# You attach the profile (not the role directly) to an EC2 instance.
resource "aws_iam_instance_profile" "app_server" {
name = "${var.role_name}-profile"
role = aws_iam_role.app_server.name
}outputs.tf
output "role_arn" { value = aws_iam_role.app_server.arn }
output "instance_profile_name" { value = aws_iam_instance_profile.app_server.name }
output "policy_arn" { value = aws_iam_policy.app_permissions.arn }Workflow Commands
terraform init
# Supply the required ARNs via -var flags or a .tfvars file
terraform plan \
-var="s3_bucket_arn=arn:aws:s3:::my-app-bucket" \
-var="sqs_queue_arn=arn:aws:sqs:us-east-1:123456789012:my-queue"
terraform apply -var-file="dev.tfvars"
# Verify the role and its attached policies
aws iam list-attached-role-policies \
--role-name $(terraform output -raw role_arn | cut -d/ -f2)
terraform destroy -var-file="dev.tfvars"Apply complete! Resources: 4 added, 0 changed, 0 destroyed. Outputs: instance_profile_name = "app-server-role-profile" policy_arn = "arn:aws:iam::123456789012:policy/app-server-role-permissions" role_arn = "arn:aws:iam::123456789012:role/app-server-role"

