Module Standards#
When developing new modules, or editing existing ones, the following standards should be met to ensure a consistent workflow is used.
tl;dr#
The following rules apply and are broken down into more detail below:
- All module inputs should be stored in
inputs.tf
- All module outputs should be stored in
outputs.tf
- Any
locals{}
that are required should go inlocals.tf
- All
variable{}
s should use input validation and type constraints - All
variable{}
s should have a description property defined - Any
variable{}
s that are defined should go invariables.tf
- Filenames should be named after the service they manage (
ec2.tf
,rds.tf
) - Filenames may include a specific service name such as
ami
if the nameec2.tf
is too broad, for exampleec2_ami.tf
for code that manages AMIs - Resource names should never contain the type of resource they’re managing - it’s in the resource type already
- Resource names should only ever be lower case and use letters and numbers
- Resource names should be a singular noun
- Using dashes (
-
) in strings is preferred whenever it’s possible - Modules should always be stored in their own git repository
- Git repositories should make use of semantic versioning and use git tags to mark release candidates
- All modules require a
README.MD
file explaining how-to use the module effectively
Below we’ll break down the rules a bit more and provide examples (when applicable or appropriate.)
Rules#
Follow these rules so that you're getting a more consistent experience as your code base grows and grows.
All module inputs should be stored in inputs.tf
#
Providing an inputs.tf
file makes it clear and obvious where an engineer has to look to review or manage a module’s variable{}
code.
All module outputs should be stored in outputs.tf
#
Providing an outputs.tf
file makes it clear and obvious where an engineer has to look to review or manage a module’s variable{}
code.
Any locals{}
that are required should go in locals.tf
#
Any locals{}
that are defined should be stored in a central location called locals.tf
so that it’s easy to locate, review and manage such information.
All variable{}
s should use input validation and type constraints#
Terraform affords us the ability to validate input as of 0.13
(it’s considered beta in 0.12.29
). This grants us some obvious benefits but also the not-so-obvious benefit of saving time: instead of waiting for the AWS or Azure APIs to tell us a field’s value (such as an AMI ID, or a Key Vault secret name) is incorrect we’ll learn of this fact at compile time, locally, without any network activity at all.
Using validation is easy:
1 2 3 4 5 6 7 8 9 10 |
|
We can also provide our variable with a type constraint. This further allows Terraform to catch problems at compile time and also acts as a form of documentation. An example of this can be seen above with the type property.
All variable{}
s should have a description property defined#
This should be self explanatory - it documents the purpose of the variable.
Any variable{}
s that are defined should go in variables.tf
#
This is about discovery again and is similar in concept to inputs.tf
and outputs.tf
- it makes it easy to find where variables are being defined as opposed to hunting around loads of files or using an IDEs go to reference capabilities (which can take people out of their flow.)
Filenames should be named after the service they manage (ec2.tf
, rds.tf
)#
Being able to locate the configuration for an Auto Scaling Group by going to ec2_asg.tf
is obviously easier than trying to find it among many ambiguous files. This is also about discoverability and making it easy to find the code you’re looking for.
Filenames may include a specific service name such as AMI if the name ec2.tf
is too broad, for example ec2_ami.tf
for code that manages AMIs#
Files can get big quickly if you’re using a lot of EC2 services and you’ve got them all in ec2.tf
. Instead it’s acceptable to break them out into individual files such as ec2_ami.tf
, vpc_subnets.tf
, vpc_sg.tf
and so on. This is a better option than using sub-directories (which are Terraform modules by definition) to organise code, which just results in modules within modules and causes a lot of extra work and problems further down the line.
Resource names should never contain the type of resource they’re managing - it’s in the resource type already#
Writing resource “aws_instance” “ec2-web-server”
is pointless because the same information is present in the resource type - instance
- so we know it’s an EC2 resource. The same with virtually every other resource type.
Remember that all the information you need about resources is available in the console using various Terraform commands.
Resource names should only ever be lowercase and use letters and numbers#
When naming a resource, use lower case letters and numbers only. Don't add other characters if you can avoid it. Avoid CamelCase.
Resource names should be a singular noun#
It's tempting to call an aws_instance
something like web_servers_01
because it's 1 of 3 servers/resources, but it's better to keep names singular: web_server_1
.
Using dashes (-
) in strings is preferred whenever it’s possible#
This makes it easier to move around the code using keyboard shortcuts. For example using Alt+<Arrow Keys>
on the following strings yields different results:
my-web-server
will allow the cursor to move from the very beginning of the string to the beginning of the next word, web- but the string
my_web_server
pushes the cursor to the end of the string, making it more difficult and time consuming to edit the word web inside of the string
This is a small detail but when you’re editing this code every day it quickly becomes very convenient to move between words regardless of your IDE choice.
Modules should always be stored in their own git repository#
If you’re going to write a module and just keep it alongside the code that import the module then you’re best not even using a module to begin with. The sole purpose of modules existing as a concept is they can be shared with others - sticking them in a sub-directory isn’t sharing it organising code.
When we agree that some code is repeated often enough and is going to be encapsulated in a module we must keep the module in its own repository. This comes with many advantages:
- You can version control the individual module and have code bases pull the module based on a specific version;
- This is good practice and prevents breaking changes to legacy environments;
- Others can also pin their use of your module to specific version;
- You can control access to the module, locking it down to specific users;
- So a module aimed at managing IAM policies should be locked down to security personnel versus allowing anyone to edit their own access to the system;
- It becomes a lot easier to share with others given its isolated nature;
- You can use a private CI pipeline for the module to do tests and linting on changes;
- You get a separate commit log for easy auditing of who did what, when, etc.;
But the most important advantage is the shareable nature of a module in a git repository.
All modules require a README.MD
file explaining how-to use the module effectively#
Because documentation is important. At minimum the README should contain:
- What the module manages for the user
- What inputs it takes and which have default values (and what that value is)
- What attributes it exposes
- Who wrote it and how they can be contacted
And anything else that you believe can assist others to better understand the nature and function of the module.