cloudsoft.io

Indexers using count and for_each

Maeztro recognizes the attributes count and for_each on any resource block as a way of producing 0, 1, or more instances indexed by a number or element in a map or collection. The syntax is exactly as in Terraform:

  • count = NUMBER will result in NUMBER instances, indexed from 0; the index can be accessed elsewhere in the resource definition using count.index
  • for_each = MAP_SET_OR_LIST where the MAP_SET_OR_LIST is iterated through to give the instances; the index key and value can be accessed as each.key and each.value

In Maeztro, this is called the “index definition” for “indexed instances”. As with Terraform, a for_each map must have string keys. A set of strings is treated as an identity map, with both each.key and each.value returning the string. In addition, Maeztro permits lists and sets containing other types, and in these cases each.value contains the element and each.key contains the numeric position of the element, starting at 0. To facilitate consistent coding, Maeztro also sets each.index to the numeric position of the element, and for count resources, each.{index,key,value} is set the same as count.index. As with Terraform, the indexed instances are referred to by appending [i] to the usual name for the resource, where i is the value of each.key.

resource maeztro "demo" {
  for_each = ["a","b"]
  config {
    k = each.key
    v = each.value
    i = each.index
  }
}

For example, the code above will create resources maeztro.demo[0] and maeztro.demo[1], with maeztro.demo[1].k = 1 (and v = "b", i = 1). If instead for_each = toset(["a","b"]), resources maeztro.demo["a"] and maeztro.demo["b"] will be created, with maeztro.demo["b"].k = "b" (with v and i as before).

Maeztro also explicitly represents the “indexer” where the keyword count or for_each is defined, as a resource e.g. maeztro.demo. The indexed instances, e.g. maeztro.demo["a"] are treated as members of the indexer. An index resource block can be used to define behavior for the indexer, distinct from the indexed resources. This makes it easy in Maeztro to write workflows at the indexer resource which run over all indexed resources, via self.members, as shown below:

resource maeztro "demo" {
  for_each = ["a","b","c"]
  name = "Indexed Instance: ${upper(each.value)}"

  effector "sample" {
    steps = [ "return ${each.index}:${each.value}" ]
  }

  index resource {
    name = "Indexer"
    effector "sample_everywhere" {
      steps = [ "foreach indexed_resource in ${self.members} do invoke-effector sample on ${indexed_resource}" ]
    }
  }
}

This will create maeztro.demo as a top-level resource called “Indexer”, and 3 resources underneath it: maeztro.demo[0] named “Indexed Instance: A”, and similar for 1 (B) and 2 (C). The 3 indexed instances will each have an effector sample returning e.g. 0:a, and the indexer wiill have an effector sample_everywhere which invokes sample at each indexed instance. This makes it easy to apply operations across all indexed resources, with a concurrency level and error checking, as described here.

Where a resource comes from Terraform, and count or for_each is specified there, Maeztro automatically creates indexer resources in its model. As with normal Terraform resources, maeztro extend can be used to extend the definition, and an index resource block can be used, and the parent can be specified for both the indexer and for the indexed instances:

// assume TF code defines aws_instance.vms with a count or for_each
maeztro extend resource "aws_instance.vms" {
  name = "VM #${each.index + 1}"
  parent = each.index==0 ? maeztro.first : maeztro.others

  effector "restart" {
    steps = [
      {
        step: "ssh sudo reboot now",
        on-error: "no-op"
      }
    ]
  }

  index resource {
    parent = maeztro.front_end
    name = "Indexer"
    effector "restart_all" {
      steps = [
        "invoke-effector restart_first"
        {
          step: "invoke-effector restart_others"
          concurrency: 5
        }
      ]
    }
    effector "restart_first" {
      steps = [ "foreach vm in ${maeztro.first.children} do invoke-effector restart on ${vm}" ]
    }
    effector "restart_others" {
      steps = [ "foreach vm in ${maeztro.others.children} do invoke-effector restart on ${vm}" ]
    }
  }
}
resource maeztro "front_end" {}
resource maeztro "first" {
   parent = "aws_instance.vms"
}
resource maeztro "others" {
  parent = "aws_instance.vms"
}

This results in the following arrangement:

mz extend terraform count

Calling restart_all will restart the first machine and then restart the others 5 at a time.

Other Indexing Types

The Maeztro discovery and group types can also create indexed resources, the partitions in both cases, and the discovered items in the case of the former. In some ways, these are similar to the use of the count and for_each attribute, but for cases where there there are steps to discovering the resources and/or where there is heterogeneity or categorization within the items being indexed. The address notation is identical.

Where it is necessary to distinguish between these different modes of indexing resources, the following vocabulary is used:

  • A keyword-based indexer is one where count or for_each defines the index
  • A discovery-based indexer is one where the type – group or discovery – defines how the resources are indexed
  • The indexed instances are the resources created by the indexer and their addresses are formed by appending the key in brackets after the indexer’s address

Advanced Details

In some cases it is necessary to understand whether attributes and blocks in the definition apply to the indexer or to the indexed resources or both. The attributes count and for_each apply to the indexer only, as they define the set of indexed instances, as does the index resource block if present. If an index resource block is not present, the attribute parent applies to the indexer (with the indexed instances parented by the indexer), and name applies to both. All other blocks and attributes set in the definition – such as effector and config – always apply to the instances and not the indexer. If the index resource is present, its content applies only to the indexer, including things like effector and config and optionally a name different to the name of the indexed instances. When using an index resource, any parent for the indexer must be specified in that block, and the instances can be explicitly parented somewhere else by also using a parent attribute in the outer definition. The count and each variables can only be used at indexed instances, as they are not be defined for the indexer. Thus if using them to specify the parent or name of indexed instances, the value for the indexer should be set in the index resource (or using the can or try function). This pattern can be used to create indexed instances as children resources underneath other indexed or dynamically discovered resources.

Some other best practices and edge cases are:

  • The expression self.members is recommended as a universal way to get all indexed instances from a keyword-based indexer, because self.children may include other children (if other resources are assigned the indexer as a parent) and may exclude the indexed instances (if they are explicitly assigned a different parent).

  • If an index resource block is present, it is an error to specify a parent in the outer definition without also specifying a parent in the index resource. The variable module_root can be used to refer to the root of the module.

  • Where Maeztro extends a Terraform resource, it is not permitted to use for_each or count in Maeztro.