Score:0

Terraform: How to dynamically generate a block of a JSON policy?

cn flag

I have the following resource:

resource "aws_iam_user_policy" "ses_send_policy" {
  count       = var.enabled ? 1 : 0
  name_prefix = var.user_policy_name_prefix
  user        = aws_iam_user.ses_smtp_user[0].name

  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ses:SendEmail",
                "ses:SendRawEmail"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "ses:FromAddress": [
                        "${var.user_email_address}"
                    ]
                }
            }
        }
    ]
}
EOF
}

I'd like to ask how to make the "Condition" block in the policy optional and based on a bool type variable? I want to have it only if var.condition = true.

Score:0
ph flag

In the documentation for aws_iam_user_policy at the time of this answer the main usage example shows setting policy like this:

  # Terraform's "jsonencode" function converts a
  # Terraform expression result to valid JSON syntax.
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "ec2:Describe*",
        ]
        Effect   = "Allow"
        Resource = "*"
      },
    ]
  })

Notice that it recommends using the jsonencode function to produce the entire value, rather than trying to construct JSON from parts via template concatenation, since that ensures that the result will always be valid JSON syntax.

It also has the benefit that you can use any expressions you need to make dynamic decisions about the data structure. In your case you already have a dynamic reference to the user's email address from a variable, so let's start by translating what you have into a form more like the example in the documentation:

  policy = jsonencode({
    Version = 2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ses:SendEmail",
          "ses:SendRawEmail",
        ]
        Resource = "*"
        Condition = {
          StringEquals = {
            "ses:FromAddress" = [
              var.user_email_address,
            ]
          }
        }
      }
    ]
  })

Notice that the value is now written in Terraform's own expression syntax, rather than JSON syntax. Terraform will construct the valid JSON syntax itself as part of evaluating the jsonencode function call.

Your new requirement is to omit Condition entirely in certain cases. To describe that requirement as a Terraform expression requires merging the object which describes the parts that are always present (Effect, Action, and Resource) with another expression which describes the optional parts.

For example:

  policy = jsonencode({
    Version = 2012-10-17"
    Statement = [
      merge(
        {
          Effect = "Allow"
          Action = [
            "ses:SendEmail",
            "ses:SendRawEmail",
          ]
          Resource = "*"
        },
        coalesce(var.condition ? {
          Condition = {
            StringEquals = {
              "ses:FromAddress" = [
                var.user_email_address,
              ]
            }
          }
        } : null, {}),
      )
    ]
  })

What I've changed here is a bit subtle and perhaps hard to see with all of the other content that didn't change, so here's a cut down version with some of the previous elements replaced by comments just to make the new content more prominent:

  policy = jsonencode({
    Version = 2012-10-17"
    Statement = [
      merge(
        {
          # (common attributes here)
        },
        coalesce(var.condition ? {
          # (conditional attributes here)
        } : null, {}),
      )
    ]
  })

The merge function takes multiple objects and returns a single object containing the elements from all of them taken together. In this case, the second argument to merge is a more complex expression which produces either an object with a Condition attribute or an empty object depending on the condition value. Merging an empty object into an object changes nothing about the result, and so in this case the condition controls whether that second argument will contribute any attributes at all.

Jacek avatar
cn flag
Great explanation, thank you.
Jacek avatar
cn flag
One question, the code works but the policy it in reverted order, why is that?
ph flag
I'm not sure what you mean by "reverted order", but perhaps you've noticed that `jsonencode` will always sort the object attributes by their names because an object in is an _unordered_ collection of attributes, and so `jsonencode` has to decide some order itself.
mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.