Table of Contents
When using infrastructure as code (IaC), basic architectural principals can be used as guidelines to create purpose built, operationally simple, platforms. These principles ensure that resources created are immutable and grouped according to lifecycle and purpose.
Grouping resources allows for an approach where an environment is built from different layers, with each subsequent layer having a shorter lifecycle than the one it is built upon. This means we can make changes for a specific group of resources, which reduces the ‘blast radius’, and reduces operational complexity of changes. It also theoretically allows to shift ownership of a certain layer without impacting platform architecture.
Layers in Infrastructure as Code
I’m a big fan of Terraform as a tool to create your infrastructure. From here onwards I’ll be using Terraform as the IaC tool to show how one could layer their code. But this can be extrapolated to be used with other tools like Pulumi or AWS CDK.
We want to have harmonized environments that are consistent with each other, with any differences being limited to sizing. For each environment we separate the infrastructure into layers according to our lifecycle grouping.
- A Layer:
- can implement generic component modules where applicable (Buy over Build)
- should only use generic component modules that are published in an approved Module Registry (eg. the official Terraform Module Registry or an approved Terraform Cloud Private Module Registry)
- should preferably have no more than 1 layer of module nesting
- has its own Terraform plan, which implements the Layer (and in theory nothing else)
In the root of an environment (
env_name) directory there should be all the layers that are needed to create the environment. Each layer folder should have at least the basic
- main.tf: the base of the environment module code. This section references Terraform modules to be used.
- backend.tf: define where the backend for the
.tfsatefile is set.
- provider.tf: define provider being used to deploy the module here.
- override.tf.example: any example of possible portions of the module that should be overridden. This should usually not be needed, more info can be found in the Terraform documentation.
Whenever a Terraform plan is run within each layer, used to create the underlying environment, a
terraform.tfstate file is created.
This is used to control and version the Terraform IaC. This should be stored in a remote bucket and locked as described in the state documentation.
When creating your modules, just like everything else, there isn’t a one size fits all and business requirements need to be considered. But I’ve found the following structure to be a good starting point when designing a platform.
- Any Terraform module:
- should conform to the official Terraform Module Structure
- include proper documentation (README, CONTRIBUTING, LICENSE)
- have properly documented inputs and outputs, if any (this can be automated via pre-hooks)
In the root of a
module_name directory there should be a
README.md file, and the basic
- main.tf: the base of the module code. Create resources here.
- locals.tf: here you define ‘local’ values. These can be defaults that should not be changed or that are composed from variables.
- data.tf: here you define any data resources which you might need from previous layers. At times it might be feasible to collate data resources within the
- outputs.tf: define module outputs here.
- variables.tf: define variables here.
- versions.tf: define Terraform or provider versions here.
The resulting IaC structure would end up having the following format:
envs ├── <env_name> ├──── <layer> ├────── main.tf ├────── backend.tf ├────── provider.tf ├────── override.tf.example modules ├── <module_name> ├──── README.md ├──── main.tf ├──── locals.tf ├──── data.tf ├──── variables.tf └──── versions.tf
Note: This is not a comprehensive list of files that can exist in the
module_namedirectories. Examples of other files could be
The following repo has an example of how I would, start to, structure my Terraform code to create various environments, each having a GKE cluster with a managed Postgres CloudSQL database.
Note: Even though the repo has a
prdenvironment example, the code is NOT production ready. There are a fair few improvements which I would want to implement before even considering the code production ready.
In this post we’ve seen how you can start structuring Terraform code for your needs. Following basic architectural principals allows you to structure your IaC according to lifecycle groupings. With Terraform this can be achieved by grouping modules in layers within environment specific folders. This should allow you to have harmonized environments that are reproducible and that can be deployed with a “click of a button”.