Terraform Modules

Introduction

This note follows from the previous note.

A Terraform module cannot see variables from the parent automatically. The parent must explicitly pass values into it. It works a bit l ike a function call.

E.g. in Python:

def replica(name, port, server_id):
  print(name, port, server_id);

We can not call the function without providing some arguments first:

replica("server4", 3314, 4)

Terraform modules behave the same way.

Calling a module

In the root terraform file, a module can be called like this:

module "replica" {
  source = "./modules/mariadb-replica"

  for_each = local.servers

  name         = each.key
  port         = each.value.port
  server_id    = each.value.server_id
  docker_image = var.docker_image
  network_name = docker_network.lab.name
}

This basically says, run mariadb-replica module once for every server, and then pass those variables into it.

Module Contents

It is usually nice to keep variable definitions in a separate file. Much like the case for the top-level terraform files, files in a module also get unified together into a single file when being interpreted.

So in the module directory, we can have a variables.tf file like:

variable "name" {
  type = string
}

variable "docker_image" {
  type = string
}

variable "port" {
  type = number
}

variable "server_id" {
  type = number
}

variable "network_name" {
  type = string
}

Basically, here we just want to let terraform know what variables exist, their types, and whether they are required.

Now, we can build the module’s main.tf:

terraform {
  required_providers {
    docker = {
      source = "kreuzwerker/docker"
    }
  }
}

resource "docker_container" "server" {
  name     = var.name
  image    = var.docker_image
  hostname = var.name

  env = [
    "MARIADB_PORT=${var.port}",
    "MARIADB_SERVER_ID=${var.server_id}",
  ]

  ports {
    internal = var.port
    external = var.port
  }

  networks_advanced {
    name = var.network_name
  }

  labels {
    label = "deployed_by"
    value = "terraform"
  }
}

Everything works just like it did before, but the main difference now is that we need to reference the existence of this module in the top level’s main.tf file as shown earlier.

It is important to also clarify the expected providers a module may need to use, similarly to how it works in the top-level.

Outputs

Optionally, a module can pass information back to the parent. This is basically like return of a function:

def create_container():
    container_id = "abc123"
    return container_id

The terraform analogy is:

output "container_id" {
  value = docker_container.server.id
}

Otherwise, the parent can not see or reference resources created internally within a child-module. Much like how variables in functions tend to be locally scoped in many programming languages.

Conclusion

The takeaway is that modules are useful, just don’t try to turn it into something more complicated than “making things easier to read”.