현대 클라우드 환경에서 인프라를 코드로 관리하는 것은 필수가 되었습니다. Terraform은 이러한 흐름을 선도하는 도구이지만, 복잡한 멀티 환경 구성에서는 여러 한계에 부딪히게 됩니다. 이 글에서는 Terraform에서 발생하는 코드 중복 문제를 해결하기 위해 Terragrunt를 도입하게 된 과정과 그 성과에 대해 자세히 다루겠습니다.
Terraform을 여러 환경(개발, 테스트, 스테이징, 운영)에서 사용하다 보니 다음과 같은 명확한 한계점이 드러났습니다:
각 환경별로 거의 동일한 코드를 복사-붙여넣기 하는 상황이 빈번했습니다. 예를 들어:
# 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
}
}
이러한 구조에서는 환경마다 달라지는 값이 몇 개 없음에도 불구하고 전체 코드를 반복해야 했습니다.
모든 환경에서 원격 상태 저장을 위한 백엔드 구성이 반복되었습니다. 백엔드 설정 변경 시 모든 환경의 코드를 수정해야 했습니다.
환경별 변수를 관리하는 것이 복잡했습니다. 특히 변수 파일이 늘어날수록 관리가 어려웠습니다:
project/
├── dev/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvars
├── stage/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvars
└── prod/
├── main.tf
├── variables.tf
└── terraform.tfvars
모듈 간 종속성을 관리하기 위해 복잡한 출력 변수 참조가 필요했으며, 이로 인해 코드가 더 복잡해졌습니다.
환경마다 동일한 코드를 유지해야 하는데, 한 환경에서 코드를 개선했을 때 다른 환경에도 똑같이 적용해야 하는 번거로움이 있었습니다.
Terragrunt는 Terraform의 얇은 래퍼(wrapper)로, Terraform의 기능을 확장하여 이러한 문제를 해결합니다. 구체적인 기능을 살펴보겠습니다:
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"
}
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
}
}
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
}
이는 모듈 간 명확한 종속성을 정의하고, 출력 변수를 쉽게 참조할 수 있게 합니다.
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
}
}
여러 레벨의 입력 변수를 결합할 수 있어 코드 중복을 최소화합니다:
# 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"
}
Terragrunt가 어떻게 DRY 원칙을 실현하는지 자세히 살펴보겠습니다:
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"
}
이를 통해 환경별, 서비스별 설정을 계층적으로 관리할 수 있습니다.
백엔드 구성을 동적으로 생성하여 모든 모듈에서 백엔드 설정 코드를 제거할 수 있습니다:
# 루트 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
파일을 생성합니다. 이로써 백엔드 설정을 한 번만 정의하고 모든 모듈에서 사용할 수 있습니다.
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"]
}
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 도입 후 다음과 같은 구체적인 변화가 있었습니다:
기존 Terraform 코드는 환경당 약 500줄 정도였으나, Terragrunt 도입 후 환경별 설정은 50줄 이하로 줄였습니다. 전체적으로는 약 70%의 코드 감소 효과가 있었습니다.
여러 모듈이 의존성을 가진 경우, terragrunt run-all apply
를 통해 의존성 순서를 자동으로 계산하고 병렬로 배포할 수 있어 배포 시간이 약 40% 단축되었습니다.
환경 간 설정 불일치로 인한 오류가 85% 이상 감소했습니다. 한 환경에서 검증된 코드는 다른 환경에서도 동일하게 작동했습니다.
새로운 환경(예: QA, Sandbox)을 추가할 때 필요한 시간이 기존 2-3일에서 1시간 이내로 단축되었습니다.
모든 환경의 백엔드 설정을 한 번에 변경할 수 있어, 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
이러한 구조에서 각 컴포넌트별 설정은 다음과 같이 구성됩니다:
# 원격 상태 관리
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
}
locals {
aws_account_id = "123456789012" # AWS 계정 ID
aws_region = "ap-northeast-2" # 기본 AWS 리전
}
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
}
}
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
}
}
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"
}
}
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"
}
}
}
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는 Terraform 모듈을 효율적으로 참조할 수 있습니다:
terraform {
source = "git::https://github.com/org/terraform-modules.git//vpc?ref=v1.2.3"
}
의존성 구조를 시각화할 수 있습니다:
terragrunt graph-dependencies | dot -Tpng > dependencies.png
여러 모듈을 병렬로 배포하여 시간을 단축할 수 있습니다:
terragrunt run-all apply --terragrunt-parallelism 10
태그 또는 패턴을 사용하여 특정 모듈만 실행할 수 있습니다:
terragrunt run-all apply --terragrunt-include-dir "**/vpc"
Terragrunt는 단순한 기술적 개선을 넘어 다음과 같은 비즈니스 가치를 제공합니다:
Terraform을 사용하면서 코드 중복, 모듈 관리, 환경별 구성의 어려움을 겪고 계신다면, Terragrunt는 이러한 문제를 해결할 수 있는 강력한 도구입니다. DRY 원칙을 기반으로 인프라 코드의 품질과 유지보수성을 크게 향상시켜 보다 안정적이고 효율적인 인프라 관리가 가능해집니다.