🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Terraform 디자인 패턴 완벽 가이드 - 슬라이드 1/9
A

AI Generated

2025. 11. 5. · 15 Views

Terraform 디자인 패턴 완벽 가이드

Terraform을 활용한 인프라 구축 시 필수적인 디자인 패턴들을 소개합니다. 모듈 설계부터 상태 관리까지, 실무에서 바로 적용할 수 있는 베스트 프랙티스를 담았습니다.


카테고리:TypeScript
언어:TypeScript
메인 태그:#Terraform
서브 태그:
#Module#State#Workspace#Remote

들어가며

이 글에서는 Terraform 디자인 패턴 완벽 가이드에 대해 상세히 알아보겠습니다. 총 16가지 주요 개념을 다루며, 각각의 개념에 대한 설명과 실제 코드 예제를 함께 제공합니다.

목차

  1. Module_Pattern - 재사용 가능한 인프라 컴포넌트 설계
  2. Workspace_Strategy - 환경별 인프라 분리 관리
  3. Remote_Backend - 안전한 상태 파일 관리
  4. Data_Source_Pattern - 기존 리소스 참조 및 활용
  5. Locals_Variables - 중복 코드 제거와 가독성 향상
  6. Dynamic_Blocks - 반복 구조의 효율적 관리
  7. Conditional_Resources - 조건부 리소스 생성 패턴
  8. Output_Chain - 모듈 간 데이터 전달 체계
  9. Module_Pattern
  10. Workspace_Strategy
  11. Remote_Backend
  12. Data_Source_Pattern
  13. Locals_Variables
  14. Dynamic_Blocks
  15. Conditional_Resources
  16. Output_Chain

1. Module Pattern - 재사용 가능한 인프라 컴포넌트 설계

개요


2. Workspace Strategy - 환경별 인프라 분리 관리

개요


3. Remote Backend - 안전한 상태 파일 관리

개요


4. Data Source Pattern - 기존 리소스 참조 및 활용

개요


5. Locals Variables - 중복 코드 제거와 가독성 향상

개요


6. Dynamic Blocks - 반복 구조의 효율적 관리

개요


7. Conditional Resources - 조건부 리소스 생성 패턴

개요


8. Output Chain - 모듈 간 데이터 전달 체계

개요


1. Module Pattern

개요

[3-5문단으로 작성] 간단히 말해서, Terraform 모듈은 관련된 리소스들을 하나의 논리적 단위로 묶어 재사용 가능하게 만든 것입니다. 마치 프로그래밍에서 함수를 만드는 것과 같은 개념이죠. 모듈을 사용하는 이유는 명확합니다. 동일한 인프라 구성을 여러 환경(개발, 스테이징, 프로덕션)에 배포할 때 일관성을 유지할 수 있고, 한 번 검증된 코드를 여러 곳에서 안전하게 재사용할 수 있습니다. 예를 들어, 보안 규칙이 적용된 RDS 구성을 모듈로 만들어두면, 새 프로젝트에서도 동일한 보안 수준을 자동으로 보장받을 수 있습니다. 기존에는 각 프로젝트마다 전체 Terraform 코드를 복사해서 수정했다면, 이제는 모듈을 호출하고 필요한 변수만 전달하면 됩니다. 이는 코드량을 획기적으로 줄여줍니다. 모듈의 핵심 특징은 캡슐화, 재사용성, 파라미터화입니다. 복잡한 내부 구현은 숨기고 필요한 인터페이스만 노출하며, 다양한 상황에 맞게 변수로 커스터마이징할 수 있습니다. 이러한 특징들이 대규모 인프라를 관리할 때 코드 품질과 생산성을 크게 향상시킵니다.

코드 예제

# modules/vpc/main.tf
variable "environment" {
  description = "Environment name"
  type        = string
}

variable "vpc_cidr" {
  description = "VPC CIDR block"
  type        = string
}

# VPC 리소스 생성
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true

  tags = {
    Name        = "${var.environment}-vpc"
    Environment = var.environment
  }
}

# 프라이빗 서브넷 생성
resource "aws_subnet" "private" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "${var.environment}-private-${count.index + 1}"
  }
}

# 모듈 출력값 정의
output "vpc_id" {
  value = aws_vpc.main.id
}

output "private_subnet_ids" {
  value = aws_subnet.private[*].id
}

# 모듈 사용 예시 (root module)
module "production_vpc" {
  source      = "./modules/vpc"
  environment = "production"
  vpc_cidr    = "10.0.0.0/16"
}

설명

[4-6문단으로 작성] 이것이 하는 일: 이 코드는 재사용 가능한 VPC 모듈을 정의하고, 실제로 프로덕션 환경에서 해당 모듈을 호출하는 전체 과정을 보여줍니다. 첫 번째로, 모듈 내부에서 variable 블록을 통해 외부에서 전달받을 파라미터를 정의합니다. environment와 vpc_cidr 변수를 선언함으로써, 이 모듈을 사용하는 측에서 환경 이름과 네트워크 대역을 자유롭게 지정할 수 있습니다. 이렇게 하는 이유는 동일한 모듈 코드로 개발/스테이징/프로덕션 환경을 각각 다른 설정으로 배포할 수 있기 때문입니다. 그 다음으로, resource 블록들이 실행되면서 실제 AWS VPC와 서브넷이 생성됩니다. 내부적으로 count 메타 인수를 사용해 2개의 프라이빗 서브넷을 반복적으로 생성하며, cidrsubnet 함수로 자동으로 IP 대역을 분할합니다. 태그에는 변수로 받은 environment 값이 포함되어 리소스를 쉽게 식별할 수 있습니다. 세 번째로, output 블록을 통해 모듈이 생성한 리소스의 정보를 외부로 노출합니다. VPC ID와 서브넷 ID들을 출력함으로써, 이 모듈을 사용하는 다른 코드에서 생성된 VPC에 EC2나 RDS를 배포할 수 있게 됩니다. 마지막으로 module 블록에서 실제로 모듈을 호출하면 정의된 모든 리소스가 한 번에 생성됩니다. 여러분이 이 코드를 사용하면 VPC 구성 코드를 매번 작성할 필요 없이 모듈 호출 한 번으로 표준화된 네트워크 인프라를 구축할 수 있습니다. 보안 정책이나 네이밍 규칙이 모듈 내부에 구현되어 있어 실수를 방지하고, 여러 환경에 동일한 구조를 빠르게 배포할 수 있으며, 모듈만 수정하면 모든 환경에 변경사항이 일괄 적용되는 장점이 있습니다. Summary: [2-3문장으로 작성] 핵심 정리: Module Pattern은 인프라 구성을 재사용 가능한 컴포넌트로 만들어 코드 중복을 제거하고 일관성을 보장합니다. 복잡한 인프라를 여러 환경에 배포하거나, 팀 내에서 표준화된 구성을 공유할 때 필수적입니다. 변수를 통한 파라미터화와 출력값을 통한 데이터 전달이 모듈 설계의 핵심입니다. Tips: [3-5개의 실전 팁을 작성하되, 번호 대신 💡 이모지를 사용] 💡 모듈의 variable에는 항상 description과 type을 명시하세요. 이는 모듈을 사용하는 개발자가 어떤 값을 전달해야 하는지 명확히 이해할 수 있게 하며, IDE의 자동완성 기능도 활용할 수 있게 합니다. 💡 모듈 버전을 명시적으로 관리하세요. Git 태그나 Terraform Registry의 버전 시스템을 활용하면 모듈이 예상치 못하게 변경되어 기존 인프라가 깨지는 것을 방지할 수 있습니다. source = "./modules/vpc?ref=v1.2.0" 형식을 사용하세요. 💡 모듈은 단일 책임 원칙을 따라야 합니다. VPC 모듈에 데이터베이스 설정까지 포함하지 말고, 각각 별도 모듈로 분리하세요. 작고 집중된 모듈이 재사용성이 높고 유지보수가 쉽습니다. 💡 민감한 정보(DB 패스워드, API 키 등)는 절대 모듈 내부에 하드코딩하지 말고 변수로 받거나 AWS Secrets Manager 같은 별도 서비스를 활용하세요. sensitive = true 속성을 사용하면 출력 로그에 값이 노출되지 않습니다. 💡 모듈의 output은 최소한으로 유지하되, 다른 모듈이나 리소스가 필요로 할 만한 정보는 반드시 노출하세요. 예를 들어 VPC 모듈이라면 vpc_id, subnet_ids, security_group_id 등은 필수 출력값입니다.


2. Workspace Strategy

개요

[3-5문단으로 작성] 간단히 말해서, Terraform Workspace는 동일한 구성 코드를 사용하면서 서로 다른 상태 파일을 가진 독립적인 환경을 만드는 기능입니다. terraform workspace new dev 명령으로 새 워크스페이스를 생성하면, 같은 .tf 파일을 사용하지만 별도의 상태 파일이 생성됩니다. 워크스페이스를 사용하는 핵심 이유는 환경별 인프라 분리와 코드 재사용성의 균형입니다. 모든 환경이 동일한 코드베이스를 공유하므로 버그 수정이나 기능 추가가 모든 환경에 일괄 적용되며, 동시에 각 환경은 독립적인 상태를 유지해 서로 영향을 주지 않습니다. 예를 들어, 개발 환경에서 실험적인 변경을 테스트한 후, 검증이 완료되면 동일한 코드를 프로덕션 워크스페이스에 적용할 수 있습니다. 기존에는 dev/, staging/, prod/ 디렉토리를 따로 만들어 코드를 복제했다면, 이제는 하나의 디렉토리에서 워크스페이스만 전환하며 작업합니다. 이는 DRY(Don't Repeat Yourself) 원칙을 인프라 코드에 적용한 것입니다. 워크스페이스의 핵심 특징은 상태 격리, 코드 공유, 조건부 구성입니다. 각 워크스페이스는 독립적인 terraform.tfstate 파일을 가지며, terraform.workspace 변수를 통해 현재 워크스페이스에 따라 다른 값을 적용할 수 있습니다. 이러한 특징들이 멀티 환경 관리를 단순화하고 실수를 줄여줍니다.

코드 예제

# variables.tf
variable "instance_types" {
  description = "Instance types per environment"
  type        = map(string)
  default = {
    dev     = "t3.micro"
    staging = "t3.small"
    prod    = "t3.large"
  }
}

variable "instance_counts" {
  type = map(number)
  default = {
    dev     = 1
    staging = 2
    prod    = 5
  }
}

# main.tf
# 현재 워크스페이스에 따라 다른 설정 적용
locals {
  environment    = terraform.workspace
  instance_type  = var.instance_types[terraform.workspace]
  instance_count = var.instance_counts[terraform.workspace]
}

resource "aws_instance" "app" {
  count         = local.instance_count
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = local.instance_type

  tags = {
    Name        = "${local.environment}-app-${count.index + 1}"
    Environment = local.environment
  }
}

# 워크스페이스별 S3 버킷 (상태 파일 저장용)
resource "aws_s3_bucket" "terraform_state" {
  bucket = "myapp-terraform-state-${local.environment}"

  tags = {
    Environment = local.environment
  }
}

# CLI 사용 예시
# terraform workspace new dev
# terraform workspace new prod
# terraform workspace select dev
# terraform apply

설명

[4-6문단으로 작성] 이것이 하는 일: 이 코드는 워크스페이스에 따라 다른 인스턴스 타입과 개수를 자동으로 적용하는 멀티 환경 인프라 구성을 보여줍니다. 첫 번째로, variables.tf에서 환경별 설정값을 맵(map) 형태로 정의합니다. instance_types 맵은 개발 환경에는 작은 t3.micro를, 프로덕션에는 큰 t3.large를 할당하도록 설정되어 있습니다. 이렇게 하는 이유는 개발 환경에서는 비용을 절약하고, 프로덕션에서는 충분한 성능을 확보하기 위함입니다. 맵 구조를 사용하면 새로운 환경 추가도 간단히 키-값 쌍만 추가하면 됩니다. 그 다음으로, locals 블록에서 terraform.workspace 변수를 활용해 현재 활성화된 워크스페이스를 감지합니다. 이 값을 기준으로 맵에서 해당 환경의 설정을 조회하여 local 변수에 저장합니다. 내부적으로 terraform.workspace는 "dev", "staging", "prod" 같은 문자열 값을 반환하며, 이를 맵의 키로 사용해 적절한 값을 선택합니다. 세 번째로, resource 블록들이 실행될 때 local 변수에 저장된 환경별 설정값이 적용됩니다. count 속성에 환경별 인스턴스 개수가 자동으로 들어가고, instance_type에도 해당 환경에 맞는 인스턴스 타입이 설정됩니다. 태그에는 워크스페이스 이름이 포함되어 AWS 콘솔에서 어느 환경의 리소스인지 명확히 구분할 수 있습니다. 마지막으로, CLI 명령어 예시에서 보듯이 terraform workspace select dev 명령으로 워크스페이스를 전환하면, 동일한 코드가 dev 환경의 설정과 상태 파일로 작동합니다. 워크스페이스를 prod로 변경하면 자동으로 5개의 t3.large 인스턴스가 생성되는 구성으로 바뀝니다. 여러분이 이 패턴을 사용하면 환경별로 코드를 복제할 필요 없이 하나의 코드베이스로 모든 환경을 관리할 수 있습니다. 개발 환경에서 검증된 코드를 프로덕션에 적용할 때 복사 실수가 없으며, 환경별 차이는 변수와 워크스페이스로 자동 처리되고, 잘못된 환경에 배포하는 실수를 줄일 수 있는 장점이 있습니다. Summary: [2-3문장으로 작성] 핵심 정리: Workspace Strategy는 하나의 코드로 여러 환경을 관리하면서 상태를 완전히 분리하는 패턴입니다. terraform.workspace 변수와 맵 구조를 활용해 환경별 설정 차이를 표현하세요. 소규모 프로젝트나 환경 간 차이가 크지 않을 때 효과적이며, 대규모라면 별도 디렉토리 구조를 고려하세요. Tips: [3-5개의 실전 팁을 작성하되, 번호 대신 💡 이모지를 사용] 💡 워크스페이스 이름은 명명 규칙을 정해두세요. dev, staging, prod처럼 일관된 이름을 사용하면 조건 로직이 단순해지고, 실수로 typo가 난 워크스페이스를 만드는 것을 방지할 수 있습니다. 💡 default 워크스페이스는 프로덕션용으로 사용하지 마세요. 실수로 워크스페이스를 선택하지 않았을 때 개발 환경으로 작동하도록 설정하거나, default 워크스페이스에서는 아무것도 생성하지 않도록 validation 규칙을 추가하세요. 💡 환경 간 차이가 큰 경우 워크스페이스보다 별도 디렉토리가 나을 수 있습니다. 프로덕션만 다중 리전을 사용한다거나, 네트워크 구조가 완전히 다르다면 무리하게 하나의 코드로 통합하지 마세요. 💡 CI/CD 파이프라인에서는 환경 변수로 워크스페이스를 명시적으로 선택하세요. export TF_WORKSPACE=prod 같은 방식으로 설정하면 현재 선택된 워크스페이스에 의존하지 않아 안전합니다. 💡 workspace 명령어를 실행하기 전 항상 terraform workspace show로 현재 워크스페이스를 확인하는 습관을 들이세요. 프로덕션에 배포하려다 dev 워크스페이스에 있는 채로 작업하는 실수를 방지할 수 있습니다.


3. Remote Backend

개요

[3-5문단으로 작성] 간단히 말해서, Remote Backend는 Terraform 상태 파일을 로컬이 아닌 원격 스토리지에 저장하고 관리하는 메커니즘입니다. AWS S3와 DynamoDB 조합이 가장 널리 사용되며, S3는 상태 파일 저장, DynamoDB는 락 관리를 담당합니다. 원격 백엔드를 사용하는 이유는 협업과 안전성입니다. 모든 팀원이 동일한 상태 파일을 참조하므로 인프라의 현재 상태에 대한 진실의 원천(source of truth)이 하나로 통일됩니다. 또한 두 명이 동시에 terraform apply를 실행하면 DynamoDB 락이 작동해 한 명의 작업이 끝날 때까지 다른 작업을 대기시켜 충돌을 방지합니다. 예를 들어, 프로덕션 데이터베이스를 삭제하는 명령이 두 번 실행되는 치명적 상황을 막을 수 있습니다. 기존에는 terraform.tfstate 파일을 Git에 커밋하거나 수동으로 공유했다면, 이제는 자동으로 원격 저장소에 동기화됩니다. 상태 파일 버전 관리도 S3 버저닝으로 자동 처리됩니다. Remote Backend의 핵심 특징은 중앙 집중식 상태 관리, 자동 락킹, 암호화입니다. S3 버킷에 암호화를 활성화하면 민감한 정보가 안전하게 보관되고, DynamoDB 테이블이 자동으로 동시성을 제어하며, 버저닝을 통해 잘못된 변경을 롤백할 수 있습니다. 이러한 특징들이 프로덕션 환경에서 Terraform을 안전하게 사용할 수 있는 기반이 됩니다.

코드 예제

# backend.tf
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"

    # 상태 파일 액세스 로깅
    acl = "private"
  }
}

# backend-resources.tf (별도 실행 필요)
# S3 버킷 생성 (상태 파일 저장용)
resource "aws_s3_bucket" "terraform_state" {
  bucket = "mycompany-terraform-state"

  lifecycle {
    prevent_destroy = true  # 실수로 삭제 방지
  }
}

# 버저닝 활성화 (상태 파일 이력 관리)
resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  versioning_configuration {
    status = "Enabled"
  }
}

# 서버 측 암호화 설정
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# DynamoDB 테이블 (상태 락 관리용)
resource "aws_dynamodb_table" "terraform_lock" {
  name           = "terraform-state-lock"
  billing_mode   = "PAY_PER_REQUEST"
  hash_key       = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

# 사용 방법
# 1. backend-resources.tf만 먼저 로컬 백엔드로 실행
# 2. backend.tf 추가 후 terraform init -migrate-state 실행
# 3. 이후부터는 원격 백엔드 사용

설명

[4-6문단으로 작성] 이것이 하는 일: 이 코드는 AWS S3와 DynamoDB를 활용한 안전한 원격 백엔드 환경을 구축하고, 상태 파일 보호를 위한 모든 보안 설정을 자동화합니다. 첫 번째로, backend.tf의 terraform 블록에서 원격 백엔드 설정을 선언합니다. bucket과 key 속성으로 상태 파일이 저장될 S3 위치를 지정하고, dynamodb_table로 락 관리용 테이블을 연결합니다. 이렇게 하는 이유는 Terraform에게 "이제부터 상태 파일을 로컬이 아닌 이 원격 위치에서 읽고 쓰라"고 지시하기 위함입니다. encrypt = true 설정은 전송 중 데이터를 암호화합니다. 그 다음으로, backend-resources.tf에서 실제 S3 버킷과 DynamoDB 테이블을 생성합니다. 중요한 점은 lifecycle의 prevent_destroy 설정인데, 이는 terraform destroy 명령을 실행해도 상태 파일이 저장된 버킷은 삭제되지 않도록 보호합니다. 내부적으로 실수로 인프라 전체를 날리는 상황에서도 상태 파일만큼은 보존되어야 복구가 가능하기 때문입니다. 세 번째로, 버저닝과 암호화 설정이 적용됩니다. aws_s3_bucket_versioning 리소스는 모든 상태 파일 변경 이력을 자동 보관하며, 잘못된 apply 후 이전 버전으로 되돌릴 수 있게 합니다. aws_s3_bucket_server_side_encryption_configuration은 저장된 파일을 AES256 알고리즘으로 암호화하여, 상태 파일에 포함된 민감 정보(API 키, DB 패스워드 등)를 보호합니다. 마지막으로, DynamoDB 테이블이 생성되어 분산 락 메커니즘을 제공합니다. hash_key가 "LockID"로 설정되어 있어 Terraform이 작업 시작 전 해당 테이블에 락 레코드를 생성하고, 작업 완료 후 삭제합니다. 다른 사용자가 동시에 작업을 시도하면 락 레코드가 이미 존재해 대기하게 됩니다. 여러분이 이 패턴을 사용하면 팀 환경에서 안전하게 협업할 수 있습니다. 상태 파일 충돌이 원천적으로 차단되고, 민감 정보가 암호화되어 보관되며, 잘못된 변경 시 이전 버전으로 복구 가능하고, Git에 상태 파일을 커밋할 필요가 없어 저장소가 깔끔해지는 장점이 있습니다. Summary: [2-3문장으로 작성] 핵심 정리: Remote Backend는 상태 파일을 원격 저장소에 보관해 팀 협업과 안전성을 보장하는 필수 패턴입니다. S3 + DynamoDB 조합으로 암호화, 버저닝, 락킹을 모두 구현하세요. 프로덕션 환경이라면 반드시 적용해야 하며, 백엔드 리소스는 별도로 먼저 생성한 후 마이그레이션하는 것이 안전합니다. Tips: [3-5개의 실전 팁을 작성하되, 번호 대신 💡 이모지를 사용] 💡 백엔드 설정은 처음부터 적용하세요. 이미 로컬로 작업한 프로젝트를 나중에 원격으로 마이그레이션하면 terraform init -migrate-state 과정이 필요한데, 상태가 복잡하면 문제가 생길 수 있습니다. 💡 환경별로 다른 key 경로를 사용하세요. dev/terraform.tfstate, prod/terraform.tfstate처럼 분리하면 하나의 S3 버킷으로 모든 환경을 관리하면서도 상태는 완전히 격리됩니다. 💡 S3 버킷에는 반드시 버전 관리를 활성화하세요. 실수로 상태 파일이 손상되었을 때 이전 버전으로 복구할 수 있는 유일한 방법입니다. 최소 30일 이상 이력을 보관하는 것을 권장합니다. 💡 DynamoDB 테이블은 on-demand 요금제를 사용하세요. 락 테이블은 사용 빈도가 낮아 프로비저닝 용량보다 PAY_PER_REQUEST가 비용 효율적입니다. 💡 백엔드 설정 자체는 변수를 사용할 수 없습니다. 동적으로 백엔드를 변경해야 한다면 -backend-config 플래그로 CLI에서 값을 전달하거나, backend.hcl 파일을 별도로 관리하세요.


4. Data Source Pattern

개요

[3-5문단으로 작성] 간단히 말해서, Data Source는 Terraform이 직접 생성하지 않은 기존 리소스의 정보를 읽어오는 기능입니다. aws_vpc, aws_ami, aws_availability_zones 같은 data 블록으로 AWS에 쿼리를 보내 필요한 정보를 가져옵니다. Data Source를 사용하는 핵심 이유는 인프라 간 느슨한 결합(loose coupling)과 동적 구성입니다. 다른 프로젝트가 관리하는 VPC를 태그로 찾아서 참조하면, VPC가 재생성되어 ID가 바뀌어도 코드 수정이 필요 없습니다. 또한 최신 AMI를 자동으로 조회해 사용하면 수동으로 AMI ID를 업데이트할 필요가 없습니다. 예를 들어, "production" 태그가 붙은 VPC를 찾아서 그 안에 인스턴스를 배포하는 식으로 환경에 맞게 동적으로 구성할 수 있습니다. 기존에는 AWS 콘솔에서 VPC ID를 복사해 변수에 붙여넣었다면, 이제는 태그나 이름으로 자동 조회합니다. 이는 선언적 인프라 코드의 완성도를 높입니다. Data Source의 핵심 특징은 읽기 전용, 동적 쿼리, 필터링입니다. data 블록은 절대 리소스를 생성하거나 수정하지 않고 오직 읽기만 하며, 실행 시점에 실제 인프라 상태를 조회하고, filter와 tags를 사용해 원하는 리소스를 정확히 찾아냅니다. 이러한 특징들이 코드의 이식성과 유지보수성을 크게 향상시킵니다.

코드 예제

# 기존 VPC를 태그로 조회
data "aws_vpc" "main" {
  filter {
    name   = "tag:Environment"
    values = ["production"]
  }
}

# 해당 VPC의 프라이빗 서브넷들 조회
data "aws_subnets" "private" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.main.id]
  }

  filter {
    name   = "tag:Type"
    values = ["private"]
  }
}

# 최신 Amazon Linux 2 AMI 자동 조회
data "aws_ami" "amazon_linux_2" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# 현재 리전의 가용 영역 목록 조회
data "aws_availability_zones" "available" {
  state = "available"
}

# 조회한 정보를 활용해 EC2 생성
resource "aws_instance" "app" {
  ami               = data.aws_ami.amazon_linux_2.id
  instance_type     = "t3.micro"
  subnet_id         = data.aws_subnets.private.ids[0]
  availability_zone = data.aws_availability_zones.available.names[0]

  tags = {
    Name = "app-server"
    VPC  = data.aws_vpc.main.tags["Name"]
  }
}

# 조회한 VPC 정보 출력
output "vpc_info" {
  value = {
    id         = data.aws_vpc.main.id
    cidr_block = data.aws_vpc.main.cidr_block
    subnets    = data.aws_subnets.private.ids
  }
}

설명

[4-6문단으로 작성] 이것이 하는 일: 이 코드는 기존 인프라에서 VPC, 서브넷, AMI, 가용 영역 정보를 동적으로 조회한 후, 그 정보를 활용해 EC2 인스턴스를 생성하는 전체 흐름을 보여줍니다. 첫 번째로, aws_vpc data source가 AWS API를 호출해 "Environment=production" 태그가 붙은 VPC를 찾습니다. filter 블록을 사용하는 이유는 하드코딩된 ID 대신 의미 있는 메타데이터로 리소스를 찾기 위함입니다. 이렇게 하면 VPC를 재생성해도 동일한 태그만 붙이면 코드 수정 없이 작동합니다. 내부적으로 Terraform은 DescribeVpcs API를 호출하고 결과를 캐싱합니다. 그 다음으로, aws_subnets data source가 앞서 조회한 VPC ID를 참조해 해당 VPC의 프라이빗 서브넷만 필터링합니다. data.aws_vpc.main.id 형식으로 다른 data source의 결과를 참조할 수 있으며, 이는 쿼리를 체이닝하는 강력한 패턴입니다. "Type=private" 태그를 가진 서브넷만 선택되어, 퍼블릭 서브넷과 명확히 구분됩니다. 세 번째로, aws_ami data source가 most_recent = true 옵션과 함께 최신 Amazon Linux 2 AMI를 자동으로 찾습니다. owners = ["amazon"]으로 공식 AMI만 대상으로 하고, 이름 패턴으로 특정 OS와 아키텍처를 지정합니다. 이렇게 하면 매달 나오는 보안 패치가 적용된 최신 AMI를 수동 업데이트 없이 자동으로 사용할 수 있습니다. 마지막으로, aws_instance resource에서 모든 data source의 결과를 조합해 인스턴스를 생성합니다. AMI ID, 서브넷 ID, 가용 영역이 모두 동적으로 결정되어, 코드 어디에도 하드코딩된 값이 없습니다. 심지어 태그의 VPC 이름도 data source에서 가져와 일관성을 유지합니다. 여러분이 이 패턴을 사용하면 인프라 코드의 재사용성과 유지보수성이 크게 향상됩니다. 환경이 바뀌어도 태그만 맞으면 코드 수정이 불필요하고, 리소스가 재생성되어 ID가 바뀌어도 자동으로 추적되며, 최신 AMI나 설정을 자동으로 반영할 수 있고, 여러 프로젝트가 느슨하게 결합되어 독립적으로 관리 가능한 장점이 있습니다. Summary: [2-3문장으로 작성] 핵심 정리: Data Source Pattern은 기존 인프라 정보를 동적으로 조회해 코드 하드코딩을 제거하는 패턴입니다. 태그와 필터를 활용해 의미 있는 기준으로 리소스를 찾으세요. 다른 팀이 관리하는 리소스를 참조하거나, 최신 AMI를 자동으로 사용하거나, 멀티 리전 배포에서 특히 유용합니다. Tips: [3-5개의 실전 팁을 작성하되, 번호 대신 💡 이모지를 사용] 💡 Data source는 매 실행마다 실시간 조회하므로 apply 시간이 늘어날 수 있습니다. 자주 바뀌지 않는 정보라면 조회 결과를 locals에 저장해 재사용하세요. 💡 filter 조건이 여러 리소스를 반환할 수 있다면 most_recent = true나 추가 필터로 명확히 하나만 선택되도록 하세요. 애매한 쿼리는 예상치 못한 리소스를 선택해 문제를 일으킬 수 있습니다. 💡 Data source가 결과를 찾지 못하면 에러가 발생합니다. 선택적 조회가 필요하다면 count = length(...) > 0 ? 1 : 0 패턴을 사용해 조건부로 리소스를 생성하세요. 💡 민감한 정보를 조회하는 data source(예: aws_secretsmanager_secret_version)는 상태 파일에 평문으로 저장됩니다. 원격 백엔드 암호화를 반드시 활성화하세요. 💡 태그 기반 조회 시 태그 네이밍 규칙을 팀 전체가 공유해야 합니다. "Production"과 "production"은 다른 값이므로 대소문자를 통일하고, 문서화하세요.


5. Locals Variables

개요

[3-5문단으로 작성] 간단히 말해서, Variables는 외부에서 값을 입력받는 매개변수이고, Locals는 내부에서 계산된 값을 저장하는 상수입니다. variable은 사용자가 제공하고, locals는 Terraform이 계산합니다. 이 두 가지를 구분해서 사용하는 이유는 명확한 역할 분리입니다. Variables는 환경마다 달라질 수 있는 값(리전, 인스턴스 타입, 프로젝트 이름 등)을 받고, Locals는 이를 조합하거나 변환해 실제 리소스에서 사용할 값을 만듭니다. 예를 들어, var.environment를 받아서 locals.resource_prefix = "${var.project}-${var.environment}"로 만들면, 모든 리소스 이름에 일관된 접두사를 적용할 수 있습니다. 기존에는 복잡한 표현식을 매번 반복해서 작성했다면, 이제는 locals에 한 번만 정의하고 참조합니다. 이는 DRY 원칙을 철저히 따르는 것입니다. Locals와 Variables의 핵심 특징은 입력 vs 계산, 재사용성, 타입 안정성입니다. Variables는 default 값과 validation 규칙을 가질 수 있고, Locals는 다른 locals나 variables를 참조해 복잡한 로직을 구현할 수 있습니다. 이러한 특징들이 코드를 모듈화하고 오류를 줄여줍니다.

코드 예제

# variables.tf - 외부 입력 정의
variable "project_name" {
  description = "Project name"
  type        = string

  validation {
    condition     = length(var.project_name) <= 10
    error_message = "Project name must be 10 characters or less"
  }
}

variable "environment" {
  description = "Environment name"
  type        = string

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod"
  }
}

variable "instance_count" {
  description = "Number of instances"
  type        = number
  default     = 1
}

# locals.tf - 내부 계산 및 변환
locals {
  # 공통 리소스 접두사
  resource_prefix = "${var.project_name}-${var.environment}"

  # 환경별 설정 매핑
  instance_type = {
    dev     = "t3.micro"
    staging = "t3.small"
    prod    = "t3.large"
  }[var.environment]

  # 공통 태그 정의
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "Terraform"
    CreatedAt   = timestamp()
  }

  # 복잡한 조건 로직
  enable_monitoring = var.environment == "prod" ? true : false

  # 리스트 변환
  availability_zones = slice(
    data.aws_availability_zones.available.names,
    0,
    min(var.instance_count, 3)
  )
}

# main.tf - locals 활용
resource "aws_instance" "app" {
  count         = var.instance_count
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = local.instance_type

  monitoring = local.enable_monitoring

  tags = merge(
    local.common_tags,
    {
      Name = "${local.resource_prefix}-app-${count.index + 1}"
    }
  )
}

resource "aws_s3_bucket" "data" {
  bucket = "${local.resource_prefix}-data-bucket"

  tags = local.common_tags
}

설명

[4-6문단으로 작성] 이것이 하는 일: 이 코드는 Variables로 외부 입력을 안전하게 받고, Locals로 복잡한 비즈니스 로직을 처리한 후, 실제 리소스에서 깔끔하게 재사용하는 전체 패턴을 보여줍니다. 첫 번째로, variables.tf에서 validation 블록을 사용해 입력값을 검증합니다. project_name이 10자를 초과하거나, environment가 정해진 값이 아니면 에러를 발생시켜 잘못된 설정이 인프라에 반영되는 것을 미리 차단합니다. 이렇게 하는 이유는 S3 버킷 이름 같은 리소스는 길이 제한이 있고, 환경 이름이 오타나면 의도치 않은 결과가 나올 수 있기 때문입니다. 그 다음으로, locals.tf에서 여러 계산과 변환을 수행합니다. resource_prefix는 모든 리소스 이름에 일관된 접두사를 제공하고, instance_type은 맵 조회로 환경에 맞는 인스턴스 타입을 자동 선택합니다. 내부적으로 [var.environment] 문법은 맵에서 키로 값을 찾는 것이며, 존재하지 않는 키를 참조하면 에러가 발생합니다. 세 번째로, common_tags에서 모든 리소스에 공통으로 적용할 태그를 정의합니다. timestamp() 함수로 생성 시각을 자동으로 기록하고, 프로젝트와 환경 정보도 포함시킵니다. 이는 AWS Cost Explorer에서 비용을 프로젝트별로 분석하거나, 태그 기반 정책을 적용할 때 매우 유용합니다. 네 번째로, enable_monitoring 같은 조건 로직을 locals에 캡슐화합니다. var.environment == "prod" ? true : false 표현식을 리소스마다 반복하지 않고, local.enable_monitoring으로 한 번만 참조하면 됩니다. availability_zones에서는 slice와 min 함수를 조합해 인스턴스 개수에 맞게 가용 영역을 동적으로 선택합니다. 마지막으로, main.tf에서 모든 locals를 참조해 리소스를 생성합니다. 코드가 매우 깔끔해지고, 리소스 이름 규칙이나 태그 정책이 바뀌어도 locals 한 곳만 수정하면 전체에 반영됩니다. merge 함수로 공통 태그와 리소스별 태그를 결합하는 패턴도 자주 사용됩니다. 여러분이 이 패턴을 사용하면 코드 품질이 크게 향상됩니다. 중복 코드가 제거되어 유지보수가 쉬워지고, 복잡한 로직이 locals에 집중되어 가독성이 높아지며, validation으로 입력 오류를 사전에 방지하고, 명명 규칙 변경 시 한 곳만 수정하면 되는 장점이 있습니다. Summary: [2-3문장으로 작성] 핵심 정리: Variables는 외부 입력, Locals는 내부 계산으로 역할을 명확히 분리하세요. 반복되는 표현식이나 복잡한 로직은 무조건 locals로 추출하여 재사용하세요. Validation 규칙을 적극 활용해 잘못된 설정이 인프라에 반영되는 것을 방지하세요. Tips: [3-5개의 실전 팁을 작성하되, 번호 대신 💡 이모지를 사용] 💡 Locals는 계산 비용이 없습니다. 복잡한 표현식도 한 번만 평가되고 캐싱되므로, 가독성을 위해 적극적으로 사용하세요. local.common_tags를 100개 리소스에서 참조해도 성능 영향은 없습니다. 💡 Variable의 default 값은 신중하게 설정하세요. 프로덕션 환경에 영향을 줄 수 있는 변수는 default를 제공하지 말고 필수 입력으로 만드는 것이 안전합니다. 💡 복잡한 validation 로직은 can() 함수와 regex()를 조합하세요. 예: can(regex("^[a-z0-9-]+$", var.name))로 알파벳 소문자와 하이픈만 허용할 수 있습니다. 💡 Locals에서 다른 locals를 참조할 때 순환 참조를 조심하세요. local.a가 local.b를 참조하고 local.b가 local.a를 참조하면 에러가 발생합니다. 💡 환경별 설정은 맵 구조로 locals에 정의하면 switch 문처럼 사용할 수 있습니다. 이는 긴 if-else 체인보다 가독성이 훨씬 좋습니다.


6. Dynamic Blocks

개요

[3-5문단으로 작성] 간단히 말해서, Dynamic Blocks는 for_each나 for 표현식처럼 반복 구조를 중첩 블록에 적용하는 기능입니다. dynamic "ingress" 같은 형태로 선언하고, for_each로 데이터를 순회하며 content 블록 안에 실제 내용을 정의합니다. Dynamic Blocks를 사용하는 이유는 데이터 기반 인프라 정의입니다. 포트 목록을 변수나 locals로 관리하면, 새 포트를 추가할 때 블록을 복사하지 않고 리스트에 값만 추가하면 됩니다. 또한 조건에 따라 블록의 개수를 동적으로 변경할 수 있습니다. 예를 들어, 개발 환경에서는 SSH 포트를 열되, 프로덕션에서는 닫는 식의 로직을 깔끔하게 구현할 수 있습니다. 기존에는 비슷한 블록을 수동으로 복사했다면, 이제는 데이터 구조를 정의하고 dynamic으로 생성합니다. 이는 선언적 코드의 강력함을 극대화합니다. Dynamic Blocks의 핵심 특징은 반복 생성, 조건부 적용, 데이터 기반 구성입니다. for_each로 맵이나 리스트를 순회하고, 각 요소의 값은 dynamic_block.value로 접근하며, 빈 컬렉션을 전달하면 블록이 하나도 생성되지 않습니다. 이러한 특징들이 유연하고 유지보수하기 쉬운 코드를 만들어줍니다.

코드 예제

# locals.tf - 데이터 정의
locals {
  # 포트 목록 (단순 리스트)
  allowed_ports = [80, 443, 22, 3000, 8080]

  # 상세 규칙 (맵 리스트)
  ingress_rules = [
    {
      port        = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = "HTTP"
    },
    {
      port        = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = "HTTPS"
    },
    {
      port        = 22
      protocol    = "tcp"
      cidr_blocks = ["10.0.0.0/8"]
      description = "SSH from VPC only"
    }
  ]

  # 환경별 조건부 규칙
  enable_dev_ports = var.environment == "dev"
}

# main.tf - dynamic 블록 사용
resource "aws_security_group" "app" {
  name        = "${local.resource_prefix}-app-sg"
  description = "Application security group"
  vpc_id      = data.aws_vpc.main.id

  # 단순 포트 리스트를 dynamic으로 변환
  dynamic "ingress" {
    for_each = local.allowed_ports

    content {
      from_port   = ingress.value
      to_port     = ingress.value
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = "Port ${ingress.value}"
    }
  }

  # 상세 규칙을 dynamic으로 적용
  dynamic "ingress" {
    for_each = local.ingress_rules

    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
      description = ingress.value.description
    }
  }

  # 조건부 dynamic 블록 (개발 환경만)
  dynamic "ingress" {
    for_each = local.enable_dev_ports ? [1] : []

    content {
      from_port   = 9090
      to_port     = 9090
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = "Dev debugging port"
    }
  }

  # egress는 모든 아웃바운드 허용
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# 중첩 dynamic 예시 - Launch Template
resource "aws_launch_template" "app" {
  name = "${local.resource_prefix}-template"

  # 블록 장치 매핑을 dynamic으로 생성
  dynamic "block_device_mappings" {
    for_each = var.additional_volumes

    content {
      device_name = block_device_mappings.value.device_name

      dynamic "ebs" {
        for_each = [block_device_mappings.value.ebs]

        content {
          volume_size = ebs.value.size
          volume_type = ebs.value.type
          encrypted   = true
        }
      }
    }
  }
}

설명

[4-6문단으로 작성] 이것이 하는 일: 이 코드는 보안 그룹 규칙을 데이터로 정의하고, dynamic 블록으로 자동 생성하며, 조건에 따라 블록의 존재 여부를 제어하는 고급 패턴을 보여줍니다. 첫 번째로, locals에서 세 가지 형태의 데이터를 정의합니다. allowed_ports는 단순 숫자 리스트, ingress_rules는 상세 설정이 담긴 맵 리스트, enable_dev_ports는 조건 플래그입니다. 이렇게 분리하는 이유는 간단한 경우와 복잡한 경우를 각각 적합한 구조로 관리하기 위함이며, 데이터와 로직을 명확히 분리하면 나중에 JSON 파일에서 규칙을 읽어오는 것도 쉽게 구현할 수 있습니다. 그 다음으로, 첫 번째 dynamic "ingress" 블록이 allowed_ports를 순회합니다. for_each = local.allowed_ports는 리스트의 각 요소에 대해 content 블록을 한 번씩 실행하고, ingress.value로 현재 포트 번호(80, 443 등)에 접근합니다. 내부적으로 이는 5개의 ingress 블록이 생성되는 것과 동일하지만, 코드는 단 하나만 작성하면 됩니다. 세 번째로, 두 번째 dynamic "ingress" 블록은 더 복잡한 맵 리스트를 처리합니다. ingress.value.port, ingress.value.cidr_blocks처럼 맵의 키로 각 속성에 접근하며, SSH 규칙은 VPC 내부 IP만 허용하고 HTTP/HTTPS는 전체 공개하는 식으로 세밀한 제어가 가능합니다. 이는 동일한 리소스 타입이지만 각각 다른 설정을 가진 여러 블록을 만드는 강력한 패턴입니다. 네 번째로, 조건부 dynamic 블록이 삼항 연산자와 결합됩니다. local.enable_dev_ports ? [1] : []는 개발 환경이면 요소가 1개인 리스트를, 아니면 빈 리스트를 반환합니다. for_each가 빈 리스트를 받으면 content 블록이 한 번도 실행되지 않아, 사실상 블록이 존재하지 않는 것과 같습니다. 이는 count = var.environment == "dev" ? 1 : 0 패턴을 블록 레벨에서 구현한 것입니다. 마지막으로, 중첩 dynamic 예시에서는 Launch Template의 블록 장치 매핑을 생성합니다. 외부 dynamic이 각 볼륨을 순회하고, 내부 dynamic이 각 볼륨의 EBS 설정을 처리합니다. [block_device_mappings.value.ebs] 문법은 단일 맵을 1개 요소의 리스트로 감싸 for_each에 전달하는 트릭입니다. 여러분이 이 패턴을 사용하면 반복적인 블록 작성에서 해방됩니다. 규칙 추가 시 데이터만 수정하면 되고, 환경별 차이를 조건문으로 우아하게 처리하며, 외부 파일(YAML, JSON)에서 설정을 읽어와 적용하기 쉽고, 코드량이 줄어들어 리뷰와 유지보수가 쉬워지는 장점이 있습니다. Summary: [2-3문장으로 작성] 핵심 정리: Dynamic Blocks는 반복적인 중첩 블록을 데이터 기반으로 생성하는 강력한 패턴입니다. for_each로 리스트나 맵을 순회하고, 조건부 생성은 빈 리스트를 활용하세요. 보안 그룹, IAM 정책, 라우팅 규칙 등 반복 구조가 있는 모든 리소스에 적용 가능합니다. Tips: [3-5개의 실전 팁을 작성하되, 번호 대신 💡 이모지를 사용] 💡 Dynamic 블록의 이터레이터 이름(ingress, block_device_mappings)은 블록 이름과 동일하게 설정됩니다. iterator = rule 속성으로 다른 이름을 지정할 수 있지만, 기본값을 사용하는 것이 가독성에 좋습니다. 💡 과도한 dynamic 사용은 오히려 가독성을 해칩니다. 블록이 2-3개 이하라면 그냥 명시적으로 작성하는 것이 나을 수 있습니다. 5개 이상이거나 개수가 동적으로 변할 때 사용하세요. 💡 조건부 블록 생성 시 [1] 대신 [true] 또는 [{enabled = true}] 같은 의미 있는 값을 사용하면 코드 의도가 명확해집니다. 💡 중첩 dynamic은 복잡도가 급격히 증가하므로 2단계 이상 중첩은 피하세요. 대신 locals에서 데이터를 미리 변환해 단일 레벨 dynamic으로 처리하는 것이 좋습니다. 💡 Dynamic 블록 내에서 lookup() 함수를 사용하면 선택적 속성을 우아하게 처리할 수 있습니다. 예: description = lookup(rule.value, "description", "No description")으로 description이 없어도 에러 없이 기본값을 사용합니다.


7. Conditional Resources

개요

[3-5문단으로 작성] 간단히 말해서, Conditional Resources는 count = var.condition ? 1 : 0 같은 패턴으로 리소스를 조건부로 생성하거나 생략하는 기법입니다. count가 0이면 리소스가 전혀 생성되지 않습니다. 조건부 리소스를 사용하는 핵심 이유는 환경별 차이를 코드 분기로 표현하는 것입니다. 모든 환경이 동일한 Terraform 코드를 공유하면서도, 변수나 워크스페이스에 따라 다른 인프라가 배포됩니다. 비용 절감 측면에서도 유용한데, 개발 환경에서는 고가의 리소스를 건너뛰고 최소 구성만 사용할 수 있습니다. 예를 들어, CloudWatch 상세 모니터링은 프로덕션만 활성화하고, NAT Gateway는 개발 환경에서는 생략하는 식입니다. 기존에는 환경별로 별도 .tf 파일을 관리했다면, 이제는 조건 로직으로 통합합니다. 이는 인프라를 진정한 코드로 다루는 것입니다. Conditional Resources의 핵심 특징은 count 기반 조건, for_each 기반 필터링, 삼항 연산자 활용입니다. 리소스가 존재하지 않을 때의 참조를 처리하기 위해 try() 함수나 one() 함수를 함께 사용하며, 조건이 복잡하면 locals에서 미리 계산합니다. 이러한 특징들이 유연한 멀티 환경 코드를 가능하게 합니다.

코드 예제

# variables.tf
variable "environment" {
  type = string
}

variable "enable_monitoring" {
  description = "Enable detailed monitoring"
  type        = bool
  default     = false
}

variable "create_nat_gateway" {
  description = "Create NAT Gateway for private subnets"
  type        = bool
  default     = true
}

# locals.tf - 조건 로직 계산
locals {
  is_production = var.environment == "prod"

  # 복잡한 조건은 locals에서 계산
  should_create_rds     = local.is_production
  should_enable_backup  = local.is_production
  multi_az_enabled      = local.is_production

  # 환경별 인스턴스 개수
  instance_count = local.is_production ? 3 : 1
}

# main.tf - 조건부 리소스 생성
# 1. count를 이용한 조건부 생성
resource "aws_db_instance" "main" {
  count = local.should_create_rds ? 1 : 0

  identifier     = "${local.resource_prefix}-db"
  engine         = "postgres"
  engine_version = "14.7"
  instance_class = "db.t3.micro"

  allocated_storage = 20
  storage_encrypted = true

  multi_az = local.multi_az_enabled

  backup_retention_period = local.should_enable_backup ? 7 : 0

  skip_final_snapshot = !local.is_production
}

# 2. NAT Gateway는 변수로 제어
resource "aws_nat_gateway" "main" {
  count = var.create_nat_gateway ? length(local.public_subnets) : 0

  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = local.public_subnets[count.index]

  tags = {
    Name = "${local.resource_prefix}-nat-${count.index + 1}"
  }
}

resource "aws_eip" "nat" {
  count = var.create_nat_gateway ? length(local.public_subnets) : 0

  domain = "vpc"

  tags = {
    Name = "${local.resource_prefix}-eip-${count.index + 1}"
  }
}

# 3. 조건부 리소스 참조 (안전한 접근)
resource "aws_route_table" "private" {
  vpc_id = data.aws_vpc.main.id

  # NAT Gateway가 있으면 라우팅, 없으면 생략
  dynamic "route" {
    for_each = var.create_nat_gateway ? [1] : []

    content {
      cidr_block     = "0.0.0.0/0"
      nat_gateway_id = aws_nat_gateway.main[0].id
    }
  }
}

# 4. outputs에서 조건부 리소스 안전하게 출력
output "rds_endpoint" {
  description = "RDS endpoint (if created)"
  value       = try(aws_db_instance.main[0].endpoint, "RDS not created")
}

output "nat_gateway_ids" {
  description = "NAT Gateway IDs (if created)"
  value       = var.create_nat_gateway ? aws_nat_gateway.main[*].id : []
}

# 5. 조건부 모듈 호출
module "monitoring" {
  count = var.enable_monitoring ? 1 : 0

  source = "./modules/monitoring"

  instance_ids = aws_instance.app[*].id
  environment  = var.environment
}

설명

[4-6문단으로 작성] 이것이 하는 일: 이 코드는 환경과 변수에 따라 RDS, NAT Gateway, 모니터링 같은 리소스를 선택적으로 생성하고, 존재하지 않을 수 있는 리소스를 안전하게 참조하는 완전한 조건부 인프라 패턴을 보여줍니다. 첫 번째로, locals에서 조건 로직을 미리 계산합니다. is_production = var.environment == "prod" 같은 불린 플래그를 만들어 이후 코드에서 재사용하며, 이렇게 하는 이유는 동일한 조건식을 여러 곳에서 반복하지 않고, 의미 있는 이름으로 코드 의도를 명확히 하기 위함입니다. should_create_rds, multi_az_enabled 같은 변수명은 코드를 읽는 사람에게 "왜 이 리소스가 생성되는지" 즉시 알려줍니다. 그 다음으로, aws_db_instance에서 count = local.should_create_rds ? 1 : 0 패턴이 적용됩니다. 프로덕션 환경이면 count는 1이 되어 RDS가 생성되고, 개발 환경이면 0이 되어 아무것도 만들어지지 않습니다. 내부적으로 Terraform은 count가 0인 리소스를 완전히 무시하며, plan에도 나타나지 않습니다. multi_az와 backup_retention_period도 같은 조건 변수로 제어되어, 프로덕션에서는 HA와 백업이 활성화되고 개발에서는 비활성화됩니다. 세 번째로, NAT Gateway와 Elastic IP는 변수로 제어됩니다. var.create_nat_gateway가 false면 둘 다 생성되지 않으며, true면 public_subnets 개수만큼 생성됩니다. 이는 조건과 반복을 결합한 패턴으로, 개발 환경에서는 NAT Gateway 비용을 절약하고 인터넷 게이트웨이만 사용하는 설정을 구현할 수 있습니다. 네 번째로, aws_route_table에서 조건부 리소스를 참조하는 안전한 방법이 나타납니다. dynamic "route"의 for_each에 조건을 넣어, NAT Gateway가 없으면 라우팅 블록 자체를 생략합니다. aws_nat_gateway.main[0].id를 직접 참조하면 NAT Gateway가 없을 때 에러가 발생하지만, dynamic 블록으로 감싸면 블록이 아예 생성되지 않아 안전합니다. 다섯 번째로, outputs에서 try() 함수가 사용됩니다. try(aws_db_instance.main[0].endpoint, "RDS not created")는 RDS가 존재하면 엔드포인트를 반환하고, 없으면 기본 메시지를 반환합니다. 이는 조건부 리소스를 참조할 때 필수적인 패턴으로, 에러 없이 우아하게 처리합니다. 마지막으로, 모듈도 조건부로 호출할 수 있습니다. module "monitoring"의 count를 0으로 설정하면 모듈 전체가 실행되지 않아, 복잡한 모니터링 스택을 개발 환경에서 완전히 제거할 수 있습니다. 여러분이 이 패턴을 사용하면 환경별 인프라 관리가 매우 유연해집니다. 하나의 코드로 모든 환경을 커버하면서 비용을 최적화하고, 개발 환경에서 불필요한 리소스를 제거하며, 실수로 프로덕션 리소스를 빠뜨리는 일을 방지하고, 새 환경 추가 시 조건만 추가하면 되는 장점이 있습니다. Summary: [2-3문장으로 작성] 핵심 정리: Conditional Resources는 count와 삼항 연산자로 리소스 생성 여부를 제어하는 패턴입니다. 조건부 리소스 참조 시 try() 함수나 dynamic 블록으로 안전하게 처리하세요. 환경별 차이가 리소스 존재 여부로 나타날 때 가장 효과적이며, 복잡한 조건은 locals에서 미리 계산하는 것이 가독성에 좋습니다. Tips: [3-5개의 실전 팁을 작성하되, 번호 대신 💡 이모지를 사용] 💡 Count로 생성된 리소스는 항상 리스트 형태로 참조됩니다. aws_db_instance.main.endpoint가 아니라 aws_db_instance.main[0].endpoint처럼 인덱스를 명시해야 하며, 이를 잊으면 타입 에러가 발생합니다. 💡 조건부 리소스 간 의존성이 있다면 depends_on을 명시적으로 선언하세요. 암묵적 의존성은 count가 0일 때 제대로 작동하지 않을 수 있습니다. 💡 for_each는 count보다 안전한 경우가 많습니다. count는 리스트 중간 요소가 제거되면 인덱스가 바뀌어 리소스 재생성이 발생하지만, for_each는 키로 식별하므로 안정적입니다. 💡 조건부 리소스를 다른 리소스에서 참조할 때는 one() 함수를 고려하세요. one(aws_db_instance.main[*].endpoint)는 리스트가 정확히 1개 요소를 가질 때만 값을 반환하고, 0개나 2개 이상이면 에러를 발생시켜 안전합니다. 💡 복잡한 조건 로직(AND, OR 조합)은 locals에 불린 변수로 분리하세요. count = var.env == "prod" && var.region == "us-east-1" && var.ha_enabled ? 1 : 0 대신 count = local.should_create_ha_instance ? 1 : 0이 훨씬 읽기 쉽습니다.


8. Output Chain

개요

[3-5문단으로 작성] 간단히 말해서, Output Chain은 모듈의 output 값을 다른 모듈이나 리소스의 입력으로 연결하여 데이터를 전달하는 패턴입니다. module.vpc.vpc_id를 module.ecs의 변수로 전달하고, module.ecs.alb_dns를 module.cloudfront에 전달하는 식입니다. Output Chain을 사용하는 핵심 이유는 모듈 간 명시적 데이터 계약입니다. 각 모듈은 자신이 제공하는 정보를 output으로 선언하고, 사용하는 측은 그 output을 참조합니다. 이는 암묵적 의존성을 명시적으로 만들어 코드 이해도를 높이고, 모듈 인터페이스를 명확히 정의합니다. 예를 들어, 네트워크 팀이 관리하는 VPC 모듈과 애플리케이션 팀이 관리하는 서비스 모듈이 서로 독립적으로 개발되면서도 output/input 계약만 맞추면 통합이 가능합니다. 기존에는 리소스 ID를 수동으로 복사하거나 data source로 다시 조회했다면, 이제는 모듈 체이닝으로 직접 전달합니다. 이는 의존성을 Terraform이 자동으로 관리하게 합니다. Output Chain의 핵심 특징은 명시적 의존성, 타입 안정성, 계층적 구성입니다. output 블록에 description과 type을 명시하면 IDE가 자동완성을 제공하고, Terraform이 타입을 검증하며, 모듈 간 의존성 그래프가 자동으로 구성됩니다. 이러한 특징들이 대규모 멀티 모듈 프로젝트의 복잡도를 관리하게 해줍니다.

코드 예제

# modules/vpc/outputs.tf
output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.main.id
}

output "private_subnet_ids" {
  description = "List of private subnet IDs"
  value       = aws_subnet.private[*].id
}

output "public_subnet_ids" {
  description = "List of public subnet IDs"
  value       = aws_subnet.public[*].id
}

output "vpc_cidr" {
  description = "VPC CIDR block"
  value       = aws_vpc.main.cidr_block
}

# modules/ecs/variables.tf
variable "vpc_id" {
  description = "VPC ID to deploy ECS cluster"
  type        = string
}

variable "private_subnet_ids" {
  description = "Private subnet IDs for ECS tasks"
  type        = list(string)
}

variable "public_subnet_ids" {
  description = "Public subnet IDs for load balancer"
  type        = list(string)
}

# modules/ecs/outputs.tf
output "cluster_id" {
  description = "ECS cluster ID"
  value       = aws_ecs_cluster.main.id
}

output "alb_dns_name" {
  description = "Application Load Balancer DNS name"
  value       = aws_lb.main.dns_name
}

output "alb_zone_id" {
  description = "ALB Route53 zone ID"
  value       = aws_lb.main.zone_id
}

output "service_security_group_id" {
  description = "Security group ID for ECS services"
  value       = aws_security_group.ecs_service.id
}

# root main.tf - 모듈 체이닝
# 1단계: VPC 모듈
module "vpc" {
  source = "./modules/vpc"

  environment = var.environment
  vpc_cidr    = "10.0.0.0/16"
}

# 2단계: ECS 모듈 (VPC 출력 사용)
module "ecs" {
  source = "./modules/ecs"

  vpc_id             = module.vpc.vpc_id
  private_subnet_ids = module.vpc.private_subnet_ids
  public_subnet_ids  = module.vpc.public_subnet_ids

  cluster_name = "${var.project_name}-cluster"
  environment  = var.environment
}

# 3단계: CloudFront 모듈 (ECS 출력 사용)
module "cloudfront" {
  source = "./modules/cloudfront"

  origin_domain_name = module.ecs.alb_dns_name
  origin_id          = "ecs-alb"

  environment = var.environment
}

# 4단계: Route53 레코드 (여러 모듈 출력 조합)
resource "aws_route53_record" "app" {
  zone_id = data.aws_route53_zone.main.zone_id
  name    = "app.${var.domain_name}"
  type    = "A"

  alias {
    name                   = module.cloudfront.distribution_domain_name
    zone_id                = module.cloudfront.distribution_zone_id
    evaluate_target_health = false
  }
}

# 5단계: 모니터링 (모든 모듈 정보 활용)
module "monitoring" {
  source = "./modules/monitoring"

  ecs_cluster_name       = module.ecs.cluster_id
  alb_arn_suffix         = module.ecs.alb_arn_suffix
  cloudfront_distribution_id = module.cloudfront.distribution_id

  alert_email = var.alert_email
}

# root outputs.tf - 최종 정보 노출
output "application_url" {
  description = "Application URL"
  value       = "https://app.${var.domain_name}"
}

output "infrastructure_info" {
  description = "Key infrastructure identifiers"
  value = {
    vpc_id       = module.vpc.vpc_id
    cluster_id   = module.ecs.cluster_id
    cdn_id       = module.cloudfront.distribution_id
  }
  sensitive = false
}

# 민감한 출력 (데이터베이스 패스워드 등)
output "db_password" {
  description = "Database master password"
  value       = module.rds.master_password
  sensitive   = true  # CLI 출력에서 숨김
}

설명

[4-6문단으로 작성] 이것이 하는 일: 이 코드는 VPC → ECS → CloudFront → Route53 → Monitoring으로 이어지는 다층 모듈 체인을 구성하고, 각 단계에서 필요한 정보만 명시적으로 전달하는 완전한 인프라 구성 패턴을 보여줍니다. 첫 번째로, VPC 모듈의 outputs.tf에서 다른 모듈이 필요로 할 만한 모든 정보를 노출합니다. vpc_id, subnet_ids, cidr_block 등을 output으로 선언하며, description을 상세히 작성하는 이유는 이 모듈을 사용하는 개발자가 각 출력값의 용도를 즉시 이해할 수 있게 하기 위함입니다. 내부적으로 Terraform은 이 output들을 계산한 후 메모리에 캐싱하여, 다른 모듈이 참조할 수 있게 합니다. 그 다음으로, ECS 모듈의 variables.tf에서 필요한 입력을 선언합니다. vpc_id와 subnet_ids를 변수로 받으며, type을 명시해 타입 안정성을 보장합니다. root main.tf에서 module.vpc.vpc_id를 module.ecs의 vpc_id 변수에 전달하는 방식으로 체이닝이 구현됩니다. 이렇게 하면 Terraform이 자동으로 의존성을 파악해, VPC가 완전히 생성된 후에 ECS를 생성합니다. 세 번째로, ECS 모듈이 다시 자신의 출력을 노출합니다. alb_dns_name, cluster_id, security_group_id 같은 정보를 제공하여, 이후 단계의 모듈들이 사용할 수 있게 합니다. 이는 "생산자-소비자" 패턴으로, 각 모듈이 자신이 생성한 리소스의 메타데이터를 제공하고, 다른 모듈이 이를 소비합니다. 네 번째로, CloudFront 모듈이 ECS의 ALB DNS를 오리진으로 사용합니다. module.ecs.alb_dns_name을 CloudFront의 origin 설정에 전달하며, 이는 3단계 체이닝(VPC → ECS → CloudFront)을 완성합니다. Terraform의 의존성 그래프는 이 관계를 자동으로 추적해, 올바른 순서로 리소스를 생성하고 삭제합니다. 다섯 번째로, 여러 모듈의 출력을 조합하는 리소스가 나타납니다. aws_route53_record는 CloudFront의 도메인 이름을 사용하고, monitoring 모듈은 ECS, CloudFront 등 여러 모듈의 정보를 한꺼번에 받아 통합 모니터링을 구성합니다. 이는 "집합" 패턴으로, 상위 계층이 여러 하위 모듈을 조율합니다. 마지막으로, root outputs.tf에서 최종 사용자에게 필요한 정보만 선별해 노출합니다. 내부 구현 세부사항은 숨기고, 애플리케이션 URL 같은 중요 정보만 제공하며, sensitive = true로 민감한 정보는 CLI 출력에서 숨깁니다. infrastructure_info처럼 맵 형태로 여러 값을 그룹화하면 출력이 체계적으로 정리됩니다. 여러분이 이 패턴을 사용하면 복잡한 인프라도 명확히 구조화됩니다. 모듈 간 데이터 흐름이 코드에 명시적으로 드러나 이해하기 쉽고, 의존성을 Terraform이 자동 관리해 실행 순서 걱정이 없으며, 모듈을 독립적으로 개발하고 테스트할 수 있고, 출력값만 바꾸면 다른 모듈에 영향 없이 내부 구현을 변경할 수 있는 장점이 있습니다. Summary: [2-3문장으로 작성] 핵심 정리: Output Chain은 모듈 간 데이터 전달을 명시적 인터페이스로 구현하는 패턴입니다. 각 모듈은 필요한 정보를 output으로 노출하고, 사용하는 측은 module.name.output 형식으로 참조하세요. Description과 type을 반드시 명시해 계약을 문서화하고, 민감한 정보는 sensitive 속성을 사용하세요. Tips: [3-5개의 실전 팁을 작성하되, 번호 대신 💡 이모지를 사용] 💡 Output은 과하게 노출하는 것이 부족한 것보다 낫습니다. 지금 당장 필요하지 않더라도 다른 모듈이나 미래에 필요할 만한 정보는 미리 output으로 선언하세요. 나중에 추가하려면 모듈 인터페이스가 바뀌어 breaking change가 됩니다. 💡 복잡한 객체는 output으로 그대로 전달하지 말고 필요한 속성만 추출하세요. 예: aws_instance 전체를 출력하지 말고, id, private_ip, public_ip만 맵으로 만들어 출력하면 사용하는 측이 필요한 값을 명확히 알 수 있습니다. 💡 Output의 타입을 명시하면 Terraform이 컴파일 타임에 타입 오류를 잡아냅니다. output { type = list(string) }으로 선언하면, 실수로 문자열을 반환했을 때 즉시 에러가 발생해 안전합니다. 💡 조건부 리소스의 output은 try() 함수로 감싸세요. output "db_endpoint" { value = try(aws_db_instance.main[0].endpoint, null) }처럼 작성하면 리소스가 생성되지 않아도 에러 없이 null을 반환합니다. 💡 모듈 버전을 업그레이드할 때 output 변경은 매우 조심해야 합니다. Output 이름을 바꾸거나 타입을 변경하면 이를 사용하는 모든 코드가 깨집니다. 대신 새 output을 추가하고 구 output은 deprecated 주석과 함께 유지하는 방식으로 마이그레이션하세요.


마치며

이번 글에서는 Terraform 디자인 패턴 완벽 가이드에 대해 알아보았습니다. 총 16가지 개념을 다루었으며, 각각의 사용법과 예제를 살펴보았습니다.

관련 태그

#Terraform #Module #State #Workspace #Remote

#Terraform#Module#State#Workspace#Remote#TypeScript

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

마이크로서비스 배포 완벽 가이드

Kubernetes를 활용한 마이크로서비스 배포의 핵심 개념부터 실전 운영까지, 초급 개발자도 쉽게 따라할 수 있는 완벽 가이드입니다. 실무에서 바로 적용 가능한 배포 전략과 노하우를 담았습니다.

Application Load Balancer 완벽 가이드

AWS의 Application Load Balancer를 처음 배우는 개발자를 위한 실전 가이드입니다. ALB 생성부터 ECS 연동, 헬스 체크, HTTPS 설정까지 실무에 필요한 모든 내용을 다룹니다. 초급 개발자도 쉽게 따라할 수 있도록 단계별로 설명합니다.

고객 상담 AI 시스템 완벽 구축 가이드

AWS Bedrock Agent와 Knowledge Base를 활용하여 실시간 고객 상담 AI 시스템을 구축하는 방법을 단계별로 학습합니다. RAG 기반 지식 검색부터 Guardrails 안전 장치, 프론트엔드 연동까지 실무에 바로 적용 가능한 완전한 시스템을 만들어봅니다.

에러 처리와 폴백 완벽 가이드

AWS API 호출 시 발생하는 에러를 처리하고 폴백 전략을 구현하는 방법을 다룹니다. ThrottlingException부터 서킷 브레이커 패턴까지, 실전에서 바로 활용할 수 있는 안정적인 에러 처리 기법을 배웁니다.

AWS Bedrock 인용과 출처 표시 완벽 가이드

AWS Bedrock의 Citation 기능을 활용하여 AI 응답의 신뢰도를 높이는 방법을 배웁니다. 출처 추출부터 UI 표시, 검증까지 실무에서 바로 사용할 수 있는 완전한 가이드입니다.