Skip to main content

Terraform Style Guide

Modernisation Platform Terraform Style Guide

This style guide is meant to be a series of guidelines to help Modernisation Platform team members produce consistent, coherent Terraform. These guidelines aren’t meant to restrict or constrain you. Hashicorp maintain a style guide here which provides us with a good starting point.

First, and foremost, try and stay consistent with what you find in a repository. The same approach - even if it’s not the best approach - is easier to understand than multiple different approaches.

Naming

Naming things is hard. We want naming structures to be consistent, and we want names to help explain what we’re working with.

  • Use lower case characters
  • Use underscores as separators
  • Keep names comprehensible

Examples

✅ lower cased, underscored, comprehensible

resource "an_example" "this_looks_good" {}

❌ mixed case, no separation or inconsistent separation, no clear purpose

resource "another_example" "SemaphoreCricketBrick-a-brack" {}

Hard-coding values

Hard-coding values can be unavoidable, but we want to be judicious about hard-coding things. We want our Terraform to be flexible and idempotent, and hard-coding things can work against this.

  • Keep this to a minimum
  • Ensure these aren’t sensitive values
  • Retrieve values from an external source or local value

Examples

✅ loads values in through an external file

locals {
  example_values    = jsondecode(./my-values.json)
}
resource "an_example" "hard_coded_values" {
  name              = local.example_values["application-name"]
  number_of_pickles = local.example_values["pickle-capacity"]
  password          = "set-me-in-the-console" # set this in the console
}

❌holds values in-line, displays sensitive value

resource "another_example" "hard_coded_values" {
  name              = "pickle-counter"
  number_of_pickles = "3" # because production needs three, we'll give all environments 3
  password          = "MyPickles1"
}

Local values

We make heavy use of local values. We use local values compute things at runtime, and create data structures that then let us do more challenging or complicated things.

  • Think about where your locals will sit as you add them
    • locals.tf for locals used in multiple places
    • locals {} in a single file for values used only in one place
  • Be judicious about using local values
  • Explain them if they’re difficult to understand

Examples

✅ clear, explains what the secondary map does, avoids overcomplexity

locals {
  my-files   = { 
    for f in fileset(path.module, "my-files/*.json") : 
    trimsuffix(basename(f), ".json") => jsondecode(file("${path.module}/${f}")) 
  }
  # constructs a map based on json file names that contains names and favourite hotdog style
  my-hotdogs = { 
    for key, value in local.my-files : 
      key => {
        name   = value["name"]
        hotdog = value["favourite-hotdog"]
     }
   } 
}

❌ horribly complex and unhelpful, lots of type conversions, cryptic names

locals {
 _x = { for i in flatten([for f in local.files : regexall("^(.+?).json$", f)]) : 
   replace(element(i, 0), "/[^a-zA-Z0-9]/", "_") => 
   try(jsondecode(file(coalesce("${path.module}/${i[0]}", "null.json"))), {}) 
 }
}

Data sources

Data sources allow the retrieval of values from resources not directly managed in code. We have no set structure for where data sources ought to live. A specified file such as data.tf is a sensible location, but as code scales then keeping the data sources close to the code that requires them is desirable.

  • Prefer outputs from resources over data sources
  • Prefer outputs from modules over data sources
  • Be sparing in the use of external data sources

Modules

Terraform modules allow us to produce reusable blocks of code. If the need is specific to one repository consider the use of a smaller, in-line approach. When the need is more complicated, or has multiple potential customers consider the use of a separate repository.

We have two approaches for our use of modules.

  • Large modules
    • 100s of lines of Terraform
    • Complex (eg, with sub-modules)
    • Separated out into their own repositories
    • Non-modernisation-platform users like application teams deploying to MP
    • Unit tested
  • Small modules
    • 10s of lines of Terraform
    • Single purpose (eg, re-tagging RAM shared resources)
    • Used inside a repository

Examples

✅ Large modules - modernisation-platform-oidc-role - modernisation-platform-terraform-environments

✅ Small modules - Baselines config module - GitHub contributor access module

This page was last reviewed on 13 February 2025. It needs to be reviewed again on 13 August 2025 by the page owner #modernisation-platform .
This page was set to be reviewed before 13 August 2025 by the page owner #modernisation-platform. This might mean the content is out of date.