Terragrunt with OpenTofu: Keep Your Configuration DRY

· 2 min read · Terraform & OpenTofu Tutorials

What Terragrunt adds #

Terragrunt is a thin wrapper around OpenTofu and Terraform. It does not replace them: it calls tofu or terraform for you and solves three recurring problems of a multi-environment setup:

  1. Repeated code. Every directory needs the same backend and provider blocks.
  2. Dependencies between configurations. The application needs the network outputs, so the network must be applied first.
  3. Running many configurations at once. terragrunt run-all apply (or the run --all form in recent versions) applies the whole tree in dependency order.

Installation and configuration for OpenTofu #

Install Terragrunt from its releases page or with a package manager, and tell it to use OpenTofu:

$ export TG_TF_PATH=tofu
$ terragrunt --version

Terragrunt's command line and file names have changed across versions (for example, terragrunt.hcl can also be named root.hcl, and run-all has a newer run --all form), so confirm them in the documentation of your version.

Directory layout #

infra/
  root.hcl                      # shared backend and provider
  modules/
    network/
    app/
  live/
    dev/
      network/terragrunt.hcl
      app/terragrunt.hcl
    pro/
      network/terragrunt.hcl
      app/terragrunt.hcl

The modules are plain OpenTofu modules (project structure). The live tree contains only Terragrunt configuration with the inputs of each environment.

Root configuration #

root.hcl
locals {
  env = basename(dirname(path_relative_to_include()))
}

remote_state {
  backend = "s3"

  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }

  config = {
    bucket         = "ditwl-tfstate-${get_aws_account_id()}"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "eu-west-1"
    encrypt        = true
    dynamodb_table = "ditwl-tfstate-lock"
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "aws" {
  region = "eu-west-1"
}
EOF
}

Every configuration gets its own state key and the same backend, with no copy-and-paste. (Newer versions of the S3 backend can lock with a file in S3 instead of DynamoDB; see backends.)

Network and application with dependencies #

live/pro/network/terragrunt.hcl
include "root" {
  path = find_in_parent_folders("root.hcl")
}

terraform {
  source = "../../../modules/network"
}

inputs = {
  vpc_cidr = "10.10.0.0/16"
}
live/pro/app/terragrunt.hcl
include "root" {
  path = find_in_parent_folders("root.hcl")
}

terraform {
  source = "../../../modules/app"
}

dependency "network" {
  config_path = "../network"

  mock_outputs = {
    private_subnet_ids = ["subnet-00000000"]
  }
}

inputs = {
  subnet_ids = dependency.network.outputs.private_subnet_ids
}

dependency reads the outputs of the network configuration and guarantees the apply order. The mock_outputs let plan and validate work before the network exists.

Commands #

$ cd live/pro/network
$ terragrunt plan
$ terragrunt apply

$ cd live/pro
$ terragrunt run-all plan      # every configuration in dependency order

When to use it #

Terragrunt is worth it when you have several environments, regions or accounts and many small states. For one or two environments, plain directories and modules are enough and have one tool less to learn. Compare also with the dependency approach in data sources and remote state.

#Terraform #OpenTofu #Terragrunt #AWS