Continuing the Journey: Unveiling Terraform Best Practices for Speed and Modularity. Building upon our exploration of GitOps with Atlantis and Terraform, it's time to dive into the next phase of our learning journey. In this series, we'll unravel a collection of essential Terraform best practices that are poised to elevate your approach, making it faster, more efficient, and inherently modular.
Never put everything in a single place, you should not put your complete terraform code in a single place. Always try to distribute it across different directories. Let’s understand why we should not put everything in a single place.
So always try to break your code as much as possible. Let me tell you how I break my code. See the screenshot below of my sample directory structure:
Now we have already done a lot of segregation based on the folder. Now in order to avoid the
code
replication I try to use the modules as much as possible.
I am not a big fan of git submodules. Hence I keep the modules in the same Terraform directory
inside a separate directory.
We always try not to change the modules too frequently but in real life, that is not possible. You cannot design a perfect module in one go. There may be scenarios when you will have to change the module.
Now these changes can be very simple and will not impact the already created resources. but sometimes these changes can be contract-breaking as well when already created resources will be impacted and they also need to be updated after updating the module.
So to make this change seamless we should always version our terraform modules.
Whenever you are making any contract breaking which requires some code change in already created resources you must update the terraform version.
This can be simply done using a sub-directory with the version name.
Let’s take an example of the module directory structure below.
I tried using Terraform workspaces a few times and the experience is not good. Below are a few reasons for it.
Now I am leaving this thing open for now. You can let me know your experience working with Terraform workspaces.
Always try to keep the name of resources as generic as possible. Because that gives me the flexibility to copy and paste my resource file anywhere. On the other hand, I always control the environment/application-specific naming convention via variable file. In that way, I know on which particular file I have to make the change. Rest all the files now just act like a template that can be added anywhere depending on the need. Let’s understand that better with an example.
Actual file which has the resource definition of my VPC
module "vpc" {
source = "../modules/vpc/v1"
name = local.name
cidr = var.cidr
tags = local.tags
.
.
.
}
Variable file
### I am not defining any default value here as it will come from tfvars file
variable "env" {}
variable "name" {
default = "myvpc" # I will define name of my vpc here.
}
variable "cidr" {
default = "10.10.0.0/16" # I will define my vpc cidr here.
}
locals {
name = "${var.name}-${var.env}"
tags = {
Name = "${var.name}-${var.env}"
}
}
module "dev-vpc" {
source = "../modules/vpc/v1"
name = "myvpc-dev"
cidr = "10.10.0.0/16"
tags = {
Name = "myvpc-dev"
}
.
.
.
}
I have divided the tags majorly into two categories mandatory and not mandatory. They can then further be divided into a few categories.
The file from where I am calling my module.
module "sqs" {
source = "../modules/sqs/v1"
name = local.name
consumer = "some aplication name here"
.
.
.
}
The variable file inside the module
.
.
variable "consumer" {} # this variable does not have any default value and is mandatory.
.
.
Resource file inside the module
resource aws_sqs_queue "sqs" {
.
.
tags = {
Consumer = var.consumer
.
.
}
}
If you look at the code. In the first place, I made a mandatory variable(by not specifying the default value) in the module named consumer. Now I am passing the same variable as a tag value inside the module which will make sure all my sqs have that tag.
Resource file inside the module
resource aws_sqs_queue "sqs" {
.
.
tags = {
LaunchMonthYear = formatdate("MMM-YYYY", timestamp())
.
.
}
lifecycle {
ignore_changes = [
# Ignore changes to tags
tags["LaunchMonthYear"]
]
}
}
If you see the module code above. I have specified the value as well in it which will take a dynamic value every time I will apply terraform. I have also added a lifecycle ignore_changes block which will make sure the tags are only added at the time of resource creation and their value is not updated every time I am running Terraform.
Optional tags can be passed at both the module level as well as resource level. Also, let’s see after combining everything how it looks like.
The file from where I am calling my module.
module "sqs" {
source = "../modules/sqs/v1"
name = local.name
consumer = "some aplication name here"
env = var.env
tags = {
SomeOptionalTag = "SomeValue"
}
.
.
.
}
The variable file inside the module
.
.
variable "consumer" {} # this variable does not have any default value and is mandatory.
variable "env" {} # this variable does not have any default value and is mandatory.
variable "tags" {
description = "A mapping of tags to assign to all resources"
type = map(string)
default = {} # this has a default value as empty hence not mandatoy to provide a value
}
.
.
Resource file inside the module
resource aws_sqs_queue "sqs" {
.
.
tags = merge (
var.tags,
{ Consumer = var.consumer },
{ LaunchMonthYear = formatdate("MMM-YYYY", timestamp()) },
)
lifecycle {
ignore_changes = [
# Ignore changes to tags
tags["LaunchMonthYear"]
]
}
}
The above config has everything.
Now before implementing tags first, you should decide which tag falls in which category and then add it to the terraform code.
Looking to implement these Terraform best practices in your projects? Our experts are here to help! Get in touch with us (connect@iopshub.com) for tailored guidance and hands-on support.