Terraform에서 Terragrunt로: DRY 원칙을 통한 인프라 코드 혁신

문한성·2025년 5월 19일
0

들어가며

현대 클라우드 환경에서 인프라를 코드로 관리하는 것은 필수가 되었습니다. Terraform은 이러한 흐름을 선도하는 도구이지만, 복잡한 멀티 환경 구성에서는 여러 한계에 부딪히게 됩니다. 이 글에서는 Terraform에서 발생하는 코드 중복 문제를 해결하기 위해 Terragrunt를 도입하게 된 과정과 그 성과에 대해 자세히 다루겠습니다.

Terraform 사용 시 직면했던 구체적인 문제점

Terraform을 여러 환경(개발, 테스트, 스테이징, 운영)에서 사용하다 보니 다음과 같은 명확한 한계점이 드러났습니다:

1. 과도한 코드 중복

각 환경별로 거의 동일한 코드를 복사-붙여넣기 하는 상황이 빈번했습니다. 예를 들어:

# dev/main.tf
provider "aws" {
  region = "ap-northeast-2"
}

module "vpc" {
  source = "../modules/vpc"
  vpc_cidr = "10.0.0.0/16"
  environment = "dev"
  subnet_count = 2
}

module "ec2" {
  source = "../modules/ec2"
  instance_type = "t2.micro"
  environment = "dev"
  vpc_id = module.vpc.vpc_id
}

terraform {
  backend "s3" {
    bucket = "terraform-state"
    key    = "dev/terraform.tfstate"
    region = "ap-northeast-2"
    dynamodb_table = "terraform-locks"
    encrypt = true
  }
}

# prod/main.tf (거의 동일한 코드)
provider "aws" {
  region = "ap-northeast-2"
}

module "vpc" {
  source = "../modules/vpc"
  vpc_cidr = "10.1.0.0/16"  // 차이점
  environment = "prod"      // 차이점
  subnet_count = 3         // 차이점
}

module "ec2" {
  source = "../modules/ec2"
  instance_type = "t2.large"  // 차이점
  environment = "prod"        // 차이점
  vpc_id = module.vpc.vpc_id
}

terraform {
  backend "s3" {
    bucket = "terraform-state"
    key    = "prod/terraform.tfstate"  // 차이점
    region = "ap-northeast-2"
    dynamodb_table = "terraform-locks"
    encrypt = true
  }
}

이러한 구조에서는 환경마다 달라지는 값이 몇 개 없음에도 불구하고 전체 코드를 반복해야 했습니다.

2. 백엔드 구성의 반복

모든 환경에서 원격 상태 저장을 위한 백엔드 구성이 반복되었습니다. 백엔드 설정 변경 시 모든 환경의 코드를 수정해야 했습니다.

3. 변수 관리의 복잡성

환경별 변수를 관리하는 것이 복잡했습니다. 특히 변수 파일이 늘어날수록 관리가 어려웠습니다:

project/
├── dev/
│   ├── main.tf
│   ├── variables.tf
│   └── terraform.tfvars
├── stage/
│   ├── main.tf
│   ├── variables.tf
│   └── terraform.tfvars
└── prod/
    ├── main.tf
    ├── variables.tf
    └── terraform.tfvars

4. 모듈 간 종속성 관리의 어려움

모듈 간 종속성을 관리하기 위해 복잡한 출력 변수 참조가 필요했으며, 이로 인해 코드가 더 복잡해졌습니다.

5. 일관성 유지의 어려움

환경마다 동일한 코드를 유지해야 하는데, 한 환경에서 코드를 개선했을 때 다른 환경에도 똑같이 적용해야 하는 번거로움이 있었습니다.

Terragrunt란? 상세한 이해

Terragrunt는 Terraform의 얇은 래퍼(wrapper)로, Terraform의 기능을 확장하여 이러한 문제를 해결합니다. 구체적인 기능을 살펴보겠습니다:

1. 구성 파일의 계층화

Terragrunt는 terragrunt.hcl 파일을 통해 구성을 계층화합니다. 이를 통해 구성을 상속하고 재사용할 수 있습니다.

# 루트 terragrunt.hcl
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }
  config = {
    bucket         = "terraform-state-${get_aws_account_id()}"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

# 환경 공통 설정
generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite"
  contents  = <<EOF
provider "aws" {
  region = "ap-northeast-2"
  default_tags {
    tags = {
      Environment = "${local.environment}"
      Terraform   = "true"
    }
  }
}
EOF
}

# 로컬 변수 설정 (각 환경의 terragrunt.hcl에서 오버라이드)
locals {
  environment = "default"
}

2. 자동 원격 상태 관리

Terragrunt는 각 모듈의 원격 상태를 자동으로 구성합니다. 이를 통해 상태 파일 경로를 동적으로 생성하여 중복 코드를 줄입니다.

remote_state {
  backend = "s3"
  config = {
    bucket = "terraform-state"
    key    = "${path_relative_to_include()}/terraform.tfstate"
    region = "ap-northeast-2"
    dynamodb_table = "terraform-locks"
    encrypt = true
  }
}

3. 의존성 블록을 통한 명시적인 종속성 관리

Terragrunt는 dependency 블록을 통해 모듈 간 종속성을 명시적으로 정의할 수 있습니다:

# app/terragrunt.hcl
dependency "vpc" {
  config_path = "../vpc"
  
  # 의존성 모듈의 출력을 참조할 때 건너뛸 수 있는 옵션
  mock_outputs = {
    vpc_id = "mock-vpc-id"
  }
  mock_outputs_allowed_terraform_commands = ["validate", "plan"]
}

inputs = {
  vpc_id = dependency.vpc.outputs.vpc_id
}

이는 모듈 간 명확한 종속성을 정의하고, 출력 변수를 쉽게 참조할 수 있게 합니다.

4. before_hook과 after_hook을 통한 실행 맥락 확장

Terragrunt는 Terraform 명령 전후에 추가 작업을 실행할 수 있는 훅 기능을 제공합니다:

terraform {
  before_hook "before_hook" {
    commands     = ["apply", "plan"]
    execute      = ["echo", "Running Terraform"]
  }

  after_hook "after_hook" {
    commands     = ["apply"]
    execute      = ["echo", "Terraform apply completed"]
    run_on_error = true
  }
}

5. 입력 변수의 계층화

여러 레벨의 입력 변수를 결합할 수 있어 코드 중복을 최소화합니다:

# prod/region/service/terragrunt.hcl
include {
  path = find_in_parent_folders()
}

include "region_vars" {
  path = find_in_parent_folders("region.hcl")
}

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

inputs = {
  service_specific_var = "value"
}

DRY 원칙을 준수할 수 있었던 심층적 이유

Terragrunt가 어떻게 DRY 원칙을 실현하는지 자세히 살펴보겠습니다:

1. 계층적 구성 상속

Terragrunt의 핵심 기능인 구성 상속은 공통 설정을 한 번만 정의하고 모든 환경에서 재사용할 수 있게 합니다:

# terragrunt/terragrunt.hcl (루트)
remote_state {
  backend = "s3"
  config = {
    bucket = "terraform-state"
    region = "ap-northeast-2"
    encrypt = true
    dynamodb_table = "terraform-locks"
    key = "${path_relative_to_include()}/terraform.tfstate"
  }
}

# 공통 변수 정의
inputs = {
  aws_region = "ap-northeast-2"
  tags = {
    ManagedBy = "Terraform"
  }
}

# env-common.hcl (환경별 공통 설정)
locals {
  common_tags = {
    Owner = "DevOps-Team"
  }
}

# 개발 환경별 설정
inputs = merge(
  local.common_tags,
  {
    environment = "dev"
  }
)

# terragrunt/dev/vpc/terragrunt.hcl
include {
  path = find_in_parent_folders()
}

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

inputs = {
  vpc_cidr = "10.0.0.0/16"
}

이를 통해 환경별, 서비스별 설정을 계층적으로 관리할 수 있습니다.

2. 동적 백엔드 구성 생성

백엔드 구성을 동적으로 생성하여 모든 모듈에서 백엔드 설정 코드를 제거할 수 있습니다:

# 루트 terragrunt.hcl
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }
  config = {
    bucket         = "terraform-state-${get_aws_account_id()}"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = local.aws_region
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

locals {
  aws_region = "ap-northeast-2"
}

이 설정은 각 모듈 디렉토리에 Terraform 실행 시 자동으로 backend.tf 파일을 생성합니다. 이로써 백엔드 설정을 한 번만 정의하고 모든 모듈에서 사용할 수 있습니다.

3. 변수 주입 메커니즘

Terragrunt의 inputs 블록은 변수를 Terraform 모듈에 자동으로 주입합니다. 이를 통해 환경별 변수를 효율적으로 관리할 수 있습니다:

# dev/vpc/terragrunt.hcl
include {
  path = find_in_parent_folders()
}

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

inputs = {
  vpc_cidr        = "10.0.0.0/16"
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.10.0/24", "10.0.20.0/24"]
}

# prod/vpc/terragrunt.hcl
include {
  path = find_in_parent_folders()
}

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

inputs = {
  vpc_cidr        = "10.1.0.0/16"
  public_subnets  = ["10.1.1.0/24", "10.1.2.0/24", "10.1.3.0/24"]
  private_subnets = ["10.1.10.0/24", "10.1.20.0/24", "10.1.30.0/24"]
}

4. 함수와 헬퍼를 통한 동적 구성

Terragrunt는 다양한 내장 함수를 제공하여 구성을 동적으로 생성할 수 있습니다:

locals {
  account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl"))
  region_vars  = read_terragrunt_config(find_in_parent_folders("region.hcl"))
  environment_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
  
  account_id   = local.account_vars.locals.aws_account_id
  aws_region   = local.region_vars.locals.aws_region
  environment  = local.environment_vars.locals.environment
}

inputs = {
  tags = {
    Account     = local.account_id
    Region      = local.aws_region
    Environment = local.environment
  }
}

도입 후 변화: 정량적/정성적 효과

Terragrunt 도입 후 다음과 같은 구체적인 변화가 있었습니다:

1. 코드 양의 극적인 감소

기존 Terraform 코드는 환경당 약 500줄 정도였으나, Terragrunt 도입 후 환경별 설정은 50줄 이하로 줄였습니다. 전체적으로는 약 70%의 코드 감소 효과가 있었습니다.

2. 배포 시간 단축

여러 모듈이 의존성을 가진 경우, terragrunt run-all apply를 통해 의존성 순서를 자동으로 계산하고 병렬로 배포할 수 있어 배포 시간이 약 40% 단축되었습니다.

3. 오류 감소율

환경 간 설정 불일치로 인한 오류가 85% 이상 감소했습니다. 한 환경에서 검증된 코드는 다른 환경에서도 동일하게 작동했습니다.

4. 신규 환경 구성 시간 단축

새로운 환경(예: QA, Sandbox)을 추가할 때 필요한 시간이 기존 2-3일에서 1시간 이내로 단축되었습니다.

5. 백엔드 관리 간소화

모든 환경의 백엔드 설정을 한 번에 변경할 수 있어, AWS 계정 변경이나 리전 이전 시 유연성이 크게 향상되었습니다.

실제 적용 사례: 상세 구현

실제 프로젝트에서는 다음과 같은 구조로 Terragrunt를 적용했습니다:

terragrunt/
├── terragrunt.hcl            # 루트 설정 (백엔드, 프로바이더 등)
├── account.hcl               # AWS 계정 정보
├── env-common.hcl            # 환경 공통 설정
├── modules/                  # 공통 Terraform 모듈
│   ├── vpc/
│   ├── eks/
│   ├── rds/
│   └── elasticache/
├── dev/                      # 개발 환경
│   ├── env.hcl               # 개발 환경 전역 변수
│   ├── vpc/
│   │   └── terragrunt.hcl
│   ├── eks/
│   │   └── terragrunt.hcl
│   └── database/
│       └── terragrunt.hcl
└── prod/                     # 운영 환경
    ├── env.hcl               # 운영 환경 전역 변수
    ├── vpc/
    │   └── terragrunt.hcl
    ├── eks/
    │   └── terragrunt.hcl
    └── database/
        └── terragrunt.hcl

이러한 구조에서 각 컴포넌트별 설정은 다음과 같이 구성됩니다:

루트 terragrunt.hcl

# 원격 상태 관리
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }
  config = {
    bucket         = "terraform-state-${local.account_id}"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = local.aws_region
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

# AWS 공급자 구성 자동 생성
generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite"
  contents  = <<EOF
provider "aws" {
  region = "${local.aws_region}"
  default_tags {
    tags = {
      Environment = "${local.environment}"
      ManagedBy   = "Terraform"
      Workspace   = "${terraform.workspace}"
    }
  }
}
EOF
}

# 로컬 변수 설정
locals {
  # 하위 HCL 파일에서 로드
  account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl"))
  environment_vars = try(
    read_terragrunt_config(find_in_parent_folders("env.hcl")),
    { locals = { environment = "default" } }
  )
  
  account_id   = local.account_vars.locals.aws_account_id
  aws_region   = local.account_vars.locals.aws_region
  environment  = local.environment_vars.locals.environment
}

# 공통 입력 변수
inputs = {
  aws_region  = local.aws_region
  account_id  = local.account_id
  environment = local.environment
}

계정 설정: account.hcl

locals {
  aws_account_id = "123456789012"  # AWS 계정 ID
  aws_region     = "ap-northeast-2"  # 기본 AWS 리전
}

환경별 설정: dev/env.hcl

locals {
  environment = "dev"
  
  # 개발 환경 특화 설정
  domain_name = "dev.example.com"
  
  # 인프라 사이징
  instance_types = {
    bastion = "t3.micro"
    app     = "t3.small"
  }
  
  rds_config = {
    instance_class    = "db.t3.medium"
    allocated_storage = 20
    multi_az          = false
  }
}

환경별 설정: prod/env.hcl

locals {
  environment = "prod"
  
  # 운영 환경 특화 설정
  domain_name = "example.com"
  
  # 인프라 사이징
  instance_types = {
    bastion = "t3.small"
    app     = "m5.large"
  }
  
  rds_config = {
    instance_class    = "db.m5.large"
    allocated_storage = 100
    multi_az          = true
  }
}

컴포넌트 설정: dev/vpc/terragrunt.hcl

include {
  path = find_in_parent_folders()
}

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

inputs = {
  vpc_name       = "dev-vpc"
  vpc_cidr       = "10.0.0.0/16"
  azs            = ["ap-northeast-2a", "ap-northeast-2c"]
  public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.10.0/24", "10.0.20.0/24"]
  
  enable_nat_gateway = true
  single_nat_gateway = true  # 개발 환경에서는 비용 절감을 위해 단일 NAT 게이트웨이 사용
  
  tags = {
    Terraform   = "true"
    Environment = "dev"
  }
}

컴포넌트 설정: dev/eks/terragrunt.hcl

include {
  path = find_in_parent_folders()
}

# VPC 모듈에 대한 종속성 정의
dependency "vpc" {
  config_path = "../vpc"
  
  # 의존성 모듈이 아직 배포되지 않았을 때 모의 출력을 사용 (계획/검증용)
  mock_outputs = {
    vpc_id = "mock-vpc-id"
    private_subnets = ["mock-subnet-1", "mock-subnet-2"]
  }
  mock_outputs_allowed_terraform_commands = ["validate", "plan"]
}

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

inputs = {
  cluster_name = "dev-eks"
  vpc_id       = dependency.vpc.outputs.vpc_id
  subnet_ids   = dependency.vpc.outputs.private_subnets
  
  cluster_version = "1.24"
  
  # 노드 그룹 설정
  node_groups = {
    main = {
      desired_capacity = 2
      min_capacity     = 1
      max_capacity     = 3
      instance_types   = ["t3.medium"]
      disk_size        = 50
    }
  }
  
  # 관리형 노드 그룹 설정
  managed_node_groups = {
    system = {
      name           = "system"
      instance_types = ["t3.medium"]
      min_size       = 1
      max_size       = 3
      desired_size   = 1
      capacity_type  = "ON_DEMAND"
    }
  }
}

CI/CD 파이프라인과의 통합

Terragrunt는 GitHub Actions와 같은 CI/CD 시스템과 쉽게 통합됩니다:

# GitHub Actions 워크플로우 파일
name: 'Terraform Infrastructure Deployment'

on:
  push:
    branches:
      - main
    paths:
      - 'terragrunt/**'
  pull_request:
    branches:
      - main
    paths:
      - 'terragrunt/**'

jobs:
  terraform:
    name: 'Terraform'
    runs-on: ubuntu-latest
    
    steps:
      - name: 코드 체크아웃
        uses: actions/checkout@v3

      - name: AWS 자격 증명 설정
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-2

      - name: Terraform 설치
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: '1.4.0'

      - name: Terragrunt 설치
        run: |
          curl -L -o /tmp/terragrunt https://github.com/gruntwork-io/terragrunt/releases/download/v0.45.0/terragrunt_linux_amd64
          chmod +x /tmp/terragrunt
          sudo mv /tmp/terragrunt /usr/local/bin/terragrunt

      - name: Terragrunt 캐시 초기화
        run: |
          find terragrunt -type d -name ".terragrunt-cache" -exec rm -rf {} +
          find terragrunt -type d -name ".terraform" -exec rm -rf {} +

      - name: Terragrunt 초기화 및 계획
        id: plan
        run: |
          cd terragrunt/dev
          terragrunt run-all init
          terragrunt run-all plan -out=tfplan.binary

      - name: 변경사항 요약
        run: |
          cd terragrunt/dev
          terragrunt run-all show tfplan.binary | grep -A 20 "Plan:"

      - name: Terragrunt 적용
        if: github.event_name == 'push'
        run: |
          cd terragrunt/dev
          terragrunt run-all apply -auto-approve

고급 Terragrunt 기능 활용

1. 원격 모듈 참조 최적화

Terragrunt는 Terraform 모듈을 효율적으로 참조할 수 있습니다:

terraform {
  source = "git::https://github.com/org/terraform-modules.git//vpc?ref=v1.2.3"
}

2. 의존성 다이어그램 생성

의존성 구조를 시각화할 수 있습니다:

terragrunt graph-dependencies | dot -Tpng > dependencies.png

3. 병렬 실행을 통한 배포 속도 향상

여러 모듈을 병렬로 배포하여 시간을 단축할 수 있습니다:

terragrunt run-all apply --terragrunt-parallelism 10

4. 특정 모듈만 선택적 실행

태그 또는 패턴을 사용하여 특정 모듈만 실행할 수 있습니다:

terragrunt run-all apply --terragrunt-include-dir "**/vpc"

결론: Terragrunt의 비즈니스 가치

Terragrunt는 단순한 기술적 개선을 넘어 다음과 같은 비즈니스 가치를 제공합니다:

  1. 운영 효율성: 코드 중복 감소와 자동화를 통해 인프라 관리 효율성이 크게 향상됩니다.
  2. 위험 감소: 환경 간 일관성 향상으로 운영 실수와 위험이 감소합니다.
  3. 신속한 환경 프로비저닝: 새로운 환경 구축 시간이 대폭 단축되어 비즈니스 요구에 빠르게 대응할 수 있습니다.
  4. 비용 최적화: 환경별 인프라 설정을 보다 세밀하게 조정하여 비용을 최적화할 수 있습니다.
  5. 개발자 경험 향상: 간결하고 일관된 코드 작성 방식으로 개발자 만족도와 생산성이 향상됩니다.

Terraform을 사용하면서 코드 중복, 모듈 관리, 환경별 구성의 어려움을 겪고 계신다면, Terragrunt는 이러한 문제를 해결할 수 있는 강력한 도구입니다. DRY 원칙을 기반으로 인프라 코드의 품질과 유지보수성을 크게 향상시켜 보다 안정적이고 효율적인 인프라 관리가 가능해집니다.

profile
기록하고 공유하려고 노력하는 DevOps 엔지니어

0개의 댓글