How to write a Terraform module ?

Harshit Sharma
4 min readMay 14, 2021
Hashicorp Terraform logo

In today’s world of cloud computing, Terraform has gained a special place among the top infrastructure provisioning tools. I have been using it for quite some time and have carved out some good practices around it.

Let’s take a look on some factors that contribute on writing a good terraform module.

First of all, when to write a module ?

As said by Hashicorp itself,

In principle any combination of resources and other constructs can be factored out into a module, but over-using modules can make your overall Terraform configuration harder to understand and maintain, so we recommend moderation.

A good module should raise the level of abstraction by describing a new concept in your architecture that is constructed from resource types offered by providers.

For example, aws_instance and aws_elb are both resource types belonging to the AWS provider. You might use a module to represent the higher-level concept "HashiCorp Consul cluster running in AWS" which happens to be constructed from these and other AWS provider resources.

We do not recommend writing modules that are just thin wrappers around single other resource types. If you have trouble finding a name for your module that isn’t the same as the main resource type inside it, that may be a sign that your module is not creating any new abstraction and so the module is adding unnecessary complexity. Just use the resource type directly in the calling module instead.

Naming conventions ✏️

Module names

Module names must satisfy this regex pattern ^[a-zA-Z0-9\-]+$
eg: modulename, module-name, module1-name, Module-Name, MODULE-NAME, MODULE1-NAME etc.

Resource & Variable names

Resource and variable names must satisfy this regex pattern ^[a-z0-9\_]+$
eg: resourcename, resource_name, resource1_name etc.

Common conventions 👀

Variable names across modules should share a common convention, i.e. a variable to store aws ami id, if named image_id in one module then should be used all across. Another module should not have a variable ami_id for same purpose. This is required for a better understanding of modules when multiple people/teams are contributing to the modules.

Module structure 🚧

.module
|__ env
| |__ env.tfvars
|
|__ example
| |__ main.tf
|
|__ files
| |__ component.tpl
| |__ component.py
| |__ ...
|
|__ component.tf
|__ outputs.tf
|__ provider.tf
|__ variables.tf
|__ README.md

env: contains variables for different deployment environments (staging/pre-production/production)

example: contains a basic example of how the module can be used in example/main.tf

module "consul_cluster" {
source = "../"
// vars
instance_type = "c4.2xlarge"
image_id = "ami-12345678"
key_name = "sshkey"
cluster_size = 3
}

files: all the ad hoc files that the module requires, reside here. eg: python file for lambda, template file for ec2 user-data etc...

component.tf: terraform code to provision required resources

outputs.tf: distinct outputs produced by the module with description

# description of this output variable
output "variable_name" {
value = "${aws_resource_type.resource_name.attribute}"
}

provider.tf: contains which terraform providers to use along with their config

provider "aws" {
region = "ap-south-1"
}

provider "null" {}

variables.tf: contains declaration of all input variables required by the module to operate

variable "region" {
type = string
description = "Aws region"
default = "ap-south-1"
}

README.md: contains the what & how of the module. To generate a readme, use terraform-docs

Components marked with ⭐ must be included in the module

Security guidelines 🔑

Module should follow the security guidelines provided by tfsec .

Testing 📈

Unit testing

cd module-dir
terraform init
terraform validate
terraform plan -out=plan.out
terraform apply plan.out

The module must successfully pass all the above executions. And post testing, always remember to destroy the created infrastructure.

cd module-dir
terraform destroy

Contract testing

Validation rules for input variables to restrict wrong input. Declare variables in module/variables.tf as followed -

variable "cluster_size" {
type = number
default = 1
description = "Number of nodes to provision in the cluster"
validation {
condition = var.num_instances > 0 && var.num_instances < 50
error_message = "The number of nodes in a cluster must be between 1 to 50."
}
}

Module idempotency

cd module-dir
terraform init
terraform plan -out=plan.out
terraform apply plan.out

After deploying the module using above steps, run terraform plan again without making any change to the module. Here the plan should result in 0 changes, 0 additions and 0 destructions and if any of them is greater than 0, then there is something wrong going on in the module.

Blast radius 💥

A module must have minimum blast radius.

--

--