Terraform πŸ’»

Hero image for Terraform πŸ’»

Reference Resources

Linting

We use TFLint for automated code checks.

Formatting

  • Alway use terraform fmt to rewrite Terraform configuration files to a canonical format and style.

Naming

  • Use snake_case in all: resource names, data source names, variable names, outputs.
  • Only use lowercase letters and numbers.
  • Always use singular noun for names.
  • Always use double quotes.

Resource and data source arguments

  • Do not repeat resource type in resource name (not partially, nor completely)
// God
resource "aws_route_table" "public" {}

// Bad
resource "aws_route_table" "public_route_table" {}

// Bad
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 type aws_nat_gateway and multiple resources of typeaws_route_table, so aws_nat_gateway should be named this and aws_route_table should have more descriptive names - like private, public, database). Normally, we use this to delegate the output of a module with the resource itself.
# modules/nimble_alb/main.tf
resource "aws_lb" "nimble" {
  name               = "${var.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 loadbalancer 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 by depends_on and lifecycle, 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 using condition in count argument use boolean value, if it makes sense, otherwise use length or other interpolation.
count = "${var.create_public_subnets}"
count = "${length(var.public_subnets) > 0 ? 1 : 0}"
  • To make inverted conditions don’t introduce another variable unless really necessary, use 1 - boolean value instead.
count = "${1 - var.create_public_subnets}"

<di=”message-notice”> terraform modules don’t support count parameter currently. You can follow up this ticket for updates: https://github.com/hashicorp/terraform/issues/953 </div>

Variables

  • Don’t reinvent the wheel in resource modules - use the same variable names, description and default as defined in β€œArgument Reference” section for the resource you are working on.
  • Omit type = "string" declaration if there is default = "" also
  • Omit type = "list" declaration if there is default = [] also.
  • Omit type = "map" declaration if there is default = {} also.
  • Use plural form in name of variables of type list and map.
  • When defining variables order the keys: 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).

  • The general recommendation for the names of outputs is that it should be descriptive for the value it contains and be less free-form than you would normally want.
  • 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).
// Good
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)}"
}

// Bad
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 to down 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, we need to check

  • How complexity infrastructure of the project?
    • Which is Terraform provider
    • Number of related resources
    • Number of Terraform providers
  • How environments are grouped?
    • By environment, region, project

Typical structure we usually have:

  • 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,...)
It is easier and faster to work with a smaller number of resources (in the main file also in the module as well), **terraform plan** and **terraform apply** both make cloud API calls to verify the status of resources, if you have your entire infrastructure in a single composition this can take many minutes.
Keep resource modules as plain as possible, don't nested module.

Best Practices

Use shared modules

Manage terraform resource with shared modules, this will save a lot of coding time. No need re-invent the wheel!

terraform module usage

Terraform Module Registry

Need to check the shared module before using that, don't push something that you not really 100% sure to production. πŸ™ˆπŸ™ˆπŸ™ˆ
In case that the shared module include unnecessary stuff that you don't want to use it or it can't apply to your current infrastructure, just copied their code/idea and create your module. 😎😎😎

State store

  • Always using Remote state

** 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
  }
}

** Start your project with Terraform Cloud Remote State Management πŸ’―

Enable version control on this bucket. And set IAM Policy to limit the access read/write to that bucket.
  • Don’t store the tfstate on Git as it includes sensitive data also that is your infrastructure, we can’t push it to the Git as some people can access to it and know it.

Security

  • Using backend S3, set IAM policy to limit the access to that bucket.
  • Don’t hardcode sensitive data (password, ssh key,…) in the Terraform code or in the varfile, don’t push it to Git, define sensitive data on fly (terrafrom plan or terrafrom apply) or using Vault Provider to deal with sensitive data.

AWS User data (The user data to provide when launching the instance)