docs-terraform-guidelines

Terragrunt

Context pattern


βœ’οΈ Definition

Context pattern : the DRY application of WYSIWYG through Terragrunt. Each layer instantiates a single module, and its inputs (its β€œcontext”) are declared once and inherited down the tree structure.

[!NOTE] A design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design

DRY: Don’t Repeat Yourself is the core motto of Terragrunt.

Terragrunt allows you to:


.
β”œβ”€β”€ layers
β”‚   β”œβ”€β”€ bastion
β”‚   β”‚   β”œβ”€β”€ module.hcl                 # The module called by every layer below
β”‚   β”‚   β”œβ”€β”€ dev
β”‚   β”‚   β”‚   β”œβ”€β”€ inputs.hcl             # Inputs unique to this layer
β”‚   β”‚   β”‚   └── terragrunt.hcl         # πŸ‘ˆ The layer to apply (deepest file)
β”‚   β”‚   └── prod
β”‚   β”‚       β”œβ”€β”€ inputs.hcl             # Inputs unique to this layer
β”‚   β”‚       └── terragrunt.hcl         # πŸ‘ˆ The layer to apply (deepest file)
β”‚   └── application
β”‚       β”œβ”€β”€ module.hcl                 # The module called by every layer below
β”‚       β”œβ”€β”€ dev
β”‚       β”‚   β”œβ”€β”€ inputs.hcl             # Inputs unique to this layer
β”‚       β”‚   └── terragrunt.hcl         # πŸ‘ˆ The layer to apply (deepest file)
β”‚       └── prod
β”‚           β”œβ”€β”€ inputs.hcl             # Inputs unique to this layer
β”‚           └── terragrunt.hcl         # πŸ‘ˆ The layer to apply (deepest file)
β”œβ”€β”€ modules                            # Local Terraform modules (optional)
└── root.hcl                           # Root config shared by every layer

πŸ“ Rules to respect

Five rules drive this pattern:


πŸ—ƒοΈ Files and folder naming

πŸ“ Folders

πŸ“„ Files

File Where Role
root.hcl Repository root Configuration shared by every layer: remote backend, generated provider/version block, global inputs.
module.hcl Above the layers that share a module Declares the single Terraform module the layers below it instantiate and the Terragrunt layers it optionally depends on.
inputs.hcl At a layer or any parent Inputs for that layer overriding or completing the values defined higher up.
terragrunt.hcl The leaf = the layer to apply The unit you terragrunt apply. Wires the includes together.

root.hcl

Defines what every layer shares: the remote state backend (with a unique key per layer) and a generate block for the Terraform required_version and providers. Global inputs can also live here.

# root.hcl
terraform_version_constraint  = "~> x.x.x"
terragrunt_version_constraint = "~> x.x.x"

locals {
  common_tags = {
    owner = "pitouna@email.fr"
  }
  environment = basename(get_original_terragrunt_dir())
}

remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }
  config = {
    bucket = "my-project-tfstate"
    key    = "${path_relative_to_include()}/terraform.tfstate" # πŸ‘ˆ unique per layer
    region = "eu-west-3"
  }
}

generate "provider" {
  path      = "_settings.tf" # generated file, prefixed with _
  if_exists = "overwrite"
  contents  = <<EOF
terraform {
  required_version = "= 1.6.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "= 5.0.0"
    }
  }
}
EOF
}

module.hcl

Declares the single module that the layers below it instantiate. Pin remote modules to a tag. You can use outputs from others layers with dependencies.

# layers/bastion/module.hcl
terraform {
  source = "${get_repo_root()}//modules/bastion"
}

locals {
  root = read_terragrunt_config(find_in_parent_folders("root.hcl"))
}

# optional : only needed when this module consumes outputs from another layer
dependency "vpc" {
  config_path = "../../vpc/${local.root.locals.environment}"
}

inputs = {
  context    = local.root.locals.common_tags
  subnet_ids = dependency.vpc.outputs.private_subnet_ids # output exposed by the vpc layer
}

inputs.hcl

Holds the inputs specific to a layer (or a shared parent). Anything common to several layers should move up the tree to stay DRY.

# layers/database/dev/inputs.hcl
inputs = {
  database_size  = "db.t3.micro" # smaller than production
  backup_enabled = false
}

terragrunt.hcl

The leaf, and the only file you apply. It wires together the root config, the parent module.hcl, and the local inputs.hcl.

# layers/environment/dev/terragrunt.hcl
include "root" {
  path           = find_in_parent_folders("root.hcl")
  merge_strategy = "deep"
}

include "module" {
  path           = find_in_parent_folders("module.hcl")
  merge_strategy = "deep"
}

include "inputs" {
  path           = "inputs.hcl"
  merge_strategy = "deep"
}


βš–οΈ Pros and cons

Pros:

Cons: