For the last week or so I have been working on a Terraform module for deploying tasks to ECS. One of the most interesting problems that I came across while working on it was around creating IAM profiles. Each task that gets deployed needs to have a minimum set of permissions in their IAM profile, but some need to have more than others. After thinking about various ways to solve my problem, I decided to give dynamic blocks a try.

When writing IAM policies in Terraform, I prefer to do it in an aws_iam_policy_document data block rather than in pure JSON or a template file since Terraform can validate the syntax in a data block. A typical policy might look something like this:

data "aws_iam_policy_document" "role_policy" {
  statement {
    effect = "Allow"
    actions = [
      "ec2:AuthorizeSecurityGroupIngress",
      "ec2:Describe*",
      "elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
      "elasticloadbalancing:DeregisterTargets",
      "elasticloadbalancing:Describe*",
      "elasticloadbalancing:RegisterInstancesWithLoadBalancer",
      "elasticloadbalancing:RegisterTargets",
    ]
    resources = ["*"]
  }
}

The above policy is an example of something that I may want to attach to every task I launch so that it can update itself with a load balancer. So for me to be able to add a statement through my tfvars, I added a dynamic statement block to the bottom:

data "aws_iam_policy_document" "role_policy" {
  statement {
    effect = "Allow"
    actions = [
      "ec2:AuthorizeSecurityGroupIngress",
      "ec2:Describe*",
      "elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
      "elasticloadbalancing:DeregisterTargets",
      "elasticloadbalancing:Describe*",
      "elasticloadbalancing:RegisterInstancesWithLoadBalancer",
      "elasticloadbalancing:RegisterTargets",
    ]
    resources = ["*"]
  }
  dynamic "statement" {
    for_each = var.task_iam_policies
    content {
      effect = lookup(statement.value, "effect", null)
      actions = lookup(statement.value, "actions", null)
      resources = lookup(statement.value, "resources", null)
    }
  }
}

This configuration allows me to configure a variable task_iam_policies that contains any additional policy statements that I need to add:

task_iam_policies = [
  {
    effect = "Allow"
    actions = [
      "iam:PassRole",
      "ec2:TerminateInstances",
      "ec2:StopInstances",
      "ec2:StartInstances",
      "ec2:RunInstances",
      "ec2:RequestSpotInstances",
      "ec2:GetConsoleOutput",
      "ec2:DescribeSubnets",
      "ec2:DescribeSpotPriceHistory",
      "ec2:DescribeSpotIstanceRequests",
      "ec2:DescribeSecurityGroups",
      "ec2:DescribeRegions",
      "ec2:DescribeKeyPairs",
      "ec2:DescribeInstances",
      "ec2:DescribeImages",
      "ec2:DescribeAvailabilityZones",
      "ec2:DeleteTags",
      "ec2:CreateTags",
      "ec2:CancelSpotInstanceRequests",
    ]
    resources = ["*"]
  }
]

Now I can add as many statements as I need to my task when I deploy it.