Terraform module dependency with for_each

Hello, quick recap of the dependency issue

I have a TF module which creates a database role

module userorrole


resource "postgresql_role" "app_login_user" {
  provider    = postgresql
  name        = var.app_username
  login       = var.login
  search_path = var.search_path
  roles       = var.roles != "" ? var.roles : null
}

In my main module I invoke the above module as follows based on the supplied input variable by consumers.

Input variable from module consumers
*************************
schema_to_applogin_mapping = {
  apple= {
    app_login_user           = "apple _user"
    enable_password_rotation = false
  }}

module "create_std_schema_read_role" {
  for_each     = var.schema_to_applogin_mapping
  source       = "./modules/userorrole"
  app_username = "${each.key}_role_r"
  login        = false
  providers = {
    postgresql = postgresql.appusrprov
  }
}

outputs.tf
*********
output "ret_rl_name" {
  value       = var.app_username
  description = "Created Role name"
}

Based on the above code snippet and input variable I am just creating a database role called (apple_role_r).

Now the consumer is going to supply another input variable as shown below to create some additional logins and requesting to grant the above read only role(apple_role_r)

additional_logins_mapping = {
  grant_read_on_apple = {
    schema_to_grant = "apple"
    privileges      = "r"
    rotate_password = false
  }
}

I have another module which invokes the same userorrole module as follows

module "add_additional_logins" {
  for_each             = var.addtional_logins_mapping
  source               = "./modules/userorrole"
  app_username         = each.key
  search_path          = ["public", each.value["schema_to_grant"]]
  roles                = ["${each.value.schema_to_grant}_role_${each.value.privileges}"]
}

Issue


Terraform is executing the add_additional_logins ahead of time before completing the execution of the create_std_schema_read_role module.

I can add depends_on = [create_std_schema_read_role ] clause for add_additional_logins module , but every TF apply some security group related resources is getting recreated, so I won’t be able to use the module level dependency.

Ask


How else I can enforce a dependency between

add_additional_logins module and create_std_schema_read_role so that add_additional_logins module waits until completion of create_std_schema_read_role ?

Regards
RK

Hi there! You can add an output from the create_std_schema_read_role and use that as an input to your add_additional_logins module, just as a dummy variable. It doesn’t matter that it won’t be used - it’s about adding a dependency edge in the generated graph.

Your add_additional_logins call would then be something like:

module "add_additional_logins" {
  for_each             = var.addtional_logins_mapping
  source               = "./modules/userorrole"
  app_username         = each.key
  search_path          = ["public", each.value["schema_to_grant"]]
  roles                = ["${each.value.schema_to_grant}_role_${each.value.privileges}"]

  dummy_input = module.create_std_schema_read_role[each.value.schema_to_grant].dummy_output
}

Hi @rxxk-cg,

If there is a dependency between objects inside module.create_std_schema_read_role and objects inside module.add_additional_logins then the typical way to represent that is to pass values representing those objects from one module to another, using output values and input variables.

In your case, it seems like module.create_std_schema_read_role is declaring a role and module.add_additional_logins is then using that role, but you’re achieving that by just configuring them both so they end up using the same name, rather than by explicitly passing the name from one module to another. Therefore Terraform assumes that the two objects are totally independent and so tries to create them in an arbitrary order, or possibly tries to create them concurrently.

I would suggest taking a different approach to the problem where module.add_additional_logins is passed the schema name and the role names by reference to outputs from module.create_schema_read_role, rather than by constructing a name that just happens to match, and then Terraform will understand that the objects in the second module require the objects in the first module and thus you’ll achieve the required operation order.

depends_on is for hidden dependencies where some second object is needed in order for a first object to be fully-functional. For example, in AWS the data model encourages creating an IAM role separately from associating policies from the role, but other objects making use of the role don’t directly refer to the policy and so without depends_on Terraform would not be able to infer that the role is not fully-functional until the policy has been created. That doesn’t seem to be true of your situation, because the “additional logins” refer directly to the roles; in that case, the dependency is explicit rather than hidden and so it’s better to represent it by data flow between objects rather than by depends_on.

@apparentlymart Thank you for the feedback and prompt response. My initial thought was to implement an explicit dependency by passing
the role name output variable from module.create_schema_read_role to
module.add_additional_logins but I was not sure how to do that as I have pass the output from different modules based on user requested role/privilege.

Let me give some additional details, the module.add_additional_logins input variable can be as follows.

additional_logins_mapping = {
  grant_read_on_apple = {
    schema_to_grant = "apple"
    privileges      = "r" – User requested read privilege, output will be from module.create_schema_read_role
    rotate_password = false
  },
  grant_rwx_on_orange = {
    schema_to_grant = "apple"
    privileges      = "rwx" - User requested rwx privilege, output will be from module.create_schema_rwx_role
    rotate_password = false
  },
  grant_admin_on_mango = {
    schema_to_grant = "apple"
    privileges      = "admin" - user requested admin privilege, output will be from module.create_schema_admin_role
    rotate_password = false
  }}

In summary, i need to implement a nested if based on privileges requested and pass the corresponding module output as shown below in the pseudocode

module "add_additional_logins" {
  for_each             = var.addtional_logins_mapping
  source               = "./modules/userorrole"
  app_username         = each.key
  search_path          = ["public", each.value["schema_to_grant"]]
  #roles                = ["${each.value.schema_to_grant}_role_${each.value.privileges}"]
  
				          ##PSEUDOCODE  CODE BELOW
  roles                = [ if each.value.privileges ="r" then
                            module.create_std_schema_read_role[each.value.schema_to_grant].ret_rl_name
						   elsif each.value.privileges ="rwx" then
						    module.create_std_schema_rwx_role[each.value.schema_to_grant].ret_rl_name
						   else
						    module.create_std_schema_admin_role[each.value.schema_to_grant].ret_rl_name
						   end if ]
}

I am not sure how to achieve that

Regards
RK

Thanks for that extra context!

A common way to achieve an indirect lookup like this is to declare a local value mapping which has keys matching what you want to look up by, like this:

locals {
  roles = {
    "r" =   module.create_schema_read_role,
    "rwx" = module.create_schema_rwx_role
    # ...
  }
}

You can then look up a particular role’s module object using syntax like local.roles[each.value.privileges]. If you need a callback for an unrecognized key you can use the try function to provide that fallback.

From a dependency perspective, this actually tells Terraform that the local value depends on all of the output values from all of these modules, and so anything which refers to local.roles indirectly depends on them all too.

Given that your modules seem to be specifically representing the roles I assumed that was fine, but if there are other output values of these modules that you’d rather not depend on then you can be more specific in the local value declaration by referring to a specific attribute of each of the module objects, rather than to the objects as a whole. That would then potentially allow Terraform to begin working on the additional module’s objects slightly earlier, but the difference is probably marginal and so not worth worrying about; I mention it only in the hope that it’s helpful in understanding how Terraform thinks about dependencies.

@apparentlymart Thank you that was nice trick , can you please help me on how refer the module output variable as input to add_additional_logins module

Here is my local map

 locals {
  roles = { "r" = module.create_std_schema_read_role , "rwx" = module.create_std_schema_rwx_role, "admin" = module.create_std_schema_admin_role }
        }

Module create_std_schema_read_role and its input and output variables

schema_to_applogin_mapping = {
  apple = {
    app_login_user           = "apple_user"
    enable_password_rotation = false
  }}

module "create_std_schema_read_role" {
  for_each     = var.schema_to_applogin_mapping
  source       = "./modules/userorrole"
  app_username = "${each.key}_role_r"
  login        = false
  providers = {
    postgresql = postgresql.appusrprov
  }
}

output.tf 
**********
output "ret_rl_name" {
  value       = var.app_username
  description = "Created Role name"
}

In the above scenario the output of the module output is going to be “apple_role_r” and i need to pass “apple_role_r” ouput value to the below module.

 module "add_additonal_logins" {
  for_each             = var.addtional_logins_mapping
  source               = "./modules/userorrole"
  app_username         = each.key
  search_path          = ["public", each.value["schema_to_grant"]]
  password_rotation    = each.value["rotate_password"]
  login                = true

  I tried following method it seems none of them are working. 

# option 1
  roles                = [local.roles[each.value.privileges].ret_rl_name] - I need to pass the actual module output value (apple_role_r) as input 
# option 2
  roles                = [lookup(local.roles,each.value.privileges).ret_rl_name]

    providers = {
    postgresql = postgresql.appusrprov
  }  
}

None of them seems to be working as unable pass the output

Regards
RK

Did you see a specific error when you tried each of these? Or did you get a result that didn’t match what you expected?

If you can show exactly what happened when you tried each of these I can hopefully explain how to adjust to fix it.

@apparentlymart Thank you, below some options which i tried and they failed with error

additonal_logins_mapping variable as mentioned in my earlier post.

additional_logins_mapping = {
  grant_read_on_apple = {
    schema_to_grant = "apple"
    privileges      = "r" 
    rotate_password = false
  },
  grant_rwx_on_orange = {
    schema_to_grant = "apple"
    privileges      = "rwx" 
    rotate_password = false
  },
  grant_admin_on_mango = {
    schema_to_grant = "apple"
    privileges      = "admin"
    rotate_password = false
  }}

Here is my local lookup

local {
     roles = { "r" = module.create_std_schema_read_role, "rwx" = module.create_std_schema_rwx_role, "admin" = module.create_std_schema_admin_role }
}

Option 1 in add_additonal_logins module.

 module "add_additonal_logins" {
  for_each             = var.additional_logins_mapping
  source               = "./modules/userorrole"
  app_username         = each.key
  search_path          = ["public", each.value["schema_to_grant"]]
  password_rotation    = each.value["rotate_password"]
  roles                = [local.roles[each.value.privileges].ret_rl_name]
}

Below are the error messages during plan

terraform plan -var-file=input_vars.auto.tfvars
Environment variable CREDENV_CONFIG_FILES not set. No configuration file(s) will be used
â•·
│ Error: Unsupported attribute
│ 
│   on main.tf line 458, in module "add_additonal_logins":
│  458:   roles                = [local.roles[each.value.privileges].ret_rl_name]
│     ├────────────────
│     │ each.value.privileges is "admin"
│     │ local.roles is object with 3 attributes
│ 
│ This object does not have an attribute named "ret_rl_name".
╵
â•·
│ Error: Unsupported attribute
│ 
│   on main.tf line 458, in module "add_additonal_logins":
│  458:   roles                = [local.roles[each.value.privileges].ret_rl_name]
│     ├────────────────
│     │ each.value.privileges is "r"
│     │ local.roles is object with 3 attributes
│ 
│ This object does not have an attribute named "ret_rl_name".
╵
â•·
│ Error: Unsupported attribute
│ 
│   on main.tf line 458, in module "add_additonal_logins":
│  458:   roles                = [local.roles[each.value.privileges].ret_rl_name]
│     ├────────────────
│     │ each.value.privileges is "rwx"
│     │ local.roles is object with 3 attributes
│ 
│ This object does not have an attribute named "ret_rl_name".
╵
exit status 1

Option 2 i changed my locals declaration

roles_map = { r = "module.create_std_schema_read_role", rwx = "module.create_std_schema_rwx_role", admin = "module.create_std_schema_admin_role" }
module "add_additonal_logins" {
  for_each             = var.additional_logins_mapping
  source               = "./modules/userorrole"
  app_username         = each.key
  search_path          = ["public", each.value["schema_to_grant"]]
  password_rotation    = each.value["rotate_password"]
roles                = [lookup(local.roles_map, each.value.privileges).ret_rl_name]

}

Below are the error messages during plan

 terraform plan -var-file=input_vars.auto.tfvars
Environment variable CREDENV_CONFIG_FILES not set. No configuration file(s) will be used
â•·
│ Error: Unsupported attribute
│ 
│   on main.tf line 459, in module "add_additonal_logins":
│  459:   roles                = [lookup(local.roles_map, each.value.privileges).ret_rl_name]
│     ├────────────────
│     │ each.value.privileges is "rwx"
│     │ local.roles_map is object with 3 attributes
│ 
│ This value does not have any attributes.
╵
â•·
│ Error: Unsupported attribute
│ 
│   on main.tf line 459, in module "add_additonal_logins":
│  459:   roles                = [lookup(local.roles_map, each.value.privileges).ret_rl_name]
│     ├────────────────
│     │ each.value.privileges is "admin"
│     │ local.roles_map is object with 3 attributes
│ 
│ This value does not have any attributes.
╵
â•·
│ Error: Unsupported attribute
│ 
│   on main.tf line 459, in module "add_additonal_logins":
│  459:   roles                = [lookup(local.roles_map, each.value.privileges).ret_rl_name]
│     ├────────────────
│     │ each.value.privileges is "r"
│     │ local.roles_map is object with 3 attributes
│ 
│ This value does not have any attributes.
╵
exit status 1

Regards
RK

Hi @rxxk-cg,

Your first attempt seems like the most promising one. This error about the attribute named ret_rl_name seems to be reporting that this module doesn’t have an output "ret_rl_name" declared, and so therefore there’s no attribute of that name on the object constructed from the module’s output values.

Can you confirm that there is an output "ret_rl_name" block in your ./modules/userorrole module? If there is then the objects representing those module calls should then have a corresponding attribute.


In case it’s interesting to you for learning purposes: the second attempt failed because you wrote the references like module.create_std_schema_read_role in quotes, and so Terraform sees them as just strings containing letters and dots rather than as actual references. “This value does not have any attributes” because a string value never has attributes; only object values and map values support the .attribute syntax.

@apparentlymart yes i do have output.tf file in ./modules/userorrole module with the following

output "ret_rl_name" {
  value       = var.app_username
  description = "Created Role name"
}

Simplified Module code is

terraform {
  required_providers {
    cg = {
      source = "capgroup.com/itg/cg"
    }
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.71.0"
    }
    postgresql = {
      source  = "cyrilgdn/postgresql"
      version = "1.15.0"
    }
  }
}

resource "random_password" "user_password" {
  count            = var.login ? 1 : 0
  length           = 16
  special          = true
  override_special = "_!#"
}

resource "postgresql_role" "app_login_user" {
  provider    = postgresql
  name        = var.app_username
  login       = var.login
  password    = var.login ? sensitive(random_password.user_password.0.result) : null
  search_path = var.search_path
  superuser   = false
  roles       = var.roles != "" ? var.roles : null
}

Regards
RK

Hi @rxxk-cg,

Although this is not directly related to the error we’re currently debugging, I first want to note that in order to get the correct dependency ordering we were originally discussing here your output value must refer to the postgresql_role resource, rather than to the input variable, because otherwise there’s nothing to tell Terraform that the output value will become valid only once the role is created:

output "ret_rl_name" {
  value       = postgresql_role.app_login_user.name
  description = "Created Role name"
}

Notice that the name argument of postgresql_role.app_login_user is also populated from var.app_username, so in practice this will produce exactly the same value but will additionally create the needed edge in Terraform’s dependency graph, so that anything referring to this output value cannot proceed until the role has been created or updated.


With that said though, I must admit I’m not sure why you were seeing the error you shared previously, given that there is a suitable output value declared and you were referring to that value. My suggestion above should not change that error because it only affects Terraform’s order of operations for a valid configuration, not whether the configuration is valid.

When encountering situations like this where there’s lots of dynamic behavior going on I typically debug by gradually simplifying until I can get clearer feedback on the problem. In your case I think I would try temporarily simplifying module "add_additional_logins" to just use a hard-coded roles referring to just one of the three module calls and see if that leads to the same error or if it gives some different feedback:

module "add_additonal_logins" {
  # ...

  roles = [module.create_std_schema_read_role.ret_rl_name]
}

With what we tried so far, module.create_std_schema_read_role.ret_rl_name should be equivalent to local.roles["r"].ret_rl_name and so it would be interesting to see if it fails in the same way or if it causes a different result.

Another way to gather some more information, which you could do as a separate step from what I just suggested, is to temporarily comment out the module "add_additonal_logins" block altogether to disable it, and then add an output value to your root module to see exactly what value local.roles has:

output "temp_roles" {
  value = local.roles
}

Commenting out the failing module should allow terraform apply to succeed and thus allow you to see the resulting value for this output value in the messaging from Terraform. I would expect this output value to be something like the following, based on what you’ve shared so far:

  temp_roles = {
    r = {
      ret_rl_name = "apple_role_r"
    }
    rwx = {
      ret_rl_name = "apple_role_rwx"
    }
    admin = {
      ret_rl_name = "apple_role_admin"
    }
  }

If you have other output blocks in this module which you didn’t mention then those would appear in these objects too, but the ret_rl_name attribute is the one important to the error message we’re currently investigating.

@apparentlymart
First of all, I would like to thank you for taking time to review and providing valuable feedback.

I have adjusted the module output as follows

#output "ret_rl_name" {
#  value       = var.app_username
#  description = "Created Role name"
#}

output "ret_rl_name" {
  value       = postgresql_role.app_login_user.name
  description = "Created Role name"
}

on the main issue as we have for_each in “add_additonal_logins” so we need to refer the output variable “ret_rl_name” as follows

module "add_additonal_logins" {
  for_each             = var.additional_logins_mapping
  source               = "./modules/userorrole"
  app_username         = each.key
  search_path          = ["public", each.value["schema_to_grant"]]
  password_rotation    = each.value["rotate_password"]
#  roles                = [local.roles[each.value.privileges].ret_rl_name]
 roles                = [local.roles_map[each.value.privileges][each.value.schema_to_grant].ret_rl_name]
}

The above seems to be working fine. 

The above seems to be working fine.

regards
rk

@apparentlymart on the same lines do we have a way to validate user input values in the below input variable

variable "additional_logins_mapping" {
  type = map(object({
    schema_to_grant = string
    privileges      = string
    rotate_password = bool
  }))
  description = "Please supply the additional logins and corresponding schemas and privs"
  default     = {}
}

The valid values for the privileges are going to be “r” ,“rwx”, “admin”. Do we have a way to enforce that ?

Hi @rxxk-cg,

You should be able to enforce that condition using a [custom validation rule] (Input Variables - Configuration Language | Terraform by HashiCorp). Since this is a situation where you want to check a condition for each element of a collection, you would use alltrue to test the result of a list of boolean values generated by a for expression.