Skip to content

Multiple root modules

This tutorial is a light introduction to Terraplate where we take a single Terraform Root Module and split it into multiple root modules, whilst keeping things DRY.

Example Root Module

Let's setup a basic Terraform root module where we use the local provider so you don't need to worry about cloud providers.

# Check out the terraplate codebase containing the tutorials
git clone https://github.com/verifa/terraplate.git

# Go to the tutorial
cd terraplate/tutorials/multiple-root-modules

Resources (and provider)

In there we should have a Terraform file with something like the following, which will create two files: one for dev and one for prod. Ignore that this is stupidly simple and imagine instead you are creating VPCs, VMs, Kubernetes clusters, ... whatever you normally do!

main.tf
provider "local" {
  # No configuration required
}

# Create our dev environment
resource "local_file" "dev" {
  content  = "env = dev"
  filename = "${path.module}/dev.txt"
}

# Create our prod environment
resource "local_file" "prod" {
  content  = "env = prod"
  filename = "${path.module}/prod.txt"
}

Backend

The backend.tf file defines where the Terraform state should be stored. This config replicates the default backend which is to store the state in a local file called terraform.tfstate.

backend.tf
terraform {
  backend "local" {
    path = "terraform.tfstate"
  }
}

Versions

The versions.tf file contains the required providers and the Terraform CLI version.

versions.tf
terraform {
  required_version = ">= 1.0"

  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "2.1.0"
    }
  }
}

Apply the configuration

Now we will apply the configuration using basic Terraform

# Initialize the root module
terraform init

# Plan the root module
terraform plan -out tfplan

# Apply based on the plan output
terraform apply tfplan

# Check output
cat prod.txt
cat dev.txt

Great! This should've worked. And let's imagine that it took a long time to plan, because of all your resources being inside a single Root Module and therefore a single state.

Using Terraplate

Let's refactor this code and split the two local_file resources up into their own Root Modules and use Terraplate to keep things DRY. Take a look in the tutorials/multiple-root-modules-finished directory for the same codebase that has been Terraplate'd.

Terraplate Structure
# Move into the finished tutorial
cd tutorials/multiple-root-modules-finished

# Check the files we have
tree
.
├── README.md
├── local
│   ├── dev
│   │   ├── main.tf
│   │   └── terraplate.hcl
│   ├── prod
│   │   ├── main.tf
│   │   └── terraplate.hcl
│   └── terraplate.hcl
├── templates
│   └── provider_local.tmpl
└── terraplate.hcl

Resource files

Let's inspect the main.tf files in the local/dev and local/prod environments. Note that these are identical and manually maintained (NOT currently generated by Terraplate).

local/dev/main.tf
resource "local_file" "this" {
  content  = "env = ${local.environment}"
  filename = "${path.module}/${local.environment}.txt"
}

Templates

Currently we have two templates in the templates/ directory.

templates directory is a convention

The templates directory is not required but it's a convention to keep the template files organized. Putting files in a templates does not mean or do anything: you still have to declare your templates using a templates block inside your Terrafiles.

They will be processed by the Go templating engine so we could set values we want based on the Root Module where it should be templated. But for these simple files we don't need it.

templates/backend_local.tmpl
terraform {
  backend "local" {
    path = "terraform.tfstate"
  }
}
templates/provider_local.tmpl
provider "local" {
  # No configuration required
}

We need to declare these templates in our Terrafiles. The backend we want to use in every root module so we will declare it in the root Terrafile terraplate.hcl. The local provider we only want to use in the Terrafiles under the local/ directory, so we place it in the local/terraplate.hcl Terrafile and all the child directories will inherit this template.

That takes care of the backend and providers.

Versions

Defining the required Terraform version and required_providers everywhere is tiresome to do and maintain. With Terraplate we keep the required versions at each level in the directory structure where we need them, and the child directories inherit those.

At the root level, terraplate.hcl, we define the Terraform CLI version. At the local/terraplate.hcl directory level we declare the local provider.

Terrafiles

terraplate.hcl
template "backend" {
  contents = read_template("backend_local.tmpl")
}

terraform {
  required_version = ">= 1.0"
}
local/terraplate.hcl
template "provider_local" {
  contents = read_template("provider_local.tmpl")
}

terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "2.1.0"
    }
  }
}
local/dev/terraplate.hcl
locals {
  environment = "dev"
}
local/prod/terraplate.hcl
locals {
  environment = "prod"
}

Apply using Terraplate

Apply with Terraplate
# The parse command gives us a summary of the Root Modules (useful for debugging)
terraplate parse

# Let's build the templates
terraplate build

# Then we can plan (and init in the same run)
terraplate plan --init

# Finally apply the plans
terraplate plan

Want to get even DRYer?

The main.tf file is currently the same for the dev and prod environments. We could define a template for this, let's say under the local/templates directory.

local/templates/file.tmpl
resource "local_file" "dev" {
  # We can use Go templates to build the value right in the file if we want!!
  content  = "env = {{ .Locals.environment }}"
  filename = "${path.module}/${local.environment}.txt"
}
local/terraplate.hcl
template "file" {
  contents = read_template("file.tmpl")
}
# Remove the files we are about to make DRY
rm local/dev/main.tf local/prod/main.tf

# Re-build to generate our new `file.tp.tf` files
terraplate build

# Plan and see that there should be no changes...
terraplate plan

Summary

We had a single root module with a single state that we separated into two root modules and therefore two separate states. We can now create many more root modules and the version, providers and backend are inherited and templated for us by Terraplate. Thus, the steps for creating a new root module, such as a staging would be as follows:

Creating a new Root Module
mkdir local/staging

touch local/staging/terraplate.hcl

And something like the following in your terraplate.hcl file

local/staging/terraplate.hcl
locals {
  environment = "staging"
}

Then just add your .tf files, or add some more templates, and away we go!