Terraform 환경 분리 전략

Terraform 환경 분리가 중요한 이유

인프라를 코드로 관리할 때 가장 먼저 부딪히는 문제는 dev/staging/production 환경 분리입니다. 같은 인프라 코드를 여러 환경에 배포하되, 각 환경의 설정(인스턴스 크기, 레플리카 수, 도메인 등)은 달라야 합니다. Terraform은 Workspace, tfvars 파일, 디렉터리 분리 등 다양한 전략을 제공하지만, 잘못 선택하면 운영 사고로 이어집니다.

이 글에서는 Workspace의 한계, 디렉터리 기반 환경 분리, Terragrunt DRY 패턴, State 격리 전략, CI/CD 파이프라인 통합까지 실무에서 검증된 패턴을 다룹니다.

Terraform Workspace: 기본과 한계

Workspace는 같은 코드로 여러 State를 관리하는 내장 기능입니다.

# Workspace 생성 및 전환
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
terraform workspace select dev
terraform workspace list

# 코드에서 workspace 참조
locals {
  env = terraform.workspace

  instance_type = {
    dev     = "t3.small"
    staging = "t3.medium"
    prod    = "m5.xlarge"
  }

  replica_count = {
    dev     = 1
    staging = 2
    prod    = 3
  }
}

resource "aws_instance" "app" {
  instance_type = local.instance_type[local.env]
  ami           = var.ami_id

  tags = {
    Name        = "app-${local.env}"
    Environment = local.env
  }
}

resource "aws_rds_cluster" "main" {
  cluster_identifier = "db-${local.env}"
  instance_count     = local.replica_count[local.env]
  # prod만 Multi-AZ
  availability_zones = local.env == "prod" ? ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"] : ["ap-northeast-2a"]
}

Workspace의 치명적 한계

문제 설명 위험도
State 동일 백엔드 모든 환경이 같은 S3 버킷에 저장, 접근 권한 분리 어려움 높음
workspace 착각 사고 prod에서 작업 중인지 인지하기 어려움 → 실수로 destroy 치명적
환경별 프로바이더 차이 dev는 AWS, prod는 GCP 같은 구조 불가 중간
모듈 버전 고정 어려움 prod는 안정 버전, dev는 최신 버전 적용 불가 중간

결론: Workspace는 소규모 프로젝트나 개인 개발 환경에만 적합합니다. 프로덕션급 환경 분리에는 디렉터리 기반 또는 Terragrunt를 권장합니다.

디렉터리 기반 환경 분리

가장 안전하고 명확한 패턴입니다. 환경별로 완전히 독립된 State와 백엔드를 가집니다.

infrastructure/
├── modules/                    # 공유 모듈
│   ├── vpc/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── ecs-service/
│   └── rds/
├── environments/
│   ├── dev/
│   │   ├── main.tf            # 모듈 호출
│   │   ├── backend.tf         # dev 전용 State 백엔드
│   │   ├── variables.tf
│   │   ├── terraform.tfvars   # dev 설정값
│   │   └── providers.tf       # dev AWS 계정
│   ├── staging/
│   │   ├── main.tf
│   │   ├── backend.tf
│   │   ├── terraform.tfvars
│   │   └── providers.tf
│   └── prod/
│       ├── main.tf
│       ├── backend.tf         # prod 전용 별도 계정
│       ├── terraform.tfvars
│       └── providers.tf
└── global/                    # 환경 공통 (IAM, DNS 등)
    ├── iam/
    └── route53/

환경별 백엔드 분리

# environments/dev/backend.tf
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-dev"
    key            = "infrastructure/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "terraform-lock-dev"
    encrypt        = true
    # dev AWS 계정의 S3
    role_arn       = "arn:aws:iam::111111111111:role/terraform-dev"
  }
}

# environments/prod/backend.tf
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-prod"
    key            = "infrastructure/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "terraform-lock-prod"
    encrypt        = true
    # prod 전용 AWS 계정 — 완전 격리
    role_arn       = "arn:aws:iam::999999999999:role/terraform-prod"
  }
}

모듈 호출 패턴

# environments/prod/main.tf
module "vpc" {
  source = "../../modules/vpc"

  environment    = "prod"
  vpc_cidr       = var.vpc_cidr
  azs            = var.availability_zones
  enable_nat     = true
  nat_gateway_count = 3  # prod는 AZ별 NAT
}

module "api_service" {
  source = "../../modules/ecs-service"

  environment    = "prod"
  vpc_id         = module.vpc.vpc_id
  subnet_ids     = module.vpc.private_subnet_ids
  desired_count  = var.api_desired_count
  cpu            = var.api_cpu
  memory         = var.api_memory
  # prod 전용: Auto Scaling 활성화
  enable_autoscaling = true
  min_capacity       = 3
  max_capacity       = 20
}

# environments/prod/terraform.tfvars
vpc_cidr            = "10.0.0.0/16"
availability_zones  = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
api_desired_count   = 3
api_cpu             = 1024
api_memory          = 2048

Terragrunt으로 DRY 환경 관리

디렉터리 기반의 단점은 환경 간 코드 중복입니다. Terragrunt는 이를 해결하는 래퍼 도구로, Terraform State 관리 심화에서 다뤘던 State 관리와 결합하면 강력한 환경 관리 체계를 구축할 수 있습니다.

# 디렉터리 구조
infrastructure/
├── modules/
│   ├── vpc/
│   └── ecs-service/
├── terragrunt.hcl              # 루트: 공통 설정
├── dev/
│   ├── env.hcl                 # dev 환경 변수
│   ├── vpc/
│   │   └── terragrunt.hcl
│   └── ecs-service/
│       └── terragrunt.hcl
├── staging/
│   ├── env.hcl
│   ├── vpc/
│   │   └── terragrunt.hcl
│   └── ecs-service/
│       └── terragrunt.hcl
└── prod/
    ├── env.hcl
    ├── vpc/
    │   └── terragrunt.hcl
    └── ecs-service/
        └── terragrunt.hcl
# 루트 terragrunt.hcl — 백엔드 자동 생성
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
  config = {
    bucket         = "mycompany-tf-${local.env}"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "tf-lock-${local.env}"
  }
}

locals {
  env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
  env      = local.env_vars.locals.environment
}

# 공통 프로바이더
generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "aws" {
  region = "ap-northeast-2"
  default_tags {
    tags = {
      Environment = "${local.env}"
      ManagedBy   = "terraform"
    }
  }
}
EOF
}
# prod/env.hcl
locals {
  environment = "prod"
  account_id  = "999999999999"
}

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

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

inputs = {
  environment       = "prod"
  vpc_cidr          = "10.0.0.0/16"
  availability_zones = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
  enable_nat        = true
  nat_gateway_count = 3
}

# prod/ecs-service/terragrunt.hcl
include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "../../modules/ecs-service"
}

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

inputs = {
  environment    = "prod"
  vpc_id         = dependency.vpc.outputs.vpc_id
  subnet_ids     = dependency.vpc.outputs.private_subnet_ids
  desired_count  = 3
  cpu            = 1024
  memory         = 2048
}

Terragrunt의 dependency 블록으로 모듈 간 의존 관계를 선언하고, terragrunt run-all apply로 전체 환경을 올바른 순서로 배포할 수 있습니다.

CI/CD 파이프라인 통합

# .github/workflows/terraform.yml
name: Terraform
on:
  push:
    branches: [main]
    paths: ['infrastructure/**']
  pull_request:
    paths: ['infrastructure/**']

jobs:
  plan:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [dev, staging, prod]
    environment: ${{ matrix.environment }}
    steps:
      - uses: actions/checkout@v4

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

      - name: Terraform Plan
        working-directory: infrastructure/environments/${{ matrix.environment }}
        run: |
          terraform init
          terraform plan -out=tfplan -detailed-exitcode
        continue-on-error: true

      - name: Comment Plan on PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const plan = '${{ steps.plan.outputs.stdout }}'
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              body: `### ${{ matrix.environment }} Plann```n${plan}n````
            })

  apply:
    needs: plan
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    strategy:
      max-parallel: 1  # 순차 배포: dev → staging → prod
      matrix:
        environment: [dev, staging, prod]
    environment:
      name: ${{ matrix.environment }}
      # prod는 수동 승인 필요 (GitHub Environment Protection Rules)
    steps:
      - uses: actions/checkout@v4

      - name: Terraform Apply
        working-directory: infrastructure/environments/${{ matrix.environment }}
        run: |
          terraform init
          terraform apply -auto-approve

환경 분리 안전장치

# 1. prevent_destroy로 프로덕션 리소스 보호
resource "aws_rds_cluster" "main" {
  lifecycle {
    prevent_destroy = true  # terraform destroy 차단
  }
}

# 2. 프로덕션 삭제 방지 변수
variable "enable_deletion_protection" {
  type    = bool
  default = true  # prod에서는 항상 true
}

resource "aws_rds_cluster" "main" {
  deletion_protection = var.enable_deletion_protection
}

# 3. precondition으로 환경 검증
resource "aws_instance" "expensive" {
  instance_type = var.instance_type

  lifecycle {
    precondition {
      condition     = !(var.environment == "dev" && can(regex("^(m5|c5|r5)\.(2x|4x|8x|12x|16x|24x)?large", var.instance_type)))
      error_message = "dev 환경에서 고비용 인스턴스 사용 금지"
    }
  }
}

# 4. Sentinel/OPA 정책 (Terraform Cloud)
# policy.sentinel
import "tfplan/v2" as tfplan

main = rule {
  all tfplan.resource_changes as _, rc {
    rc.type is "aws_instance" and
    rc.change.after.instance_type not in ["m5.24xlarge", "c5.24xlarge"]
  }
}

Ansible Playbook 자동화 심화와 결합하면, Terraform으로 인프라를 프로비저닝하고 Ansible로 설정 관리하는 완전한 IaC 파이프라인을 구축할 수 있습니다.

전략별 비교 정리

전략 State 격리 코드 중복 복잡도 권장 규모
Workspace 같은 백엔드 없음 낮음 개인/소규모
tfvars 분리 같은 백엔드 낮음 낮음 소규모 팀
디렉터리 분리 완전 격리 중간 중간 중규모 이상
Terragrunt 완전 격리 최소 높음 대규모/멀티계정

마무리

Terraform 환경 분리의 핵심은 State 격리실수 방지입니다. Workspace는 편리하지만 프로덕션에는 위험하고, 디렉터리 분리는 안전하지만 코드가 중복되며, Terragrunt는 DRY하지만 학습 곡선이 있습니다. 팀 규모와 인프라 복잡도에 맞는 전략을 선택하되, prevent_destroy와 CI/CD 승인 게이트는 어떤 전략이든 필수입니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux