The Best Mental Model for Writing Terraform Code

This article explains my current mental model of Terraform, in the hope that it will save you some time in your learning process. The foundation of using any programming language or software tool correctly is to develop a valid mental model for it, and refine your model as you learn more. There isn’t one correct mental model, and your mental model must evolve as your understanding grows.

Your introduction to writing Terraform code is usually through a very simple example. Unfortunately, simple examples can obscure some of the fundamental concepts of the language. For example, when I first started with Terraform, I did not understand the difference between a variable and a local. Variables need to be declared in a .tf file and defined (assigned a value) in a .tfstate file (or through other means). Variables seem complicated compared to locals. You can just assign a variable to a local and start using it, like a variable in Python. Why use variables when locals seem so much easier?

Axioms of the Mental Model

1. Everything You Write is a Module

All the Terraform code you’ve ever written is a module. But, wait, you say! I’m doing the Terraform 101 tutorial, and writing modules is an advanced topic that I haven’t gotten to! That impression is understandable, but wrong. Your monolithic Terraform code is a module that defines your infrastructure. It may not be modular or reusable, and it may lack inputs and outputs, but it’s still a module. So, what’s a module?

2. A Module is a Function with Side Effects

If you think about all Terraform code as modules, and think of every module as a function, a lot of vague concepts become clear:

  • Terraform variables are function parameters. The value you assign to each variable is a function argument. Arguments in Terraform are immutable (cannot be changed within the module/function).
  • Terraform outputs are the function’s return values.
  • Terraform locals are local variables within the function.
  • Modifying infrastructure is a side effect of calling the function.

The reason that you can use locals effectively in a monolithic Terraform codebase is that you’re writing a single module (function). At this stage, you aren’t worried about inputs or outputs with a monolith; you just need local variables to hold local values.

Applying the Mental Model

Using this mental model clarifies some of the more advanced Terraform design patterns:

  • You should now understand why variables have to be declared, and why it is a good idea to add a type and a description to each variable. Variables are the parameters that others will use to call your function (use your module). It’s always good practice to validate inputs to avoid unexpected results, and to document how to call the function.
  • You should now understand why it makes sense to use variables, rather than locals, whenever possible. Once you are writing modules as functions, there will be times when variables can’t meet your needs. In these cases, use locals within the module.
  • You should now have an idea about why outputs exist. As you write a module, think about the interface that you want to present to users. What are the minimum input variables required? What information will the user need as an output?

Terragrunt Makes More Sense

Below is a typical Terragrunt repo structure:

.
├── README.md
├── service1
│   ├── instances.tf
│   ├── load_balancer.tf
│   ├── main.tf
│   └── variables.tf
│   └── outputs.tf
├── service2
│   └── ...
...
├── environments
│   └── qa
│   |   ├── service1
│   |   │   ├── terraform.tf
│   |   │   └── terragrunt.hcl
│   |   └── service2
│   |       ├── terraform.tf
│   |       └── terragrunt.hcl
│   ├── production
...

Service1 and service2 are actually modules. Think of them as functions, with inputs and outputs, that are being called by Terragrunt. Inputs and outputs can help you manage dependencies between resources, such as a VM instance that can’t be created until a VPC network exists. The instance will need to reference values returned by the module that creates the VPC network. Within a terragrunt.hcl file, you can do something like this:

dependency "network" {
  config_path  = "../network"
}

inputs = {
  vpc_network_link = dependency.network.outputs.vpc_network_link
}

With this approach, you can minimize the number of “global variables” you define. You can let Terraform track the vpc network link, rather than explicitly defining it as a “global variable.”

Do you have a better model? Leave a comment below! I look forward to learning from you.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.