Search code examples
terraforminfrastructure-as-code

Terraform loop : for_each


I need help to run for_each on terraform for following variable set

locals {
  db_users = {
    test_user1 = {      #user
      test_cluster1 = { #cluster
        db_name = ["db_a", "db_b", "db_c"]
        db_role = ["readWrite", "read", "readWrite"]
        db_type = ["CLUSTER", "CLUSTER", "CLUSTER"]
      },
      test_cluster2 = {
        db_name = ["db_a", "db_b", "db_c"]
        db_role = ["readWrite", "read", "readWrite"]
        db_type = ["CLUSTER", "CLUSTER", "CLUSTER"]
      }
    },
    test_user2 = {
      test_cluster1 = {
        db_name = ["db_d", "db_e", "db_f"]
        db_role = ["readWrite", "readWrite", "read"]
        #db_type = [["CLUSTER", "LAKE"], ["CLUSTER", "LAKE"], ["CLUSTER", "LAKE"]]
        db_type = ["CLUSTER", "CLUSTER", "CLUSTER"]
      },
      test_cluster2 = {
        db_name = ["db_d", "db_e", "db_f"]
        db_role = ["readWrite", "readWrite", "read"]
        db_type = ["CLUSTER", "CLUSTER", "CLUSTER"]
      }
    }
  }

The db_type can be both based on the situation. If we have multiple db_type then both value should be associated with the final resource.

I also tried flatten of the variable with following

value = flatten([
  for ip_key, ip in local.db_users : [
    for a, b in ip : [
      for index in range(length(b.db_name)) : {
        username = ip_key
        user_index = index
        roles = {
          role_name     = b.db_role[index]
          database_name = b.db_name[index]
        }
        scopes = {
          type = b.db_type[index]
          name = a
        }
      }
    ]
  ]
])

Output after flattening the value

[
  {
    "roles" = {
      "database_name" = "db_a"
      "role_name" = "readWrite"
    }
    "scopes" = {
      "name" = "test_cluster1"
      "type" = "CLUSTER"
    }
    "username" = "test_user1"
  },
  {
    "roles" = {
      "database_name" = "db_b"
      "role_name" = "read"
    }
    "scopes" = {
      "name" = "test_cluster1"
      "type" = "CLUSTER"
    }
    "username" = "test_user1"
  },
   ...
  {
    "roles" = {
      "database_name" = "db_d"
      "role_name" = "readWrite"
    }
    "scopes" = {
      "name" = "test_cluster1"
      "type" = "CLUSTER"
    }
    "username" = "test_user2"
  },
  {
    "roles" = {
      "database_name" = "db_e"
      "role_name" = "readWrite"
    }
    "scopes" = {
      "name" = "test_cluster1"
      "type" = "CLUSTER"
    }
    "username" = "test_user2"
  },
   ...
]

Looking into the type of the value, it is as follows

tuple([
    object({
        roles: object({
            database_name: string,
            role_name: string,
        }),
        scopes: object({
            name: string,
            type: string,
        }),
        username: string,
    }),
    ...
    object({
        roles: object({
            database_name: string,
            role_name: string,
        }),
        scopes: object({
            name: string,
            type: string,
        }),
        username: string,
    }),
])

Question: What I want to achieve???

  • I want to create the resource from this particular variable

The resource which is responsible for this is the following. There can be as many roles and scopes in the same block as possible. For this we can you dynamic block setup

resource "users" "user" {
  username = var.username
  roles {
    database_name = var.database_name
    role_name     = var.role_name
  }
  roles {
    database_name = var.database_name
    role_name     = var.role_name
  }
  ...
  scopes {
    name = var.cluster
    type = var.type
  }
  scopes {
    name = var.cluster
    type = var.type
  }
  ...
}

and the resource should finally look like the following value.

Resource No. 1

username=test_user1
role = {
  db_name=db_a
  role=readWrite
}
role = {
  db_name=db_b
  role=read
}
role = {
  db_name=db_c
  role=readWrite
}
scope = {
  name = test_cluster1
  type = "cluster"
}
scope = {
  name = test_cluster1
  type = "lake"
}

Resource No. 2

username=test_user1
role = {
  db_name=db_d
  role=readWrite
}
role = {
  db_name=db_e
  role=read
}
role = {
  db_name=db_f
  role=readWrite
}
scope = {
  name = test_cluster2
  type = "cluster"
}
scope = {
  name = test_cluster2
  type = "lake"
}

Resource No. 3

username=test_user1
role = {
  db_name=db_a
  role=readWrite
}
role = {
  db_name=db_b
  role=read
}
role = {
  db_name=db_c
  role=readWrite
}
scope = {
  name = test_cluster1
  type = "cluster"
}
scope = {
  name = test_cluster1
  type = "lake"
}

Resource No. 4

username=test_user2
role = {
  db_name=db_d
  role=readWrite
}
role = {
  db_name=db_e
  role=read
}
role = {
  db_name=db_f
  role=readWrite
}
scope = {
  name = test_cluster2
  type = "cluster"
}
scope = {
  name = test_cluster2
  type = "lake"
}

Exact Source code used. ( I know this is not correct). here username, database_name, role_name, name, and type all should be a string value

resource "users" "user" {
  for_each = local.db_users
  username = each.key
  dynamic "roles" {
    for_each = each.value
    content {
      database_name = each.value.db_name
      role_name     = each.value.db_role
    }
  }
  dynamic "scopes" {
    for_each = each.value
    content {
      name = each.key
      type = each.value.db_type
    }
  }
}

Error

│ Error: Unsupported attribute
│
│   on database_users.tf line 25, in resource "mongodbatlas_database_user" "user":
│   25:       database_name = each.value.db_name
│     ├────────────────
│     │ each.value is object with 2 attributes
│
│ This object does not have an attribute named "db_name".
╷
│ Error: Unsupported attribute
│
│   on database_users.tf line 26, in resource "mongodbatlas_database_user" "user":
│   26:       role_name     = each.value.db_role
│     ├────────────────
│     │ each.value is object with 2 attributes
│
│ This object does not have an attribute named "db_role".
╵
╷
│ Error: Unsupported attribute
│
│   on database_users.tf line 33, in resource "mongodbatlas_database_user" "user":
│   33:       type = each.value.db_type
│     ├────────────────
│     │ each.value is object with 2 attributes
│
│ This object does not have an attribute named "db_type".
╵

PS: considering the above flatten value is not used. How to solve this using the same db_users contents?

PPS: I am also okay with using of flatten value as long as it serves my purpose.


Solution

  • Your db_users should be flattened in a different way, namely:

    locals {
     
        db_users_flat = merge([
        for username, clusters in local.db_users : 
          {
             for clustername, cluster in clusters : 
               "${username}-${clustername}" => {
                     username = username
                     clustername = clustername
                     cluster = cluster
                }
           }
      ]...) # please do NOT remove the dots
     
    }
    

    then

    resource "users" "user" {
      for_each = local.db_users_flat
      username = each.value.username
      dynamic "roles" {
        for_each = range(length(each.value.cluster.db_name))
        content {
          database_name = each.value.cluster.db_name[roles.key]
          role_name     = each.value.cluster.db_role[roles.key]
        }
      }
      dynamic "scopes" {
        for_each = range(length(each.value.cluster.db_type))
        content {
          name = each.value.clustername
          type = each.value.cluster.db_type[scopes.key]
        }
      }
    }