Terraform

How to Create AWS IAM Policy Using Terraform (Tutorial)

How to Create AWS IAM Policy Using Terraform

IAM policies lie at the heart of AWS access management. They are essentially a set of permissions that can be attached to an AWS identity or resource to manage its access.

Based on the permission set, policies determine what an identity or resource is allowed to do on AWS. They are primarily stored in JSON format and are accessible through the AWS IAM console.

For more information about policies, check out our AWS IAM Policies guide.

A simple policy that allows only one action i.e rds:CreateDBInstance (the RDS instance creation action) looks like this:

{
   "Version": "2012-10-17",
   "Statement": [
       {
           "Action": [
               "rds:CreateDBInstance"
           ],
           "Resource": "*",
           "Effect": "Allow"
       }
   ]
}

Before we get started with Terraform and create our own policies, let’s go over the general structure of policies.

Structure of a Policy

Despite the fact that there are various types of policies on AWS, all policies on AWS have the same structure, which includes a policy version and a list of statement(s). Let’s look at the purpose of each key in a well-formed policy.

  • Version: Version of the policy language to use. At the time of writing, the latest version is 2012–10–17.
  • Statement: Each statement represents a permission and has all its details.
  • Sid: An optional name for a statement.
  • Action: The actions that the statement allows or denies.
  • Effect: Indicates whether an action(s) is allowed or denied using the value “Allow” or “Deny”. 
  • Principal: Represents the identity to which the policy applies. (Useful in case of resource-based policies and not required in case of IAM permission policies).
  • Resource: Represents the resources to which the actions apply. (Optional in case of resource-based policies).
  • Condition: Specifies a condition that must be fulfilled for the statement to apply.

AWS supports a variety of policies, but as previously stated, the above structure applies to all of them.

IAM Policy Types

AWS supports six types of policies, namely:

  • Identity-based policies
  • Resource-based policies
  • Permissions boundaries
  • Organizations SCPs
  • ACLs
  • Session policies

This article will primarily focus on identity-based policies and how to create them using Terraform. These learnings can be extended to any other type of AWS-supported policy.

If you would like to set up the ability to sign in to your Spacelift account using a SAML 2.0 integration with AWS IAM Identity Center (formerly known as AWS SSO), check out our AWS IAM Identity SAML Setup Guide. Or learn more about Spacelift integration with AWS, with our new Cloud Integrations section and our update to support account-level AWS integrations.

Creating and Managing IAM Policies Using Terraform

Prerequisites

  1. An AWS admin user with the ability to create resources on AWS.
  2. At least one S3 bucket that can be used to check whether a policy with the s3:ListAllMyBucket action is properly associated with a user.
  3. Configuring the AWS credentials to be used with terraform.

Once the prerequisites are out of the way, we will quickly understand what identity-based policies are before diving into creating one with Terraform.

Identity-based Policies

Identity-based policies govern what actions an identity (User, Group, Role) can perform on which resources and under what conditions.

A simple identity-based policy, such as the one shown below, allows the identity to list all S3 buckets.

{
 "Version": "2012-10-17",
 "Statement": {
   "Effect": "Allow",
   "Action": "s3:ListAllMyBuckets",
   "Resource": "*"
 }
}

Now that we have understood what identity-based policies are, we will start by creating a user that will be the subject of our hands-on exercises on creating and managing IAM policies using Terraform.

Learn also how to create AWS IAM role with Terraform.

Creating a user using Terraform

  1. Create a file with the name as main.tf in an empty folder.
  2. Copy and paste the below HCL code snippet within this file.
    This code snippet creates an AWS user and outputs a randomly generated password that can be used to log in to AWS.
terraform {
 required_providers {
   aws = {
     source  = "hashicorp/aws"
     version = "~> 4.0"
   }
 }
}

provider "aws" {
 region = "us-east-1"
}

resource "aws_iam_user" "spacelift_user" {
 name = "spacelift-user"
}

resource "aws_iam_user_login_profile" "spacelift_user_login_profile" {
 user    = aws_iam_user.spacelift_user.name
}

output "password" {
 value = aws_iam_user_login_profile.spacelift_user_login_profile.password
}
  1. Initialize the backend and the required provider plugins using the terraform init command.
  1. Verify the changes by running the terraform plan command.
Terraform will perform the following actions:

 # aws_iam_user.spacelift_user will be created
 + resource "aws_iam_user" "spacelift_user" {
     + arn           = (known after apply)
     + force_destroy = false
     + id            = (known after apply)
     + name          = "spacelift-user"
     + path          = "/"
     + tags_all      = (known after apply)
     + unique_id     = (known after apply)
   }

 # aws_iam_user_login_profile.spacelift_user_login_profile will be created
 + resource "aws_iam_user_login_profile" "spacelift_user_login_profile" {
     + encrypted_password      = (known after apply)
     + id                      = (known after apply)
     + key_fingerprint         = (known after apply)
     + password                = (known after apply)
     + password_length         = 20
     + password_reset_required = (known after apply)
     + user                    = "spacelift-user"
   }

Plan: 2 to add, 0 to change, 0 to destroy.

Changes to Outputs:
 + password = (known after apply)
  1. After you’ve verified the plan, use the terraform apply command to deploy the changes to AWS.
    A successful run would produce the following output, which includes a randomly generated password that can be used to log into AWS.
aws_iam_user.spacelift_user: Creating...
aws_iam_user.spacelift_user: Creation complete after 1s [id=spacelift-user]
aws_iam_user_login_profile.spacelift_user_login_profile: Creating...
aws_iam_user_login_profile.spacelift_user_login_profile: Creation complete after 0s [id=spacelift-user]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

password = <randomly-generated-password>
  1. Let’s log in to AWS with the newly created username and password and try to access the s3 dashboard.
username: spacelift-user
password: <randomly-generated-password-by-terraform>

A new user without any policies attached has no access to any AWS resources because AWS denies everything by default and the spacelift-user user currently has no permissions attached to it.

Note: The S3 console might not necessarily show a 403 error but would definitely not show any existing buckets due to the lack of permissions.

terraform iam policy s3 console
  1. Try accessing any other resources on AWS and verify that the new user in fact has no access.

Now that we’ve got everything in order let’s create a policy that allows our newly created user to list all S3 buckets using Terraform.

Creating an Inline Policy Using Terraform

An inline policy is one that is embedded as part of the identity and has no independent existence. When you delete the identity, you also delete the inline policy.

Inline policies can be created and attached to a user using the aws_iam_user_policy resource. Let’s get started.

  1. Create an aws_iam_user_policy resource inside the main.tf file, which attaches an inline policy to the spacelift-user.
resource "aws_iam_user_policy" "s3_list_only_policy" {
 name = "S3ListOnlyPolicy"
 user = aws_iam_user.spacelift_user.name

 policy = <<EOF
{
 "Version": "2012-10-17",
 "Statement": [{
   "Effect": "Allow",
   "Action": [
     "s3:ListAllMyBuckets"
   ],
   "Resource": "*"
 }]
}
 EOF
}

Note: Please keep in mind that we used the HEREDOC syntax here to represent the policy. This allows us to use the JSON policy directly in Terraform without any modifications.

  1. Run the terraform plan command to verify the changes.
Terraform will perform the following actions:

 # aws_iam_user_policy.s3_list_only_policy will be created
 + resource "aws_iam_user_policy" "s3_list_only_policy" {
     + id     = (known after apply)
     + name   = "S3ListOnlyPolicy"
     + policy = jsonencode(
           {
             + Statement = [
                 + {
                     + Action   = [
                         + "s3:ListAllMyBuckets",
                       ]
                     + Effect   = "Allow"
                     + Resource = "*"
                   },
               ]
             + Version   = "2012-10-17"
           }
       )
     + user   = "spacelift-user"
   }

Plan: 1 to add, 0 to change, 0 to destroy.

Note: It’s worth noting that, despite using the HEREDOC syntax to represent the policy, Terraform internally uses the jsonencode function, which is visible in the plan. In the following section, we will go over the jsonencode function in greater detail.

  1. Finally, run the terraform apply command to deploy the changes which will attach an inline policy to the spacelift-user.
  1. Great! Now let’s again log in to AWS using the spacelift-user and check the S3 console.

Awesome! The policy took effect to grant the spacelift-user permission to use the  s3:ListAllMyBuckets action.

In this exercise, we created an inline policy and represented it using the HEREDOC syntax. In the following section, we will look at all of the different ways to represent policies in Terraform.

Learn more about AWS IAM best practices .

Various Ways of Representing IAM Policies in Terraform

Terraform provides multiple ways to represent a policy in HCL, those are:

  1. HEREDOC syntax
  2. jsonencode function to convert a policy into JSON
  3. file function to load a policy from a JSON file
  4. aws_iam_policy_document data resource

We have already seen the HEREDOC syntax in the earlier tutorial. In the later sections, we will explore all the others with standalone policies.

Note: It is important to remember that any of these methods can be used in place of the others without affecting the terraform plan.

Standalone IAM Policies

Before we jump into the hands-on, let’s understand what standalone policies are and why they are important.

Take a moment to ponder upon the following questions:

  1. How can we share the same inline policy with multiple other identities? Should we re-create the inline policy for each identity?
  2. How can we delete an identity without also deleting the policies associated with it?

This is where standalone policies come into the picture. They can exist independently of any identity or resource and can be attached to any number of identities without recreating them.

Creating Standalone IAM Policies using Terraform

Using the jsonencode function

  1. In the main.tf file, create an aws_iam_policy resource, which creates a standalone policy with the name S3ReadOnlyPolicy and uses the Terraform jsonencode function to represent the policy.
resource "aws_iam_policy" "s3_read_only_policy" {
 name = "S3ReadOnlyPolicy"

 policy = jsonencode({
   Version = "2012-10-17"
   Statement = [{
     Effect = "Allow"
     Action = [
       "s3:ListBucket",
       "s3:GetObject"
     ]
     Resource = "*"
   }]
 })
}
  1. Run the terraform plan command to verify the changes.
Terraform will perform the following actions:

 # aws_iam_policy.s3_read_only_policy will be created
 + resource "aws_iam_policy" "s3_read_only_policy" {
     + arn       = (known after apply)
     + id        = (known after apply)
     + name      = "S3ReadOnlyPolicy"
     + path      = "/"
     + policy    = jsonencode(
           {
             + Statement = [
                 + {
                     + Action   = [
                         + "s3:ListBucket",
                         + "s3:GetObject",
                       ]
                     + Effect   = "Allow"
                     + Resource = "*"
                   },
               ]
             + Version   = "2012-10-17"
           }
       )
     + policy_id = (known after apply)
     + tags_all  = (known after apply)
   }

Plan: 1 to add, 0 to change, 0 to destroy.
  1. Finally, run the terraform apply command to deploy the changes to AWS.
  2. Check the IAM console to verify if the policy was created. Take note that the policy is not associated with any identity yet.
terraform iam policy console

Attaching a standalone policy to a user

A standalone policy can be attached to a user using the aws_iam_user_policy_attachment resource.

Please keep in mind that this resource is exclusively for attaching a policy to a user, unlike the aws_iam_user_policy resource, which both creates and attaches an inline policy to a user.

Let’s proceed to attach the newly created policy to the spacelift-user user.

  1. Create an aws_iam_user_policy_attachment resource, which has two attributes user and policy_arn. Set the user attribute as the name of the spacelift-user user and arn as the arn of the standalone policy we created.
resource "aws_iam_user_policy_attachment" "spacelift_user_attach_s3_read_only_policy" {
 user       = aws_iam_user.spacelift_user.name
 policy_arn = aws_iam_policy.s3_read_only_policy.arn
}
  1. Run the terraform plan to verify the changes.
Terraform will perform the following actions:

 # aws_iam_user_policy_attachment.spacelift_user_attach_s3_read_only_policy will be created
 + resource "aws_iam_user_policy_attachment" "spacelift_user_attach_s3_read_only_policy" {
     + id         = (known after apply)
     + policy_arn = "arn:aws:iam::123456789:policy/S3ReadOnlyPolicy"
     + user       = "spacelift-user"
   }

Plan: 1 to add, 0 to change, 0 to destroy.
  1. Run the terraform apply command and check if the policy was attached to the spacelift-user in the AWS IAM console.
terraform iam policy console policy attached

Awesome! Let’s now see how to achieve the same results using the file function.

Using the file function

  1. Create a json file and add the below policy to it.
{
   "Version": "2012-10-17",
   "Statement": [{
     "Effect": "Allow",
     "Action": [
       "s3:DeleteObject"
     ],
     "Resource": "*"
   }]
}
  1. When creating the aws_iam_policy resource use the file function to load the policy from the json file.
resource "aws_iam_policy" "s3_delete_only_policy" {
 name = "S3DeleteOnlyPolicy"
 policy = file("filename.json")
}

Rest of the steps are the same as in the previous example of creating the policy using the jsonencode function.

Using aws_iam_policy_document data resource

The aws_iam_policy_document data resource generates policies in JSON format that can be used with the aws_iam_policy resource or with any resource that expects a JSON policy.

Create an aws_iam_policy_document data resource in the main.tf file.

data "aws_iam_policy_document" "s3_write_only_policy_document" {
 statement {
   sid = "1"
   actions = [
     "s3:PutObject",
   ]
   resources = ["*"]
 }
}
  1. As usual, run the terraform plan command.
terraform iam policy plan

Take a moment to consider why no changes are being reflected 🤔

This is due to the fact that aws_iam_policy_document is a data resource. Data resources are used to read information and have no effect on the infrastructure.

This implies that we must use this data resource as a reference somewhere in order to actually create the generated policy.

  1. Create a new aws_iam_policy resource that refers to the data resource aws_iam_policy_document.
resource "aws_iam_policy" "s3_write_only_policy" {
 name   = "S3WriteOnlyPolicy"
 policy = data.aws_iam_policy_document.s3_write_only_policy_document.json
}
  1. Again, run the terraform plan command. This time you should see some changes that should match the below output.
Terraform will perform the following actions:

 # aws_iam_policy.s3_write_only_policy will be created
 + resource "aws_iam_policy" "s3_write_only_policy" {
     + arn       = (known after apply)
     + id        = (known after apply)
     + name      = "S3WriteOnlyPolicy"
     + path      = "/"
     + policy    = jsonencode(
           {
             + Statement = [
                 + {
                     + Action   = "s3:PutObject"
                     + Effect   = "Allow"
                     + Resource = "*"
                     + Sid      = "1"
                   },
               ]
             + Version   = "2012-10-17"
           }
       )
     + policy_id = (known after apply)
     + tags_all  = (known after apply)
   }

Plan: 1 to add, 0 to change, 0 to destroy.
  1. Finally, run the terraform apply command to create the S3WriteOnlyPolicy standalone policy.
terraform iam policy console new policy

We saw a variety of ways to represent policies using Terraform, which is fantastic. But which one should we use and when? 🤔

Choosing Between Various Ways to Represent a Policy

In general, you are free to choose any way that you like to represent policies.

However, the aws_iam_policy_document data resource approach is recommended because it allows Terraform to validate any errors without having to apply the changes, which is not possible in other cases.

Let us see how this works in practice.

Below are two policies, one with the HEREDOC syntax and one with the jsonencode function. Both of these policies have a typo in the Action key, which reads MistakeInAction at the moment.

resource "aws_iam_user_policy" "s3_list_only_policy" {
 name = "S3ListOnlyPolicy"
 user = aws_iam_user.spacelift_user.name

 policy = <<EOF
{
 "Version": "2012-10-17",
 "Statement": [{
   "Effect": "Allow",
   "MistakeInAction": [
     "s3:ListAllMyBuckets"
   ],
   "Resource": "*"
 }]
}
 EOF
}

resource "aws_iam_policy" "s3_read_only_policy" {
 name = "S3ReadOnlyPolicy"

 policy = jsonencode({
   Version = "2012-10-17"
   Statement = [{
     Effect = "Allow"
     MistakeInAction = [
       "s3:ListBucket",
       "s3:GetObject"
     ]
     Resource = "*"
   }]
 })
}

Let’s see if Terraform can detect this error.

To validate the HCL file, use the terraform validate command.

terraform iam policy terraform validate

Terraform couldn’t detect any problems with either of the policies.

Let us now try the same thing with aws_iam_policy_document data resource.

data "aws_iam_policy_document" "s3_write_only_policy_document" {
 statement {
   sid = "1"
   MistakeInActions = [
     "s3:PutObject",
   ]
   resources = ["*"]
 }
}

Run the terraform validate command again and check if Terraform is able to find the typo.

terraform iam policy terraform validate typo

Terraform was able to detect the typo, which is a huge benefit for developers.

Aside from that, when using the aws_iam_policy_document data resource, you can benefit from the auto-completion feature of IDEs, which leads to a better development experience 🙂

This brings us to the final section of the exercise, where we will learn how to associate an AWS managed policy with a user.

AWS Managed Policies

AWS managed policies are created and managed by AWS for the most common use cases.

Let’s look at how to attach an AWS managed policy to a user.

  1. Because AWS managed policies are already present on AWS, we will read the policy from AWS using the data resource aws_iam_policy.
data "aws_iam_policy" "aws_rds_full_access_policy" {
 name = "AmazonRDSFullAccess"
}data "aws_iam_policy" "aws_rds_full_access_policy" {
 name = "AmazonRDSFullAccess"
}
  1. Let us attach this policy to the spacelift-user user as we did before using the reference to the data resource.
resource "aws_iam_user_policy_attachment" "spacelift_user_attach_aws_rds_full_access_policy" {
 user       = aws_iam_user.spacelift_user.name
 policy_arn = data.aws_iam_policy.aws_rds_full_access_policy.arn
}

Take note that we can directly hardcode the arn of the policy as well instead of using the data resource.

  1. Run the terraform plan command and verify the planned changes.
Terraform will perform the following actions:

 # aws_iam_user_policy_attachment.spacelift_user_attach_aws_rds_full_access_policy will be created
 + resource "aws_iam_user_policy_attachment" "spacelift_user_attach_aws_rds_full_access_policy" {
     + id         = (known after apply)
     + policy_arn = "arn:aws:iam::aws:policy/AmazonRDSFullAccess"
     + user       = "spacelift-user"
   }

Plan: 1 to add, 0 to change, 0 to destroy.
  1. Run the terraform apply command to deploy the changes and verify them in the IAM console.
terraform iam policy terraform apply

Congratulations, we learned how to use Terraform to manage inline, standalone, and AWS managed policies and numerous ways to represent them. 🎉

As previously stated, all of these learnings can be extended to any other policy type on AWS. As an example, creating resource-based policies using Terraform is shown below.

Creating Resource-based Policies Using Terraform

The method for creating policies remains the same as with identity-based policies; the only difference is the resource used to attach the policy to an entity.

resource "aws_s3_bucket" "user_access_logs" {
 bucket = "user-access-logs"
}

data "aws_iam_policy_document" "allow_access_from_uat_account_policy_document" {
 statement {
   principals {
     type        = "AWS"
     identifiers = ["123456789012"]
   }

   actions = [
     "s3:GetObject",
     "s3:PutObject",
     "s3:ListBucket",
   ]

   resources = [
     aws_s3_bucket.user_access_logs.arn,
     "${aws_s3_bucket.user_access_logs.arn}/*",
   ]
 }
}

resource "aws_s3_bucket_policy" "allow_access_from_uat_account_policy" {
 bucket = aws_s3_bucket.user_access_logs.id
 policy = data.aws_iam_policy_document.allow_access_from_uat_account_policy_document.json
}

Key Points

In this article, we learned what IAM policies are, numerous ways to create and manage them using Terraform and how to use them in practice.

AWS IAM policies lie at the heart of access management, and therefore it is critical to master them to secure access control on AWS.

Note: New versions of Terraform will be 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 will expand on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6. OpenTofu retained all the features and functionalities that had made Terraform popular among developers while also introducing improvements and enhancements. OpenTofu is not going to have its own providers and modules, but it is going to use its own registry for them.

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.

Start free trial
Terraform CLI Commands Cheatsheet

Initialize/ plan/ apply your IaC, manage modules, state, and more.

Share your data and download the cheatsheet