Migrating from count to for_each

February 8, 2021
terraform devops bash

Prior to the 0.12 release of Terraform you could manage a group of similar resources using count. For example, you might have a manifest that looks like this:

provider "aws" {
  region = "us-east-2"
}

locals {
    user_names = ["maggie", "milly", "molly", "may"]
}

resource "aws_iam_user" "example" {
    count = length(local.user_names)
    name  = local.user_names[count.index]
}

This will create resources aws_iam_user.example[0], aws_iam_user.example[1], etc. The problem comes if you want to delete any user but the last in the list. This will cause the subsequent resource indices to change, which will trigger terraform to delete and re-create resources needlessly. In version 0.12 a new looping construct, for_each, was introduced, allowing us to write our manifest as follows:

provider "aws" {
  region = "us-east-2"
}

locals {
    user_names = {
        "maggie" = {}
        "milly"  = {}
        "molly"  = {}
        "may"    = {}
    }
}

resource "aws_iam_user" "example" {
    for_each = local.user_names
    name     = each.key
}

Now the created resources are named after the corresponding key, aws_iam_user.example["maggie"], aws_iam_user.example["milly"], etc. and we can delete any user without affecting the others. Note that the local variable user_names has changed from a list to a map. If we want to override certain attributes of the created resources, we can add these to our map:

locals {
    user_names = {
        "maggie" = {path = "/staff/"}
        "milly"  = {path = "/staff/"}
        "molly"  = {path = "/staff/"}
        "may"    = {path = "/robot/"}
    }
}

We can then reference them through each.value:

resource "aws_iam_user" "example" {
    for_each = local.user_names
    name     = each.key
    path     = each.value.path
}

The question arises: what if we started out with a manifest using count, and want to transition to the new for_each? If we simply update the manifest, we will trigger the create/destroy operations we are working to avoid. The answer is to rename the resources in the existing terraform state. For the above example, this simple bash script would do the trick:

#!/bin/bash

declare -i N=0
while read U; do
    terraform state mv "aws_iam_user.example[$N]" "aws_iam_user.example[\"$U\"]"
    (( N++ ))
done <<EOT
maggie
milly
molly
may
EOT

N.B. the resource names input to the shell must be in the exact same order as they appeared in the original terraform manifest.

A bash retry loop

Retrying failed operations and a post-increment gotcha
bash

Aero meets SSM

January 27, 2018
clojure devops cloud aws