How do I use Terraform to maintain/manage IAM users

Thanks for the answer/suggestion from Martin Atkins in hashicorp-terraform Gitter room:

The issue discussed here is that when you use "count" with an array variable Terraform doesn't really "see" the relationships between the items in the array and the resources, so when a value is deleted from the middle of the list everything after that point is suddenly "off by one" and Terraform will want to replace them all.

This is the sort of use-case that would benefit from a first-class iteration feature in Terraform, but sadly we don't have that yet I would suggest that instead of trying to pass the user list in as variables, the most robust approach for now is to have a separate program that reads the user list from somewhere and writes out a .tf.json file containing a separate aws_iam_user block for each user. That way Terraform will understand which block belongs to which user because the local identifier can be the username or some sort of user id, allowing the correlation to be maintained.


Terraform 0.12.6 added the for_each on resources, which turns out to be very useful in solving the described issue.

Suppose we wrote a terraform.tfvars that contains some IAM user names and group memberships like this:

user_names = {
  "test-user-1" = {
    path          = "/"
    force_destroy = true
    tag_email     = "[email protected]"
  }
  "test-user-2" = {
    path          = "/"
    force_destroy = true
    tag_email     = "[email protected]"
  }
}

group_memberships = {
  "test-user-1" = [ "SomeGroup", "AnotherGroup" ]
  "test-user-2" = [ "AndYetAnotherGroup" ]
}

Then we could use the for_each meta-argument to pull out the key and values like this:

resource "aws_iam_user" "user" {
  for_each = "${var.user_names}"

  name          = each.key
  path          = each.value["path"]
  force_destroy = each.value["force_destroy"]

  tags = "${map("EmailAddress", each.value["tag_email"])}"
}

resource "aws_iam_user_group_membership" "group_membership" {
  for_each = "${var.group_memberships}"

  user   = each.key
  groups = each.value

  depends_on = [ "aws_iam_user.user" ]
}

Note that each item in the map variables for user_names and group_memberships, the items must all be of the same type. We can not mix maps and lists. This is why I've separated the group_memberships from the user_names. If that wasn't the case, we could have defined another attribute for each user to specify the group memberships, but Terraform doesn't support that at this time.

If you were to make a module out of this, and wanted to output some useful attributes about each user, you could accomplish this as follows:

output "user_ids" {
  value = {
    for user_name in aws_iam_user.user:
    user_name.name => user_name.unique_id
  }
}

output "user_arns" {
  value = {
    for user_name in aws_iam_user.user:
    user_name.name => user_name.arn
  }
}

The resulting outputs would be a map keyed off of the user_name.

user_arns = {
  "test-user-1" = "arn:aws:iam::123456789012:user/test-user-1"
  "test-user-2" = "arn:aws:iam::123456789012:user/test-user-2"
}

user_ids = {
  "test-user-1" = "AB2DEDN2O2NMLMNT4KI7G"
  "test-user-2" = "AB1DEFG2O2NNLJQ9YKH7J"
}

With this, we can add, update, or delete users and/or group memberships without affecting the mapping between terraform resources as known in the tfstate and the configuration in code. This is because each resource's name now contains the user name as a part of the resource name itself, and allows us to refer to a specific resource, whereas when using "count =" we have a count.index, but no way to easily map a specific user to a specific resource.