Terraform π»
Linting
We use TFLint for automated code checks.
Formatting
- terraform fmt to rewrite Terraform configuration files to a canonical format and style.Always use
Naming
-
snake_case
in all: resource names, data source names, variable names, outputs. -
lowercase letters and numbers
. -
singular noun
for names. -
double quotes
.
Resource and data source arguments
-
Do not repeat the resource type in the resource name (not partially, nor completely)
resource "aws_route_table" "public" {}
resource "aws_route_table" "public_route_table" {}
resource "aws_route_table" "public_aws_route_table" {}
-
Resource name 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
and multiple resources of typeaws_route_table
, soaws_nat_gateway
should be namedthis
andaws_route_table
should have more descriptive names - likeprivate
,public
,database
). Normally, we usethis
to delegate theoutput
of amodule
with theresource
itself.# modules/nimble_alb/main.tf resource "aws_lb" "nimble" { name = var.load_balancer_name load_balancer_type = "application" security_groups = [var.vpc_security_group_ids] subnets = [var.subnets] } # modules/nimble_alb/outputs.tf output "this_dns" { description = "Nimble Application load balancer DNS" value = aws_lb.nimble.dns } # main.tf module "nimble_alb" { source = "./modules/nimble_alb" ... } # Get the DNS by `module.nimble_alb.this_dns`
-
Include
tags
argument, if supported by resource as the last real argument, following bydepends_on
andlifecycle
, if necessary. All of these should be separated by a single empty line.resource "aws_nat_gateway" "nimble" { count = "1" allocation_id = "..." subnet_id = "..." tags = { Name = "..." } depends_on = ["aws_internet_gateway.this"] lifecycle { create_before_destroy = true } }
-
Include
count
argument inside resource blocks as the first argument at the top and separate by newline after it.resource "aws_route_table" "public" { count = "2" vpc_id = "vpc-12345678" .... }
-
When making the condition in the
count argument
, use theboolean value
, if it makes sense, otherwise use thelength
method.# create_public_subnets: true count = var.create_public_subnets
or
count = length(var.public_subnets) > 0 ? 1 : 0
-
To make inverted conditions, use
1 - {boolean value}
.# create_public_subnets: true # 1 - true => false count = 1 - var.create_public_subnets
Variables
-
-
type = "string"
declaration if there isdefault = ""
also -
type = "list"
declaration if there isdefault = []
also. -
type = "map"
declaration if there isdefault = {}
also. -
list
andmap
. -
description
,type
,default
.
Outputs
Name for the outputs is important to make them consistent and understandable outside of its scope (when the user is using a module it should be obvious what type and attribute of the value is returned).
-
-
If the output is returning a value with interpolation functions and multiple resources, the {name} and {type} there should be as generic as possible (this is often the most generic and should be preferred).
output "this_security_group_id" { description = "The ID of the security group" value = element(concat(coalescelist(aws_security_group.this.*.id, aws_security_group.this_name_prefix.*.id), list("")), 0) }
output "security_group_id" { description = "The ID of the security group" value = element(concat(coalescelist(aws_security_group.this.*.id, aws_security_group.web.*.id), list("")), 0) } output "another_security_group_id" { description = "The ID of web security group" value = element(concat(aws_security_group.web.*.id, list("")), 0) }
-
If the returned value is a list it should have a plural name.
output "this_rds_cluster_instance_endpoints" { description = "A list of all cluster instance endpoints" value = [aws_rds_cluster_instance.this.*.endpoint] }
Main File Structure
-
Group each component in the main file together:
# provider configuration # backend store for terraform # data source arguments # resource/module # resource `null_resource`
-
Order by
top
todown
in term of execution# Create VPC first resource "aws_vpc" "nimble" {} # Create IAM role before the EC2 instance resource "aws_iam_role" "nimble_web" {} # Create EC2 instance resource "aws_instance" "nimble_web" {}
Project Structure
It depends on the project, before start doing this, prefer to have an overview of the system, by checking
-
- Which Terraform providers to use
- Number of related resources
- Number of Terraform providers
-
- By environment, region, project
Typical structure for common providers
-
Heroku:
terraform-heroku-project ββ main.tf ββ outputs.tf ββ variables.tf β ββ staging.tfvars ββ production.tfvars β ββ README.md
- main.tf: Call modules, locals and data-sources to create all resources
- variables.tf: contains declarations of variables used in main.tf
- outputs.tf: contains outputs from the resources created in main.tf
-
IaaS (AWS, Google Cloud, Azure):
terraform-project ββ modules β ββ module_1 β β ββ main.tf β β ββ outputs.tf β β ββ variables.tf β ββ module_2 β β ββ main.tf β β ββ outputs.tf β β ββ variables.tf β ββ main.tf ββ outputs.tf ββ variables.tf β ββ staging.tfvars ββ production.tfvars β ββ README.md
- module: Module is a collection of connected resources which together perform the common action (eg: aws_vpc creates VPC, subnets, NAT, etc)
- main.tf: Call modules, locals and data-sources to create all resources
- variables.tf: contains declarations of variables used in main.tf
- outputs.tf: contains outputs from the resources created in main.tf
- staging.tfvars: Configuration for Staging (like instance type,β¦)
- production.tfvars: Configuration for Production (like instance type,β¦)
Best Practices
Use shared modules
-
Manage Terraform Resources with shared modules, this will save a lot of coding time. No need re-invent the wheel!
-
Always set the
module version
.module "rds" { source = "terraform-aws-modules/rds/aws" version = "2.18.0" ... }
module "rds" { source = "terraform-aws-modules/rds/aws" ... }
State store
Always using Remote State
-
Start your project with Terraform Cloud Remote State Management π―
terraform { cloud { organization = "nimble" workspaces { name = "overblock" } } }
-
Start your project with AWS S3 π―
terraform { backend "s3" { region = "aws_region" # This bucket needs to be created beforehand bucket = "bucket_name" key = "aws-setup/state.tfstate" encrypt = true # AES-256 encryption } }
Security
-
-
variables.tf
files, do not push it to Git, define sensitive data on the fly (terraform plan
orterraform apply
) or Terraform Cloud.
AWS User data (The user data to provide when launching the instance)
- cloud-init overPrefer to use
bash script
.