stackoverflow
stackoverflow

Reputation: 19434

Terraform : How to loop over aws_instance N times as defined within object

I have the following variable

variable "instance_types" {

  default = {

    instances : [
      {
        count                  = 1
        name                   = "control-plane"
        ami                    = "ami-xxxxx"
        instance_type          = "t2.large"
        iam_instance_profile   = "xxx-user"
        subnet_id              = "subnet-xxxxx"
      },
      {
        count                  = 3
        name                   = "worker"
        ami                    = "ami-xxxxx"
        instance_type          = "t2.large"
        iam_instance_profile   = "xxx-user"
        subnet_id              = "subnet-xxxxx"
      }
    ]
  }
}

With the following instance declaration (that I'm attempting to iterate)

resource "aws_instance" "k8s-node" {

  # Problem here : How to turn an array of 2 objects into 4 (1 control_plane, 3 workers)
  for_each = {for x in var.instance_types.instances:  x.count => x}

  ami                    = lookup(each.value, "ami")
  instance_type          = lookup(each.value, "instance_type")
  iam_instance_profile   = lookup(each.value, "iam_instance_profile")
  subnet_id              = lookup(each.value, "subnet_id")

  tags = {
    Name      = lookup(each.value, "name")
    Type      = each.key
  }
}

Goal: Get the aws_instance to iterate 4 times (1 control_plane + 3 workers) and populate the values the index of instance_types.

Problem : Cannot iterate the over the object array correctly with desired result. In a typical programming language this would be achieved in a double for loop.

Upvotes: 1

Views: 1679

Answers (1)

Matthew Schuchard
Matthew Schuchard

Reputation: 28739

This can be solved easier with a data type of map(object)) for your input variable. The transformed data structure appears like:

variable "instance_types" {
  ...
  default = {
    "control-plane" = {
      count                  = 1
      ami                    = "ami-xxxxx"
      instance_type          = "t2.large"
      iam_instance_profile   = "xxx-user"
      subnet_id              = "subnet-xxxxx"
    },
    "worker" = {
      count                  = 3
      ami                    = "ami-xxxxx"
      instance_type          = "t2.large"
      iam_instance_profile   = "xxx-user"
      subnet_id              = "subnet-xxxxx"
    }
  }
}

Note the name key in the object is subsumed into the map key for efficiency and cleanliness.

If the resources are split between the control plane and worker nodes, then we are finished and can immediately leverage this variable's value in a for_each meta-argument. However, combining the resources now requires a data transformation:

locals {
  instance_types = flatten([ # need this for final structure type
    for instance_key, instance in var.instance_types : [ # iterate over variable input objects
      for type_count in range(1, instance.count + 1) : { # sub-iterate over objects by "count" value specified; use range function and begin at 1 for human readability
        new_key              = "${instance_key} ${type_count}" # for resource uniqueness
        type                 = instance_key # for easier tag value later
        ami                  = instance.ami # this and below retained from variable inputs
        instance_type        = instance.instance_type
        iam_instance_profile = instance.iam_instance_profile
        subnet_id            = instance.subnet_id
      }
    ]
  ])
}

Now we can iterate within the resource with the for_each meta-argument, and utilize the for expression to reconstruct the input for suitable usage within the resource.

resource "aws_instance" "k8s-node" {
  # local.instance_types is a list of objects, and we need a map of objects with unique resource keys
  for_each = { for instance_type in local.instance_types : instance_type.new_key => instance_type }

  ami                  = each.value.ami
  instance_type        = each.value.instance_type
  iam_instance_profile = each.value.iam_instance_profile
  subnet_id            = each.value.subnet_id

  tags = {
    Name = each.key
    Type = each.value.type
  }
}

This will give you the behavior you desire, and you can modify it for style preferences or different uses as the need arises.

Note the lookup functions are removed since they are only useful when default values are specified as a third argument, and that is not possible in object types within variable declarations except as an experimental feature in 0.14.

The absolute namespace for these resources' exported resource attributes would be:

(module?.<declared_module_name>?.)<resource_type>.<resource_name>[<resource_key>].<attribute>

For example, given an intra-module resource, first worker node, and private ip address exported attribute:

aws_instance.k8s-node["worker 1"].private_ip

Note you can also access all resources' exported attributes by terminating the namespace at <resource_name> (retaining the map of all resources instead of accessing a singular resource value). Then you could also use a for expression in an output declaration to create a custom aggregate output for all of the similar resources and their identical exported attribute(s).

{ for node_key, node in aws_instance.k8s-node : node_key => node.private_ip }

Upvotes: 2

Related Questions