Terragrunt ile Terraform Yapılandırma Yönetimi

Terraform ile ciddi altyapı yönetimi yapıyorsanız, bir noktada şu soruyla mutlaka yüzleşiyorsunuz: “Aynı modülü birden fazla ortam için nasıl yöneteyim?” Dev, staging ve production ortamlarınız var, her birinin biraz farklı değişkenleri var, ve siz kendinizi aynı terraform.tfvars dosyasını kopyalayıp yapıştırırken buluyorsunuz. İşte tam bu noktada Terragrunt devreye giriyor.

Terragrunt, Gruntwork tarafından geliştirilen ve Terraform’un üzerine oturan ince bir wrapper araç. “Thin wrapper” diyorlar ama pratikte hayatınızı epey kolaylaştırıyor. DRY (Don’t Repeat Yourself) prensibini Terraform konfigürasyonlarına uygulamanızı sağlıyor, remote state yönetimini otomatize ediyor ve bağımlılık yönetimi konusunda Terraform’un eksikliklerini kapatıyor.

Neden Terragrunt?

Saf Terraform kullanırken karşılaştığınız tipik problemleri düşünün. Her ortam için ayrı backend.tf dosyası tutuyorsunuz, remote state bucket’ı elle oluşturuyorsunuz, ve modül çağrılarını her environments/dev, environments/staging, environments/prod klasöründe tekrarlıyorsunuz. Bu durum zamanla bakım kabusuna dönüşüyor.

Terragrunt şu problemleri çözüyor:

  • DRY konfigürasyon: Aynı backend konfigürasyonunu tek bir yerde tanımlayıp her yerde kullanabiliyorsunuz
  • Otomatik remote state: S3 bucket ve DynamoDB tablosunu otomatik oluşturuyor
  • Bağımlılık yönetimi: Modüller arası bağımlılıkları açıkça tanımlayıp sıralı apply yapabiliyorsunuz
  • Hooks: Before/after hook’larla özel komutlar çalıştırabiliyorsunuz
  • run-all komutu: Tüm altyapıyı tek komutla yönetebiliyorsunuz

Kurulum ve İlk Adımlar

Terragrunt kurulumu oldukça basit. Linux üzerinde:

# Binary indirme yöntemi
wget https://github.com/gruntwork-io/terragrunt/releases/download/v0.54.0/terragrunt_linux_amd64
chmod +x terragrunt_linux_amd64
sudo mv terragrunt_linux_amd64 /usr/local/bin/terragrunt

# Homebrew ile (macOS veya Linux)
brew install terragrunt

# Versiyon kontrolü
terragrunt --version

Terragrunt, Terraform binary’sine ihtiyaç duyuyor, dolayısıyla Terraform’un da kurulu olması gerekiyor. tfenv veya asdf kullanıyorsanız versiyonları kolayca yönetebilirsiniz.

Proje Yapısı

Terragrunt projelerinde klasör yapısı kritik önem taşıyor. İyi tasarlanmış bir yapı şuna benziyor:

infrastructure/
├── terragrunt.hcl          # Root konfigürasyon (provider, backend şablonu)
├── modules/                 # Terraform modülleri
│   ├── vpc/
│   ├── eks/
│   └── rds/
└── environments/
    ├── dev/
    │   ├── terragrunt.hcl   # Ortam bazlı override
    │   ├── vpc/
    │   │   └── terragrunt.hcl
    │   ├── eks/
    │   │   └── terragrunt.hcl
    │   └── rds/
    │       └── terragrunt.hcl
    ├── staging/
    │   ├── terragrunt.hcl
    │   └── ...
    └── prod/
        ├── terragrunt.hcl
        └── ...

Bu yapıda her terragrunt.hcl dosyası kendi seviyesindeki konfigürasyonu tanımlıyor ve parent konfigürasyonları miras alabiliyor.

Root terragrunt.hcl Yapılandırması

En kritik dosya root terragrunt.hcl. Burada backend şablonunu ve ortak provider konfigürasyonunu tanımlıyorsunuz:

# infrastructure/terragrunt.hcl

locals {
  # Klasör yapısından ortam ve region bilgisini otomatik çekiyoruz
  path_components = split("/", path_relative_to_include())
  environment     = local.path_components[0]
  region          = "eu-west-1"
  account_id      = get_aws_account_id()

  # Ortak etiketler
  common_tags = {
    Environment = local.environment
    ManagedBy   = "Terragrunt"
    Repository  = "infrastructure"
  }
}

# Remote state backend şablonu
remote_state {
  backend = "s3"
  config = {
    bucket         = "mycompany-terraform-state-${local.account_id}"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = local.region
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
  }

  # Bu ayar ile Terragrunt bucket ve DynamoDB tablosunu otomatik oluşturuyor
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
}

# Provider konfigürasyonu şablonu
generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "aws" {
  region = "${local.region}"

  default_tags {
    tags = {
      Environment = "${local.environment}"
      ManagedBy   = "Terragrunt"
    }
  }
}

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  required_version = ">= 1.5.0"
}
EOF
}

# Tüm child modüllere geçilecek ortak inputlar
inputs = merge(
  local.common_tags,
  {
    region     = local.region
    account_id = local.account_id
  }
)

Bu root konfigürasyon sayesinde her modül kendi backend ve provider dosyasını manuel oluşturmak zorunda kalmıyor.

Ortam Bazlı Yapılandırma

Dev ortamı için bir örnek terragrunt.hcl:

# infrastructure/environments/dev/terragrunt.hcl

locals {
  environment = "dev"
}

# Root konfigürasyonu dahil et
include "root" {
  path   = find_in_parent_folders()
  expose = true
}

inputs = {
  environment = local.environment

  # Dev ortamında küçük instance tipleri kullanalım
  instance_type     = "t3.small"
  min_capacity      = 1
  max_capacity      = 3
  deletion_protection = false
}
# infrastructure/environments/dev/vpc/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}

include "env" {
  path = find_in_parent_folders("terragrunt.hcl")
}

# Hangi Terraform modülünü kullanacağımızı belirtiyoruz
terraform {
  source = "../../../modules/vpc"
}

inputs = {
  vpc_cidr           = "10.0.0.0/16"
  availability_zones = ["eu-west-1a", "eu-west-1b"]
  private_subnets    = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets     = ["10.0.101.0/24", "10.0.102.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = true  # Dev'de tek NAT gateway yeterli, maliyet avantajı
}

Production için aynı modül farklı değerlerle:

# infrastructure/environments/prod/vpc/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}

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

inputs = {
  vpc_cidr           = "10.2.0.0/16"
  availability_zones = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
  private_subnets    = ["10.2.1.0/24", "10.2.2.0/24", "10.2.3.0/24"]
  public_subnets     = ["10.2.101.0/24", "10.2.102.0/24", "10.2.103.0/24"]

  enable_nat_gateway     = true
  single_nat_gateway     = false  # Prod'da her AZ için NAT gateway
  one_nat_gateway_per_az = true
}

Gördüğünüz gibi aynı modülü farklı parametrelerle çağırıyoruz, herhangi bir kod tekrarı yok.

Bağımlılık Yönetimi

Terragrunt’ın en güçlü özelliklerinden biri bağımlılık yönetimi. Örneğin EKS cluster’ı VPC’ye bağımlı, RDS de hem VPC’ye hem de EKS’e bağımlı olsun:

# infrastructure/environments/dev/eks/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}

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

# VPC bağımlılığı tanımlıyoruz
dependency "vpc" {
  config_path = "../vpc"

  # Plan aşamasında mock değerler kullan (VPC henüz oluşturulmamışsa)
  mock_outputs = {
    vpc_id          = "vpc-mock-12345"
    private_subnets = ["subnet-mock-1", "subnet-mock-2"]
  }
  mock_outputs_allowed_terraform_commands = ["validate", "plan"]
}

inputs = {
  cluster_name    = "myapp-dev"
  cluster_version = "1.28"

  # VPC output'larını otomatik olarak alıyoruz
  vpc_id          = dependency.vpc.outputs.vpc_id
  subnet_ids      = dependency.vpc.outputs.private_subnets

  node_groups = {
    general = {
      instance_types = ["t3.medium"]
      min_size       = 1
      max_size       = 3
      desired_size   = 2
    }
  }
}

Bu yapıyla terragrunt apply çalıştırdığınızda Terragrunt önce VPC’nin output’larını alıp EKS modülüne besliyor. Manuel terraform output ve kopyala-yapıştır dönemini kapatıyorsunuz.

run-all ile Toplu İşlemler

Tek bir komutla tüm ortamı yönetmek için run-all kullanıyorsunuz:

# Dev ortamındaki tüm modülleri plan et
cd infrastructure/environments/dev
terragrunt run-all plan

# Tüm modülleri apply et (bağımlılık sırasına göre)
terragrunt run-all apply

# Sadece belirli dizinleri dahil et
terragrunt run-all apply --terragrunt-include-dir "vpc" --terragrunt-include-dir "eks"

# Belirli dizinleri hariç tut
terragrunt run-all plan --terragrunt-exclude-dir "rds"

# Paralel işlem sayısını sınırla
terragrunt run-all apply --terragrunt-parallelism 2

run-all bağımlılıkları analiz ediyor ve doğru sırayla apply yapıyor. Bağımsız modülleri paralel çalıştırıyor, bu da büyük altyapılarda ciddi zaman kazandırıyor.

Hooks Kullanımı

Before/after hook’ları güçlü bir özellik. Örneğin apply öncesi bir script çalıştırabilir veya sonrasında bildirim gönderebilirsiniz:

# Helm chart'larını Terraform apply sonrası güncelleyen hook örneği
terraform {
  source = "../../../modules/eks"

  before_hook "validate_kubeconfig" {
    commands = ["apply"]
    execute  = ["bash", "-c", "aws eks update-kubeconfig --name myapp-dev --region eu-west-1"]
  }

  after_hook "update_helm_repos" {
    commands     = ["apply"]
    execute      = ["helm", "repo", "update"]
    run_on_error = false
  }

  after_hook "notify_slack" {
    commands = ["apply"]
    execute  = [
      "bash", "-c",
      "curl -X POST -H 'Content-type: application/json' --data '{"text":"Dev EKS apply tamamlandi!"}' $SLACK_WEBHOOK_URL"
    ]
    run_on_error = false
  }

  # Error durumunda çalışacak hook
  error_hook "alert_on_failure" {
    commands  = ["apply"]
    execute   = ["bash", "-c", "echo 'Apply basarisiz oldu!' | mail -s 'Terragrunt Alert' [email protected]"]
    on_errors = [".*"]
  }
}

CI/CD Pipeline Entegrasyonu

Gerçek dünya senaryosunda Terragrunt’ı CI/CD pipeline’ınıza entegre etmeniz gerekiyor. GitHub Actions örneği:

# .github/workflows/terragrunt.yml
name: Terragrunt CI/CD

on:
  push:
    branches: [main]
    paths:
      - 'infrastructure/**'
  pull_request:
    branches: [main]
    paths:
      - 'infrastructure/**'

env:
  TF_VERSION: "1.6.0"
  TG_VERSION: "0.54.0"
  AWS_REGION: "eu-west-1"

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      changed-envs: ${{ steps.detect.outputs.environments }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - name: Degisen ortamlari tespit et
        id: detect
        run: |
          CHANGED=$(git diff --name-only HEAD~1 HEAD | grep '^infrastructure/environments/' | cut -d'/' -f3 | sort -u | tr 'n' ',')
          echo "environments=$CHANGED" >> $GITHUB_OUTPUT

  plan:
    needs: detect-changes
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [dev, staging]
    steps:
      - uses: actions/checkout@v4

      - name: Terraform kur
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Terragrunt kur
        run: |
          wget -q https://github.com/gruntwork-io/terragrunt/releases/download/v${{ env.TG_VERSION }}/terragrunt_linux_amd64
          chmod +x terragrunt_linux_amd64
          sudo mv terragrunt_linux_amd64 /usr/local/bin/terragrunt

      - name: AWS kimlik dogrulama
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets[format('AWS_ROLE_{0}', matrix.environment)] }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Terragrunt Plan
        working-directory: infrastructure/environments/${{ matrix.environment }}
        run: |
          terragrunt run-all plan 
            --terragrunt-non-interactive 
            -out=tfplan

  apply:
    needs: plan
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production  # GitHub environment onayı
    steps:
      - uses: actions/checkout@v4

      - name: Terragrunt Apply (Prod)
        working-directory: infrastructure/environments/prod
        run: |
          terragrunt run-all apply 
            --terragrunt-non-interactive 
            -auto-approve
        env:
          AWS_DEFAULT_REGION: ${{ env.AWS_REGION }}

Ortak Sorunlar ve Çözümleri

Terragrunt kullanırken karşılaşacağınız tipik durumlar:

State lock problemi: Uzun süren bir apply yarıda kesilirse DynamoDB lock’u kalmış olabilir.

# Lock ID'yi öğren
terragrunt force-unlock LOCK_ID

# Ya da doğrudan DynamoDB'den sil
aws dynamodb delete-item 
  --table-name terraform-state-lock 
  --key '{"LockID": {"S": "mycompany-terraform-state/environments/dev/vpc/terraform.tfstate-md5"}}'

Cache temizleme: Modül kaynak kodunda değişiklik yaptığınızda cache’i temizlemeniz gerekebilir.

# Terragrunt cache'i temizle
find . -type d -name ".terragrunt-cache" -exec rm -rf {} +

# Terraform plugin cache'i koru ama Terragrunt cache'i temizle
terragrunt run-all init --terragrunt-no-auto-init

Modül versiyonlama: Üretimde modül versiyonlarını pin’lemek kritik.

# Versiyonlu modül referansı
terraform {
  source = "git::https://github.com/mycompany/terraform-modules.git//modules/vpc?ref=v2.1.0"
}

# Ya da local path için (mono-repo yaklaşımı)
terraform {
  source = "${get_repo_root()}/modules/vpc"
}

Terragrunt Catalog ve Scaffold

Terragrunt 0.52 ile gelen yeni özelliklerden biri catalog ve scaffold komutları. Yeni bir modül eklemeniz gerektiğinde şablondan başlayabiliyorsunuz:

# Mevcut catalog'u görüntüle
terragrunt catalog

# Yeni modül için scaffolding oluştur
terragrunt scaffold github.com/gruntwork-io/terragrunt-infrastructure-modules//modules/vpc

# Bu komut otomatik olarak terragrunt.hcl dosyası oluşturuyor
# ve modülün gerekli input'larını doldurmanız için şablon hazırlıyor

Pratik İpuçları

Uzun vadeli kullanımda öğrendiğim bazı önemli noktalar:

  • get_env() fonksiyonunu kullanın: Secrets’ları konfigürasyon dosyasına gömmeyin, environment variable’lardan çekin
  • locals bloğunu verimli kullanın: Hesaplanmış değerleri locals’da tanımlayın, input bloğunu temiz tutun
  • Modül kaynaklarını tag’leyin: source = "git::...?ref=v1.0.0" şeklinde her zaman belirli bir versiyon pin’leyin
  • validate hook’u ekleyin: Her plan öncesi tfsec veya checkov çalıştırın
  • --terragrunt-log-level debug: Sorun giderirken debug log’larını açın, neler olduğunu netçe görürsünüz
  • Mock output’ları gerçekçi tutun: mock_outputs değerleri gerçek formata benzemelidir, aksi halde plan aşamasında yanıltıcı sonuçlar alırsınız
  • Paralel apply sayısına dikkat edin: AWS API rate limit’lerine takılmamak için --terragrunt-parallelism 4 ile sınır koyun

Sonuç

Terragrunt, büyüyen Terraform altyapılarında kaçınılmaz hale geliyor. Başlangıçta “sadece bir wrapper” gibi görünüyor ama DRY prensibini gerçekten uygulayabilmek, remote state yönetimini otomatize etmek ve bağımlılıkları düzgünce yönetmek için sağlam bir çözüm sunuyor.

Küçük bir altyapınız varsa ve tek ortamla çalışıyorsanız belki gereksiz bir karmaşıklık ekliyor. Ama birden fazla ortam, onlarca modül ve ekip bazlı çalışma söz konusuysa Terragrunt’ı olmadan nasıl çalıştığınızı bir süre sonra kendinize soracaksınız.

Geçiş yaparken tüm altyapıyı bir anda dönüştürmeye çalışmayın. Yeni modülleri Terragrunt ile yazın, eskilerini yavaş yavaş taşıyın. Root terragrunt.hcl yapısını iyi tasarlarsanız ileride pişman olmazsınız, bu dosya tüm altyapınızın temeli haline geliyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir