James Quigley's Blog

Merging and Overriding IAM Policies in Terraform

March 30, 2020

TL;DR

Use the source_json and/or override_json attributes on a Terraform aws_iam_policy_document.

data "aws_iam_policy_document" "foo" {
    source_json = data.aws_iam_policy_document.bar.json
}

# OR

data "aws_iam_policy_document" "buzz" {
    override_json = data.aws_iam_policy_document.fizz.json
}

Summary

When creating IAM policies in AWS, it can be really easy to either:

  • Give things way too many permissions because you’re lazy and don’t want to repeat yourself
  • End up repeating things a lot.

When using Terraform, you can get the best of both worlds by merging disparate policy documents to both avoid repeating yourself and limit permissions. This capability is especially useful when creating modules or useful base policies. You could create a base policy that enforces the best practices of your organization and use that as the source_json on every aws_iam_policy_document. Or if there is already a policy document in place, but you want to override specific statements, you can do that too!

You could accomplish some of the same things with plain JSON in a heredoc and some string interpolation, but I’ve found it to be much simpler to actually use the policy document data source.

Source vs. Override

The difference between source_json and override_json is a question of which of the two policies you want to take priority. When using source_json, the source is imported, and the current policy document will override anything in the source with a matching statement ID. When using override_json, the current policy document is overridden by the imported policy.

Note that statement IDs (sid) have to match in order to actually override any statements. Otherwise they will be merged.

Examples

Given the following policy document, let’s look at a practical example of source_json versus override_json.

data "aws_iam_policy_document" "a" {
    # Allows reading from all buckets
    statement {
        sid = "1"
        actions = ["s3:GetObject"]
        resources = ["arn:aws:s3:::*"]
    }

    # Allow put object in "some-bucket"
    statement {
        sid = "2"
        actions = ["s3:PutObject"]
        resources = ["arn:aws:s3:::some-bucket/*"]
    }
}

Using source_json

data "aws_iam_policy_document" "b" {

    source_json = data.aws_iam_policy_document.a.json
    # Allows reading from a specific bucket
    statement {
        sid = "1"
        actions = ["s3:GetObject"]
        resources = ["arn:aws:s3:::some-bucket/*"]
    }

    # Allows put object in "a-different-bucket"
    statement {
        sid = "3"
        actions = ["s3:PutObject"]
        resources = ["arn:aws:s3:::a-different-bucket/*"]
    }
}

# Final output for "b" is GetObject only for "some-bucket", but PutObject on both "some-bucket" and "a-different-bucket"

Using override_json

data "aws_iam_policy_document" "b" {

    override_json = data.aws_iam_policy_document.a.json
    # Allows reading from a specific bucket
    statement {
        sid = "1"
        actions = ["s3:GetObject"]
        resources = ["arn:aws:s3:::some-bucket/*"]
    }

    # Allows put object in "a-different-bucket"
    statement {
        sid = "3"
        actions = ["s3:PutObject"]
        resources = ["arn:aws:s3:::a-different-bucket/*"]
    }
}

# Final output for "b" is GetObject for all buckets, and PutObject on both "some-bucket" and "a-different-bucket"

See the Terraform Docs for more information.


Written by James Quigley, a DevOps Engineer, full-stack dev, and general tech nerd. Follow him on Twitter!