Reputation: 7732
I have a locals.tf
containing a bunch of lookup maps that translate tf vars with known/expected values and return the value (possibly with a suffix) for unknown values, e.g.:
locals {
Environment = lookup({
prd = "Production"
stg = "Staging"
qa = "QA"
}, var.env, var.env)
subdomain = lookup({
prd = "www"
stg = "stage"
qa = "test"
}, var.env, "${var.env}.dev")
}
I have duplicated this file in each of my modules, so I only have to pass the env
var to modules as an input instead of passing all these other calculated values.
I would like to eliminate that duplication, keeping all this information and behavior in just one place so I only have to update it in one place, but still making it accessible to all of the modules. Unfortunately the dynamic nature of the lookup, with the default values and suffixes, means a direct lookup into something like jsondecode(file("foo.json"))
won't be a sufficient alternative. What other options are available?
Upvotes: -1
Views: 77
Reputation: 74594
You have apparently made the design decision that every module accepts an env
keyword and decides for itself how to handle that, and that design decision leads to the situation where every module needs to duplicate the rule.
You cannot have it both ways: either each module is responsible for interpreting var.env
itself, or you centralize that rule and then have the other modules just accept the resulting derived values directly as input.
The best you could do with the structure you have right now is to write a shared module that consists only of the rules for translating environment name to the two derived values and then call that module as a child of each of your other modules, but that still results in some tight coupling between your modules where they all need to agree to use the same module for deriving the environment settings.
It seems like your primary concern is that you want to pass only a single value representing the environment between your modules. A different way to achieve that is to decide on a convention for passing these three values to your modules together as a single value of an object type. For example:
variable "environment" {
type = object({
identifier = string
display_name = string
subdomain = string
})
}
Your modules can then use var.environment.identifier
, var.environment.display_name
, and var.environment.subdomain
to access each value, rather than performing their own translation.
You can then define a lookup table for these in your root module. Either you can write it out literally, like this:
locals {
environments = tomap({
prd = {
identifier = "prd"
display_name = "Production"
subdomain = "www"
}
stg = {
identifier = "stg"
display_name = "Staging"
subdomain = "stage"
}
qa = {
identifier = "qa"
display_name = "QA"
subdomain = "test"
}
})
}
...or, if you prefer, you can derive the table systematically in a similar way to how you were doing it in your question:
locals {
environment_identifiers = toset(["prd", "stg", "qa"])
environments = tomap({
for ident in environment_identifiers : ident => {
identifier = ident
display_name = {
prd = "Production"
stg = "Staging"
qa = "QA"
}[ident]
subdomain = {
prd = "www"
stg = "stage"
qa = "test"
}[ident]
}
})
}
I personally tend to prefer the readability of the first literal example, but you can do it whichever way you like as long as the result is a map from environment identifier to a suitable environment object.
Then you can use var.env
in your root module only to build a suitable environment object to pass to the other modules:
variable "env" {
type = string
}
locals {
selected_environment = try(
local.environments[var.env],
{
identifier = var.env
display_name = var.env
subdomain = "${var.env}.dev"
},
)
}
module "example" {
source = "../modules/example"
# ...
environment = local.selected_environment
}
This design centralizes all of the lookup complexity in just one place -- the root module -- and gives all of your other modules a more convenient representation of the derived data that they can use without any concern about how exactly that object was constructed.
There are other examples of approaches like this -- were a module just takes directly what it needs as input and expects its caller to construct it -- in the Terraform documentation section Module Composition. The pattern above is an example of those principles, albeit in a simpler form where the data is constructed directly inside the root module rather than being fetched from elsewhere.
However, one benefit of the dependency inversion approach is that it's relatively easy to change strategy in future if you want to do this in a more dynamic way. Here's a hypothetical future evolution of your root module that could be done without modifying the child modules at all, if you decide for some reason that the environment settings should come from YAML files in an Amazon S3 bucket:
variable "env" {
type = string
}
data "aws_s3_object" "env" {
bucket = "example-environment-config"
key = "${var.env}.yaml"
}
module "example" {
source = "../modules/example"
# ...
environment = yamldecode(data.aws_s3_object.env.body)
}
As long as there is an object for the requested environment in the S3 bucket, and it contains a valid YAML serialization of a mapping that matches the three attributes of the object type the module is expecting, the child modules would be oblivious to the fact that this information is now coming from an external configuration file rather than from an inline table (and fallback logic) directly in the root module.
Upvotes: 1
Reputation: 7732
Each module can contain a symlink to the single canonical instance of the locals.tf
file, e.g.:
/main.tf
/locals.tf (the real file)
/submodule1/main.tf
/submodule1/locals.tf -> ../locals.tf
/submodule2/main.tf
/submodule1/locals.tf -> ../locals.tf
This completely deduplicates the file. However, it leaves dangling references if the original file is ever deleted without cleaning up the symlinks, and it won't be apparent in many editors that a file in a different location is being edited when opening one of the symlinks. This solution requires a little more OS/operations knowledge than a purely HCL-based solution.
Upvotes: 0