Global + stack specific locals

As far as I can see for each stack/module terraspace overwrite global locals (config/terraform/locals.rf) with stack/module specific locals (app/stack/stackX/config/terraform/locals.tf) in cache stack directory.

It would be very useful to add/merge global+stack locals so every stack can use global + its onw spec locals for DRY code.

Also layered locals would be very useful allowin DRY code at data structure definitions used to define infrastructure resource e.g.used in code with for_each (allowing different config for enviromnets).

Partial solution
In terraform locals can just be defined in different files merged togheter by the same terraform. So in terraspace that’s enough to define different “.tf” file containing locals portions to reach the same wanted result.

Example:

app/stack/stackX/config/terraform/locals.tf
app/config/terraform/locals_global.tf

In the cache directory we’ll found locals.tf and locals_global.tf and this will works.

Why partial (works for not overlapping local variables)
This is a partial solution as without more structured overriding/marge management from terraspace we’ll need to worry about and to avoid “Error: Duplicate local value definition” so not a really complete solution as we should make sure no locals overlapping take place for all stacks (sometimes very tricky !)

Mentioned the same thing in the issue: https://github.com/boltops-tools/terraspace/issues/104

Got mixed feelings here. Believe modules are like functions. Tfvars are inputs to these functions. And locals are like temporary local variables within the function.

Layering support for locals feels like reaching into a function and changing internal local variables. It might be a little bit too much rope, when maybe it’s a sign that the module can be refactored.

Probably you’re right also if I think more about locals for live configurations not for modules. In live config I think about locals as “human friendly” structured data rappresentation of objects (resources) instead of local variables as internal local vars for functions (modules).
A sample code to map ec2 instances to ebs (created on another stack used for persistent resources):

  ###   (EBS) EC2 to Non-Root EBS mapping
  ec2_to_ebs_map = {
    "host1" = {
      "svc001" = {
        "enabled"         = true
        "instance_id"     = module.host1.id
        "ebs_device_name" = "xvdf"
      }
      "host1" = {
        "enabled"         = true
        "instance_id"     = module.host1.id
        "ebs_device_name" = "xvdg"
      }
    }
    "host2" = {
      "db001" = {
        "enabled"         = true
        "instance_id"     = module.host2.id
        "ebs_device_name" = "/dev/sdb"
      }
    }
    "host3" = {
      "db002" = {
        "enabled"         = true
        "instance_id"     = module.host3.id
        "ebs_device_name" = "/dev/sdb"
      }
    }
  }

  ### ec2 to ebs mapping flatten prepared data
  ec2_to_ebs_list = flatten ([
    for ec2_name, ebss in local.ec2_to_ebs_map: [
      for ebs_name, mapcfg in ebss: {
        ec2_name        = ec2_name
        ebs_name        = ebs_name
        map_enabled     = mapcfg.enabled
        instance_id     = mapcfg.instance_id
        ebs_device_name = mapcfg.ebs_device_name
      }
    ]
  ])
  ec2_to_ebs = { for ec2 in local.ec2_to_ebs_list: "${ec2.ec2_name}_${ec2.ebs_name}" => ec2 }

ec2_to_ebs will be used by code in a for_each loop to map ec2 to related ebs. Locals layering allow devel environment less ebs for ec2 then prod environment, reducing cost. e.g. host1 with only 2 ebs in devel and 8 ebs in prod.

The same can be done for ec2 list or many other resources, having more flexible environment based sized infra.

Wondering since layering is supported for tfvars, why not make it a variable instead of a local. IE:

var.ec2_to_ebs_map vs local.ec2_to_ebs_map

So instead, the code would be:

### ec2 to ebs mapping flatten prepared data
ec2_to_ebs_list = flatten ([
  for ec2_name, ebss in var.ec2_to_ebs_map: [
    for ebs_name, mapcfg in ebss: {
      ec2_name        = ec2_name
      ebs_name        = ebs_name
      map_enabled     = mapcfg.enabled
      instance_id     = mapcfg.instance_id
      ebs_device_name = mapcfg.ebs_device_name
    }
  ]
])
ec2_to_ebs = { for ec2 in var.ec2_to_ebs_list: "${ec2.ec2_name}_${ec2.ebs_name}" => ec2 }

Thinking more about this. Layering is pretty simple because terraspace leverages existing terraform features by generating auto.tfvars in the right order. With local layering, it’s more complex since more would have to built.

Also, still feels like locals are more meant to be an internal thing to the module. Hoping for a better example where a variable could not be used instead of a local. Currently, going to pass on local layering.

Sometimes, locals are more agile especially for complex data struct (e.g. no variables definition is needed), but probably defining and checking vars is always a good practice.

Thank you. I’ll evaluate tfvars way.

Another useful use case also if it can be probably converted as variables with “extra works”, e.g. merge with locals but how to make dynamic enought to use different configuration based on envs. Following can be a local but not a variable directly as variable in variable is not allowed by terraform:

  org_accounts = {
    "account1" = {
      email           = "account1@example.com"
      allow_billing   = "ALLOW"
      parent_id       = module.org_base.roots[0].id
      admin_role_name = "orgMasterInAdmin"
      tags = {
        Environment = "org_prod"
      }
    }
    "account2" = {
      email           = "account2@example.com"
      allow_billing   = "ALLOW"
      parent_id       = module.org_ou_account1.ou_id
      admin_role_name = "orgMasterInAdmin"
      tags = {
        Environment = "org_prod"
      }
    }
   ...
}