Dynamic IAM Resources with Terraform

Dynamic IAM Resources with Terraform

IAM requirements differ between project to project. Depending on the complexity, we could create the IAM resources with Terraform in the relevant layer. However, having the IAM resources dynamically create within its own module, or even combining them with their resource counterparts, gives us great power.

Combining the IAM instance profile/role within an ASG module is a clear example of how this approach can add flexibility. We can automatically create or extend IAM policies depending on our input variables in the ASG module.

In this post, we will go over the building blocks used to create IAM resources in Terraform. We’ll also go over best practices for these and how we can leverage Terraform functions to make them truly dynamic.

The aws_iam_policy_document data source can be used to generate the JSON representation of an IAM policy document.

data "aws_iam_policy_document" "example" {
  statement {
    actions = [
      "s3:ListBucket"
    ]

    resources = [
      "arn:aws:s3:::${var.s3_bucket_name}",
    ]
  }

  statement {
    actions = [
      "s3:GetObject",
      "s3:GetObjectAcl"
    ]

    resources = [
      "arn:aws:s3:::${var.s3_bucket_name}/*",
    ]
  }
}

We can then reference the generated JSON within the aws_iam_policy resource.

resource "aws_iam_policy" "example" {
  name   = "example_policy"
  policy = data.aws_iam_policy_document.example.json
}

This is perfect for static resources. But what if we had an Auto Scaling Group module which also built the IAM role and the instance profile? Well, we could take the above example and have it extend dynamically depending on variables passed into the module. The nested statement blocks can be created using dynamic blocks or the actions and/or resources dynamically set using variables and Terraform functions.

data "aws_iam_policy_document" "example" {
count = length(var.readonly_bucket) != 0 ? 1 : 0

  statement {
    actions = [
      "s3:ListBucket"
    ]

    resources = formatlist("arn:aws:s3:::%s", var.readonly_buckets)
  }

  statement {
    actions = [
      "s3:GetObject",
      "s3:GetObjectAcl"
    ]

    resources = formatlist("arn:aws:s3:::%s/*", var.readonly_buckets)
  }
}

resource "aws_iam_policy" "example" {
count = length(var.readonly_bucket) != 0 ? 1 : 0

  name   = "example_policy"
  policy = data.aws_iam_policy_document.example.0.json
}

The actions are the same but the resources this policy applies to now dynamically changes depending on the value of the readonly_buckets variable. If it’s empty then no IAM resources are created.

One thing to note when working with IAM in Terraform is that aws_iam_policy_attachment forces an exclusive attachment. This can lead to unwanted behavior when trying to attach a single policy to multiple roles. We recommend aws_iam_role_policy_attachment, aws_iam_user_policy_attachment, or aws_iam_group_policy_attachment instead.

resource "aws_iam_role_policy_attachment" "cp_role_ssm_core" {
  count      = var.create_capacity_provider_role ? 1 : 0
  role       = aws_iam_role.cp_role.0.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

Below is another example of optionally creating IAM resources depending on options passed into a module. It also uses the formatlist function but here we search the var.replication_rules variable for distinct rules looking for the destination bucket. This allows us to fully setup the S3 replication policy for one or more destination buckets with very little effort apart from defining the replication rules themselves. The module takes care of creating the IAM resources dynamically.

resource "aws_iam_role" "replication" {
  count = var.replication_rules != [] && var.replication_role == null ? 1 : 0
  name  = "CoreS3ReplicationRole_${local.bucket_name}"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "s3.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
POLICY
}

data "aws_iam_policy_document" "replication" {
  count = var.replication_rules != [] && var.replication_role == null ? 1 : 0

  statement {
    actions = ["s3:GetReplicationConfiguration",
    "s3:ListBucket"]
    resources = ["${aws_s3_bucket.bucket.arn}"]
  }

  statement {
    actions = ["s3:GetObjectVersion",
    "s3:GetObjectVersionAcl"]
    resources = ["${aws_s3_bucket.bucket.arn}/*"]
  }

  statement {
    actions = ["s3:ReplicateObject",
    "s3:ReplicateDelete"]
    resources = formatlist("%s/*", distinct([for rule in var.replication_rules : lookup(rule, "destination").bucket]))
  }
}

resource "aws_iam_policy" "replication" {
  count  = var.replication_rules != [] && var.replication_role == null ? 1 : 0
  name   = "CoreS3ReplicationPolicy_${local.bucket_name}"
  policy = data.aws_iam_policy_document.replication.0.json
}

resource "aws_iam_role_policy_attachment" "replication" {
  count      = var.replication_rules != [] && var.replication_role == null ? 1 : 0
  role       = aws_iam_role.replication.0.name
  policy_arn = aws_iam_policy.replication.0.arn
}

If you do anything fancy with IAM and Terraform, or if you’re having specific issues feel free to drop us a line. We’d love to hear from you!

 

No Comments

Add your comment