Terraform supports creating IAM roles via the aws_iam_role
resource, where you define the trust policy (assume role policy) and attach policies either inline or via aws_iam_role_policy_attachment
.
In this article, we will look at what an IAM (Identity and Access Management) role in AWS (Amazon Web Services) is, and show a step-by-step example of how to create one using Terraform. We will cover:
An IAM role is an IAM identity with specific permissions that you can create in your account. This security principle enables you to delegate permissions to AWS resources to entities within your AWS account. It is a way to manage access to AWS services and resources without sharing long-term credentials, such as access keys or passwords.
IAM roles are typically used to grant AWS services, applications, or other AWS accounts permissions. By assuming an IAM role, an entity can temporarily acquire the permissions associated with that role.
For example, IAM roles are commonly used in scenarios where EC2 instances access other AWS services, allow AWS Lambda functions to access specific resources, or grant cross-account access.
When creating an IAM role, you can define the set of permissions and policies that specify what actions are allowed or denied. These policies can be based on AWS-managed policies, customer-managed policies, or inline policies. IAM roles can be assigned to entities such as IAM users, AWS services, or even external identities federated through identity providers like Active Directory or SAML.
Note that no roles are created by default when you first create your AWS account.
Check out our detailed guide to AWS IAM Roles and AWS IAM best practices.
Using Terraform to manage IAM roles provides consistent, scalable, and auditable infrastructure management. It allows teams to define IAM configurations as code, improving control and reducing manual errors. The key benefits include:
- Version control: IAM roles are stored as code in Git, enabling rollback, change tracking, and peer review.
- Automation: Roles and policies can be provisioned automatically during infrastructure deployment, avoiding manual configuration in the cloud console.
- Consistency: Ensures identical IAM configurations across environments (dev, staging, production).
- Modularity: Reusable modules help standardize role creation across teams or accounts.
- Auditability: Changes to IAM policies and roles are tracked as part of infrastructure code, supporting compliance and security reviews.
- Validation: Plan and apply phases allow users to preview IAM changes before they are made, reducing misconfigurations.
Let’s see what we need before we start:
- An Amazon Web Service (AWS) account. You can get a free-tier account if you don’t have one. The free tier includes 750 hours of Linux and Windows t2.micro instances (t3.micro for the regions in which t2.micro is unavailable) each month for one year. To stay within the Free Tier, use only EC2 Micro instances. The example in the article uses t2.micro.
- Terraform installed. The step-by-step guide below uses the latest version available, v1.52 (at the time of writing).
- A code editor such as VSCode.
Terraform is commonly used to manage IAM roles in cloud environments like AWS, GCP, and Azure. It enables consistent, automated access control across infrastructure.
In this example, we will use the Amazon Web Services (AWS) Terraform provider to interact with the many resources AWS supports. We will create an IAM role and assign it to a newly created EC2 instance that will allow it read-only access to a newly created S3 storage bucket.
Step 1: Configure the AWS provider
Before you can use the provider, you must configure it with the proper credentials. To authenticate with AWS, you can use one of several methods.
- Credentials can be provided by adding an access_key, secret_key, and optionally token, to the AWS provider block. Hard-coded credentials are not recommended in any Terraform configuration and risk secret leakage should this file ever be committed to a public version control system.
provider "aws" {
region = "us-east-1"
access_key = "my-access-key"
secret_key = "my-secret-key"
}
- A better way to authenticate when testing locally is to use environment variables.
$ export AWS_ACCESS_KEY_ID="my-access-key"
$ export AWS_SECRET_ACCESS_KEY="my-secret-key"
$ export AWS_REGION="us-east-1"
- If you have the AWS Command Line Interface (CLI) installed, you can run
aws configure
and enter the access key ID, secret access key, and default region. Terraform will automatically use these credentials. I will use this method in my example.
Step 2: Obtain your Access key ID and secret access key for your AWS user
Next, obtain your Access key ID and secret access key for your AWS user.
If you don’t have one, follow the steps below to retrieve one:
- Sign in to the AWS Management Console using your AWS account credentials.
- Open the IAM (Identity and Access Management) service from the console.
- In the IAM dashboard, navigate to “Users” in the left-hand navigation pane.
- Select the IAM user to generate or retrieve the Access Key ID and Secret Access Key.
- Under the “Security credentials” tab for the selected user, you will find a section labeled “Access keys.”
- If the user has no access keys, you can click on the “Create access key” button to generate a new pair of access keys.
- After creating or selecting an existing access key, you can view the Access Key ID. To view the Secret Access Key, click on the “Show” button in the “Secret Access Key” column.
Record the Secret Access Key immediately, as AWS will only show it once. If you lose the Secret Access Key, you must generate a new one.
Note: Your user will need permission to put the S3 access policy on the bucket. To check if those are in place, check the link here.
Step 3: Run aws configure
Run aws configure
in the console to authenticate. When prompted, enter your AWS Access key ID and secret access key.
Step 4: Create a new main.tf file
Open your code editor and create a new file called main.tf
.
Step 5: Set the IAM role
Copy and paste the following code into the file and save it.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_iam_role" "example_role" {
name = "examplerole"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_iam_role_policy_attachment" "example_attachment" {
role = aws_iam_role.example_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
resource "aws_iam_instance_profile" "example_profile" {
name = "example_profile"
role = aws_iam_role.example_role.name
}
resource "aws_instance" "example_instance" {
ami = "ami-06ca3ca175f37dd66"
instance_type = "t2.micro"
iam_instance_profile = aws_iam_instance_profile.example_profile.name
tags = {
Name = "exampleinstance"
}
}
resource "aws_s3_bucket_policy" "example_bucket_policy" {
bucket = "example-bucket"
policy = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${aws_iam_role.example_role.name}"
},
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::example-bucket",
"arn:aws:s3:::example-bucket/*"
]
}
]
})
}
data "aws_caller_identity" "current" {}
What this will do:
- Add the provider block to declare we will use the AWS provider and set the region (us-east-1, in the example, is North Virginia).
- Create an IAM role named “example-role” using the
aws_iam_role
resource. Theassume_role_policy
attribute specifies the trust policy, allowing EC2 instances to assume this role. - Attach an AWS-managed policy, “AmazonS3ReadOnlyAccess,” to the role using the
aws_iam_role_policy_attachment
resource. - Create the EC2 instance using the
aws_instance
resource. Theami
attribute is set to the desired Amazon Machine Image (AMI) ID. The AMI used in the example is an Amazon Linux 2023 AMI eligible for the free tier at the time of writing. To check available AMIs, log into the console at https://console.aws.amazon.com/ec2/ and click on ‘AMI Catalog’ under the Instances section.
- To associate the IAM role with the EC2 instance, we use the
iam_instance_profile
attribute and provide the name of the IAM role created earlier,aws_iam_role.example_role.name
.
Step 6: Run terraform init
With the configuration files created, open a console and type terraform init
to initialize the Terraform code.
Step 7: Run terraform validate
Run terraform validate
to check the syntax of your configuration is valid.
Step 8: Run terraform plan
In the console type terraform plan
to generate the plan. Review it and check that you are happy the plan matches what you expect to happen.
Step 9: Run terraform apply
Run terraform apply
to apply the configuration to AWS.
Step 10: See the new resources in AWS
Check the AWS for the newly created resources. You should see the IAM role, S3 bucket, and EC2 instance.
Under the EC2 instance security tab, you can see the role has been assigned.
Step 11: Clean up
To clean up, run terraform destroy
to remove all the resources in your Terraform configuration.
Check out also how to create AWS IAM policy using Terraform.
You can import existing AWS IAM roles into Terraform using the terraform import
CLI command. You can either reference the role using a data source (data "aws_iam_role"
) for read-only access, or manage it using a resource (resource "aws_iam_role"
) block for full control.
The import links the real IAM role to your Terraform state without modifying the role until a terraform apply
.
- Identify the IAM role you want to import. For example:
IAM role name: MyExistingRole
If the role has a path (e.g., service-role/), include it in the name during import:
service-role/MyExistingRole
- Define the IAM role resource in Terraform. If you want to fully manage the role with Terraform, define a resource block like:
resource "aws_iam_role" "existing" {
name = "MyExistingRole" # Must match the actual name
# The block below must match the live assume role policy
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Action = "sts:AssumeRole",
Effect = "Allow",
Principal = {
Service = "ec2.amazonaws.com"
}
}]
})
}
The assume_role_policy
must match exactly what exists in AWS, or Terraform will attempt to update it. You can fetch it using a data source or AWS CLI.
- Use the
terraform import
command:
terraform import aws_iam_role.existing MyExistingRole
If the role has a path:
terraform import aws_iam_role.existing service-role/MyExistingRole
- Run
terraform plan
.Terraform will compare your code with the imported state. If discrepancies exist between the defined resource and the real IAM role, Terraform will display a diff and potentially propose changes.After importing, inspect the current state using:
terraform state show aws_iam_role.existing
To avoid unnecessary diffs, align your Terraform definitions with the current state, or consider:
lifecycle {
ignore_changes = [assume_role_policy]
}
Referencing an IAM role without managing it
If you only need to read attributes (like arn
, role_id
, or assume_role_policy
) and don’t want Terraform to manage the IAM role, use a data block instead:
data "aws_iam_role" "existing" {
name = "MyExistingRole"
}
This does not require an import and is safe for referencing externally managed or manually created roles.
When creating IAM roles with Terraform, focus on minimizing permissions, isolating trust policies, and structuring configurations for clarity and reusability in AWS environments.
- Separate role and permissions logic: Define the
aws_iam_role
with a minimalassume_role_policy
, then attach permissions via aws_iam_policy andaws_iam_role_policy_attachment
. This prevents tight coupling and improves auditability. - Use principals precisely: Explicitly define trusted entities (e.g., EC2, Lambda) in the trust policy using correct service principals like
"Service": "lambda.amazonaws.com"
. - Avoid inline policies for shared logic: Prefer
aws_iam_policy
resources to centralize reusable permissions across roles instead of duplicating inline blocks. - Parameterize with constraints: To prevent misconfiguration, use variables with input validation (
validation
blocks) for ARNs, paths, or tags. - Modularize role patterns: Wrap IAM configurations in Terraform modules for roles with consistent patterns (e.g., ECS task roles), enabling versioned reuse across environments.
- Avoid wildcards in permissions: Replace overly broad statements like
"Action": "*"
with specific API actions to tighten access control.
Read more: 20 Best Practices for Using Terraform
Terraform is really powerful, but to achieve an end-to-end secure GitOps approach, you need to use a product that can run your Terraform workflows. Spacelift takes managing Terraform to the next level by giving you access to a robust CI/CD workflow and unlocking features such as:
- Policies (based on Open Policy Agent) – You can control how many approvals you need for runs, what kind of resources you can create, and what kind of parameters these resources can have, and you can also control the behavior when a pull request is open or merged.
- Multi-IaC workflows – Combine Terraform with Kubernetes, Ansible, and other infrastructure-as-code (IaC) tools such as OpenTofu, Pulumi, and CloudFormation, create dependencies among them, and share outputs
- Build self-service infrastructure – You can use Blueprints to build self-service infrastructure; simply complete a form to provision infrastructure based on Terraform and other supported tools.
- Integrations with any third-party tools – You can integrate with your favorite third-party tools and even build policies for them. For example, see how to integrate security tools in your workflows using Custom Inputs.
Spacelift enables you to create private workers inside your infrastructure, which helps you execute Spacelift-related workflows on your end. Read the documentation for more information on configuring private workers.
You can check it out for free by creating a trial account or booking a demo with one of our engineers.
An IAM role is a secure way to manage and delegate permissions in AWS, allowing you to control access to resources without sharing long-term credentials and providing a mechanism for temporary permissions for trusted entities.
IAM roles can be created using Terraform and assigned to resources as shown in the step-by-step tutorial.
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.
Frequently asked questions
How do I create an IAM role in Terraform?
To create an IAM role in Terraform, define an aws_iam_role resource with a trust policy that specifies which service or principal can assume the role. You typically also attach one or more IAM policies to define permissions.
You can then attach policies using aws_iam_role_policy
or aws_iam_role_policy_attachment
.
How do I attach an AWS managed policy to a Terraform IAM role?
To attach an AWS managed policy to an IAM role in Terraform, use the aws_iam_role_policy_attachment
resource and reference the managed policy by its ARN. For example:
resource "aws_iam_role_policy_attachment" "example" {
role = aws_iam_role.my_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
Ensure the IAM role is defined beforehand using aws_iam_role. Managed policies must use the full AWS ARN.
What’s the difference between aws_iam_role_policy and aws_iam_role_policy_attachment?
aws_iam_role_policy
defines an inline policy embedded directly within an IAM role. It’s tightly coupled to that role and managed entirely through Terraform.
aws_iam_role_policy_attachment
attaches a standalone managed policy (AWS-managed or customer-managed) to a role. It references an existing aws_iam_policy
resource or AWS-managed ARN, keeping policy and role definitions separate.
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.