Search code examples
terraformpagerduty

for_each in terraform nested block


I've searched quite a bit and don't think I've found the answer I really need. I'm trying to loop through a nested block and am successful in doing this if all of the attributes are on the same root object. This is great if I want to loop over the entire set of attributes. However this situation is a bit different. I need to loop over an entire set of attributes and also a sub-set.

In this Terragrunt example, you can see the desired inputs since we want to loop over the escalation policy entirely as well as loop the rule and its targets so that we can create many escalation policies with many rules/targets in them.

  /// PagerDuty Escalation Policies

  create_escalation_policy = true
  escalation_policies = [
    {
    name        = "TEST Engineering Escalation 1"
    description = "My TEST engineering escalation policy 1"
    teams       = ["111N1CV"]
    num_loops   = 2
    rule = [
      {
      escalation_delay_in_minutes = 15
      target = {
        type = "user_reference"
        id   = "ABCB8F3"
        }
      },
      {
      escalation_delay_in_minutes = 15
      target = {
        type = "user_reference"
        id   = "NBCB1A1"
        }
      }
    }
  ]

However, after quite a bit of trial and error, I'm able to loop over the entire escalation policy but not if we have values inside of rule = { which returns a generic error that Terraform can't find those attributes in the object which I have confirmed is the root object instead of the nested one. This was validated by simply moving those attributes out to the root of the object input block.

│ Error: Unsupported attribute
│ 
│   on main.tf line 121, in resource "pagerduty_escalation_policy" "this":
│  121:         id   = rule.value.id
│     ├────────────────
│     │ rule.value is object with 5 attributes
│ 
│ This object does not have an attribute named "id".

For reference, here is the variable for var.escalation_policies

variable "escalation_policies" {
  description = "A list of escalation policies and rules for a given PagerDuty service."
  type        = any
}

and the resource

resource "pagerduty_escalation_policy" "this" {
  for_each = { 
    for key in var.escalation_policies : key.name => {
      name                        = key.name
      description                 = key.description
      num_loops                   = key.num_loops
      teams                       = key.teams
    }
    if var.create_escalation_policy == true 
  }
  name        = each.value.name
  description = each.value.description
  num_loops   = each.value.num_loops
  teams       = each.value.teams
  dynamic "rule" {
    for_each = {
      for k, v in var.escalation_policies : k => v }
    content {
      escalation_delay_in_minutes = rule.value.escalation_delay_in_minutes
      target {
        type = rule.value.type
        id   = rule.value.id
      }
    }
  }
} 

Solution

  • With your current example the dynamic "rule" block has a for expression that isn't really doing anything useful:

    { for k, v in var.escalation_policies : k => v }
    

    This expression is strange in two ways:

    • Taking the expression alone, it's unusual to project k, v directly to k => v because that doesn't really change anything about the key or the value. Since your source var.escalation_policies is a list rather than a map this is changing the data type of the result and making Terraform convert the integer indices to strings instead, but otherwise the elements are the same as var.escalation_policies.
    • Considering the context, this is also unusual because it's repeating the nested block based on the same collection as the containing resource: there will be one pagerduty_escalation_policy.this instance per var.escalation_policy element and then each one will have one nested rule block for each of your escalation policies.

    To get a useful result the for_each in your dynamic block should use a different collection as the basis for its repetition. I think in your case you're intending to use the nested lists inside the rule attributes of each of your policies, but your outermost for_each expression doesn't include the rules so you'll first need to update that:

    resource "pagerduty_escalation_policy" "this" {
      for_each = { 
        for policy in var.escalation_policies : policy.name => {
          name                        = policy.name
          description                 = policy.description
          num_loops                   = policy.num_loops
          teams                       = policy.teams
          rules                       = policy.rule
        }
        if var.create_escalation_policy == true 
      }
      # ...
    }
    

    This means that each.value will now include an additional attribute rules which has the same value as the corresponding attribute in each element of var.escalation_policies.

    You can then refer to that rules attribute in your dynamic block:

      dynamic "rule" {
        for_each = each.value.rules
        content {
          escalation_delay_in_minutes = rule.value.escalation_delay_in_minutes
          target {
            type = rule.value.target.type
            id   = rule.value.target.id
          }
        }
      }
    

    This tells Terraform to generate a dynamic rule block for each element of each.value.rules, which is the rules attribute for the current policy.

    Inside the content block rule.value is the current rule object, so you can refer to attributes like escalation_delay_in_minutes and target from that object.