Christoph's 2 Cents

A Backup for My Brain!

bashDevOpsOracle Cloud InfrastructrureOracle Developementshell scriptingTerraform

Taming the Terraform Module Madness

I have to admit, Terraform module syntax has driven me mad. Especially since I’ve been away from Terraform since before the pandemic.

Now I’ve had the chance to get re-acquainted with it and experienced some frustration with the module syntax. Here is my attempt to clarify it, so that when I come back in a couple of years I can throw this post away, because things have changed, I can pick up right where I have left off.

So here are some quick notes regarding modules:

Modules allow you to encapsulate resource definitions. They are called child modules and create the resources defined in them by just passing a few variables to them. This can reduce the amount of typing you have to do quite a bit.

Here is an example with pseudo code:

# Child module
resource "compute_instance" "foo" {
  name   = var.name
  shape  = var.shape
  subnet = "cool_subnet_id_12345"
  #...lots of other stuff
}
# Module block
module "myInstance" {
  source "modules/compute/main.tf"
  name  = "huckle"
  shape = "VM.Standard2.1"
}

module "myOtherInstance" {
  source "modules/compute/main.tf"
  name  = "berry"
  shape = "VM.Standard2.4"
}

What happens here is that when a terrafrom apply is issued from the project’s root directory (root module), Terraform picks up the main.tf and builds two compute instances, which two different names and shapes. Since all other instance attributes are defined in modules/compute/main.tf, each instance will look the same (except name and shape). So there is no need to repeat any of the other attributes. In this example, both instances would be created in subnet cool_subnet_12345.

To make this even more concise you could simply loop over the module calls:

variable instances { 
  type        = map
  description = "Parameters for multiple compute instances"
  default  = {
    huckle = {
      name  = "huckle"
      shape = "VM.Standard2.1"
    },
    berry  = {
      name  = "berry"
      shape = "VM.Standard2.4"
    },
  }

# Module block
module "myInstances" {
  source "modules/compute/main.tf"
  for_each = var.instances
  name     = each.value.name
  shape    = each.value.shape
}

So here the instance details are captured in a map variable instances, then the module call loops over the variable to create the instances.

If the map variable instances were changed, a terraform apply would take care of the changes. So if the name “huckle” were changed to “chuckle”, Terraform would simply update the name of the existing instance. If berry were to be removed from the map, Terraform would destroy that instance when applied. If another instance were added to the map, the next terraform apply would create the new instance.

What may throw someone of about the whole thing, is that the syntax seems backwards: A module calls a resource defined in a resource (*.tf) file in some other directory.

The module {} block in the root module, references the resources (resource "x" "y" {}) in a child module. (If you can explain it better, leave a comment).

There are Modules and there are Modules

Module Block

A module block is the HCL syntax construct that references the configuration files in a given directory. Any variable values set in the module block are passed to the configuration files. In this example, the module “myInstances” passes the variable values of name and shape to the configuration files in modules/compute/main.tf.

module "myInstance" {
  source "modules/compute/main.tf"
  name  = "huckle"
  shape = "VM.Standard2.1"
}

Module Source

The actual resource configuration files are placed in a separate “module” directory. This directory can hold a single resource file (*.tf) for a single resource type, or it can contain a complete resource stack consisting of compute instance, databases, load balancers, networks, etc.

No matter how many resources are contained in a module directory, only a single module block is needed to execute them.

In this example the main project directory project_x contains three module directories: app_stack, compute, and network.

project_x
├── main.tf
└── modules
    ├── app_stack
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── compute
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    └── network
        ├── main.tf
        ├── outputs.tf
        └── variables.tf

The root module project_x/main.tf contains the module block calling the project_x/modules/compute module directory.

Root Module

The root module is basically the top level directory (project_x) from which you invoke Terraform. When you create a new child module, you need to issue a terrarform init from the root module, so the new child module gets registered.

Tamed!

The word module in Terraform can be confusing. The module block refers to the HCL syntax that passes variable values to the module source directory. The root module is the top-level directory of your project.

Special thanks to @MartinDBA for helping me on this. Check out his terrific Terraform texts on his Martins Blog.