콘텐츠로 이동

Terraform / IaC Basics

분류: Layer 5 - 플랫폼 엔지니어링 & 자동화 | 선수지식: VPC/Subnet/SG, IAM

Terraform은 인프라를 코드(HCL)로 정의하고, 명령 한 번으로 클라우드 리소스를 생성·수정·삭제할 수 있게 해주는 Infrastructure as Code(IaC) 도구이다.

AWS 콘솔에서 VPC, ECS, RDS를 수동으로 만들면 재현이 불가능하고 실수가 생긴다. Terraform으로 코드화하면 Git으로 버전 관리되고, 누가 언제 무엇을 바꿨는지 추적 가능하며, 동일한 인프라를 dev/staging/prod에 일관되게 복제할 수 있다. 2026년 기준 플랫폼 엔지니어에게 Kubernetes와 함께 “모든 플랫폼 엔지니어가 알아야 할 기본”으로 꼽힌다.

IaC(Infrastructure as Code)란

인프라 구성을 사람이 읽을 수 있는 코드 파일로 관리하는 것. 수동 콘솔 클릭 대신 코드로 선언하고, 코드를 실행해서 인프라를 만든다.

비유: AWS 콘솔로 인프라를 만드는 것은 “수작업으로 요리”이고, Terraform으로 만드는 것은 “레시피를 적어두고 언제든지 같은 음식을 재현”하는 것이다. 레시피(코드)는 Git에 저장되어 버전 관리되고, 팀원 누구나 동일한 환경을 만들 수 있다.

HCL (HashiCorp Configuration Language)

Terraform의 설정 언어. JSON처럼 읽기 쉽고 선언적이다.

# 예: VPC 생성
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "my-vpc"
Environment = "production"
}
}
# 예: S3 버킷 생성
resource "aws_s3_bucket" "logs" {
bucket = "my-team-logs-2026"
}

HCL 최소 문법 — variable, output, locals

Terraform 코드를 처음 읽을 때 반드시 알아야 할 3가지 블록이다.

# variable: 외부에서 주입받는 입력값. terraform apply -var="env=prod" 또는 .tfvars 파일로 전달
variable "environment" {
type = string
description = "배포 환경 (dev / staging / prod)"
default = "dev" # 기본값. 없으면 실행 시 반드시 입력해야 함
}
# locals: 파일 내부에서만 쓰는 계산된 값. 중복 제거에 유용
locals {
name_prefix = "${var.environment}-myapp" # "prod-myapp" 처럼 조합
common_tags = {
Environment = var.environment
ManagedBy = "terraform"
}
}
# output: apply 완료 후 출력할 값. 다른 모듈에서 참조하거나 CI 로그에 표시
output "vpc_id" {
value = aws_vpc.main.id
description = "생성된 VPC의 ID"
}

실무에서는 보통 variables.tf, locals.tf, outputs.tf로 파일을 분리한다. variable은 “API(입력)”, locals는 “내부 계산”, output은 “반환값”으로 이해하면 된다.

핵심 워크플로: Init → Plan → Apply — 왜 이 순서인가

비유: Terraform은 건축 감리사다. plan은 “이 도면대로 지으면 기존 건물에서 무엇이 바뀌는지 미리 보여주는 검토서”이고, apply는 “실제로 공사 시작”이다. 감리서 없이 공사하면 실수가 생긴다.

terraform init ← Provider 플러그인 다운로드 (AWS, GCP 등)
+ terraform.lock.hcl 생성 (버전 고정)
terraform plan ← "이 코드를 적용하면 무엇이 바뀌는지" 미리보기
(실제 AWS 상태 읽기 → 코드와 비교 → diff 출력)
terraform apply ← 실제 인프라 생성/수정 실행
(plan 결과 재확인 → "yes" 입력 → API 호출)
terraform destroy ← 인프라 삭제 (주의!)

내부 동작 원리 — plan이 실행될 때 무슨 일이 벌어지나

Terraform은 두 가지 핵심 레이어로 구성된다:

  • Terraform Core: Go로 작성된 실행 엔진. State 관리, 의존성 계산, 변경 사항 결정
  • Provider 플러그인: AWS, GCP, Azure 등 각 플랫폼의 API와 통신하는 별도 바이너리

plan 실행 시 내부적으로 **DAG(Directed Acyclic Graph, 방향성 비순환 그래프)**를 만든다. 리소스 간 의존 관계를 파악해서, 독립적인 리소스는 병렬로 생성하고 의존 관계가 있는 리소스는 순서를 보장한다.

예: VPC → Subnet → Security Group → EC2 순서로 VPC가 먼저 있어야 Subnet을 만들 수 있다. Terraform이 이 순서를 자동으로 파악한다.

# terraform plan 실행 내부 흐름
1. terraform.tfstate 읽기 (현재 Terraform이 알고 있는 인프라 상태)
2. AWS API 호출 → 실제 현재 상태 확인 (Refresh)
3. .tf 파일의 원하는 상태와 비교
4. DAG 생성 → 변경해야 할 리소스 순서 결정
5. diff 출력 (+는 생성, ~는 수정, -는 삭제)
# terraform plan 예상 출력
$ terraform plan
Terraform will perform the following actions:
# aws_s3_bucket.logs will be created
+ resource "aws_s3_bucket" "logs" {
+ bucket = "my-team-logs-2026"
+ id = (known after apply)
+ region = (known after apply)
}
# aws_vpc.main will be updated in-place
~ resource "aws_vpc" "main" {
id = "vpc-0a1b2c3d"
~ tags = {
~ "Name" = "old-vpc" -> "my-vpc"
}
}
Plan: 1 to add, 1 to change, 0 to destroy.

📖 더 보기: Terraform State 공식 문서 — State 파일이 왜 필요한지, 어떤 정보를 저장하는지 상세 설명

State (상태 파일) — Terraform의 기억

Terraform은 terraform.tfstate 파일에 현재 인프라 상태를 기록한다. 이 파일이 없으면 Terraform은 AWS에 무엇이 있는지 알 수 없어 매번 새로 만들려 한다. 이 파일로 “코드에 정의된 상태”와 “실제 AWS 상태”를 비교한다.

State가 중요한 이유:

  1. 변경 계산의 기준: State가 없으면 Terraform은 모든 리소스를 새로 만들려 함
  2. 성능: 매번 AWS API를 전부 조회하지 않고 State를 캐시로 활용
  3. 메타데이터 추적: 리소스 간 의존 관계를 State에 기록
# terraform.tfstate 파일 내용 (단순화)
{
"version": 4,
"resources": [
{
"type": "aws_s3_bucket",
"name": "logs",
"instances": [{
"attributes": {
"bucket": "my-team-logs-2026",
"id": "my-team-logs-2026",
"region": "ap-northeast-2"
}
}]
}
]
}

팀 작업 시 State를 S3 + DynamoDB에 저장해서 공유 (Remote Backend):

# Remote Backend 설정 (팀 협업 필수) — Terraform 1.9 이하 / 레거시 방식
terraform {
backend "s3" {
bucket = "my-team-terraform-state"
key = "prod/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-state-lock" # 동시 수정 방지 Lock
encrypt = true
}
}

선언적 State reconciliation — 크로스 도메인 패턴

Terraform의 State reconciliation은 독립적으로 발명된 패턴이 아니다. “원하는 상태를 선언하고, 컨트롤러가 실제 상태를 그것에 맞춘다”는 패턴이 여러 도메인에서 동일하게 나타난다.

도메인원하는 상태(Desired State)실제 상태(Actual State)ReconcilerDrift 처리
Terraform.tf 파일 + tfstateAWS API 응답terraform applyplan-refresh-only로 감지, 수동 apply
KubernetesDeployment YAML (replicas: 3)Running Pod 수kube-controller-manager자동 재조정 (항상 루프)
Argo CDGit 레포 YAML클러스터 실제 오브젝트Argo CD Application ControllerSelf-heal 자동 적용 (설정 시)
FluxGitRepository + Kustomization클러스터 실제 오브젝트Flux Source/Kustomize Controller자동 reconcile (기본 동작)
DB 마이그레이션마이그레이션 파일(Flyway/Liquibase)현재 스키마마이그레이션 엔진체크섬으로 미적용 diff 감지

핵심 차이: Terraform은 명령형 트리거 (사람이 apply 실행) 방식이고, Kubernetes/Argo CD/Flux는 지속 reconciliation 루프 (컨트롤러가 상시 감시) 방식이다. Terraform Cloud의 Drift Detection은 이 간극을 좁히려는 시도이지만, Argo CD의 Self-heal처럼 완전 자동은 아니다.

실무 전이 포인트: Terraform으로 EKS 클러스터를 프로비저닝하고 → Argo CD가 K8s 오브젝트를 GitOps로 관리하는 계층 분리가 2026년 사실상 표준이다. Terraform은 “플랫폼 레이어(VPC, 클러스터)”, Argo CD/Flux는 “앱 레이어(Deployment, Service)” 를 담당한다.

S3 Native State Locking — Terraform 1.10+ 신기능 (DynamoDB 불필요)

Terraform 1.10(2024년 AWS re:Invent 발표)부터 DynamoDB 없이 S3 자체의 조건부 쓰기(conditional writes)로 State Lock을 구현할 수 있다. DynamoDB 테이블을 별도로 생성·관리할 필요가 없어졌다.

# Terraform 1.10+ — S3 Native Locking (DynamoDB 불필요)
terraform {
backend "s3" {
bucket = "my-team-terraform-state"
key = "prod/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
use_lockfile = true # S3 조건부 쓰기로 Lock 구현 (DynamoDB 대체)
# dynamodb_table 제거 — 더 이상 필요 없음
}
}
# S3 Native Locking 동작 원리
terraform apply 실행 시:
1. S3에 .tfstate.lock 파일을 조건부로 생성 (이미 있으면 실패 → Lock 획득 실패)
2. apply 완료 → .tfstate 업데이트 → .tfstate.lock 삭제
3. 다른 사람이 동시에 apply하려 하면 .tfstate.lock이 이미 있어서 에러
"Error acquiring the state lock: ConditionalCheckFailedException"

📖 더 보기: S3 Native State Locking — HashiCorp 공식 문서use_lockfile 옵션 설명, DynamoDB 방식과 차이 설명 (중급)

Provider

Terraform이 어떤 클라우드/서비스와 통신할지 정의. AWS, GCP, Azure, GitHub 등 수천 개의 Provider가 있다. Provider는 별도의 Go 바이너리로 terraform init 실행 시 .terraform/providers/ 디렉토리에 다운로드된다.

provider "aws" {
region = "ap-northeast-2" # 서울 리전
}
# terraform init 시 Provider 다운로드 과정
$ terraform init
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.89.0...
- Installed hashicorp/aws v5.89.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default.

📖 더 보기: Terraform Provider Configuration 공식 문서 — Provider 버전 고정, alias로 멀티 리전 설정, Provider 인증 방법 상세 설명

Module (모듈) — 인프라 패턴 표준화

자주 쓰는 인프라 구성을 재사용 가능한 패키지로 만든 것. 비유: 모듈은 “팀이 합의한 인프라 레시피 북”이다. VPC 모듈, ECS 모듈처럼 팀 표준을 코드로 정의하면, 신규 환경을 만들 때 모듈 한 줄로 표준 인프라를 찍어낼 수 있다.

# 모듈 사용 예시 — 팀 표준 VPC를 한 줄로 재사용
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["ap-northeast-2a", "ap-northeast-2c"]
}

AWS 공식 EKS 모듈을 활용하면 프로덕션 수준의 EKS 클러스터를 빠르게 만들 수 있다:

# EKS 클러스터 생성 (terraform-aws-modules/eks 사용)
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.0"
cluster_name = "my-nestjs-cluster"
cluster_version = "1.32"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
eks_managed_node_groups = {
default = {
min_size = 2
max_size = 5
desired_size = 2
instance_types = ["t3.medium"]
}
}
}
# terraform apply 후 EKS 클러스터 확인 예상 출력
$ terraform apply
...
Apply complete! Resources: 42 added, 0 changed, 0 destroyed.
Outputs:
cluster_name = "my-nestjs-cluster"
cluster_endpoint = "https://ABCDEF1234567890.gr7.ap-northeast-2.eks.amazonaws.com"

📖 더 보기: Terraform EKS 공식 튜토리얼 - HashiCorp — Terraform으로 EKS 클러스터를 단계별로 프로비저닝하는 공식 가이드

프로덕션 모듈 디렉토리 구조 — 팀 표준 설계

실제 프로덕션 Terraform 코드베이스는 단순히 .tf 파일을 나열하지 않는다. 모듈을 관심사별로 분리하고, 환경별 설정을 격리하는 구조를 갖춘다:

terraform/
├── modules/ ← 재사용 가능한 팀 내부 모듈
│ ├── vpc/
│ │ ├── main.tf ← 리소스 정의 (로직)
│ │ ├── variables.tf ← 입력 변수 (API)
│ │ └── outputs.tf ← 출력 값 (반환값)
│ ├── eks/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── rds/
│ └── ...
├── environments/ ← 환경별 분리 (각자 독립 State)
│ ├── dev/
│ │ ├── main.tf ← module 조합
│ │ ├── terraform.tfvars ← 환경별 변수값
│ │ └── backend.tf ← dev용 Remote Backend
│ ├── staging/
│ │ └── ...
│ └── prod/
│ └── ...
└── shared/ ← 환경 공통 리소스 (Route53, ECR 등)
└── main.tf

각 환경이 독립적인 State 파일을 가지도록 분리하는 것이 핵심이다. dev의 State와 prod의 State를 공유하면 한 환경의 변경이 다른 환경에 영향을 줄 수 있어 위험하다.

EKS Blueprints for Terraform — AWS 공식 프로덕션 패턴

AWS는 프로덕션 수준 EKS 클러스터를 Terraform으로 구축하는 검증된 패턴 모음인 EKS Blueprints를 제공한다. ArgoCD, Karpenter, KEDA, AWS Load Balancer Controller 등 실무에서 필요한 Add-on이 모두 통합되어 있다:

# EKS Blueprints 사용 예시 — Karpenter + ArgoCD 포함 클러스터
module "eks_blueprints_addons" {
source = "aws-ia/eks-blueprints-addons/aws"
version = "~> 1.0"
cluster_name = module.eks.cluster_name
cluster_endpoint = module.eks.cluster_endpoint
cluster_version = module.eks.cluster_version
oidc_provider_arn = module.eks.oidc_provider_arn
enable_argocd = true # ArgoCD 설치
enable_karpenter = true # Karpenter 설치
enable_aws_load_balancer_controller = true
enable_metrics_server = true
}
# Blueprints apply 후 확인
$ kubectl get pods -A | grep -E "argocd|karpenter"
argocd argocd-server-xxx 1/1 Running 0 2m
argocd argocd-repo-server-xxx 1/1 Running 0 2m
karpenter karpenter-xxx 1/1 Running 0 2m
# → 한 번의 terraform apply로 ArgoCD + Karpenter가 모두 설치됨

Workspace — 동일 코드로 여러 환경 관리

Terraform Workspace를 사용하면 동일한 .tf 코드로 dev/staging/prod 환경을 분리해서 관리할 수 있다.

Terminal window
# 환경별 Workspace 생성 및 전환
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
terraform workspace select prod # prod 환경으로 전환
terraform apply # prod 환경에만 적용
# Workspace에 따라 다른 값 사용
locals {
env_config = {
dev = { instance_type = "t3.small", min_size = 1 }
staging = { instance_type = "t3.medium", min_size = 2 }
prod = { instance_type = "t3.large", min_size = 3 }
}
config = local.env_config[terraform.workspace]
}

Terraform Drift Detection — 인프라 드리프트 감지

누군가 AWS 콘솔에서 직접 리소스를 수정하면 Terraform State와 실제 인프라가 달라진다. 이것이 **Drift(드리프트)**다. 실제 환경에서는 긴급 상황에서 콘솔 수동 변경, AWS 서비스가 자동으로 생성하는 숨겨진 리소스 등 다양한 이유로 드리프트가 발생한다.

Terminal window
# Drift 감지 — terraform plan으로 현재 상태와 코드 비교
$ terraform plan -refresh-only
# → "~ aws_security_group.api: 콘솔에서 80 포트 룰이 추가됨" 같은 변경 사항 감지
# 또는 terraform refresh로 State를 실제 AWS 상태로 업데이트
$ terraform refresh

드리프트를 예방하는 근본적인 방법은 “Terraform으로 관리하는 리소스는 반드시 Terraform으로만 변경한다”는 팀 규칙이다. 긴급 상황에서 콘솔로 변경했다면, 이후 반드시 코드에도 반영한다.

Lifecycle Meta-Arguments — 리소스 동작을 세밀하게 제어하는 방법

Terraform이 리소스를 생성/수정/삭제할 때의 동작을 lifecycle 블록으로 제어할 수 있다. 프로덕션 환경에서는 거의 모든 중요 리소스에 lifecycle 설정이 필요하다.

비유: lifecycle은 “건축물 리모델링 규칙서”다. “이 벽은 절대 철거 금지(prevent_destroy)”, “리모델링 시 임시 건물을 먼저 짓고 이사 후 기존 건물 철거(create_before_destroy)”, “페인트 색은 시설 관리팀이 알아서 바꾸니 무시(ignore_changes)” 같은 규칙을 미리 정해놓는 것이다.

# lifecycle meta-arguments 4가지
resource "aws_rds_instance" "prod_db" {
identifier = "nestjs-prod-db"
engine = "postgres"
instance_class = "db.t3.medium"
lifecycle {
# 1. prevent_destroy: terraform destroy 시 이 리소스 삭제를 차단
# → "Error: Instance cannot be destroyed" 에러로 실수 방지
prevent_destroy = true
# 2. create_before_destroy: 변경 시 새 리소스를 먼저 만들고 기존 것을 삭제
# → 다운타임 없는 교체 (ALB, Launch Template 등에서 자주 사용)
create_before_destroy = true
# 3. ignore_changes: 특정 속성 변경을 Terraform이 무시
# → VPA가 자동 조정하는 값, 수동 태그 등에 사용
ignore_changes = [
tags["LastModifiedBy"], # 다른 시스템이 자동으로 수정하는 태그
]
# 4. precondition/postcondition: 리소스 생성 전후 조건 검증
precondition {
condition = var.environment != "prod" || var.multi_az == true
error_message = "프로덕션 RDS는 반드시 Multi-AZ로 설정해야 합니다."
}
}
}
# prevent_destroy가 설정된 리소스를 삭제하려 할 때
$ terraform destroy
│ Error: Instance cannot be destroyed
│ on main.tf line 5:
│ 5: resource "aws_rds_instance" "prod_db" {
│ Resource aws_rds_instance.prod_db has lifecycle.prevent_destroy set,
│ but the plan calls for this resource to be destroyed.
# → 프로덕션 DB가 실수로 삭제되는 것을 차단

📖 더 보기: Terraform Lifecycle Meta-Arguments 공식 문서 — 4가지 meta-argument의 동작 방식, precondition/postcondition 활용 예시

Import Block — 기존 리소스를 코드로 가져오기 (선언적 Import)

Terraform 1.5부터 import 블록을 .tf 파일에 선언해서 기존 AWS 리소스를 State에 등록할 수 있다. 기존 terraform import CLI 명령어보다 안전하고 코드 리뷰가 가능하다.

# 기존에 AWS 콘솔에서 만든 S3 버킷을 Terraform으로 가져오기
import {
to = aws_s3_bucket.legacy_logs
id = "my-existing-logs-bucket" # AWS 리소스의 실제 ID
}
# 가져올 리소스의 코드도 함께 작성해야 함
resource "aws_s3_bucket" "legacy_logs" {
bucket = "my-existing-logs-bucket"
}
# import 블록이 있는 상태에서 plan 실행
$ terraform plan
aws_s3_bucket.legacy_logs: Preparing import... [id=my-existing-logs-bucket]
aws_s3_bucket.legacy_logs: Refreshing state... [id=my-existing-logs-bucket]
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
# → import할 리소스를 미리 확인 가능 (기존 CLI import는 바로 실행)
$ terraform apply
aws_s3_bucket.legacy_logs: Importing... [id=my-existing-logs-bucket]
aws_s3_bucket.legacy_logs: Import complete [id=my-existing-logs-bucket]
Apply complete! Resources: 1 imported, 0 added, 0 changed, 0 destroyed.

📖 더 보기: Terraform Import Block 공식 문서 — 선언적 import의 문법, 기존 CLI import와의 차이, 복잡한 리소스 import 방법

Data Source — 기존 AWS 리소스 값을 코드에서 참조하는 방법

Terraform resource는 새 리소스를 만들지만, data(data source)는 이미 존재하는 리소스의 정보를 읽어오는 것이다. Terraform 외부에서 만들어진 리소스(수동 생성, 다른 팀의 Terraform State, AWS 기본 리소스 등)의 ID나 ARN을 하드코딩하지 않고 동적으로 가져올 때 사용한다.

비유: resource는 “새 부품을 주문”이고, data는 “창고에 이미 있는 부품의 사양서를 조회”하는 것이다.

# data source 예시 — 기존 VPC와 Subnet 정보 가져오기
data "aws_vpc" "existing" {
tags = {
Name = "my-production-vpc" # 태그로 기존 VPC를 찾아옴
}
}
data "aws_subnets" "private" {
filter {
name = "vpc-id"
values = [data.aws_vpc.existing.id] # 위에서 가져온 VPC ID 사용
}
tags = {
Tier = "private"
}
}
# EKS 클러스터를 기존 VPC의 프라이빗 Subnet에 생성
module "eks" {
source = "terraform-aws-modules/eks/aws"
vpc_id = data.aws_vpc.existing.id # 하드코딩 대신 data source 참조
subnet_ids = data.aws_subnets.private.ids
}
# 현재 AWS 계정 ID와 Region을 동적으로 가져오기 (자주 사용)
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
resource "aws_s3_bucket" "logs" {
# 계정 ID를 버킷 이름에 포함시켜 전 세계 유일성 보장
bucket = "myteam-logs-${data.aws_caller_identity.current.account_id}"
}
# data source 조회 결과 확인
$ terraform console
> data.aws_vpc.existing.id
"vpc-0a1b2c3d4e5f67890"
> data.aws_caller_identity.current.account_id
"123456789012"
> data.aws_region.current.name
"ap-northeast-2"

📖 더 보기: Terraform Data Sources 공식 문서 — data source 문법, 필터링 방법, remote state에서 output 값 가져오기(terraform_remote_state)

Terragrunt — 대규모 멀티 환경 Terraform 관리 도구

Terraform만으로 여러 환경(dev/staging/prod)과 여러 리전을 관리하면 코드 중복이 생긴다. 각 환경 디렉토리에 backend 설정, provider 설정, 공통 변수를 반복해서 작성해야 한다. Terragrunt는 이 반복을 제거하는 Terraform 래퍼(wrapper) 도구다.

비유: Terraform이 “레시피 책”이라면 Terragrunt는 “모든 주방에서 공통으로 쓰는 레시피 관리 시스템”이다. 공통 설정은 한 곳에 두고 각 주방(환경)은 차이점만 정의한다.

# Terragrunt 프로젝트 구조 — DRY(Don't Repeat Yourself) 원칙 적용
infra/
├── terragrunt.hcl ← 루트: Remote Backend, Provider 공통 설정 (1번만)
├── _modules/ ← 재사용 모듈 (Terraform 모듈과 동일)
│ ├── vpc/
│ ├── eks/
│ └── rds/
└── environments/
├── dev/
│ ├── vpc/
│ │ └── terragrunt.hcl ← "루트 설정 상속 + dev용 변수만 정의"
│ └── eks/
│ └── terragrunt.hcl
└── prod/
├── vpc/
│ └── terragrunt.hcl
└── eks/
└── terragrunt.hcl
# 루트 terragrunt.hcl — Remote Backend를 한 번만 정의 (모든 환경이 상속)
remote_state {
backend = "s3"
config = {
bucket = "myteam-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-state-lock"
encrypt = true
}
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
}
# AWS Provider도 한 번만 정의
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "ap-northeast-2"
}
EOF
}
# environments/prod/eks/terragrunt.hcl — prod 환경 EKS 설정만 정의
include "root" {
path = find_in_parent_folders() # 루트 terragrunt.hcl 자동 상속
}
terraform {
source = "../../../_modules/eks" # 공통 모듈 참조
}
# prod 환경에서만 다른 변수
inputs = {
cluster_name = "my-nestjs-cluster-prod"
cluster_version = "1.35"
min_size = 3
max_size = 10
instance_types = ["t3.large"]
}
Terminal window
# Terragrunt 명령어 — terraform과 동일한 문법, 앞에 terragrunt만 붙임
terragrunt plan # 현재 디렉토리 실행
terragrunt run-all plan # environments/ 하위 모든 모듈 동시 plan
terragrunt run-all apply --terragrunt-non-interactive # CI에서 전체 apply
# 의존 관계 지정 — VPC가 먼저 만들어진 후 EKS 생성
# environments/prod/eks/terragrunt.hcl 에 추가:
dependency "vpc" {
config_path = "../vpc"
mock_outputs = { vpc_id = "vpc-00000000" } # plan 시 mock 값으로 대체
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
}

📖 더 보기: Terragrunt 공식 문서 — DRY 원칙 적용, run-all 명령어, dependency 블록으로 모듈 간 의존 관계 관리

⚠️ State 파일에 시크릿이 포함될 수 있다

DB 비밀번호, API Key 등이 State에 평문으로 저장된다. State 파일은 반드시 암호화된 백엔드(S3 + 서버사이드 암호화)에 저장하고, Git에는 절대 커밋하지 않는다.

디버깅 — TF_LOG로 상세 로그 확인

Terraform이 예상과 다르게 동작할 때 TF_LOG 환경변수로 상세 로그를 볼 수 있다:

Terminal window
# DEBUG 레벨로 실행 — AWS API 호출 내용까지 모두 출력
TF_LOG=DEBUG terraform plan 2>&1 | head -100
# 로그 파일로 저장
TF_LOG=DEBUG TF_LOG_PATH=./terraform.log terraform apply

의존성 순환(Dependency Cycle) — Terraform이 거부하는 설계 패턴

Terraform은 리소스 간 의존 관계를 DAG로 관리하는데, 순환 의존(A → B → C → A)이 발생하면 어떤 리소스도 먼저 만들 수 없어 에러가 난다. 실무에서 가장 흔한 순환 의존은 Security Group 간 상호 참조다.

# ❌ 순환 의존 — terraform plan 에러 발생
resource "aws_security_group" "api" {
ingress {
security_groups = [aws_security_group.db.id] # db SG 참조
}
}
resource "aws_security_group" "db" {
ingress {
security_groups = [aws_security_group.api.id] # api SG 참조 → 순환!
}
}
# ✅ 해결 — aws_security_group_rule을 별도로 분리
resource "aws_security_group" "api" {}
resource "aws_security_group" "db" {}
resource "aws_security_group_rule" "api_to_db" {
type = "ingress"
security_group_id = aws_security_group.db.id
source_security_group_id = aws_security_group.api.id
from_port = 5432
to_port = 5432
protocol = "tcp"
}
resource "aws_security_group_rule" "db_to_api" {
type = "ingress"
security_group_id = aws_security_group.api.id
source_security_group_id = aws_security_group.db.id
from_port = 3000
to_port = 3000
protocol = "tcp"
}
# 순환 의존 에러 메시지
$ terraform plan
│ Error: Cycle: aws_security_group.api, aws_security_group.db
# → 두 SG가 서로를 참조해서 순환 발생
# DAG 시각화로 의존 관계 확인
$ terraform graph | dot -Tpng > graph.png
# → 순환이 있으면 그래프에 원형 경로가 보임

EKS Pod Identity — IRSA의 진화형, 2025년 권장 표준

Terraform으로 EKS 클러스터를 구축할 때 Pod가 AWS 리소스에 접근하는 권한을 부여하는 방식이 2024~2025년에 크게 변화했다. 기존 **IRSA(IAM Roles for Service Accounts)**는 OIDC Provider를 설정하고 Service Account에 IAM 역할을 어노테이션으로 연결하는 방식이었다. 새로운 EKS Pod Identity는 각 Node에서 실행되는 에이전트(Pod Identity Agent)가 자격증명을 대신 발급하는 방식이다.

비유: IRSA는 “직원이 회사 정문 RFID 카드를 발급받아 출입”하는 방식이고, Pod Identity는 “내부 경비원(에이전트)이 직원을 확인하고 출입증을 즉석에서 발급”하는 방식이다. 직원이 바뀌어도 경비원 시스템만 있으면 되니 운영이 훨씬 단순하다.

항목IRSAEKS Pod Identity
설정 복잡도OIDC Provider, Trust Policy 설정 필요에이전트 설치 후 바로 사용
멀티 클러스터클러스터마다 Trust Policy 재설정 필요같은 IAM Role을 여러 클러스터에서 재사용 가능
클러스터 외부 지원EKS Anywhere, 자체 관리 K8s에서도 사용 가능EKS 전용
2025년 권장기존 클러스터 유지신규 클러스터의 기본값
# Terraform으로 EKS Pod Identity 에이전트 설치 및 권한 설정
resource "aws_eks_addon" "pod_identity_agent" {
cluster_name = module.eks.cluster_name
addon_name = "eks-pod-identity-agent"
addon_version = "v1.3.4-eksbuild.1"
}
# Pod Identity 연결 — Nest.js API가 S3에 접근하는 예시
resource "aws_eks_pod_identity_association" "nestjs_s3_access" {
cluster_name = module.eks.cluster_name
namespace = "production"
service_account = "nestjs-api" # Kubernetes Service Account 이름
role_arn = aws_iam_role.nestjs_api.arn
}
# IAM Role — Trust Policy를 pods.eks.amazonaws.com으로 설정 (IRSA의 OIDC 대신)
data "aws_iam_policy_document" "pod_identity_assume" {
statement {
principals {
type = "Service"
identifiers = ["pods.eks.amazonaws.com"]
}
actions = ["sts:AssumeRole", "sts:TagSession"]
}
}
# Pod Identity 연결 확인
$ aws eks list-pod-identity-associations --cluster-name my-nestjs-cluster
{
"associations": [{
"namespace": "production",
"serviceAccount": "nestjs-api",
"roleArn": "arn:aws:iam::123456789012:role/nestjs-api-role"
}]
}
# → OIDC Provider 설정 없이 Pod가 IAM Role 권한을 사용할 수 있음

📖 더 보기: EKS Pod Identity vs IRSA 2025 비교 - KubeBlogs — 두 방식의 아키텍처 차이, 마이그레이션 고려사항 상세 분석

EKS 프로덕션 보안 설정 — Terraform으로 한 번에 적용

AWS EKS 보안 권장사항을 Terraform 코드로 반영하는 패턴이다. 콘솔에서 체크리스트를 확인하는 것보다 코드로 표준화해서 모든 환경에 일관되게 적용하는 것이 핵심이다.

module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.0"
cluster_name = "my-nestjs-cluster"
cluster_version = "1.35" # 2026년 기준 권장 버전
# 프라이빗 엔드포인트 — 제어 플레인에 외부 인터넷 접근 차단
cluster_endpoint_private_access = true
cluster_endpoint_public_access = false # VPN/Bastion 통해서만 접근
# Kubernetes Secrets 봉투 암호화 (KMS 사용)
cluster_encryption_config = {
provider_key_arn = aws_kms_key.eks.arn
resources = ["secrets"]
}
# 제어 플레인 로그 — CloudWatch로 전송
cluster_enabled_log_types = [
"api", "audit", "authenticator",
"controllerManager", "scheduler"
]
eks_managed_node_groups = {
default = {
# IMDSv2 강제 (IMDSv1 비활성화 → 인스턴스 메타데이터 보호)
metadata_options = {
http_endpoint = "enabled"
http_tokens = "required" # IMDSv2 강제
http_put_response_hop_limit = 1 # Pod가 노드 메타데이터 접근 차단
}
}
}
}
# 보안 설정 적용 확인
$ aws eks describe-cluster --name my-nestjs-cluster \
--query 'cluster.{endpoint:endpoint,logging:logging,encryptionConfig:encryptionConfig}'
{
"endpoint": "https://ABCDEF.gr7.ap-northeast-2.eks.amazonaws.com",
"logging": {
"clusterLogging": [{"enabled": true, "types": ["api","audit","authenticator","controllerManager","scheduler"]}]
},
"encryptionConfig": [{"provider": {"keyArn": "arn:aws:kms:..."}, "resources": ["secrets"]}]
}
# → 암호화, 로깅, 프라이빗 엔드포인트 모두 활성화 확인

IaC 도구 선택 매트릭스 — 팀 규모별 판단 기준

Terraform이 IaC의 표준이지만, 팀 규모·기술 스택·클라우드 환경에 따라 다른 도구가 더 적합한 경우가 있다.

상황추천 도구이유
팀 5명 이하, 단일 AWS스크립트(AWS CLI/CloudFormation)Terraform State 관리 오버헤드가 팀 규모 대비 과함
팀 10명 이하, 단일 클라우드Terraform (단독)학습 곡선·커뮤니티·Provider 생태계 최강
팀 10~50명, 멀티 환경Terraform + TerragruntDRY 원칙 유지, 환경별 State 격리
팀 50명+, 멀티 클라우드Pulumi 또는 Terraform + TerragruntPulumi: TypeScript/Python으로 사내 라이브러리 재사용; Terraform: 표준화된 HCL로 거버넌스
K8s 네이티브 플랫폼 팀CrossplaneK8s CRD로 AWS 리소스를 kubectl로 관리, GitOps 단일 컨트롤 플레인
일회성 프로토타입/PoCAWS 콘솔 or CDKState 파일 없이 빠르게 실험, 이후 폐기
개발자가 IaC를 직접 작성Pulumi익숙한 프로그래밍 언어(TypeScript/Python) 사용 가능

참고: AWS CDKTF(CDK for Terraform)는 2025년 12월 공식 deprecated + GitHub 저장소 archived됨. CDKTF 대신 Pulumi 또는 순수 HCL Terraform 사용을 권장.

Terraform을 쓰지 말아야 하는 상황 (Anti-recommendation)

Terraform이 항상 정답은 아니다. 다음 상황에서는 대안을 적극적으로 고려해야 한다:

  1. 일회성 프로토타입 / PoC: State 파일 초기화, Remote Backend 설정, .tfvars 관리 오버헤드가 “빠르게 만들고 폐기”하는 목적과 맞지 않는다. AWS 콘솔이나 CDK의 cdk deploy가 더 빠르다.

  2. 팀 5명 이하 소규모, 단일 서비스: State Lock 관리, Remote Backend S3 버킷 별도 운영, 팀원 전체 Terraform 학습 비용이 실익을 초과할 수 있다. CloudFormation이나 AWS CDK로 시작하고, 팀이 성장하면 Terraform으로 마이그레이션하는 경로가 현실적이다.

  3. K8s 네이티브 환경에서 앱 리소스 관리: EKS 위의 Kubernetes 오브젝트(Deployment, Service, ConfigMap)를 Terraform kubernetes_deployment 리소스로 관리하면 Argo CD/Flux의 지속 reconciliation 루프와 충돌한다 (VPA/HPA가 바꾼 값을 Terraform이 덮어쓰는 문제). K8s 오브젝트는 GitOps 도구에 맡기고 Terraform은 “EKS 클러스터 자체”만 관리하는 계층 분리가 권장된다.

  4. Crossplane이 도입된 K8s 환경: 이미 Crossplane으로 AWS RDS, S3 등을 CRD로 관리 중이면, Terraform을 함께 쓰면 두 도구가 같은 리소스를 관리하려 해서 State 충돌이 발생한다. 컨트롤 플레인을 하나로 통일하는 것이 원칙이다.

  5. BSL 라이선스 제약이 있는 환경: HashiCorp BSL(2023년 변경)은 Terraform을 기반으로 한 경쟁 서비스 제공을 금지한다. IaC 관리 SaaS를 직접 구축하거나 내부 플랫폼 팀이 외부 고객에게 Terraform-as-a-Service를 제공하는 경우라면 OpenTofu(MPL-2.0)로 전환이 필요하다.

2025~2026년 Terraform/IaC 생태계 변화

Terraform Stacks (GA) — 복잡한 인프라를 하나의 단위로 관리

Terraform Stacks는 VPC, EKS 클러스터, RDS, 애플리케이션 인프라처럼 여러 레이어로 구성된 인프라를 하나의 관리 단위(Stack)로 묶어서 배포하는 기능이다. 기존에는 여러 Terraform 디렉토리와 State를 각각 실행해야 했는데, Stacks는 이것을 하나의 워크플로로 오케스트레이션한다.

# Terraform Stacks 개념
Stack: "프로덕션 환경 전체"
├── Component: vpc (VPC + Subnet + SG)
├── Component: eks (EKS 클러스터) ← vpc에 의존
├── Component: rds (RDS 인스턴스) ← vpc에 의존
└── Component: app (EKS 앱 배포) ← eks, rds에 의존
# → 한 번의 apply로 전체 스택을 순서에 맞게 배포

OpenTofu vs Terraform — 2026년 선택 기준

HashiCorp가 2023년 Terraform의 라이선스를 BSL(Business Source License)로 변경한 이후, 오픈소스 포크인 OpenTofu가 CNCF 프로젝트로 편입되어 빠르게 성장했다. 2026년 기준으로 두 도구는 기본 문법과 사용법이 거의 동일하지만, 차이점이 생기기 시작했다:

항목Terraform (HashiCorp)OpenTofu (CNCF)
라이선스BSL (상업적 제한)MPL-2.0 (완전 오픈소스)
Ephemeral Resources미지원1.11+ 지원
Provider Iteration미지원1.9+ 지원
Terraform StacksHCP 유료 기능로드맵 검토 중
상태 파일 호환상호 호환상호 호환

OpenTofu의 주목할 기능 — Ephemeral Resources (임시 리소스)

OpenTofu 1.11에서 도입된 Ephemeral Resources는 State 파일에 저장되지 않는 임시 값을 처리하는 기능이다. AWS Secrets Manager의 비밀번호처럼 Terraform apply 실행 중에만 필요하고 State에 남기면 안 되는 데이터를 안전하게 처리할 수 있다.

# Ephemeral Resource 예시 — DB 비밀번호를 State에 저장하지 않고 사용
ephemeral "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/nestjs-api/db-password"
}
resource "aws_db_instance" "main" {
password = ephemeral.aws_secretsmanager_secret_version.db_password.secret_string
# → State 파일에 비밀번호가 저장되지 않음 (보안 강화)
}

📖 더 보기: OpenTofu vs Terraform 2026 비교 - DEV Community — 라이선스 차이, 기능 비교, 마이그레이션 고려사항 상세 정리

  • VPC, Subnet, Security Group 등 네트워크 인프라 코드화
  • ECS/EKS 클러스터 및 서비스 배포 자동화
  • RDS, S3, CloudWatch 등 AWS 리소스 일관된 생성
  • dev/staging/prod 환경을 동일한 코드로 복제
  • CI/CD 파이프라인에서 terraform plan → 리뷰 → terraform apply
  • EKS Blueprints로 ArgoCD, Karpenter 등 Add-on이 포함된 프로덕션 클러스터 한 번에 구축
  • 팀 인프라가 수동으로 만들어져 있다면, Terraform으로 코드화하는 것이 플랫폼 엔지니어링의 핵심 작업
  • 새 환경(스테이징 등) 생성 요청 시 Terraform 모듈로 빠르게 복제 가능
  • 인프라 변경의 리뷰(PR 기반) → 사고 방지
  • “지금 인프라가 어떻게 되어 있는지” 코드로 파악 가능
  • EKS 클러스터 프로비저닝 시 terraform-aws-modules/eks로 팀 표준 클러스터 생성
  • EKS Blueprints로 Karpenter + ArgoCD + 모니터링 스택을 한 번에 설치
개념 A개념 B차이점
TerraformCloudFormationTerraform은 멀티 클라우드, CloudFormation은 AWS 전용
TerraformAnsibleTerraform은 인프라 프로비저닝, Ansible은 서버 설정/구성 관리
terraform planterraform applyplan은 미리보기(변경 없음), apply는 실제 실행
Local StateRemote StateLocal은 내 PC에 저장, Remote는 S3 등에 팀 공유
OpenTofuTerraformOpenTofu는 Terraform의 오픈소스 포크 (라이선스 변경 후 등장)
Workspace별도 디렉토리Workspace는 같은 코드 다른 State, 디렉토리 분리는 코드 자체를 분리

🔧 Error acquiring the state lock — State 파일이 잠겨서 apply 실패

섹션 제목: “🔧 Error acquiring the state lock — State 파일이 잠겨서 apply 실패”

증상: terraform plan 또는 terraform apply 실행 시 “Error acquiring the state lock” 에러와 함께 Lock ID가 출력됨

원인: 이전에 실행한 terraform apply가 중간에 강제 종료(파이프라인 타임아웃, 네트워크 단절 등)되면서 DynamoDB의 Lock이 해제되지 않고 남아 있는 경우

해결:

  1. 다른 팀원이 현재 apply 중인지 먼저 확인 (실행 중이면 기다려야 함)
  2. 정말 아무도 실행 중이 아닐 때만 강제 해제:
    Terminal window
    terraform force-unlock <Lock-ID>
    # 예: terraform force-unlock a1b2c3d4-e5f6-7890-abcd-ef1234567890
  3. 또는 AWS 콘솔 → DynamoDB → 테이블(terraform-state-lock) → 해당 Lock 항목 직접 삭제
  4. 주의: 실제 apply가 실행 중일 때 force-unlock하면 State 파일이 꼬일 수 있음

🔧 Error: Unsupported argument 또는 This object does not have an attribute — 코드와 Provider 버전 불일치

섹션 제목: “🔧 Error: Unsupported argument 또는 This object does not have an attribute — 코드와 Provider 버전 불일치”

증상: terraform plan 실행 시 “Unsupported argument” 또는 “An argument named X is not expected here” 에러

원인: AWS Provider 버전이 바뀌면서 리소스의 속성(argument) 이름이 변경되거나 deprecated된 경우. 또는 .terraform.lock.hcl에 고정된 버전과 실제 코드가 요구하는 버전이 다른 경우

해결:

  1. terraform init -upgrade로 Provider를 최신 버전으로 업그레이드
  2. Terraform AWS Provider Upgrade Guides 에서 breaking change 확인
  3. 에러 메시지에서 정확한 attribute 이름 확인 후 .tf 파일 수정
# 에러 예시
│ Error: Unsupported argument
│ on main.tf line 12, in resource "aws_s3_bucket" "logs":
│ 12: acl = "private"
│ An argument named "acl" is not expected here. Did you mean to
│ use the "aws_s3_bucket_acl" resource?
# → AWS Provider v4에서 acl 속성이 별도 리소스로 분리됨

🔧 State와 실제 AWS 리소스 불일치 — “Resource already exists” 또는 삭제된 리소스가 State에 남아 있음

섹션 제목: “🔧 State와 실제 AWS 리소스 불일치 — “Resource already exists” 또는 삭제된 리소스가 State에 남아 있음”

증상 A: terraform apply 시 “ResourceAlreadyExistsException” 에러 — AWS 콘솔에서는 리소스가 있는데 Terraform은 새로 만들려 함

증상 B: terraform plan에서 이미 AWS에서 수동으로 삭제한 리소스를 다시 만들려 함

원인: AWS 콘솔에서 직접 리소스를 만들었거나 삭제했을 때 State 파일이 갱신되지 않아 불일치 발생

해결:

  • 증상 A (AWS에 있지만 State에 없음): terraform import로 기존 리소스를 State에 등록
    Terminal window
    terraform import aws_s3_bucket.logs my-team-logs-2026
  • 증상 B (State에 있지만 AWS에 없음): terraform state rm으로 State에서 해당 항목 제거
    Terminal window
    terraform state rm aws_s3_bucket.logs
  • 근본 해결: Terraform으로 관리하는 리소스는 반드시 Terraform으로만 변경한다

🔧 BucketAlreadyExists — S3 버킷 이름 충돌

섹션 제목: “🔧 BucketAlreadyExists — S3 버킷 이름 충돌”

증상: terraform apply 시 “BucketAlreadyExists” 에러로 S3 버킷 생성 실패

원인: S3 버킷 이름은 전 세계 AWS 계정에서 유일해야 한다. 다른 계정에서 이미 사용 중인 이름을 지정하면 실패한다. 초보자들이 my-app-logs 같은 일반적인 이름을 사용할 때 자주 발생

해결:

  1. 팀 이름 + 환경 + AWS 계정 ID 조합으로 고유한 이름 사용:
    resource "aws_s3_bucket" "logs" {
    bucket = "myteam-prod-logs-${data.aws_caller_identity.current.account_id}"
    }
  2. 또는 random_id 리소스로 고유 접미사 자동 생성

🔧 Plan is stale — plan 결과가 만료됨

섹션 제목: “🔧 Plan is stale — plan 결과가 만료됨”

증상: CI 파이프라인에서 terraform plan을 먼저 실행하고 승인 후 terraform apply를 실행하는데 “Plan is stale” 에러가 남

원인: plan을 실행한 이후 다른 사람이 apply를 해서 State가 변경된 경우, 이전 plan 결과는 더 이상 유효하지 않음

해결:

  1. terraform apply를 plan 결과 파일과 함께 실행:
    Terminal window
    terraform plan -out=tfplan # plan 결과를 파일로 저장
    terraform apply tfplan # 저장된 plan 결과로 apply (State 변경 감지)
  2. 파이프라인 승인 시간을 짧게 유지하거나, State Lock으로 동시 실행 방지

🔧 VPA 설정과 Terraform이 충돌 — 리소스 설정이 계속 원복됨

섹션 제목: “🔧 VPA 설정과 Terraform이 충돌 — 리소스 설정이 계속 원복됨”

증상: EKS에 VPA(Vertical Pod Autoscaler)를 사용 중인데, terraform apply 할 때마다 Pod의 CPU/메모리 requests 값이 Terraform 코드의 값으로 덮어써짐

원인: VPA가 런타임에 Pod의 resource requests를 자동으로 조정하는데, Terraform apply 시 Deployment 스펙이 코드에 정의된 값으로 다시 설정됨. 두 시스템이 같은 값을 경쟁적으로 수정하는 충돌 상황

해결:

  1. Terraform 코드의 Deployment 리소스 정의에서 resources 블록을 lifecycle.ignore_changes로 무시:
    resource "kubernetes_deployment" "nestjs_api" {
    # ...
    lifecycle {
    ignore_changes = [
    spec[0].template[0].spec[0].container[0].resources
    ]
    }
    }
  2. 또는 Terraform으로 Kubernetes 오브젝트를 직접 관리하지 않고, ArgoCD + GitOps로 Deployment를 관리하며 Terraform은 EKS 클러스터 프로비저닝에만 사용

🔧 EKS 클러스터 업그레이드 후 Node Group이 UPDATE_FAILED 상태

섹션 제목: “🔧 EKS 클러스터 업그레이드 후 Node Group이 UPDATE_FAILED 상태”

증상: terraform apply로 EKS 클러스터 버전을 1.31 → 1.32로 업그레이드했는데, Node Group이 UPDATE_FAILED 상태로 멈추고 기존 Pod들이 Pending으로 전환됨

원인: EKS 클러스터 업그레이드는 제어 플레인 먼저, Node Group은 별도로 업그레이드해야 한다. Terraform이 두 리소스를 같은 apply에서 변경하면 순서 문제가 발생할 수 있다. 또한 Node Group 업그레이드 중 PodDisruptionBudget(PDB)이 너무 엄격하게 설정되어 있으면 Node Drain이 막혀서 업그레이드가 멈춘다.

해결:

  1. 클러스터 버전과 Node Group 버전을 분리해서 단계별 apply:

    Terminal window
    # 1단계: 제어 플레인만 업그레이드
    terraform apply -target=module.eks.aws_eks_cluster.this
    # 2단계: 제어 플레인 업그레이드 완료 확인 후 Node Group 업그레이드
    terraform apply -target=module.eks.aws_eks_node_group.workers
  2. PDB 확인: kubectl get pdb -AALLOWED DISRUPTIONS가 0이면 업그레이드 차단됨

  3. 업그레이드 완료 후 Node Group 상태 확인: aws eks list-nodegroups --cluster-name my-cluster

# UPDATE_FAILED 상태 확인
$ aws eks describe-nodegroup \
--cluster-name my-nestjs-cluster \
--nodegroup-name default \
--query 'nodegroup.{status:status,health:health}'
{
"status": "UPDATE_FAILED",
"health": {
"issues": [{
"code": "NodeCreationFailure",
"message": "Unhealthy nodes in the NodeGroup"
}]
}
}
# → 노드 생성 실패 → 로그에서 원인 확인 후 -target으로 단계별 재적용

🔧 terraform destroy로 의도치 않게 프로덕션 리소스 삭제

섹션 제목: “🔧 terraform destroy로 의도치 않게 프로덕션 리소스 삭제”

증상: 개발 환경을 정리하려다가 Workspace를 바꾸지 않은 채 terraform destroy를 실행해서 프로덕션 리소스가 삭제됨

원인: Terraform은 확인 없이 yes를 입력하면 State에 기록된 모든 리소스를 삭제한다. Workspace 또는 작업 디렉토리를 잘못 선택한 경우 발생

해결 (사전 방지):

  1. 중요한 리소스에는 prevent_destroy lifecycle 설정:
    resource "aws_rds_instance" "prod_db" {
    # ...
    lifecycle {
    prevent_destroy = true # terraform destroy 시 에러 발생 → 삭제 차단
    }
    }
  2. CI/CD 파이프라인에서는 terraform destroy를 별도 승인 단계로 분리
  3. apply/destroy 전에 terraform workspace show로 현재 Workspace 확인하는 스크립트 추가
  • IaC가 뭔지, 왜 콘솔 수동 작업보다 나은지 설명할 수 있다
  • Terraform의 Init → Plan → Apply 워크플로를 설명할 수 있다
  • State 파일의 역할과 Remote Backend가 필요한 이유를 설명할 수 있다
  • HCL로 간단한 AWS 리소스(S3 버킷 등)를 정의하는 코드를 읽을 수 있다
  • Module이 뭔지, 왜 사용하는지 설명할 수 있다
  • DAG가 리소스 생성 순서를 어떻게 결정하는지 설명할 수 있다
  • Workspace로 환경을 분리하는 방법을 설명할 수 있다
  • Drift가 무엇이고 어떻게 감지/예방하는지 설명할 수 있다
  • terraform import와 terraform state rm이 각각 언제 필요한지 설명할 수 있다
  • lifecycle meta-arguments 4가지(prevent_destroy, create_before_destroy, ignore_changes, precondition)를 설명할 수 있다
  • import 블록으로 기존 리소스를 선언적으로 가져오는 방법을 설명할 수 있다
  • 의존성 순환(Dependency Cycle)이 발생하는 원인과 해결 방법을 설명할 수 있다
  • data source가 resource와 무엇이 다른지 설명할 수 있다
  • data.aws_caller_identity.current.account_id를 쓰는 이유를 설명할 수 있다
  • Terragrunt가 Terraform 단독 사용과 비교해 어떤 문제를 해결하는지 설명할 수 있다

OpenTofu, Terraform Cloud, Terragrunt, Terraform Module Registry, tfvars, data source, output, 변수(variable), Drift Detection, Import, Workspace, Karpenter, terraform-aws-modules, EKS Blueprints, prevent_destroy, lifecycle ignore_changes

  • Terraform 설치 후 terraform version으로 확인

    $ terraform version
    Terraform v1.10.3
    on darwin_arm64
  • AWS Provider 설정 + S3 버킷 하나를 만드는 .tf 파일 작성 → terraform initterraform plan 실행

    $ terraform init
    Initializing the backend...
    Initializing provider plugins...
    - Finding hashicorp/aws versions matching "~> 5.0"...
    - Installing hashicorp/aws v5.89.0...
    Terraform has been successfully initialized!
    $ terraform plan
    Plan: 1 to add, 0 to change, 0 to destroy.
  • terraform plan 출력에서 + create, ~ update, - destroy 표시 읽어보기

    # + 는 새로 생성, ~ 는 수정, - 는 삭제
    + resource "aws_s3_bucket" "logs" { ... } ← 생성 예정
    ~ resource "aws_vpc" "main" { ... } ← 수정 예정
    - resource "aws_subnet" "old" { ... } ← 삭제 예정
  • terraform state list로 현재 State에 기록된 리소스 목록 확인

    $ terraform state list
    aws_s3_bucket.logs
    aws_vpc.main
    aws_subnet.public_a
    aws_subnet.public_c
  • 팀 인프라 중 Terraform으로 관리되는 리포가 있는지 확인하고 .tf 파일 구조 파악

  • TF_LOG=DEBUG terraform plan 2>&1 | head -50으로 내부 동작 로그 직접 확인

  • AWS 콘솔에서 Terraform이 관리하는 태그 없는 리소스를 찾아보고, terraform import로 State에 등록해보기

    # 기존 S3 버킷을 Terraform State에 등록
    $ terraform import aws_s3_bucket.legacy my-existing-bucket-name
    aws_s3_bucket.legacy: Importing from ID "my-existing-bucket-name"...
    aws_s3_bucket.legacy: Import prepared!
    aws_s3_bucket.legacy: Refreshing state... [id=my-existing-bucket-name]
    Import successful!
  • LocalStack으로 로컬 AWS 시뮬레이션 → VPC → Subnet → SG 순서 실습 (AWS 계정 없이 가능)

    Terminal window
    # 1. LocalStack 설치 및 실행 (Docker 필요)
    pip install localstack awscli-local
    docker run -d -p 4566:4566 localstack/localstack
    # 2. tflocal 래퍼 설치 — endpoint를 자동으로 LocalStack으로 바꿔줌
    pip install terraform-local
    # main.tf — LocalStack용 Provider (endpoint 지정 불필요, tflocal이 자동 처리)
    terraform {
    required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
    }
    }
    provider "aws" {
    region = "us-east-1"
    access_key = "test" # LocalStack은 임의 값 허용
    secret_key = "test"
    skip_credentials_validation = true
    skip_metadata_api_check = true
    skip_requesting_account_id = true
    }
    resource "aws_vpc" "lab" { cidr_block = "10.0.0.0/16" }
    resource "aws_subnet" "public" {
    vpc_id = aws_vpc.lab.id
    cidr_block = "10.0.1.0/24"
    availability_zone = "us-east-1a"
    }
    resource "aws_security_group" "web" {
    vpc_id = aws_vpc.lab.id
    ingress {
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    }
    egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    }
    }
    Terminal window
    # 3. tflocal로 init → plan → apply → state list
    tflocal init
    tflocal plan # → 3 to add
    tflocal apply -auto-approve
    tflocal state list
    # 예상 출력:
    # aws_vpc.lab
    # aws_subnet.public
    # aws_security_group.web
    # 4. awslocal(LocalStack용 AWS CLI)로 실제 생성 확인
    awslocal ec2 describe-vpcs --query 'Vpcs[*].{VpcId:VpcId,Cidr:CidrBlock}'
    # → [{"VpcId": "vpc-...", "Cidr": "10.0.0.0/16"}]

    실습 포인트: VPC가 없으면 Subnet을 만들 수 없고, Subnet이 없으면 SG에 VPC가 필요하다. Terraform이 DAG로 순서를 자동 결정하는 것을 tflocal plan 출력에서 확인할 수 있다. 로컬 실습이므로 AWS 비용 발생 없음.

  • terraform plan -refresh-only로 콘솔 수동 변경으로 인한 드리프트를 감지해보기

    $ terraform plan -refresh-only
    # AWS 콘솔에서 Security Group 룰 하나 추가 후 실행
    ~ resource "aws_security_group" "api" {
    ~ ingress = [
    + {
    + from_port = 8080
    + to_port = 8080
    + protocol = "tcp"
    }
    ]
    }
    # → 코드에는 없는 8080 포트가 실제 SG에 추가된 것을 감지
  1. Terraform은 인프라를 코드(HCL)로 정의해서 재현 가능하고 버전 관리되게 만드는 IaC 도구이다
  2. Init → Plan → Apply 워크플로로 안전하게 인프라를 변경한다 (plan은 비행 전 체크리스트)
  3. State 파일로 코드와 실제 인프라의 차이를 추적하며, 팀 작업 시 S3+DynamoDB Remote Backend 필수이다
  4. Module과 EKS Blueprints로 프로덕션 EKS + 애드온 스택을 한 번에 표준화해서 구축할 수 있다
  5. Drift Detection + prevent_destroy로 콘솔 수동 변경을 감지하고 중요 리소스를 실수 삭제로부터 보호한다
terraform plan 실행 흐름
├── terraform.tfstate 읽기 (Terraform이 알고 있는 현재 상태)
├── AWS API 호출 → 실제 현재 상태 확인 (Refresh)
├── .tf 파일의 원하는 상태와 비교
├── DAG 생성 (리소스 의존 순서: VPC → Subnet → SG → EKS)
└── diff 출력 (+ 생성 / ~ 수정 / - 삭제)
State 관리 패턴
├── Remote Backend: S3(파일 저장) + DynamoDB(Lock — 동시 실행 방지)
└── 환경별 분리: environments/dev/, /staging/, /prod/ 각각 독립 State
Drift 발생 → terraform plan -refresh-only로 감지
└── 예방: "Terraform 관리 리소스는 Terraform으로만 변경" 팀 규칙
Lifecycle 제어
├── prevent_destroy: 중요 리소스(RDS, S3) 삭제 차단
├── create_before_destroy: 다운타임 없는 리소스 교체
├── ignore_changes: 외부 시스템이 자동 수정하는 속성 무시
└── import 블록: 기존 AWS 리소스를 코드로 가져오기 (plan으로 미리 확인 가능)
2025~2026 생태계 분기
├── Terraform (HashiCorp/BSL): Terraform Stacks GA, Terraform Cloud
└── OpenTofu (CNCF/MPL-2.0): Ephemeral Resources, Provider Iteration
→ State 파일 상호 호환, 기본 문법 동일
  • “Terraform과 CloudFormation의 차이?” → Terraform은 멀티 클라우드(AWS+GCP+Azure), CloudFormation은 AWS 전용. Terraform은 Provider 생태계가 훨씬 넓음
  • “State 파일을 왜 Git에 커밋하면 안 되나?” → DB 비밀번호 등 시크릿이 평문으로 포함되어 있고, 팀 여러 명이 각자 다른 State를 갖게 되어 apply 시 충돌 발생
  • “terraform plan과 apply 중 어느 것이 더 위험한가?” → apply. plan은 읽기만 하지만 apply는 실제 리소스를 생성/수정/삭제. 프로덕션 apply는 반드시 plan 결과를 먼저 리뷰해야 함
  • “Workspace와 디렉토리 분리의 차이?” → Workspace는 같은 코드 + 다른 State, 디렉토리 분리는 코드 자체를 분리. 프로덕션은 환경 격리가 강한 디렉토리 분리가 권장됨
  • “lifecycle의 prevent_destroy와 create_before_destroy는 언제 쓰나?” → prevent_destroy는 RDS, S3 같은 데이터 리소스 실수 삭제 방지. create_before_destroy는 ALB, Launch Template 등 교체 시 다운타임 방지
  • “Dependency Cycle이 발생하면 어떻게 해결하나?” → Security Group 간 상호 참조가 대표적. SG를 먼저 만들고 Rule을 별도 리소스로 분리하면 순환이 끊김
  • Remote Backend(S3 + DynamoDB)를 설정하고 S3 버킷에 버전 관리와 암호화를 켰는가?
  • 중요 리소스(RDS, EKS 등)에 lifecycle { prevent_destroy = true }를 설정했는가?
  • CI/CD에서 terraform plan -out=tfplan → 리뷰 → terraform apply tfplan 순서로 실행하는가?
  • State 파일을 Git에 커밋하지 않도록 .gitignore*.tfstate*를 추가했는가?
  • Provider 버전을 .terraform.lock.hcl로 고정했는가? (팀원 모두 같은 버전 사용)
  • 환경별(dev/staging/prod) State를 독립적으로 분리했는가?
  • terraform plan -refresh-only를 주기적으로 실행해서 드리프트를 모니터링하는가?
  • 중요 리소스에 lifecycle { prevent_destroy = true }를 설정했는가?
  • precondition으로 프로덕션 환경의 필수 조건(Multi-AZ 등)을 코드로 검증하는가?

최종 수정: 2026-04-01