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
Five rules drive this pattern:
terragrunt.hcl is always the deepest file of a branch. It marks a
layer: the directory where you run terragrunt apply. Nothing lives below
it.inputs.hcl below the layer to apply. Inputs are defined at the
layer or above it, never deeper than the terragrunt.hcl they feed.root.hcl, module.hcl,
inputs.hcl and the terragrunt.hcl leaf that defines the layer.π’ Uniqueness: each layer defines one and only one module. You cannot create a new resource independently without adding it to the module, which forces you to ask the questions:
Should this new resource really be added to this layer?
Isnβt this resource a new project need?
Consequently, you can either:
layers/ for terragrunt files and modules/ for terraform 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.hclDefines 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.hclDeclares 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.hclHolds 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.hclThe 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:
Cons:
root.hcl, parent inputs.hcl, module.hcl), so you
have to follow the include chain to know exactly what a layer applies.