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"
      }
    }
   ...
}

Hi @tung,
here another use case showing how the locals layering would be very useful. Locals after all are variables (although a special local form) and as you say and also as apparentlymart says on the terraform community (e.g. Putting csvdecode in security group rule) “Input variables are Terraform’s equivalent of arguments to a function in a general-purpose language…”.

As now (terraform does not support variables - interpolation - in variables) so locals are the only solution for many DRY cases, another among many shown here:

Translate locals + interpolation into variables form

I know variables layering uses sequence and overriding with numbered tfvars (allowed by terraform) but locals layering would be very useful also without numbering and overriding, with duplications catched by terraform, so in a secure way.

Call locals layering not layering to avoid confusion :slightly_smiling_face:

Stumbled upon this thread because I was looking to do the same because I was coming from a Terragrunt project where all we have is like locals{} defined at various levels in different files: account.hcl, region.hcl, and env.hcl (or layers in Terraspace speak).

Aping that structure isn’t a terrible idea is it?

Also you don’t have to define variables all over the place to map that type of thing into a stack then either.

There’s also a top level terragrunt.hcl with a pile of locals and a standard set of tags (local.tags) and then they merge all the locals from the 3 files at end into input = (but this reminds me of the layering that Terraspace already does w/ tfvars).

The setup is very simliar to:

I would be curious to know how someone would map this type of project to Terrasapce… would each piece under ACCOUNT/REGION/ENV/{mysql/web, etc.} map to a Terraspace stack and then you’d wire then together with ERB in the tfvars? Sorry if this is too many questions in one comment/thread.

This recursive hell is why I wanted to go away from Terragrunt, but the example there is more like the real world structure of a Terragrunt project.