linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
home/terraform/lessons/tf-advanced-01-terragrunt

lesson ── terraform-advanced ── ~18 мин ── 6 шагов

Terragrunt, DRY across dev/stage/prod

Terragrunt, the Gruntwork wrapper around Terraform. It solves the "directory-per-env" duplication problem: one terragrunt.hcl per env, shared through include. In this lesson you build dev and prod from a single module, and you get a feel for generate blocks and dependency.

▶ интерактивный sandbox

Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.

запустить sandbox →

stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя

Шаги

  1. 01

    The shared app module

    bash
    cd /home/student/tg
    cat > modules/app/main.tf <<'EOF'
    terraform {
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 5.60"
        }
      }
    }
    resource "aws_s3_bucket" "app" {
      bucket = "linuxlab-tg-${var.env}-${var.app_name}"
      tags = {
        Environment = var.env
        App         = var.app_name
      }
    }
    output "bucket_arn" {
      value = aws_s3_bucket.app.arn
    }
    EOF
    cat > modules/app/variables.tf <<'EOF'
    variable "env" {
      type = string
    }
    variable "app_name" {
      type = string
    }
    EOF

    ✓ The shared module is ready. Now you wrap it with Terragrunt.

  2. 02

    Root terragrunt.hcl, shared by every env

    bash
    cat > live/terragrunt.hcl <<'EOF'
    remote_state {
      backend = "s3"
      config = {
        bucket   = "tf-state-tg-demo"
        key      = "${path_relative_to_include()}/terraform.tfstate"
        region   = "us-east-1"
        encrypt  = true
        s3_bucket_tags = {
          ManagedBy = "terragrunt"
        }
        # LocalStack settings, usually not needed against real AWS
        endpoints = {
          s3       = "http://localstack:4566"
          dynamodb = "http://localstack:4566"
        }
        skip_credentials_validation = true
        skip_metadata_api_check     = true
        skip_requesting_account_id  = true
        force_path_style            = true
      }
      generate = {
        path      = "backend.tf"
        if_exists = "overwrite_terragrunt"
      }
    }
    generate "provider" {
      path      = "provider.tf"
      if_exists = "overwrite_terragrunt"
      contents  = <<HCL
    provider "aws" {
      region                      = "us-east-1"
      access_key                  = "test"
      secret_key                  = "test"
      s3_use_path_style           = true
      skip_credentials_validation = true
      skip_metadata_api_check     = true
      skip_requesting_account_id  = true
      endpoints {
        s3       = "http://localstack:4566"
        iam      = "http://localstack:4566"
        sts      = "http://localstack:4566"
        dynamodb = "http://localstack:4566"
      }
    }
    HCL
    }
    EOF

    This file is the shared config. Each env pulls it in through include.

    ✓ Root terragrunt.hcl is written. Now the envs.

  3. 03

    Per-env terragrunt.hcl

    bash
    cat > live/dev/terragrunt.hcl <<'EOF'
    include "root" {
      path = find_in_parent_folders()
    }
    terraform {
      source = "../../modules/app"
    }
    inputs = {
      env      = "dev"
      app_name = "api"
    }
    EOF
    cat > live/prod/terragrunt.hcl <<'EOF'
    include "root" {
      path = find_in_parent_folders()
    }
    terraform {
      source = "../../modules/app"
    }
    inputs = {
      env      = "prod"
      app_name = "api"
    }
    EOF
    tree live/ modules/

    Two envs, the same structure. The only difference is inputs.

    ✓ The env roots are ready. Each one points at env-specific inputs.

  4. 04

    terragrunt apply in dev

    bash
    cd /home/student/tg/live/dev
    terragrunt apply --terragrunt-non-interactive -auto-approve 2>&1 | tail -20

    What happened behind the scenes:

    1. Terragrunt read live/dev/terragrunt.hcl.
    2. It pulled in the root through find_in_parent_folders().
    3. It copied the module from modules/app into .terragrunt-cache/.
    4. It generated backend.tf and provider.tf in the cache folder.
    5. It ran terraform init + apply with the inputs.

    The bucket is created in LocalStack.

    bash
    aws --endpoint-url=http://localstack:4566 s3 ls

    You see linuxlab-tg-dev-api.

    ✓ Dev is deployed. One HCL, one env.

  5. 05

    The same config, the prod env

    bash
    cd /home/student/tg/live/prod
    terragrunt apply --terragrunt-non-interactive -auto-approve 2>&1 | tail -10
    aws --endpoint-url=http://localstack:4566 s3 ls

    Now there are two buckets: dev and prod. From a single module, no copy-paste.

    path_relative_to_include() in the root uses the relative path to the env directory as the key in S3, dev/terraform.tfstate and prod/terraform.tfstate, separate states with separate locks.

    ✓ Prod is deployed. State isolation works.

    The same thing on OpenTofu

    OpenTofu keeps its CLI and state compatible with Terraform for the commands in this step: migration usually goes through mv .terraform .terraform.bak; tofu init -upgrade. On your first switch, though, back up the state and run it on a feature branch first, the differences cluster in the newer features (variables in the backend, state encryption, OCI registry-backed modules). See tf-opentofu-parity for the full matrix.

    • → OpenTofu parity
  6. 06

    run-all for orchestration

    When you have many envs, running them one at a time gets tedious.

    bash
    cd /home/student/tg/live
    terragrunt run-all plan --terragrunt-non-interactive 2>&1 | tail -20

    run-all plan walks every terragrunt.hcl recursively, builds a dependency DAG (if there are dependency blocks), and runs them in the right order.

    For destroy:

    bash
    terragrunt run-all destroy --terragrunt-non-interactive -auto-approve 2>&1 | tail -5

    It tears down both, in the correct reverse order.

    ✓ The Terragrunt orchestration ran. Across 10+ envs the difference is real.

    The dependency block, cross-stack outputs

    A real scenario: the network stack creates the VPC, the app stack reads its id.

    hcl
    # live/prod/app/terragrunt.hcl
    include "root" {
      path = find_in_parent_folders()
    }
    terraform {
      source = "../../../modules/app"
    }
    dependency "network" {
      config_path = "../network"
      mock_outputs = {
        vpc_id     = "mock-vpc"
        subnet_ids = ["mock-subnet"]
      }
    }
    inputs = {
      env        = "prod"
      vpc_id     = dependency.network.outputs.vpc_id
      subnet_ids = dependency.network.outputs.subnet_ids
    }

    mock_outputs lets terragrunt plan work even when network has not been created yet. Handy in the init phase of CI, or for standalone plans.

    terragrunt run-all apply works it out on its own: network first, then app. A DAG across the stacks.

    • → Terragrunt in full
    • → The state hierarchy

Что ты узнал

Structure: a root terragrunt.hcl (shared provider, backend) plus a per-env live/<env>/terragrunt.hcl (inputs, include). Generate blocks create backend.tf/provider.tf on the fly. terragrunt apply runs inside a single env; terragrunt run-all apply runs across all of them.

команды

  • terragrunt applyapply in the current env.
  • terragrunt run-all planplan across all envs, in parallel.
  • terragrunt run-all apply --terragrunt-non-interactiveapply to all envs, for CI.

концепции

  • · include + find_in_parent_folders(), the root config
  • · generate { path = "backend.tf" }, creates backend.tf automatically
  • · dependency "X", for cross-stack outputs

следующий →

Hello, S3: your first resource in Terraform

Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies