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.
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.
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.
Prerequisites
- An AWS admin user with the ability to create resources on AWS.
- 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. - 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 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
- Create a file with the name as
main.tf
in an empty folder. - 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
}
- Initialize the backend and the required provider plugins using the
terraform init
command.
- 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)
- 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>
- 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.
- 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.
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.
- Create an
aws_iam_user_policy
resource inside themain.tf
file, which attaches an inline policy to thespacelift-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.
- 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.
- Finally, run the
terraform apply
command to deploy the changes which will attach an inline policy to thespacelift-user
.
- 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 .
Terraform provides multiple ways to represent a policy in HCL, those are:
- HEREDOC syntax
- jsonencode function to convert a policy into JSON
- file function to load a policy from a JSON file
- 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
.
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:
- How can we share the same inline policy with multiple other identities? Should we re-create the inline policy for each identity?
- 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.
Using the jsonencode function
- In the
main.tf
file, create anaws_iam_policy
resource, which creates a standalone policy with the nameS3ReadOnlyPolicy
and uses the Terraformjsonencode
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 = "*"
}]
})
}
- 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.
- Finally, run the
terraform apply
command to deploy the changes to AWS. - Check the IAM console to verify if the policy was created. Take note that the policy is not associated with any identity yet.
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.
- Create an
aws_iam_user_policy_attachment
resource, which has two attributesuser
andpolicy_arn
. Set theuser
attribute as the name of thespacelift-user
user andarn
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
}
- 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.
- Run the
terraform apply
command and check if the policy was attached to thespacelift-user
in the AWS IAM console.
Awesome! Let’s now see how to achieve the same results using the file
function.
Using the file function
- Create a
json
file and add the below policy to it.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"s3:DeleteObject"
],
"Resource": "*"
}]
}
- When creating the
aws_iam_policy
resource use thefile
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 = ["*"]
}
}
- As usual, run the
terraform plan
command.
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.
- Create a new
aws_iam_policy
resource that refers to the data resourceaws_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
}
- 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.
- Finally, run the
terraform apply
command to create theS3WriteOnlyPolicy
standalone policy.
We saw a variety of ways to represent policies using Terraform, which is fantastic. But which one should we use and when? 🤔
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 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 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 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.
- 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"
}
- 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.
- 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.
- Run the
terraform apply
command to deploy the changes and verify them in the IAM console.
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.
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
}
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 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.