Featured image of post Terraform - reduce complexity by using conventions

Terraform - reduce complexity by using conventions

Documenting how I've used workspace or cloud identifiers to configure terraform resources and overall reduce the number of variables and validations I need to do with variable input

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.

Built with Hugo
Theme Stack designed by Jimmy