Managing IAM Policy Documents in HCL with Terraform

IAM Policy Documents are ubiquitous in AWS - they are used not only for standalone policies you might attach to users or roles, but also for S3 bucket policies, SNS topic policies and more. Unfortunately, the JSON syntax can be error prone to hand write, and the default mechanism for creating policies in many configuration management tools is template rendering.

Terraform 0.7 introduced the concept of data sources, one of which allows definition of policy documents in Terraform's native HCL configuration format which is designed for humans to read and write.

Let's take an IAM Role as an example. Previously, we would have written the configuration like this:

resource "aws_iam_role" "role" {  
    name = "my-role"

    assume_role_policy = <<EOF
{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": [
          "ec2.amazonaws.com"
        ]
      },
      "Effect": "Allow"
    }
  ]
}
EOF  
}

Note that the meat of this resources is contained in a HEREDOC. We could improve this slightly, using an indented HEREDOC, by using the <<-EOF form, but Terraform has no way to ensure the JSON is valid. Instead, we can use the aws_iam_policy_document data source, like this:

data "aws_iam_policy_document" "assume_role" {  
    statement {
        effect = "Allow"
        actions = [
            "sts:AssumeRole",
        ]
        principals {
            type = "Service"
            identifiers = ["ec2.amazonaws.com"]
        }
    }
}

resource "aws_iam_role" "role" {  
    name = "my-role"
    assume_role_policy = "${data.aws_iam_policy_document.assume_role.json}"
}

We use the DSL in order to write policies that can then be associated to the iam_role. Terraform renders the JSON from our HCL, so we can interpolate into the aws_iam_role resource. This means that policy documents can be reused without duplication, as is common for role assumption policies for example:

resource "aws_iam_role" "role1" {  
    name = "my-role1"
    assume_role_policy = "${data.aws_iam_policy_document.assume_role.json}"
}

resource "aws_iam_role" "role2" {  
    name = "my-role2"
    assume_role_policy = "${data.aws_iam_policy_document.assume_role.json}"
}

resource "aws_iam_role" "role3" {  
    name = "my-role3"
    assume_role_policy = "${data.aws_iam_policy_document.assume_role.json}"
}

The same style of approach can be taken when looking at applying policies to other AWS resources. Let's take an S3 bucket as an example:

variable "bucket_name" {  
    type = "string"
}

resource "aws_s3_bucket" "repo" {  
    bucket = "${var.bucket_name}"
    acl = "private"
}

data "aws_iam_policy_document" "lock_it_down" {  
    statement {
        sid = "AllowReadFromVPC"
        effect = "Allow"
        principals {
            type = "AWS"
            identifiers = ["*"]
        }
        actions = ["s3:GetObject"]
        resources = [
            "${aws_s3_bucket.repo.arn}",
            "${aws_s3_bucket.repo.arn}/*"
        ]
        condition {
            test = "StringEquals"
            variable = "aws:sourceVpc"
            values = ["${var.vpc_id}"]
        }
    }
}

resource "aws_s3_bucket_policy" "repo" {  
    bucket = "${aws_s3_bucket.repo.bucket}"
    policy = "${data.aws_iam_policy_document.lock_it_down.json}"
}

The full range of possibilities is not covered in this article - the entire IAM policy grammar can be represented. For more information, check out the full documentation for the aws_iam_policy_document data source, and give it a try!