This document is an attempt to systematically describe key points in writing good reusable Terraform modules.
Author: Anton Babenko Date: 27.11.2017
- Concepts
- Examples of directory structure
- Naming
- Styling
There are these key concepts:
- Resource
- Resource module
- Infrastructure module
- Composition
- Data sources
- Remote state
- Provider, provisioner, etc
Resource is aws_vpc
, aws_db_instance
, etc. Resource belongs to provider, accepts arguments, outputs attributes.
Resource module is a collection of connected resources which together perform the common action (for eg, AWS VPC Terraform module creates VPC, subnets, NAT gateway, etc). It depends on provider configuration, which can be defined in it, or in higher level structures (eg, infrastructure module).
Infrastructure module is a collection of resource modules, which can be logically not connected, but in current situation/project/setup are serving the same purpose. It defines configuration for providers, which is passed to the downstream resource modules and to resources. It is normally limited to work in one entity per logical separator (eg, AWS Region, Google Project)
Composition is a collection of infrastructure modules, which can span across several logically separated areas (eg., AWS Regions, several AWS accounts). Composition is used to describe the complete infrastructure required for the whole organization/project.
Composition consists of infrastructure modules, which consist of resources modules, which implement individual resources.
Since data source performs read-only operation and is dependant on provider configuration, it is used in a resource module or an infrastructure module.
Data source terraform_remote_state
acts as a glue for higher level modules and compositions.
Infrastructure modules and compositions should persist their state in a remote location which can be reached by others in a controllable way (ACL, versioning, logging).
Providers, provisioners and few other terms are described very well on the official documentation and there is no point to repeat it here. To my opinion they have little to do with writing good Terraform modules.
While individual resources are like atoms in the infrastructure, resource module is a molecule. It is a smallest versioned and shareable unit. It has exact list of arguments, implement basic logic for such unit to do required function. Eg. terraform-aws-security-group creates aws_security_group and aws_security_group_list based on dynamic input. This resource module by itself can be used together with other modules to create infrastructure module.
Access between molecules (resource modules and infrastructure modules) is performed using data sources.
.
├── README.md
├── ...
112213 directories, 122110 files
- Do not repeat resource type in resource id (not partially, nor completely)
Good: resource "aws_route_table" "public" {}
Bad: resource "aws_route_table" "public_route_table" {}
Bad: resource "aws_route_table" "public_aws_route_table" {}
-
Resource id should be named
this
if there is no more descriptive and general name available, or if resource module creates single resource of this type (eg, there is single resource of typeaws_nat_gateway
, but multipleaws_route_table
, soaws_nat_gateway
can be namedthis
, butaws_route_table
should be more descriptive). -
Include
count
argument inside resource blocks as the first argument at the top and separate by newline after it.
Good:
resource "aws_route_table" "public" {
count = "2"`
vpc_id = "vpc-12345678"
# ... remaining arguments omited
}
Bad:
resource "aws_route_table" "public" {
vpc_id = "vpc-12345678"
count = "2"`
# ... remaining arguments omited
}
- Include
tags
argument, if supported by resource as the last real argument, following mydepends_on
andlifecycle
, if necessary. All of these should be separated by single empty line.
Good:
resource "aws_nat_gateway" "this" {
count = "1"
allocation_id = "..."
subnet_id = "..."
tags = "..."
depends_on = ["aws_internet_gateway.this"]
lifecycle {
create_before_destroy = true
}
}
Bad:
resource "aws_nat_gateway" "this" {
count = "1"
tags = "..."
depends_on = ["aws_internet_gateway.this"]
lifecycle {
create_before_destroy = true
}
allocation_id = "..."
subnet_id = "..."
}
- When using condition in
count
argument use boolean value, if it makes sense, otherwise uselength
or other interpolation. Good 1:
count = "${var.create_public_subnets}"
Good 2:
count = "${length(var.public_subnets) > 0 ? 1 : 0}"
Bad:
count = "${var.dont_need_public_subnets}"
- To make inverted conditions don't introduce another variable unless really necessary, use
1 - boolean value
. Good:
count = "${1 - var.create_public_subnets}"
-
Try to avoid using
-
inside resource ids and make it to matcha-z0-9_
.
WIP