Terraform AWS IAM - How to Create IAM Users, Roles and Policies (with aws_iam_policy_document examples)

This blog post is going to be a comprehensive guide on managing AWS IAM Users, IAM Roles and IAM Policies using Terraform.

Managing IAM Users, Roles and Policies is always crucial from a security perspective. Proper management of the IAM User, IAM Roles and IAM Policies can really reduce the operational burden as well as improve the security of your AWS environment. And once it is in Terraform, every permission change goes through a pull request where somebody can review it - which is exactly what an auditor wants to see.

In this blog post, we are going to use Terraform to manage the IAM User, IAM Roles and IAM Policies. By the end of the post we will have built the following, all with Terraform -

  1. An IAM User - Alice
  2. An IAM Role - AliceRole, which EC2 is allowed to assume
  3. An IAM Policy - AllowS3Access, written with the aws_iam_policy_document data source
  4. The attachments between them, an IAM instance profile, and an EC2 instance using that profile

Table of Content

  1. Prerequisites
  2. Understanding IAM Users, Roles and Policies
  3. Create an IAM User(aws_iam_user) - Alice
  4. Create an IAM Role(aws_iam_role) - alice_role
  5. Create IAM Policy(aws_iam_policy) - AllowS3Access
  6. Create IAM Role and Policy attachment
  7. Create an aws_s3_bucket
  8. Create AWS IAM Instance Profile (aws_iam_instance_profile) - AliceProfile
  9. Create IAM user(aws_iam_user) and IAM Policy(aws_iam_policy) attachment
  10. Attaching IAM Instance profile to EC2 instance
  11. IAM Groups, AWS managed policies and inline policies with Terraform
  12. Common IAM errors with Terraform
  13. Conclusion

1. Prerequisites

1. An active AWS account and a basic understanding of AWS services - Before you begin and start following the steps mentioned in this blog post, you must have an active AWS account.

If you are using AWS for the first time, then you can use the free tier, and you should be able to perform all the steps mentioned in this blog post without any cost (IAM itself is free, and the t3.micro instance in Step 10 is covered by the free tier).

2. Terraform is installed on your machine - This complete blog post is focused around setting up the IAM User, IAM Roles and IAM Policies using Terraform, so you must install the Terraform CLI onto your working machine.

Please refer to this guide on - How to install Terraform

3. AWS credentials for Terraform - The AWS provider needs credentials with permission to manage IAM. Do not hard-code them in the .tf file. Use an AWS CLI profile, environment variables or (the best option) an IAM role. I have written a dedicated post on how to handle AWS credentials in Terraform.

4. Familiarity with Terraform syntax (HCL) - This particular blog requires you to be familiar with Terraform. If this is the first time you are trying to learn Terraform, then I would highly recommend building your understanding by following this tutorial series: - On getting started with Terraform

Here is the provider block which we are going to use for all the examples -

 1terraform {
 2  required_version = ">= 1.5"
 3
 4  required_providers {
 5    aws = {
 6      source  = "hashicorp/aws"
 7      version = "~> 6.0"
 8    }
 9  }
10}
11
12provider "aws" {
13  region = "eu-central-1"
14}

2. Understanding IAM Users, Roles and Policies

Before we start writing Terraform code, it is really important to understand the three building blocks of IAM, because people (including me, in the beginning) mix them up all the time -

1. IAM User - An identity for a person or an application that needs long-term credentials - a console password and/or access keys. Alice is an IAM user.

2. IAM Role - An identity that does not have permanent credentials. Instead it is assumed for a short time by a user, an AWS service (like EC2 or Lambda) or another AWS account. Every role has two policies -

  • A trust policy (assume_role_policy) - who is allowed to assume this role
  • One or more permission policies - what the role can do once assumed

Here are some benefits that you get with IAM roles -

  • No secrets to leak - An EC2 instance with a role gets temporary credentials automatically, there is no access key to put in a config file.
  • Granular - You can create one role per workload, each with exactly the permissions it needs.
  • Scalable - When you manage IAM roles with Terraform, any developer can propose a permission change from the code.

3. IAM Policy - A JSON document that defines the permissions. A policy on its own does nothing - it has to be attached to a user, a group or a role. Here are the types of policies that exist -

  • AWS managed policies - Written and maintained by AWS, e.g. AmazonS3ReadOnlyAccess. Quick to use, but usually broader than you need.
  • Customer managed policies - Written by you (aws_iam_policy). Reusable, can be attached to many identities. This is what we will build.
  • Inline policies - Embedded directly in one user, group or role (aws_iam_role_policy, aws_iam_user_policy). Deleted together with the identity. Fine for a one-off permission that belongs to exactly one role.

3. Create an IAM User(aws_iam_user) - Alice

Let's start by creating a User which we call an IAM user. To keep our Terraform project code clean, I am going to create a file called iam-user.tf and put the following code inside the file.

1# Step 1 : Create an IAM user named Alice
2
3resource "aws_iam_user" "alice" {
4  name = "Alice"
5
6  tags = {
7    ManagedBy = "terraform"
8  }
9}
  1. The above code will create an IAM user named Alice.
  2. To run the above code, simply run the terraform init, terraform plan and terraform apply commands.
  3. After running the terraform code, you can verify the user by going into the AWS console -> IAM -> Access Management -> Users

Terraform create IAM user

What about access keys for Alice? You can create them with Terraform -

1resource "aws_iam_access_key" "alice" {
2  user = aws_iam_user.alice.name
3}
4
5output "alice_secret_key" {
6  value     = aws_iam_access_key.alice.secret
7  sensitive = true
8}

but be aware that the secret key will be stored in plain text inside your Terraform state file. If you do this, your state must be in a private, encrypted remote backend - read how to manage the Terraform state file and how to secure sensitive data in Terraform. For humans, AWS now recommends IAM Identity Center (SSO) instead of IAM users with access keys, and for applications running on AWS, roles (which we will create next) instead of keys.


4. Create an IAM Role(aws_iam_role) - alice_role

After creating the IAM user in Step-3, let's create an IAM Role. Here is the Terraform code for creating the IAM role -

 1resource "aws_iam_role" "alice_role" {
 2  name = "AliceRole"
 3
 4  assume_role_policy = jsonencode({
 5    Version = "2012-10-17"
 6    Statement = [
 7      {
 8        Action = "sts:AssumeRole"
 9        Effect = "Allow"
10        Principal = {
11          Service = "ec2.amazonaws.com"
12        }
13      }
14    ]
15  })
16}
  1. The above code will create an IAM role named AliceRole.
  2. To run the above code, simply run the terraform plan and terraform apply commands.
  3. After running the Terraform code, you can verify the role by going into the AWS console -> IAM -> Access Management -> Roles

Create IAM role by the name AliceRole

  1. Click on the AliceRole to view the details about the role, e.g. Creation Date, ARN, Instance Profile ARN etc.

IAM Role description for AliceRole

  1. To view the Trust relationships of the role click on the Trust relationships tab.

IAM role trust policy for AliceRole

Here is a detailed explanation of each line -

  • AliceRole - The name of the IAM role
  • assume_role_policy - This attribute sets the trust policy for the IAM role, which defines who can assume the role.
  • Version = "2012-10-17": The policy language version. It is always this date, it is not something you update.
  • Statement = [...]: An array of policy statements. In this example, there's only one statement.
  • Action = "sts:AssumeRole": The action to be allowed, which is to assume the IAM role.
  • Effect = "Allow": This indicates that the action is allowed.
  • Principal = { ... }: The principal (the entity that is allowed to assume the role). In this example, it is an AWS service.
  • Service = "ec2.amazonaws.com": The service that is allowed to assume the role. In this case, Amazon EC2.

The same trust policy with aws_iam_policy_document - Instead of jsonencode() you can write the policy in HCL with the aws_iam_policy_document data source. I prefer this on real projects because Terraform validates it for you at plan time -

 1data "aws_iam_policy_document" "ec2_assume_role" {
 2  statement {
 3    actions = ["sts:AssumeRole"]
 4
 5    principals {
 6      type        = "Service"
 7      identifiers = ["ec2.amazonaws.com"]
 8    }
 9  }
10}
11
12resource "aws_iam_role" "alice_role" {
13  name               = "AliceRole"
14  assume_role_policy = data.aws_iam_policy_document.ec2_assume_role.json
15}

Both versions produce exactly the same JSON. Pick one style and stick to it in your project.


5. Create IAM Policy(aws_iam_policy) - AllowS3Access

An AWS IAM policy is a set of rules that decides who can do what inside your AWS account. These rules make sure that people or programs can only get to the things they need and can't touch anything they shouldn't.

Here is the IAM Policy for AllowS3Access, written with the aws_iam_policy_document data source -

 1data "aws_iam_policy_document" "allow_s3_access" {
 2  statement {
 3    sid    = "AllowListBucket"
 4    effect = "Allow"
 5
 6    actions = [
 7      "s3:ListBucket"
 8    ]
 9
10    resources = [
11      "arn:aws:s3:::test-bucket-for-alice"
12    ]
13  }
14
15  statement {
16    sid    = "AllowReadWriteObjects"
17    effect = "Allow"
18
19    actions = [
20      "s3:GetObject",
21      "s3:PutObject"
22    ]
23
24    resources = [
25      "arn:aws:s3:::test-bucket-for-alice/*"
26    ]
27  }
28}
29
30resource "aws_iam_policy" "allow_s3_access" {
31  name        = "AllowS3Access"
32  description = "Allow Alice to access specific S3 resources"
33  policy      = data.aws_iam_policy_document.allow_s3_access.json
34}

Here is a detailed explanation of the above policy -

  • AllowS3Access - The name of the policy.
  • description - A meaningful description to tell more about the policy.
  • policy - The JSON string containing all the Actions and Resources, generated by the data source.
  • statement - I have split it into two statements, because s3:ListBucket applies to the bucket ARN while s3:GetObject/s3:PutObject apply to the objects (/*). Putting all three actions on both ARNs works too, but this is the correct and least-privilege way.
  • sid - An optional statement id. It makes the policy much easier to read in the console.
  • "arn:aws:s3:::test-bucket-for-alice": The Amazon Resource Name (ARN) of the S3 bucket itself.
  • "arn:aws:s3:::test-bucket-for-alice/*": All objects within the bucket.

If you prefer the jsonencode() style from the previous section, this is the equivalent -

 1resource "aws_iam_policy" "allow_s3_access" {
 2  name        = "AllowS3Access"
 3  description = "Allow Alice to access specific S3 resources"
 4
 5  policy = jsonencode({
 6    Version = "2012-10-17"
 7    Statement = [
 8      {
 9        Sid      = "AllowListBucket"
10        Effect   = "Allow"
11        Action   = ["s3:ListBucket"]
12        Resource = ["arn:aws:s3:::test-bucket-for-alice"]
13      },
14      {
15        Sid      = "AllowReadWriteObjects"
16        Effect   = "Allow"
17        Action   = ["s3:GetObject", "s3:PutObject"]
18        Resource = ["arn:aws:s3:::test-bucket-for-alice/*"]
19      }
20    ]
21  })
22}

How to Create the Policy?

  1. To create the policy, run the above code: simply run the terraform plan and terraform apply commands
  2. After running the Terraform code, you can verify the policy by going into the AWS console -> IAM -> Access Management -> Policies

AWS IAM Policy AllowS3Access

  1. Click on the AllowS3Access Policy and you will find all the details related to that policy.

AWS IAM S3 Policy with Read, List and Write permission


6. Create IAM Role and Policy attachment

So far the role AliceRole exists but it has no permissions - a role with only a trust policy can be assumed but cannot do anything. The permissions come from attaching a policy to the role.

Here is the IAM Role and Policy Attachment for the AliceRole and AllowS3Access policy -

1# IAM Role and IAM Policy Attachment
2
3resource "aws_iam_role_policy_attachment" "attach_s3_access" {
4  role       = aws_iam_role.alice_role.name
5  policy_arn = aws_iam_policy.allow_s3_access.arn
6}

Why is this the right way to do it, instead of writing the permissions inline inside the role?

1. One policy, many roles - AllowS3Access can be attached to AliceRole today and to a Lambda role tomorrow. You define the rules once.

2. Easy to change - If the bucket name changes, you update one aws_iam_policy and every role which has it attached gets the new permissions on the next apply.

3. Easy to audit - In the console you can open the policy and see exactly which users, groups and roles are using it.

Note - There is also an aws_iam_role_policy_attachments_exclusive resource in the AWS provider which makes sure the role has only the policies you list in Terraform and removes anything attached by hand in the console. It is really useful for locking down production roles.


7. Create an aws_s3_bucket

In the previous 6 steps, we have created the IAM User, IAM Role, IAM Policy and IAM Role Policy Attachment but we haven't yet created the S3 Bucket which the policy refers to.

Here is the Terraform code to create an S3 bucket -

 1# S3 Bucket creation by the name - test-bucket-for-alice
 2
 3resource "aws_s3_bucket" "example" {
 4  bucket = "test-bucket-for-alice"
 5
 6  tags = {
 7    Name        = "test-bucket-for-alice"
 8    Environment = "Dev"
 9  }
10}

Apply the change by running the Terraform commands -

  1. To create the S3 bucket, run the terraform plan and terraform apply commands.
  2. Here is the screenshot from my AWS Console after creating an S3 Bucket by the name - test-bucket-for-alice

S3 Bucket Creation

Tip - S3 bucket names are globally unique, so test-bucket-for-alice will most probably already be taken when you try it. Change the name in both the bucket resource and the policy - or better, reference it from the policy with aws_s3_bucket.example.arn and "${aws_s3_bucket.example.arn}/*" so that you only have to change it in one place.


8. Create AWS IAM Instance Profile (aws_iam_instance_profile) - AliceProfile

aws_iam_instance_profile is the Terraform resource used to create and manage an AWS IAM Instance Profile.

An instance profile is simply a container for a role that EC2 understands. You cannot attach a role to an EC2 instance directly - you attach an instance profile, and the instance profile holds the role. (If you create a role in the AWS console it silently creates an instance profile with the same name for you, which is why many people have never heard of it. In Terraform you create it explicitly.)

Here is the Terraform code to create an AWS IAM instance profile -

1# AWS IAM Instance Profile - AliceProfile
2
3resource "aws_iam_instance_profile" "alice_profile" {
4  name = "AliceProfile"
5  role = aws_iam_role.alice_role.name
6}

To create the AWS IAM instance profile, run the terraform plan and terraform apply commands.


9. Create IAM user(aws_iam_user) and IAM Policy(aws_iam_policy) attachment

We have set up everything that is needed: IAM User, IAM Role and IAM Policy, but we did not attach the IAM User to the IAM Policy yet.

Let's write the Terraform resource to attach the IAM User(aws_iam_user) to the IAM Policy(aws_iam_policy) -

1# Attaching the IAM user(aws_iam_user) to IAM Policy(aws_iam_policy)
2
3resource "aws_iam_user_policy_attachment" "alice_s3_access" {
4  user       = aws_iam_user.alice.name
5  policy_arn = aws_iam_policy.allow_s3_access.arn
6}

Attach IAM User(aws_iam_user) to IAM Policy(aws_iam_policy)

Here is the screenshot I have taken from the AWS console, which shows all the permissions associated with the IAM User - Alice

IAM user permissions for S3 bucket

Notice that the same policy AllowS3Access is now attached to both the user Alice and the role AliceRole. That is the whole point of a customer managed policy.


10. Attaching IAM Instance profile to EC2 instance

Alright, so far we have covered a lot of topics and been able to create the following -

  • IAM User : Alice
  • IAM Role : AliceRole
  • IAM Policy : AllowS3Access
  • IAM Role Policy Attachment : AliceRole + AllowS3Access
  • IAM Instance Profile : AliceProfile -> AliceRole
  • IAM User Policy Attachment : Alice + AllowS3Access

Next Task - Let's bind the IAM Instance Profile to an EC2 instance, and we will do it the Terraform way. But let's take a peek into the AWS Console first, so that we know where to find these settings to attach the IAM Instance Profile option.

IAM Instance Profile to Attach Role to EC2 Instance

Here is the Terraform code to provision the EC2 instance as well as attach the IAM Instance Profile -

(Note - Here is my GitHub Repository where you can find the complete project. If you are new to Terraform and want to learn more about it, then follow this post - How to Setup EC2 instance with Terraform)

 1# Look up the latest Ubuntu 24.04 AMI instead of hard-coding an AMI id
 2data "aws_ami" "ubuntu" {
 3  most_recent = true
 4  owners      = ["099720109477"] # Canonical
 5
 6  filter {
 7    name   = "name"
 8    values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
 9  }
10}
11
12# Setup EC2 instance with iam_instance_profile
13resource "aws_instance" "ec2_example" {
14  ami           = data.aws_ami.ubuntu.id
15  instance_type = "t3.micro"
16
17  key_name               = "aws_key"
18  vpc_security_group_ids = [aws_security_group.main.id]
19
20  # Attach IAM instance profile with EC2 instance
21  iam_instance_profile = aws_iam_instance_profile.alice_profile.name
22
23  tags = {
24    Name = "test-t3-micro"
25  }
26}

Here is the screenshot I have taken from my AWS console after provisioning the EC2 instance with aws_iam_instance_profile

Attach IAM instance profile to EC2 instance

Let's verify it actually works - SSH into the instance and run the AWS CLI without configuring any credentials. The instance picks up temporary credentials from the role through the instance metadata -

1ubuntu@ip-10-0-1-25:~$ aws sts get-caller-identity
2{
3    "UserId": "AROAEXAMPLE123456:i-0abc12345def67890",
4    "Account": "123456789012",
5    "Arn": "arn:aws:sts::123456789012:assumed-role/AliceRole/i-0abc12345def67890"
6}
7
8ubuntu@ip-10-0-1-25:~$ aws s3 ls s3://test-bucket-for-alice/

The assumed-role/AliceRole in the ARN tells you the instance is using the role, and the s3 ls works because of the AllowS3Access policy. No access keys anywhere.


11. IAM Groups, AWS managed policies and inline policies with Terraform

The Alice example covers the core, but on a real project you will need three more things very quickly -

1. IAM Groups - Do not attach policies to users one by one. Put users in a group and attach the policy to the group -

 1resource "aws_iam_group" "developers" {
 2  name = "Developers"
 3}
 4
 5resource "aws_iam_group_membership" "developers" {
 6  name  = "developers-membership"
 7  group = aws_iam_group.developers.name
 8  users = [aws_iam_user.alice.name]
 9}
10
11resource "aws_iam_group_policy_attachment" "developers_s3" {
12  group      = aws_iam_group.developers.name
13  policy_arn = aws_iam_policy.allow_s3_access.arn
14}

2. AWS managed policies - You do not need to write every policy yourself. To attach an AWS managed policy, you just need its ARN -

1resource "aws_iam_role_policy_attachment" "ssm_core" {
2  role       = aws_iam_role.alice_role.name
3  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
4}

(AmazonSSMManagedInstanceCore is the one I attach to almost every EC2 role, it lets you open a shell on the instance with Session Manager without any SSH key or open port 22.)

3. Inline policies - When a permission belongs to exactly one role and will never be reused, an inline policy keeps it next to the role -

 1resource "aws_iam_role_policy" "alice_logs" {
 2  name = "AllowCloudWatchLogs"
 3  role = aws_iam_role.alice_role.id
 4
 5  policy = jsonencode({
 6    Version = "2012-10-17"
 7    Statement = [
 8      {
 9        Effect   = "Allow"
10        Action   = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
11        Resource = "arn:aws:logs:*:*:*"
12      }
13    ]
14  })
15}

12. Common IAM errors with Terraform

1. MalformedPolicyDocument: Has prohibited field Resource - You put a Resource inside a trust policy (assume_role_policy). Trust policies only have Principal, not Resource. I have a full post on this one - Terraform MalformedPolicyDocument error.

2. EntityAlreadyExists: User with name Alice already exists - The user exists in AWS but not in your state file (created by hand, or state lost). Use terraform import - terraform import aws_iam_user.alice Alice.

3. DeleteConflict: Cannot delete entity, must detach all policies first - You removed the aws_iam_policy from Terraform while the attachment still exists. Remove the attachment resources first (or in the same apply - Terraform handles the order when both are in the code).

4. AccessDenied: User is not authorized to perform: iam:CreateRole - The credentials Terraform is using do not have IAM permissions. Whoever runs this needs IAMFullAccess or an equivalent policy.

5. The EC2 instance cannot access S3 even though the role has the policy - 90% of the time the instance profile was created but the role was not attached to it (role = ... missing), or you attached the role before the policy attachment finished. Run aws sts get-caller-identity on the instance as shown in Step 10 to see which role it is actually using.


13. Conclusion

I hope this blog post will help you get started with How to manage IAM Roles, IAM Policies and IAM Users with Terraform. All the steps mentioned in this blog are really necessary for setting up the correct IAM roles, IAM policies, and IAM permissions.

The three things I would ask you to take away -

  1. Write policies with aws_iam_policy_document so Terraform validates them, and keep them least privilege - separate statements for bucket and object ARNs.
  2. Prefer roles for anything running on AWS and groups for humans. Avoid long-lived access keys, and if you must create them with Terraform, protect the state file.
  3. Attach policies with the *_policy_attachment resources rather than inline, unless the permission truly belongs to one identity.

The same steps can be customised to set up other IAM roles, IAM policies, and IAM user permissions as well. If you have any questions or suggestions, then please put them in the comment section.


Read More - Terragrunt -

  1. How to use Terragrunt?