Skip to content

Instantly share code, notes, and snippets.

@RoseSecurity
Created August 13, 2024 16:07
Show Gist options
  • Save RoseSecurity/bb47390adc1c5a15eadfa0cbecb1eb81 to your computer and use it in GitHub Desktop.
Save RoseSecurity/bb47390adc1c5a15eadfa0cbecb1eb81 to your computer and use it in GitHub Desktop.

Terraform Best Practices

Introduction

Terraform is a powerful tool for managing infrastructure as code. However, like any tool, Terraform has its own set of best practices that you should follow to ensure that your infrastructure is secure, reliable, and maintainable. This guide provides opinionated recommendations for Terraform best practices based on CloudPosse and HashiCorp guidance.

Variables

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.

For example:

variable "instance_type" {
  type        = string
  description = "The type of instance to launch"
  default     = "t2.micro"
}

Use positive variable names to avoid double negatives

All variable inputs that enable or disable a setting should be formatted <variable_name>_enabled. It is acceptable for default values to be either false or true.

For example:

variable "public_subnet_enabled" {
  type        = bool
  description = "Enables the network subnet to be accessible to the internet"
  default     = true
}

Use description to document the purpose of the variable

All variable inputs need a description field. When the field is provided by an upstream provider, use the same wording as the upstream documentation.

The default value should ensure the most secure configuration

For example:

variable "encryption_enabled" {
  type        = bool
  description = "Enable encryption for the bucket"
  default     = true
}

Use nullable = false where appropriate

When passing an argument to a resource, passing null means to use the default value. Prior to Terraform version 1.1.0, passing null to a module input set that value to null rather than to the default value.

Starting with Terraform version 1.1.0, variables can be declared as nullable = false which:

  1. Prevents the variable from being set to null
  2. Causes the variable to be set to the default value if null is passed in

You should always use nullable = false for all variables which should never be set to null. This is particularly important for lists, maps, and objects, which, if not required, should default to empty values (i. e. {} or []) rather than null. It can also be useful to set strings to default to "" rather than null and set nullable = false. This will simplify the code since it can count on the variable having a non-null value.

The default nullable = true never needs to be explicitly set. Leave variables with the default nullable = true if a null value is acceptable.

Prefer a single object over multiple simple inputs for related configuration

However, now that Terraform supports complex objects with field-level defaults, we recommend using a single object input variable with such defaults to group related configuration, taking into consideration the trade-offs listed in the above caution. This makes the interface easier to understand and use.

For example:

variable "eip_timeouts" {
  type = object({
    create = optional(string)
    update = optional(string)
    delete = optional(string, "30m")
  }))
  default = {}
  nullable = false
}

rather than:

variable "eip_create_timeout" {
  type = string
  default = null
}
variable "eip_update_timeout" {
  type = string
  default = null
}
variable "eip_delete_timeout" {
  type = string
  default = "30m"
}

Use custom validators to enforce custom constraints

Use the validation block to enforce custom constraints on input variables. A custom constraint is one that, if violated, would not otherwise cause an error, but would cause the module to behave in an unexpected way.

Use variables for all secrets with no default value and mark them "sensitive"

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).

Use sensitive = true to mark all secret variables as sensitive. This ensures that the value is not printed to the console.

Outputs

Use the 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 SSM Parameter Store.

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.

Symmetrical Names

Language

Use indented HEREDOC syntax

Using <<-EOT (as opposed to <<EOT without the -) ensures the code can be indented inline with the other code in the project. Note that EOT can be any uppercase string (e.g. CONFIG_FILE)

For example:

block {
  value = <<-EOT
  hello
    world
  EOT
}

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

Do not use long HEREDOC configurations

Use instead the templatefile function and move the configuration to a separate template file.

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 locals to identify and describe opaque values

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment