How to use count with modules in Terraform 0.13

The upcoming 0.13 release of Terraform adds many new features. In my opinion none are more exciting than finally being able using count when calling a module. At last this means that we can define a reusable chunk of code, in the form of a module, and use the fantastic count feature of Terraform as if we were inside a resource.

Modules


In it's most basic form a module is a block of re-usable code. Confusingly, a module in Terraform is technically any set of templates in a folder. However, in my opinion, it makes more sense to divide modules up into "parent" and "child". The parent module defines infrastructure by passing variables to the children.

You can find the code used for this post on Github at IronicBadger/terraform-0.13-examples.

This concept took me a long time to "get" until it finally clicked when I drew a correlation with Ansible. If you're familiar with Ansible you'll be used to the concept of a playbook and a role. It helped me to think of child modules much like an Ansible role and parent modules, where you likely define your infrastructure and include other modules, like a playbook.

Modules behave much like functions in general-purpose programming languages accepting variables but also returning them as well.

def example_function(param1, param2) 
  echo "Hello, #{param1} #{param2}"
end

# Other places in your code
example_function("foo", "bar")

This is probably best explained with an example. Let's create some virtual machines on Digitalocean, though of course these concepts translate to any resources or providers.

.
├── infra
│   └── prod
│       ├── main.tf
│       ├── variables.tf
│       └── versions.tf
└── modules
    └── droplet
        ├── main.tf
        ├── variables.tf
        └── versions.tf

Here we have a parent module called prod. It utilises the child module droplet to create a droplet (what DigitalOcean call a VM or instance).

Doing it the old way


Traditionally we'd write the following Terraform code to define a digitalocean_droplet resource called web (full documentation here).

# Create a new Web Droplet in the nyc2 region
resource "digitalocean_droplet" "web" {
  image  = "ubuntu-18-04-x64"
  name   = "web-1"
  region = "nyc2"
  size   = "s-1vcpu-1gb"
}

If we're not careful we'd end up specifying many of these variables many times. It would be better if we could declare we want X number of droplets with these parameters please wouldn't it?

# Create a new Web Droplet in the nyc2 region
resource "digitalocean_droplet" "web" {
  count  = 2
  image  = "ubuntu-18-04-x64"
  name   = "web-1"
  region = "nyc2"
  size   = "s-1vcpu-1gb"
}
Spot the problem.

Easy enough, we just add count and Terraform will create two instances. Except, there's a problem here.

Some attributes, such as name must be unique. Here we can leverage the count arrays index and append it to the name attribute like so:

# Create a new Web Droplet in the nyc2 region
resource "digitalocean_droplet" "web" {
  count  = 2
  image  = "ubuntu-18-04-x64"
  name   = "web-${count.index + 1}"
  region = "nyc2"
  size   = "s-1vcpu-1gb"
}
That's better. Name is now unique.

The resulting names would be web-1, web-2 and so on. We perform the +1 operation on the interpolation because arrays start at 0 but my brain starts at 1 and this gets us there.

Doing it the new way


As you can see the old way involves a lot of hard-coding specific values. It would be better to write a generic module that can be fed some variables instead. Here's what that looks like:

# modules/droplet/main.tf

resource "digitalocean_droplet" "droplet" {
    name     = var.droplet_name
    image    = var.droplet_image
    size     = var.droplet_size
    region   = var.do_region
    ssh_keys = var.ssh_keys
}

On it's own, this code does nothing. We need to feed it some inputs.

Note here that the module resource references var.droplet_name and var.droplet_image and so on. When feeding in our variables as we call the module, these are the names we must use. For example:

# infra/prod/main.tf

module "example-prod" {
    source = "../../modules/droplet"
    count  = 2

    droplet_name       = "tf-prod-${count.index + 1}"
    droplet_image      = var.droplet_image
    droplet_size       = var.droplet_size_1vcpu_1gb
    do_region          = var.do_region
    ssh_keys           = var.do_ssh_keys
}

You have the option in your parent module to define these values as your requirements dictate. Use data objects, reference other variables in your code as we have done here with var.do_region for example or straight-up hard-code the values as we did with droplet_name.

Variables in the parent module are defined in the parents variables.tf and terraform.tfvars files. They are scoped to that module. In other words, the folder you're currently in, is the scope of these variables.

Perhaps you noticed that we snuck count into our infra/prod/main.tf file. This wasn't possible before Terraform 0.13! Hurrah for progress.

So, that looks the same? I'm confused.


A small, but vitally important difference between 0.11/0.12 and 0.13 is that the definition of count is moved out of the individual resource creation step and moved to module definition instead. This includes references to count.index and so on.

Instead of accessing elements via their index during resource creation, we do so during module definition instead. When referring to variables or other data in the module code there is no concept of count at that level. We are dealing with one resource at a time.

At the small scale in the example repo it might not seem like a big deal. But when we take things a step further...

Taking it a step further


The reason I learned about modules was because I use Terraform to deploy Openshift clusters. Openshift is Red Hat's enterprise flavour of Kubernetes. It often requires half a dozen machines to be spun up and down at the same time. As you can imagine, doing this by hand is not an option numerous times a day for testing as I often do.

The code in the ironicbadger/ocp4 repo is a much more complicated example of using count with 0.13. It uses outputs to template HAProxy configuration files and targets VMware, not DigitalOcean.

Wrap-up


That should be it for getting you started with modules, count and Terraform 0.13.

Feel free to reach out on to me Twitter, I'm @IronicBadger, with any questions or comments. You might consider giving my podcast a listen over at selfhosted.show if you're in Self-Hosting / NAS's and general home server nerdiness.