Terragrunt: Simplifying and Scaling Your Terraform

Introduction

  • CI/CD drift — setiap stack harus di-plan/apply manual, atau Anda tulis orchestration sendiri.

Di sinilah Terragrunt masuk. Terragrunt adalah thin wrapper untuk Terraform yang menambahkan:

MasalahSolusi Terragrunt
Konfigurasi berulang root.hcl + include block → config ditulis sekali
Backend boilerplateremote_state block diwariskan ke semua stack
Cross-stack dependencydependency block baca output stack lain
Multi-env orchestrationterragrunt run-all plan/apply
Provider generationGenerate block (dynamic provider per stack)

▎ TL;DR: Terragrunt bukan replacement Terraform — Terragrunt tetap panggil terraform di belakang layar. Fungsinya adalah mengorkestrasi banyak root module Terraform dengan konfigurasi bersama.

iac/
  ├── modules/                       # reusable, versioned Terraform modules
  │   ├── aws/
  │   │   ├── network/               # VPC + subnets + NAT + IGW
  │   │   ├── sg/                    # Security group reusable
  │   │   └── ec2/                   # EC2 instance (public/private variant)
  └── live/                          # Terragrunt stack tree
      ├── aws/
      │   ├── root.hcl               # region, provider, remote_state, common inputs
      │   ├── dev/
      │   │   ├── vpc/               # → uses modules/aws/network
      │   │   ├── sg/                # → uses modules/aws/sg
      │   │   ├── ec2-public/        # → uses modules/aws/ec2
      │   │   └── ec2-private/       # → uses modules/aws/ec2

Kita coba untuk environment Dev

Konsep kunci:

  • root.hcl di setiap cloud = single source of truth untuk region, account, remote state, dan provider.
  • Setiap folder di bawah live/{env}/ adalah satu stack Terragrunt independen dengan state-nya sendiri.
  • Module di modules/ adalah Terraform root module reusable, dipanggil via terraform { source = “../../../modules/aws/vpc” }.

Step 1 — Struktur Folder

Pisahkan module (reusable) dari live (env-specific):

mkdir -p iac/modules/aws/{network,sg,ec2}
mkdir -p iac/live/aws/dev/{vpc,sg,ec2-public,ec2-private}
mkdir -p iac/live/aws/prod/network

Prinsip: module = kode Terraform polos, tidak tahu env. Live = Terragrunt config, tahu env, region, akun.

Step 2 — Tulis Module (Terraform)

Module tidak perlu tahu soal Terragrunt. Contoh modules/aws/network/main.tf:

resource "aws_vpc" "this" {
    cidr_block = var.cidr_block
    tags = merge(var.tags, {
      Name = var.name
    })
  }

  resource "aws_subnet" "public" {
    count             = length(var.public_subnets)
    vpc_id            = aws_vpc.this.id
    cidr_block        = var.public_subnets[count.index]
    availability_zone = var.azs[count.index]
    map_public_ip_on_launch = true
    tags = merge(var.tags, { Name = "${var.name}-public-${count.index + 1}" })
  }

  # ... dst: private subnet, IGW, NAT, route table

  Output outputs.tf:

  output "vpc_id" {
    value = aws_vpc.this.id
  }

  output "public_subnet_ids" {
    value = aws_subnet.public[*].id
  }

▎ Module harus input-driven — semua yang berbeda antar env/vpc dilewatkan via var.*.

Step 3 — root.hcl

live/aws/root.hcl — single source of truth untuk AWS:

locals {
    aws_region = "ap-southeast-3"

    common_tags = {
      ManagedBy = "terragrunt"
      Project   = "platform-engineer"
    }
  }

  # Backend state (S3 + DynamoDB lock) — comment out dulu kalau masih local
  # remote_state {
  #   backend = "s3"
  #   config = {
  #     bucket         = "my-tf-state-ap-southeast-3"
  #     key            = "${path_relative_to_include()}/terraform.tfstate"
  #     region         = local.aws_region
  #     dynamodb_table = "my-tf-locks"
  #     encrypt        = true
  #   }
  # }

  # Provider di-generate per stack
  generate "provider" {
    path      = "provider.tf"
    if_exists = "overwrite_terragrunt"
    contents  = <<EOF
  provider "aws" {
    region = "${local.aws_region}"
    default_tags {
      tags = ${jsonencode(local.common_tags)}
    }
  }
  EOF
  }

Step 4 — terragrunt.hcl per Stack

Setiap stack cukup include root, lalu isi input spesifik env.

include "root" {
    path = find_in_parent_folders()
  }

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

  inputs = {
    name           = "vpc-dev"
    cidr_block     = "10.10.0.0/16"
    azs            = ["ap-southeast-3a", "ap-southeast-3b"]
    public_subnets = ["10.10.1.0/24", "10.10.2.0/24"]
    private_subnets = ["10.10.11.0/24", "10.10.12.0/24"]

    tags = {
      Environment = "dev"
    }
  }

live/aws/dev/ec2-public/terragrunt.hcl — contoh cross-stack dependency:

include "root" {
    path = find_in_parent_folders()
  }

  terraform {
    source = "../../../modules/aws/ec2"
  }

  dependency "vpc" {
    config_path = "../vpc"

    mock_outputs = {
      vpc_id            = "vpc-mock"
      public_subnet_ids = ["subnet-mock-1", "subnet-mock-2"]
    }
  }

  dependency "sg" {
    config_path = "../sg"
    mock_outputs = {
      sg_id = "sg-mock"
    }
  }

  inputs = {
    name              = "ec2-public-dev"
    subnet_id         = dependency.vpc.outputs.public_subnet_ids[0]
    security_group_id = dependency.sg.outputs.sg_id
    associate_public_ip = true
  }

Perhatikan mock_outputs — ini membuat stack EC2 bisa di-plan meskipun stack VPC belum di-apply. Penting untuk CI/PR flow.

Step 5 — Plan & Apply

Single stack:

cd iac/live/aws/dev/vpc
terragrunt validate
terragrunt init
terragrunt plan
terragrunt apply

Semua stack di AWS sekaligus:

cd iac/live/aws
terragrunt run-all plan
terragrunt run-all apply # hati-hati!

Output dari stack lain:

cd iac/live/aws/dev/vpc
terragrunt output

Step 6 — Tambah Env Baru (Misal staging)

Seluruh proses cuma copy + edit minimal:

mkdir -p iac/live/aws/staging/network
cp iac/live/aws/dev/vpc/terragrunt.hcl iac/live/aws/staging/network/
 Edit 2 hal:
# - name = "vpc-staging"
# - cidr_block = "10.50.0.0/16"
# - tags.Environment = "staging"

Backend, provider, region — semua otomatis dari root.hcl. Zero duplication.

State Management

State Terragrunt tersimpan di .terragrunt-cache/ (lokal) atau di remote backend (S3/GCS).

Untuk production, migrate ke remote:

AWS (S3 + DynamoDB lock):

aws s3api create-bucket --bucket my-tf-state --region ap-southeast-3 \
    --create-bucket-configuration LocationConstraint=ap-southeast-3
  aws s3api put-bucket-versioning --bucket my-tf-state \
    --versioning-configuration Status=Enabled
  aws dynamodb create-table --table-name my-tf-locks \
    --attribute-definitions AttributeName=LockID,AttributeType=S \
    --key-schema AttributeName=LockID,KeyType=HASH \
    --billing-mode PAY_PER_REQUEST

Lalu uncomment block remote_state di root.hcl.

Leave a Reply

Your email address will not be published. Required fields are marked *