Skip to main content

OPG Terraform Practices

General Principles

Simple is better than Complex - Code for readability and inheritability

Layers of loops, dynamic blocks, and ternaries obscure what it is you’re actually doing, making it difficult to work with for junior engineers, you in 12 months time or people trying to get an understanding of Terraform.

Code should be correct, clear and concise, in that order.

Repo Organisation

As a general practice across OPG we have a separation of accounts for development, preproduction, and production; with a goal of multiple environments in development, with preproduction and production being identical and isolated to one environment per account.

The high level folder structure should contain an account and environment folder, to separate out the resources. Each folder may have a sub folder for modules relevant to that layer. VPC resources should be accessed as data sources, and should be stored in vpc.tf within the environment directory.

/account
/account/modules
/environment
/environment/modules
/environment/vpc.tf

Use Terraform Workspaces

We use Terraform workspaces to stop the duplication and customisation of code for each account or enviroment which increases maintainability and ease of promotion. Workspaces allow for environments to be consistent by default, but allow features and resources to be toggled to enable specificfunctionality.

Usage in Pipelines

Whenever running Terraform in a CI/CD pipeline always run a terraform plan stage, even if applying with an auto-approve. This allows you to understand exactly what Terraform changed and why should there ever be an issue, and allows traceability of when specific changes were applied to an environment.

Use .editorconfig in all repos for consistent whitespace

Every mainstream IDE supports plugins for the .editorconfig standard to make it easier to enforce whitespace consistency.

We recommend adopting the whitespace convention of a particular language or project.

This is the standard .editorconfig that we use.

# Override for Makefile
[{Makefile, makefile, GNUmakefile, Makefile.*}]
indent_style = tab
indent_size = 4

[*.yaml]
intent_style = space
indent_size = 2

[*.sh]
indent_style = tab
indent_size = 4

[*.{tf,tfvars,tpl}]
indent_size = 2
indent_style = space

Naming Conventions

Resource Names

Resource names should be descriptive to allow the usage of the resource to be easily understood.

Resources defined in Terraform should be all lowercase with underscores as separators for consistency with Terraform resource object names, also known as snake_case. Filenames within the Terraform project should also follow this convention.

Example:

resource "aws_security_group" "api_ecs_task" { … }

Resources that exist in AWS should be all lowercase separated by hyphens, they should all include the environment or account that they belong to in order to determine uniqueness and to be easily identifiable in the AWS Console. Using hyphens rather than underscores allows a resource to be easily identified as either a Terraform definition of a resource, or a “real” AWS resource.

Example:

resource "aws_security_group" "api_ecs" {
  name = "api-ecs-${terraform.workspace}"
}

Language

Version pin all providers

Terraform’s providers are constantly in flux. To avoid unexpected instability we should ensure that all providers are pinned.

Use Terraform Linting

Linting helps to ensure a consistent code formatting, improves code quality and catches common errors with syntax.

Run terraform fmt before committing all code. Use a pre-commit hook to do this automatically.

Do not use HEREDOC for JSON, YAML or IAM Policies

There are better ways to achieve the same outcome using Terraform interpolations or resources

For JSON, use a combination of a local and the jsonencode function.

For YAML, use a combination of a local and the yamlencode function.

For IAM Policy Documents, use the native iam_policy_document resource.

Use proper datatype

Using proper datatypes in Terraform makes it easier to validate inputs and document usage.

  • Use null instead of empty strings (“”)
  • Use bool instead of strings or integers for binary true/false
  • Use string for freeform text
  • Use object sparingly as it makes it harder to document and validate

Use locals to baptize opaque resource IDs

Using locals makes code more descriptive and maintainable. Rather than using complex expressions as parameters to some Terraform resource, instead move that expression to a local and reference the local in the resource.

Variables

Use upstream module or provider variable names where applicable

When writing a module that accepts variable inputs, make sure to use the same names as the upstream to avoid confusion and ambiguity.

Use all lowercase with underscores as separators

Avoid introducing any other syntaxes commonly found in other languages such as CamelCase or pascalCase. For consistency we want all variables to look uniform. This is also inline with the HashiCorp naming conventions.

Use positive variable names to avoid double negatives

All variable inputs that enable/disable a setting should be formatted ...._enabled (e.g. encryption_enabled).

It is acceptable for default values to be either false or true.

Use feature flags to enable/disable functionality

All modules should incorporate feature flags to enable or disable functionality. All feature flags should end in _enabled and should be of type bool.

Use description field for all inputs

All variable inputs need a description field. When the field is provided by an upstream provider (e.g. terraform-aws-provider), use same wording as the upstream docs.

Use sane defaults where applicable

Modules should be as turnkey as possible. The default value should ensure the most secure configuration (E.g. with encryption enabled).

Use variables for all secrets with no default value

All variable inputs for secrets must never define a default value. This ensures that terraform is able to validate user input. The exception to this is if the secret is optional and will be generated for the user automatically when left null or "" (empty).

Outputs

Use description field for all outputs

All outputs must have a description set. The description should be based on (or adapted from) the upstream Terraform provider where applicable. Avoid simply repeating the variable name as the output description.

Use well-formatted snake case output names

Avoid introducing any other syntaxes commonly found in other languages such as CamelCase or pascalCase. For consistency we want all variables to look uniform. It also makes code more consistent when using outputs together with Terraform remote_state to access those settings from across modules.

Never output secrets

Secrets should never be outputs of modules. Rather, they should be written to secure storage such as AWS Secrets Manager, AWS SSM Parameter Store with KMS encryption, or S3 with KMS encryption at rest.

Our preferred mechanism on AWS is using Secrets Manager.

Use symmetrical names

We prefer to keep Terraform outputs symmetrical as much as possible with the upstream resource or module, with exception of prefixes. This reduces the amount of entropy in the code or possible ambiguity, while increasing consistency. Below is an example of what *not to do. The expected output name is user_secret_access_key. This is because the other IAM user outputs in the upstream module are prefixed with user_, and then we should borrow the upstream’s output name of secret_access_key to become user_secret_access_key for consistency.

State

Use remote state

Use Terraform to create state bucket

This requires a two-phased approach, whereby you first provision the bucket without the remote state enabled. Then enable remote state (e.g. s3 {}) and import remote state by simply rerunning terraform init. We recommend this strategy because it promotes using the best tool for the job and makes it easier to define requirements and use consistent tooling.

Use backend with support for state locking

We recommend using the S3 backend with DynamoDB for state locking.

Use encrypted S3 bucket with versioning, encryption and strict IAM policies

We recommend not commingling state in the same bucket. This could cause the state to get overridden or compromised. Note, the state contains cached values of all outputs.

Use .gitignore to exclude Terraform state files, state directory backups and core dumps

.terraform
.terraform.tfstate.lock.info
*.tfstate
*.tfstate.backup

Module Design

Small Opinionated Modules

We believe that modules should do one thing very well. But in order to do that, it requires being opinionated on the design. Simply wrapping Terraform resources for the purposes of modularizing code is not that helpful. Implementing a specific use-case of those resource is more helpful.

This page was last reviewed on 20 March 2024. It needs to be reviewed again on 20 June 2024 by the page owner #opg-webops-community .
This page was set to be reviewed before 20 June 2024 by the page owner #opg-webops-community. This might mean the content is out of date.