Constructing AWS ARNs using arguments from nested object via for loop with for_each created resources

Hi all,

I’m trying to manually construct AWS ARNs for a data "aws_iam_policy_document" source for use in an AWS SNS topic policy so CloudWatch alarms in multiple accounts/regions can publish a message to defined SNS topics. I have created an object structure in which I define accounts and regions via a nested object, and I want to set the product so I have every possible option based on these two arguments from my nested object, however I’m having problems trying to get setproduct() to work and then split these up into seperate ARNs using a for loop. My object structure is the following, followed with my directory structure:

modules/sns/variables.tf

variable "sns_topics" {
  type = map(object({
    name                    = string
    cloudwatch_alarm_policy = list(object({
      accounts = list(string)
      regions = list(string)
    }))
    kms_key_id = string
  }))
  default     = {}
}

modules/sns/locals.tf

locals {
  accounts = flatten([
    for name, config in var.sns_topics : [
      for k in config.cloudwatch_alarm_policy : {
        account = k.accounts
      }
    ]
  ])
  
  regions = flatten([
    for name, config in var.sns_topics : [
      for k in config.cloudwatch_alarm_policy : {
        region = k.regions
      }
    ]
  ])

  join = [
    for pair in setproduct(local.accounts, local.regions) : {
      account = pair[0].account
      region = pair[1].region
    }
  ]
}

modules/sns/sns.tf

data "aws_iam_policy_document" "this" {
  for_each = { for name, config in var.sns_topics : name => config if config.cloudwatch_alarm_policy != [] }

  policy_id = "__default_policy_ID"

  statement {
    sid = "__default_statement_ID"

    principals {
      type        = "AWS"
      identifiers = ["*"]
    }

    condition {
      test     = "StringEquals"
      variable = "AWS:SourceOwner"
      values   = [data.aws_caller_identity.current.account_id]
    }

    actions = [
      "SNS:GetTopicAttributes",
      "SNS:SetTopicAttributes",
      "SNS:AddPermission",
      "SNS:RemovePermission",
      "SNS:DeleteTopic",
      "SNS:Subscribe",
      "SNS:ListSubscriptionsByTopic",
      "SNS:Publish",
      "SNS:Receive"
    ]

    resources = [
      "arn:aws:sns:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${each.value.name}"
    ]
  }
  
  statement {
    sid = "CrossAccountAlarms"

    principals {
      type        = "AWS"
      identifiers = ["*"]
    }

    condition {
      test     = "ArnLike"
      variable = "aws:SourceArn"
      values = [ for k in local.join : "arn:aws:cloudwatch:${k.region}:${k.account}:alarm:*" ]
    }

    actions = [
      "SNS:Publish"
    ]

    resources = [
      "arn:aws:sns:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${each.value.name}"
    ]
  }
}

resource "aws_sns_topic" "this" {
  for_each                   = var.sns_topics
  name                        = each.value.name
  display_name           = each.value.name
  kms_master_key_id = each.value.kms_key_id != "" ? each.value.kms_key_id : null
  policy                        = each.value.cloudwatch_alarm_policy != [] ? data.aws_iam_policy_document.this[each.key].json : null
}

main.tf

module "cloudwatch_sns_topics" {
  source = "./modules/sns"

  sns_topics = {
    ops_team = {
      name = "ops_team_mgmt"
      cloudwatch_alarm_policy = [{
        accounts = ["111111111111", "222222222222", "333333333333"]
        regions = ["eu-west-1", "eu-west-2"]
      }]
      kms_key_id = ""
    }
  }
}

I’ve been trying for a few hours to get it to work and this is the best I’ve got, however I get an error saying:

Error: Invalid template interpolation value

  on modules/sns/main.tf line 48, in data "aws_iam_policy_document" "this":
   48:   values = [ for k in local.join : "arn:aws:cloudwatch:${k.region}:${k.account}:alarm:*" ]
    |----------------
    | k.region is list of string with 2 elements

Cannot include the given value in a string template: string required.


Error: Invalid template interpolation value

  on modules/sns/main.tf line 48, in data "aws_iam_policy_document" "this":
   48:   values = [ for k in local.join : "arn:aws:cloudwatch:${k.region}:${k.account}:alarm:*" ]
    |----------------
    | k.account is list of string with 3 elements

Cannot include the given value in a string template: string required.

The aim is so based on the above it would be 6 ARNs for the values parameter for the IAM Policy Document data source:

  • arn:aws:cloudwatch:eu-west-1:111111111111:alarm:*
  • arn:aws:cloudwatch:eu-west-2:333333333333:alarm:*

I feel I might be missing something so simple, and since I’ve spent so long on this I’m also wondering whether I’ve over complicated the solution. I could generalise this in some seperate variables, outside of the object structure and have that applicable to all SNS topics, but I want to follow the principle of least privilege and only give what permissions the topics need and not more.

Thanks!