Terraform is one of the most used toops wich alows management of infraestructure as code. It allows developers to do a lot of things and does not restrict them from doing thing in ways that will be hard to support or integrate with. Learn with us the 10 terraform best practices to help you achieve a better version of your infraestructure management.
1. Use modules (DRY code)
Don’t Repeat Yourself (DRY) is one of the software development principles. This principle aims to avoid redundancy in the code declaration.
Whenever you find yourself writing the same terraform code more than once, use modules.
Modules are containers that hold resources that are used together
# using the module server_deployment_module and declaring some variables
module "server_deployment" {
source = "github.com/myuser/server_deployment_module"
# module variables
server_name = "storage-west"
server_size = "c5.xlarge"
server_username = "diego"
}
2. Learn how to use local
Learning how to use terraform locals is essential for mastering terraform best practices. In a nutshell locals let you manipulate data at runtime. Many people confuse variables with locals, but they are not the same
variables: They are given as input and are not mutable
variable "first_name" {
type = string
default = "John"
}
locals: They are set at runtime and are mutable. You can manipulate data from variables and resources created at runtime, so they are pretty powerful. Learn how to use them.
locals {
full_name = join(" ", [var.first_name, var.last_name])
}
# local.full_name = "John Doe"
3. Make use of backends
Terraform has a file called terraform.tfstate where it saves all the deployment configurations done to the created resources. By default, it will save the file in your local filesystem, but having it in your local machine is a complete mistake, and here is why:
- If something happens to your storage, you’re pretty much screwed
- If you work with a distributed team, they won’t have access to the file (unless you hand it to them)
- It is not versioned, so you won’t know past states (useful for debugging and compliance)
⚠️ The backend is only declared in the main.tf file, not in a module
# Sample backend using S3 buckets.
# Supported backends here https://www.terraform.io/language/settings/backendsterraform {
backend "s3" {
bucket = "my-s3-bucket"
key = "production/terraform.tfstate"
encrypt = true
region = "us-east-1"
}
4. Layout your project properly
├── backend.tf # not needed if it's a module
├── template_files
│ ├── user_data.sh
│ └── config_file.conf
├── README.md
├── main.tf
├── outputs.tf
├── variables.tf
├── versions.tf
There is a standard layout for terraform modules that you can use for pretty much all your projects, here it is.
As you can see is pretty easy to figure out where each resource is just by looking at the tree structure. We want this to be simple to understand because other people might work on this code in the future.
5. Encrypt your sensitive variables
Do you want to share your terraform with a colleage but don’t want to expose the secrets you may have? sops might be what you are looking for. With it, you can commit your terraform with an encrypted file containing your secrets (this is much better than hardcoding them into your variables file). Here is a nice provider I strongly recommend for using sops with terraform
terraform {
required_providers {
sops = {
source = "carlpett/sops"
version = "~> 0.5"
}
}
}
data "sops_file" "secret" {
source_file = "secrets.json"
}
output "root-value-password" {
# Access the password variable from the map
value = data.sops_file.secret.data["password"]
}
💡 Sops is just for sharing secrets used by terraform. If you want to store infrastructure secrets you might want to look at Vault, AWS Secrets Manager or K8s Secrets
6. Specify variable and output types
It is hard to figure out the datatype expected by a variable, especially when it’s a map or a complex object.
Specifying the data type of variables has benefits
- Makes your terraform code easier to work with
- When specified, terraform enforces that type, so it will error out if another type is given
- For complex objects, it allows you to understand the data better.
variable "docker_ports" {
type = list(object({
internal = number
external = number
protocol = string
}))
default = [
{
internal = 8300
external = 8300
protocol = "tcp"
}
]
}
Just by looking above we can tell that the docker_ports variable is expecting a list of maps containing internal, external and protocol values, simple. (The more you use them, the better you get at reading them)
Also note that if at runtime we enter another property inside the map like dummy
{
internal = 8300
external = 8300
dummy = "im a dummy key"
protocol = "tcp"
}
dummy field won’t get passed, we’d still see the 3 keys defined in the
docker_ports variable. This is great to avoid unwanted fields and increase security.
7. Use terraform built-in functions
Terraform has many powerful built-in functions, use them alongside your locals or inside your resources. You can find terraform all built-in functions here.
There are functions for many things. Strings, maps, objects, date and time, filesystem, cryptography, data manipulation, networking and many more
# Sample lower function
lower("HELLO")
hello
lower("АЛЛО!")
алло!
8. Specify terraform, provider and module versions
Terraform modules change regularly, as well as terraform providers. This is why you should always version your providers as well as your modules
Versioning module
# Use branch or tag v1.0.0 from github repo
module "server_deployment" {
source = "github.com/myuser/server_deployment_module?ref=v1.0.0"
# module variables
server_name = "storage-west"
server_size = "c5.xlarge"
server_username = "diego"
}
This will ensure we use a working version of this module at any point in time
Versioning terraform and provider
# versions.tf file
terraform {
required_version = "1.2.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
By versioning your provider and your terraform version we make sure any person using this in the future will use working versions we used to deploy this.
9. Document your code!
I know, I know, you hate documentation, but I have news for you… You can automate this! Check out terraform docs, the only thing you need to do is follow the steps I mentioned you above for best practices like specifying variable and output types as well as defining your versions. terraform-docs will then read all those settings and write professional-looking markdown readme for you, its pretty cool!
# This will print the generated readme
# in the shell in markdown format
$ terraform-docs markdown .
(Advanced) You can also automate the documentation creation with Github Actions
10. Learn how to navigate Terraform docs and registry
A lot of times you will find yourself reading how to do things in terraform, and turns out someone else already did it for you, even terraform themselves!
Before trying to reinvent the wheel, take a look at the docs and the registry, maybe someone already made a module for whatever you want to build. Or maybe terraform has the function you need to make your locals work (trust me, I’ve been there many times).
Besides, they have pretty great docs, I have to admit.
You can find Terraform Documentation here.
11. (Bonus) Format your code
Terraform has a native command for formatting your code.
You can add this command to github pre-commits so your code geta formatted automatically
Conclusion
In conclusion, mastering Terraform best practices for AWS is essential for optimizing your infrastructure as code (IaC) projects and ensuring the seamless deployment and management of cloud resources. At SolarDevs, we understand the significance of adhering to these principles to enhance efficiency, scalability, and reliability across your cloud environments.
Our team specializes in implementing Terraform best practices to empower your organization’s infrastructure management journey. From modularization and version control to remote state management and automated testing, we ensure that your Terraform deployments are robust, maintainable, and aligned with your business objectives.