We use terraform a lot at work and reading some of the terraform configuration or modules and the huge number of variables that sometimes exist, I start to question whether I should just use the underlying resource directly instead of the module abstraction. It comes from the need to paramaterise everything. You get so many variables that it seems like you’ve built another domain specific type that is more complex than the pieces you’re abstracting.
Below I’ll outline how I’ve started to construct out my terraform files prefering naming conventions. The other benefit, apart from reducing the variable count in terraform, is repeatable naming that makes associated resources easy to find in cloud consoles.
Determining a common environment
Terraform workspaces as identifiers
Each deployment environment has it’s own terraform workspace and naming them to correlate with their environment makes a lot of sense. So let’s say we have:
- myproject-dev
- myproject-uat
- myproject-prod
In this case, I then setup these local variables within terraform:
locals {
environment = replace(terraform.workspace, "myproject-", "")
short_environment = substr(local.environment, 0, 1)
}
now we have variables such as local.environment
that might be dev
, uat
,
or prod
and local.short_environment
that might be d
, u
, or p
. Perfect
for use in string interpolation to name resources
Cloud data as identifiers
Another approach to get uniqueness is to correlate some sort of cloud resource to the appropriate environment. In Azure, this might be the subscription, in GCP, this might be the project etc. This has the added benefit that it’s really hard to deploy resources to the wrong environment. For example, if you have an Azure subscription per environment (dev, uat, prod) then you can use a data resource and map to correlate the environment.
data "azurerm_subscription" "current" {}
locals {
// Subscription IDs that correspond to operating environment
subscriptions_to_environment = {
"<dev-subscription-guid>" = "dev",
"<uat-subscription-guid>" = "uat",
"<prod-subscription-guid>" = "prod"
}
// Map the subscription to the environment name
environment = local.subscriptions_to_environment[data.azurerm_subscription.current.subscription_id]
}
Now, whichever azure subscription is active in the current context, resource naming and conventions will be accurate. No variables to switch between environments.
Using locals instead of variables
The benefit this comes with is setting parameters based on the environment. For
example, our storage account might be now
"myprojectstorage-${local.environment}"
. Another example is setting different
scale or configuration parameters based on the environment. Take an example
where we want geo-redundant storage in prod, but single region storage for
other environments to save costs. An azure storage account resource could be
setup like:
resource "azurerm_storage_account" "example" {
name = "myprojectstorage${local.environment}"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = local.environment == "prod" ? "Premium" : "Standard"
account_replication_type = {
dev = "LRS"
uat = "ZRS"
prod = "RAGRS"
}[local.environment]
tags = {
environment = local.environment
}
}
The account_tier
above is just different for prod, whereas the
account_replication_type
varies by each environment and we can use a map to
set these up. To achieve this with variables would take two variables, both
with validation and description messages for the developer to use the right
value and doesn’t code in practices to follow for everyone using the module.
Conclusion
I’ve found that using conventions by environment and inline maps or ternary conditions significantly reduces the complexity of a terraform configuration to read and maintain. On the upside, the configuration is right next to the resource it’s configuring, again making things easier to find. While this might not be for everyone, overall, I’ve enjoyed maintaining terraform that’s setup like this vs an over-abundance of variables.