들어가며: Terraform의 진짜 힘
한 줄 명령의 복잡성
$ terraform apply
이 명령 하나로 Terraform은:
1. **HCL 파일들을 파싱** (*.tf).
2. 모든 리소스 정의를 **DAG (Directed Acyclic Graph)** 로 변환.
3. **State file**에서 현재 상태 로드.
4. **Provider**를 통해 클라우드에 실제 상태 조회 (refresh).
5. Desired vs actual 차이를 **plan**으로 표시.
6. 의존성 순서대로 **concurrent apply**.
7. State file 업데이트.
이 모든 것이 **수 분** 안에 일어난다. AWS, GCP, Azure, Kubernetes, DataDog, GitHub — **2000개 이상의 provider**가 같은 모델을 따른다.
Terraform이 바꾼 것
2014년 HashiCorp가 발표한 Terraform은 **인프라 관리의 패러다임**을 바꿨다:
**Before Terraform**:
- 웹 콘솔에서 수동 클릭.
- 스크립트로 `aws ec2 run-instances`.
- 상태 추적 불가.
- Reproducibility 없음.
- Drift 발견 어려움.
**After Terraform**:
- **선언형 HCL**로 원하는 상태 기술.
- **Git에 commit**되는 인프라.
- **Plan**으로 변경 사항 미리 확인.
- **State**로 정확한 추적.
- **Provider**로 표준 인터페이스.
2023년의 충격: BSL 라이선스
2023년 8월, HashiCorp가 Terraform 라이선스를 **BSL (Business Source License)** 로 변경. 오픈소스 커뮤니티에 큰 충격. 곧이어 **OpenTofu**가 fork로 출범. Linux Foundation 산하.
**이 글은 Terraform과 OpenTofu 둘 다**에 적용된다 (내부 구조는 거의 동일).
이 글에서 다룰 것
1. **HCL**: Terraform의 DSL.
2. **DAG**: 의존성 그래프.
3. **State**: 진실의 원천.
4. **Provider**: 클라우드와의 인터페이스.
5. **Plan**: 변경 미리보기.
6. **Apply**: 실제 변경 실행.
7. **Modules**: 재사용 단위.
8. **Workspaces**: 환경 분리.
9. **Backend**: State 저장.
10. **Drift Detection**: 변경 감지.
1. HCL: Terraform의 언어
HCL이란
**HCL (HashiCorp Configuration Language)** 은 JSON/YAML과 JavaScript 사이의 중간 언어. **선언적이면서도 프로그래밍 가능**.
변수
variable "region" {
type = string
default = "us-east-1"
}
리소스
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "WebServer"
}
}
Output
output "instance_ip" {
value = aws_instance.web.public_ip
}
HCL vs JSON vs YAML
**JSON**:
{
"resource": {
"aws_instance": {
"web": {
"ami": "ami-0c55b159cbfafe1f0",
"instance_type": "t3.micro"
}
}
}
}
- 기계 친화적.
- 주석 없음.
- 문법 엄격.
**YAML**:
- 사람 친화적.
- **들여쓰기 기반** (위험).
- 기호 많음.
**HCL**:
- 사람 친화적 + 기계 친화적.
- **주석 있음**.
- 함수, 조건, 반복 지원.
- 타입 시스템.
HCL의 주요 구조
**Block**: `type "label1" "label2" { ... }`
resource "aws_instance" "web" {
...
}
provider "aws" {
...
}
variable "count" {
...
}
**Expressions**: 값 계산.
count = 3
name = "server-${count.index}"
ips = [for i in range(3) : cidrhost("10.0.0.0/24", i)]
**Functions**: 내장 함수.
length(var.list)
format("hello-%s", var.name)
jsonencode({foo = "bar"})
조건, 반복
**조건**:
resource "aws_instance" "web" {
instance_type = var.env == "prod" ? "t3.large" : "t3.micro"
}
**반복 (count)**:
resource "aws_instance" "web" {
count = 3
ami = "ami-..."
}
web[0], web[1], web[2]
**반복 (for_each)**:
resource "aws_instance" "web" {
for_each = {
web1 = "t3.micro"
web2 = "t3.small"
web3 = "t3.medium"
}
ami = "ami-..."
instance_type = each.value
tags = {
Name = each.key
}
}
web["web1"], web["web2"], ...
**for_each vs count**: `for_each`는 **map/set 기반**이라 안정적. `count`는 인덱스 기반이라 **삭제 시 혼란**.
표현식의 평가
HCL 표현식은 **lazy 평가**:
- 참조가 있을 때만 평가.
- 순환 참조는 에러.
- 의존성 자동 추적.
**예**:
resource "aws_instance" "web" {
ami = "ami-..."
}
resource "aws_eip" "web_ip" {
instance = aws_instance.web.id # 이 참조가 의존성 생성
}
Terraform이 이를 분석해:
- `web_ip`는 `web`에 의존.
- 먼저 `web` 생성, 그 다음 `web_ip`.
이 의존성이 **DAG**를 만든다.
2. DAG: 의존성 그래프
모든 것은 그래프다
Terraform의 핵심: **DAG (Directed Acyclic Graph)**. 모든 리소스와 모듈, 변수가 **노드**. 참조가 **엣지**.
**예시**:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
}
**DAG**:
aws_vpc.main
/ | \
↓ ↓ ↓
aws_subnet aws_igw (참조)
.public .igw ↓
└────────→aws_route_table.public
`aws_vpc.main`은 다른 모든 리소스의 **부모**. 다른 리소스들은 VPC가 먼저 생성되어야.
DAG의 역할
**1. Concurrency**:
독립적인 리소스는 **병렬 생성**:
resource "aws_instance" "web1" {
ami = "ami-..."
}
resource "aws_instance" "web2" {
ami = "ami-..."
}
`web1`과 `web2`는 서로 의존성 없음 → **동시에 생성**.
**기본 concurrency**: `-parallelism=10` (동시 10개). 조정 가능.
**2. Ordering**:
의존성 있는 리소스는 **순서대로**:
VPC 생성 → Subnet 생성 → Instance 생성
**3. Cycle 감지**:
순환 참조는 에러:
resource "a" "x" {
b = b.y.id
}
resource "b" "y" {
a = a.x.id # 순환!
}
**에러**: `Error: Cycle in dependency graph`.
Graph 시각화
terraform graph | dot -Tpng > graph.png
`dot`은 GraphViz 도구. PNG 이미지로 DAG 시각화.
의존성 종류
**1. Explicit Reference (암시적)**:
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id # 자동 감지
}
**2. Explicit Dependency (명시적)**:
resource "aws_instance" "web" {
...
depends_on = [aws_security_group.web]
}
드물게 필요. 일반적으로 reference로 충분.
Graph Walker
Terraform이 DAG를 순회하는 로직:
1. **Leaf nodes 식별**: 들어오는 엣지 없는 노드.
2. **Concurrent execution**: 현재 준비된 노드 모두 실행.
3. **완료 대기**.
4. **의존자로 이동**: 새로 "준비된" 노드.
5. **반복**.
이는 **Topological sort**의 변형.
3. State: 진실의 원천
State란
**Terraform state**는 관리하는 리소스의 **현재 상태** 기록. JSON 파일.
**역할**:
- **Resource → Cloud object 매핑**: `aws_instance.web` → `i-0123456789`.
- **의존성 추적**: 어떤 리소스가 어떤 것에 의존하나.
- **Drift detection**: 마지막 상태와 실제 상태 비교.
- **Performance**: 매번 모든 것을 조회하지 않음.
- **Metadata**: 민감 정보 포함 가능.
State 파일 예시
{
"version": 4,
"terraform_version": "1.6.0",
"serial": 42,
"lineage": "abc-123-xyz",
"outputs": {
"instance_ip": {
"value": "54.123.45.67",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "web",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 1,
"attributes": {
"id": "i-0123456789abcdef0",
"ami": "ami-0c55b159cbfafe1f0",
"instance_type": "t3.micro",
"tags": {
"Name": "WebServer"
}
},
"dependencies": ["aws_vpc.main"]
}
]
}
]
}
**주요 필드**:
- `serial`: 버전 번호. 매 변경마다 증가.
- `lineage`: state의 UUID. 여러 state 혼동 방지.
- `resources[].instances[].attributes`: **리소스의 전체 속성**.
Local vs Remote State
**Local state** (`terraform.tfstate`):
- 기본값.
- 로컬 파일.
- **Git에 commit 금지**. 민감 정보 포함.
**Remote state**:
- 백엔드에 저장.
- 팀 협업 가능.
- **Locking**: 동시 수정 방지.
- **Versioning**: 이전 상태 복구.
Backend
**Backend**: State가 저장되는 곳.
**S3 + DynamoDB (AWS 표준)**:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
- **S3**: State file 저장. 버저닝 + 암호화.
- **DynamoDB**: Lock 관리. 동시 apply 방지.
**GCS**:
terraform {
backend "gcs" {
bucket = "my-terraform-state"
prefix = "prod"
}
}
**Azure**:
terraform {
backend "azurerm" {
resource_group_name = "tfstate"
storage_account_name = "tfstate"
container_name = "tfstate"
key = "prod.terraform.tfstate"
}
}
**Terraform Cloud / HCP Terraform**:
terraform {
cloud {
organization = "my-org"
workspaces {
name = "prod"
}
}
}
State Locking
**문제**: 두 사람이 동시에 `terraform apply` → race condition → state 손상.
**해결**: **Lock**. Apply 시작 시 lock 획득. 끝나면 해제.
**DynamoDB lock**:
{
"LockID": "my-terraform-state/prod/terraform.tfstate-md5",
"Info": "...",
"Operation": "OperationTypeApply",
"Who": "alice@laptop.local",
"Version": "1.6.0",
"Created": "2025-04-15T10:00:00Z"
}
다른 사람이 시도하면:
Error: Error acquiring the state lock
Lock Info:
ID: abc-123
Path: my-terraform-state/prod/terraform.tfstate
Operation: OperationTypeApply
Who: alice@laptop.local
**Force unlock** (긴급):
terraform force-unlock abc-123
**주의**: 실제 apply가 진행 중이면 state 손상 위험.
State의 민감성
State 파일에는 **민감 정보**가 포함될 수 있다:
- 데이터베이스 암호.
- API 키.
- 인증서 내용.
- Sensitive variables.
**Git에 절대 commit 금지**. Remote backend 사용 + 암호화 필수.
**State 보안**:
민감한 값 보기
terraform output -json
**Sensitive output**:
output "db_password" {
value = aws_db_instance.main.password
sensitive = true
}
CLI에 안 보이지만 state 파일엔 저장됨.
State 조작 명령어
**`terraform state list`**: 리소스 목록.
terraform state list
aws_vpc.main
aws_subnet.public
aws_instance.web
**`terraform state show`**: 리소스 상세.
terraform state show aws_instance.web
**`terraform state mv`**: 리소스 이동 (rename, module 이동).
terraform state mv aws_instance.web aws_instance.web_server
**`terraform state rm`**: State에서 제거 (실제 리소스는 남음).
terraform state rm aws_instance.legacy
**`terraform import`**: 기존 리소스를 state에 추가.
terraform import aws_instance.web i-0123456789abcdef0
4. Provider: 클라우드와의 다리
Provider란
**Provider**는 Terraform과 **외부 시스템 (AWS, GCP 등)** 사이의 plugin.
**역할**:
- 리소스 타입 정의 (`aws_instance`, `aws_vpc` 등).
- API 호출 구현.
- Schema 제공.
- CRUD 동작 매핑.
Provider 생태계
**Terraform Registry**: 2000+ providers.
- Official: AWS, Azure, GCP, Kubernetes 등.
- Partner: Datadog, GitHub, MongoDB Atlas 등.
- Community: 수많은 커뮤니티 provider.
Provider 사용
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Environment = "prod"
}
}
}
`terraform init` 시 provider를 **.terraform/providers**에 다운로드.
Provider Protocol
**Terraform ↔ Provider** 통신은 **gRPC**.
Terraform Core ←→ gRPC ←→ Provider Plugin (별도 프로세스)
**왜 별도 프로세스**:
- 언어 독립: provider는 Go 외 언어로도 가능.
- 격리: provider crash가 core를 죽이지 않음.
- 버전 관리: 여러 provider 동시 실행.
Provider의 책임
각 리소스 타입마다:
**1. Schema**: 속성 정의.
schema.Resource{
Schema: map[string]*schema.Schema{
"ami": {
Type: schema.TypeString,
Required: true,
},
"instance_type": {
Type: schema.TypeString,
Required: true,
},
// ...
},
}
**2. CRUD functions**:
- **Create**: 새 리소스 생성.
- **Read**: 현재 상태 조회.
- **Update**: 변경 적용.
- **Delete**: 리소스 삭제.
**3. Diff**: Desired와 actual의 차이 계산.
Terraform Plugin Framework
**Terraform Plugin Framework** (2022+): Go SDK의 새 버전.
- 더 엄격한 타입 시스템.
- 더 나은 에러 처리.
- **Nested attributes**: 복잡한 구조.
**이전: SDK v2** (여전히 많이 사용).
Custom Provider 작성
직접 provider를 작성할 수 있다:
package main
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"context"
)
func main() {
providerserver.Serve(context.Background(), NewProvider, ...)
}
**용도**:
- 내부 API.
- Custom infrastructure.
- 공개 서비스에 대한 Terraform wrapper.
Provider 버전 관리
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # 5.x, but not 6.0
}
}
}
**버전 연산자**:
- `= 5.23.0`: 정확히.
- `>= 5.0`: 이상.
- `~> 5.0`: 5.x (패치만).
- `~> 5.20`: 5.20.x.
**Lock file** (`.terraform.lock.hcl`): 정확한 버전 + checksum. Commit.
5. Plan: 변경 미리보기
Plan의 목적
**Plan**은 `apply`가 **실제로 할 일**을 보여준다:
Terraform will perform the following actions:
aws_instance.web will be created
+ resource "aws_instance" "web" {
+ ami = "ami-0c55b159cbfafe1f0"
+ instance_type = "t3.micro"
+ id = (known after apply)
+ public_ip = (known after apply)
}
aws_vpc.main will be updated in-place
~ resource "aws_vpc" "main" {
id = "vpc-12345"
~ cidr_block = "10.0.0.0/16" -> "10.0.0.0/8"
}
aws_eip.old will be destroyed
- resource "aws_eip" "old" {
- id = "eipalloc-12345"
}
Plan: 1 to add, 1 to change, 1 to destroy.
**기호**:
- `+`: Create.
- `~`: Update in-place.
- `-`: Destroy.
- `-/+`: Destroy and recreate.
Plan의 단계
**Plan 계산**:
1. **HCL 파싱**: 모든 .tf 파일.
2. **Module 해석**: `module` 블록 확장.
3. **State 로드**: Backend에서.
4. **Refresh**: Provider로 현재 상태 조회 (API 호출).
5. **DAG 생성**: 의존성 분석.
6. **Diff 계산**: Desired vs refreshed state.
7. **출력**: 사람이 읽을 수 있는 포맷.
Refresh 단계
**Refresh**: 각 리소스에 대해 **provider.Read()** 호출.
For each resource in state:
current = provider.Read(resource.id)
if current != state:
update state
**문제**: 많은 리소스 → 많은 API 호출 → 느림.
**최적화**:
- **병렬 실행**: `-parallelism=10`.
- **Targeted refresh**: `-target`.
- **Refresh 생략**: `-refresh=false` (위험).
Plan File
Plan 결과를 파일로 저장 가능:
terraform plan -out=plan.tfplan
이후:
terraform apply plan.tfplan
**이점**:
- Plan과 apply 분리.
- 승인 워크플로우.
- CI/CD 통합.
Known after apply
일부 값은 apply 전까지 **알 수 없음**:
+ public_ip = (known after apply)
+ id = (known after apply)
이는 정상. 실제로 리소스 생성 후에야 알 수 있는 값들.
**영향**: 다른 리소스가 이에 의존하면, 그 리소스도 "known after apply".
In-place vs Replace
**In-place update**: 리소스 수정.
~ tags = {
~ "Name" = "Old" -> "New"
}
**Replace (destroy + create)**: 일부 속성은 변경 불가. 재생성 필요.
-/+ resource "aws_instance" "web" {
~ availability_zone = "us-east-1a" -> "us-east-1b" # forces replacement
}
**주의**: Replace는 **다운타임** 유발 가능. `create_before_destroy`로 완화:
resource "aws_instance" "web" {
...
lifecycle {
create_before_destroy = true
}
}
새 리소스 생성 → 확인 → 구 리소스 삭제. 다운타임 없음 (거의).
6. Apply: 실제 변경
Apply의 흐름
1. **Plan 재확인** (또는 plan file 사용).
2. **사용자 확인** (`yes`).
3. **State lock 획득**.
4. **DAG 순회**:
- 병렬 실행 가능한 리소스 모두 시작.
- 완료 시 다음 단계.
5. **각 작업**:
- Provider에 API 요청.
- 결과로 state 업데이트.
6. **State 저장**.
7. **Lock 해제**.
Concurrency
기본 `-parallelism=10`:
- 10개 리소스 동시 처리.
- Rate limit, API throttling 조심.
**증가**:
terraform apply -parallelism=20
**감소** (API 제한):
terraform apply -parallelism=1
Partial Apply
**실패 시나리오**:
aws_vpc.main: Creating...
aws_subnet.public: Creating...
aws_subnet.private: Creating...
aws_subnet.public: Creation complete
aws_subnet.private: Error: AccessDenied
- `aws_vpc.main`, `aws_subnet.public`: **생성됨**, state에 저장.
- `aws_subnet.private`: **실패**, state에 없음.
**문제 해결**:
1. 에러 원인 수정 (권한 등).
2. 다시 `terraform apply`.
3. Terraform이 이미 있는 리소스는 건드리지 않고, 실패한 것만 재시도.
Rollback은 없다
Terraform에는 **rollback 기능 없음**. 실패하면:
1. 에러 수정.
2. 다시 apply.
**이유**:
- 인프라는 파일 시스템이 아님.
- "이전 상태로 복원"은 복잡.
- 명시적 관리가 더 안전.
**Git revert**로 이전 코드 적용 → apply. 이것이 "rollback".
Apply 시 주의점
**1. Plan 없이 apply 금지**:
나쁨
terraform apply -auto-approve
좋음
terraform plan -out=plan.tfplan
terraform apply plan.tfplan
**2. 긴급 수동 변경 후 동기화**:
terraform refresh # 또는 apply -refresh-only
**3. Drift 감지**:
terraform plan
예상하지 못한 변경이 있으면 drift
7. Modules: 재사용의 열쇠
Module의 필요성
여러 환경에 같은 구조 배포:
- Dev, Staging, Prod.
- 여러 리전.
- 여러 팀.
**복사-붙여넣기 금지**. Module로 재사용.
Module 기본
**Module 정의**:
modules/vpc/main.tf
variable "cidr_block" {
type = string
}
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
}
output "vpc_id" {
value = aws_vpc.this.id
}
**Module 사용**:
module "vpc" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
}
출력 참조
output "my_vpc_id" {
value = module.vpc.vpc_id
}
Module Sources
**Local**:
module "vpc" {
source = "./modules/vpc"
}
**Git**:
module "vpc" {
source = "git::https://github.com/myorg/modules.git//vpc?ref=v1.0"
}
**Terraform Registry**:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
}
**S3, HTTP**: 기타 다양한 소스.
Registry의 힘
**Public Terraform Registry** (registry.terraform.io):
- **terraform-aws-modules**: 가장 유명. VPC, EKS, RDS 등.
- **Azure-verified**: Microsoft가 검증.
- **Google Cloud**: GCP 모듈.
**예시**:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
}
20개가 넘는 리소스를 **한 블록**으로 구성.
Module Versioning
**Semantic versioning**:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.1.0" # 5.1.x
}
**Git tag**:
source = "git::...repo.git?ref=v1.2.3"
프로덕션은 **정확한 버전 pinning** 필수.
Module 디자인 원칙
**1. Single Responsibility**:
나쁨:
module "everything" {
source = "./modules/full-stack"
VPC, EKS, RDS, ALB, CloudFront, ...
}
좋음:
module "vpc" { ... }
module "eks" { ... }
module "rds" { ... }
**2. Composable**:
Output으로 다른 module에 전달:
module "vpc" {
source = "./vpc"
...
}
module "eks" {
source = "./eks"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
}
**3. Minimal Variables**:
사용자에게 선택권 주되 합리적 default:
variable "instance_type" {
type = string
default = "t3.micro"
}
variable "tags" {
type = map(string)
default = {}
}
**4. Clear Outputs**:
다른 module이 필요한 값 노출:
output "vpc_id" {}
output "subnet_ids" {}
output "security_group_id" {}
Module 테스팅
**Terratest** (Go 기반):
func TestVPC(t *testing.T) {
opts := &terraform.Options{
TerraformDir: "./modules/vpc",
}
defer terraform.Destroy(t, opts)
terraform.InitAndApply(t, opts)
vpcID := terraform.Output(t, opts, "vpc_id")
assert.NotEmpty(t, vpcID)
}
**Terraform Test** (Terraform 1.6+):
tests/basic.tftest.hcl
run "create_vpc" {
command = plan
assert {
condition = aws_vpc.this.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR mismatch"
}
}
8. Workspaces: 환경 분리
Workspace란
같은 코드로 **여러 환경** 관리:
- Dev, Staging, Prod.
- 각 환경은 별도 state.
terraform workspace list
* default
dev
staging
prod
terraform workspace new prod
terraform workspace select prod
사용
resource "aws_instance" "web" {
instance_type = terraform.workspace == "prod" ? "t3.large" : "t3.micro"
}
**단점**:
- **같은 코드**를 공유해야.
- 환경별 설정 차이 관리 어려움.
- 실수로 잘못된 workspace에서 apply 위험.
Workspace의 대안
**Directory per environment**:
environments/
├── dev/
│ ├── main.tf
│ └── terraform.tfvars
├── staging/
│ ├── main.tf
│ └── terraform.tfvars
└── prod/
├── main.tf
└── terraform.tfvars
각 디렉토리가 독립. 실수 방지.
**Terragrunt**: HashiCorp가 아닌 도구. DRY Terraform.
9. Drift Detection
Drift란
**Drift**: Terraform state ≠ 실제 클라우드 상태.
**원인**:
1. **수동 변경**: 누군가 콘솔에서 수정.
2. **외부 자동화**: Auto-scaling, Lambda 등.
3. **다른 도구**: CloudFormation과 병행 사용.
Drift 감지
terraform plan
State에 있는 리소스를 provider로 refresh → 차이가 있으면 plan에 표시.
**예시**:
~ resource "aws_instance" "web" {
id = "i-0123456789abcdef0"
~ instance_type = "t3.micro" -> "t3.small" # 누군가 콘솔에서 변경!
}
대응
**옵션 1: Terraform에 반영**:
코드를 실제 상태에 맞게 수정:
instance_type = "t3.small"
**옵션 2: Revert**:
`apply`로 원래 상태로 복원:
terraform apply
Terraform이 `t3.small` → `t3.micro`로 되돌림.
Refresh-only
변경 없이 state만 업데이트:
terraform apply -refresh-only
클라우드 변경을 인정.
Continuous Drift Detection
**Atlantis, Terraform Cloud** 등이 자동화:
- 주기적으로 `plan` 실행.
- Drift 감지 시 알림.
- 자동 수정 또는 수동 확인.
10. 실전: CI/CD with Terraform
GitHub Actions 예시
name: Terraform
on:
pull_request:
push:
branches: [main]
jobs:
terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./terraform
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.6.0
- run: terraform init
- run: terraform fmt -check
- run: terraform validate
- name: Terraform Plan
run: terraform plan -out=tfplan
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY }}
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -auto-approve tfplan
Best Practices
**1. PR 단위 Plan**:
- Pull request마다 plan 실행.
- 결과를 PR 코멘트로 게시.
- Merge 전 검토.
**2. State 보안**:
- IAM 권한 엄격.
- Encryption at rest.
- Access logging.
**3. Secret 관리**:
- `sensitive = true`.
- 외부 secret manager 사용 (Vault, AWS Secrets Manager).
- Environment variables.
**4. Lock Timeout**:
terraform apply -lock-timeout=10m
Long-running apply에 유용.
**5. Targeted Operations** (조심):
terraform apply -target=aws_instance.web
긴급 상황에만. 일반적으론 전체 apply.
11. OpenTofu
왜 fork 되었나
**2023년 8월 10일**: HashiCorp가 Terraform을 **BSL (Business Source License)** 로 전환.
**BSL의 제약**:
- 경쟁자 (Terraform Cloud에 대한 경쟁 서비스)는 **4년 제한**.
- 오픈소스 커뮤니티의 우려.
**반응**:
- **OpenTF Manifesto**: 커뮤니티가 fork 선언.
- **OpenTofu**로 명명.
- **Linux Foundation** 산하.
- 2024년 1.6 release.
OpenTofu vs Terraform
**기술적**: 거의 동일. HCL 호환. 대부분의 provider 작동.
**라이선스**: OpenTofu는 **MPL 2.0** (순수 오픈소스).
**기능**:
- OpenTofu가 **더 빠르게 혁신** 중.
- State encryption, for_each in provider 등 OpenTofu 먼저.
- Terraform도 따라잡는 중.
마이그레이션
Terraform → OpenTofu
tofu init
tofu plan
tofu apply
대부분 그대로 작동. 고급 기능 몇 개 차이.
어느 것을 쓸까
**Terraform**:
- HashiCorp 생태계 통합.
- Terraform Cloud/Enterprise.
- 가장 많은 provider.
**OpenTofu**:
- 순수 오픈소스 원할 때.
- BSL 우려 있을 때.
- 커뮤니티 주도 혁신.
**많은 조직이 관망 중**. 앞으로 1-2년에 결정될 것.
12. 실전 운영과 트러블슈팅
흔한 실수
**1. 잘못된 workspace에서 apply**:
- 해결: 디렉토리 per environment.
**2. State 파일 Git commit**:
- 해결: `.gitignore`에 `*.tfstate*`.
**3. Hardcoded credentials**:
나쁨
provider "aws" {
access_key = "AKIAIOSFODNN7EXAMPLE"
secret_key = "wJalrXUtnFEMI/K7MDENG/..."
}
좋음: Environment variables
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
**4. Terraform version 불일치**:
terraform {
required_version = "~> 1.6.0"
}
**5. Manual state 수정**:
- 위험. `terraform state` 명령어 사용.
트러블슈팅
**State lock stuck**:
terraform force-unlock <lock_id>
**Drift**:
terraform plan
Review changes
terraform apply -refresh-only
**Provider 에러**:
Debug
TF_LOG=DEBUG terraform plan
**의존성 순환**:
terraform graph | grep -i cycle
성능 최적화
**1. Parallelism 조정**:
terraform apply -parallelism=20
**2. Targeted plan**:
terraform plan -target=module.vpc
**3. Refresh 생략**:
terraform plan -refresh=false
**주의**: State가 오래되면 잘못된 plan.
**4. Provider cache**:
export TF_PLUGIN_CACHE_DIR="$HOME/.terraform.d/plugin-cache"
여러 프로젝트에서 provider 재사용.
모듈화 전략
**작게, 재사용 가능하게**:
modules/
├── vpc/ # VPC + subnets
├── eks/ # EKS cluster
├── rds/ # Database
├── alb/ # Load balancer
└── monitoring/ # CloudWatch, alarms
**Composition**:
module "vpc" {
source = "./modules/vpc"
}
module "eks" {
source = "./modules/eks"
vpc_id = module.vpc.vpc_id
}
퀴즈로 복습하기
**A.**
**DAG (Directed Acyclic Graph)** 는 Terraform의 **가장 중요한 내부 구조**다. 모든 리소스, 모듈, 변수를 **노드**로, 참조를 **엣지**로 표현.
**DAG가 가능하게 하는 것들**:
**1. 자동 의존성 관리**:
사용자가 명시적으로 순서를 지정하지 않아도:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id # 이 참조가 의존성 생성
}
Terraform이 자동으로:
- VPC가 subnet보다 먼저 생성되어야 함을 추론.
- 삭제 시엔 역순 (subnet 먼저, VPC 나중).
**이유**: `aws_subnet.public`이 `aws_vpc.main.id`를 참조하기 때문에 DAG에 엣지 생성.
**2. 병렬 실행**:
독립적인 리소스는 **동시에** 처리:
resource "aws_instance" "web1" {}
resource "aws_instance" "web2" {}
resource "aws_instance" "web3" {}
세 개는 서로 의존성 없음 → **병렬 생성**.
**성능 영향**:
- Sequential: 3분 (1분 × 3).
- Parallel: 1분.
큰 인프라 (100+ 리소스)에선 **수십 배 빠름**.
**3. Cycle 감지**:
순환 참조는 **불가능**:
resource "aws_security_group" "web" {
ingress {
security_groups = [aws_security_group.db.id] # db 참조
}
}
resource "aws_security_group" "db" {
ingress {
security_groups = [aws_security_group.web.id] # web 참조 → 순환!
}
}
Terraform이 즉시 감지:
Error: Cycle in graph
**실제로는 해결 가능**: SG를 먼저 만들고, rule을 **별도 리소스**로:
resource "aws_security_group" "web" {}
resource "aws_security_group" "db" {}
resource "aws_security_group_rule" "web_to_db" {
security_group_id = aws_security_group.web.id
source_security_group_id = aws_security_group.db.id
}
Rule이 별도 노드라 cycle 없음.
**4. Graph 시각화**:
terraform graph | dot -Tpng > graph.png
전체 인프라의 의존성을 시각적으로. 복잡한 시스템 이해에 유용.
**5. Targeted operations**:
terraform apply -target=aws_instance.web
DAG 분석으로 `web`과 그 **의존성만** 처리. 나머지는 건드리지 않음.
**6. Destroy 순서**:
생성 순서를 반대로. 역순 topological sort:
Create: VPC → Subnet → Instance
Destroy: Instance → Subnet → VPC
DAG가 자동으로 처리.
**DAG의 수학적 배경**:
**Topological Sort**: DAG의 노드를 의존성 순서로 정렬.
**알고리즘**:
1. In-degree가 0인 노드 찾기 (leaves).
2. 이들을 처리.
3. 그들의 out-edge 제거.
4. 새로 in-degree 0이 된 노드 처리.
5. 모든 노드 처리 완료까지 반복.
이 과정이 **병렬 실행**을 자연스럽게 만든다: 각 단계에서 "동시 처리 가능한" 노드 집합.
**Graph 구조 예시**:
resource "aws_vpc" "main" {}
resource "aws_subnet" "public" { vpc_id = aws_vpc.main.id }
resource "aws_subnet" "private" { vpc_id = aws_vpc.main.id }
resource "aws_instance" "web" { subnet_id = aws_subnet.public.id }
resource "aws_instance" "db" { subnet_id = aws_subnet.private.id }
DAG:
aws_vpc.main
/ \
↓ ↓
aws_subnet.public aws_subnet.private
↓ ↓
aws_instance.web aws_instance.db
실행 순서:
1. `aws_vpc.main` (혼자).
2. `aws_subnet.public`, `aws_subnet.private` (병렬).
3. `aws_instance.web`, `aws_instance.db` (병렬).
**3 단계, 최대 병렬성**.
**DAG vs Imperative Scripts**:
**Imperative (bash, Python)**:
aws ec2 create-vpc --cidr-block 10.0.0.0/16
aws ec2 create-subnet --vpc-id vpc-xxx --cidr-block 10.0.1.0/24
aws ec2 create-instance --subnet-id subnet-xxx
**문제**:
- 순서를 **수동 관리**.
- 병렬화 수동 구현.
- 실패 시 recovery 복잡.
- 의존성 추적 없음.
- 삭제 순서 또 수동.
**DAG (Terraform)**:
- 순서 자동.
- 병렬 자동.
- Recovery: 다시 apply.
- 의존성 명시적.
- 삭제는 자동 역순.
**코드 양 비교**: 종종 **Terraform이 더 짧다**. 선언형 + 자동화.
**실전 이점**:
**1. 대규모 인프라 관리**:
- 1000+ 리소스를 30분 내 배포.
- 순서 문제 없음.
- 에러 시 해당 부분만 재시도.
**2. 팀 협업**:
- 누군가 새 리소스 추가.
- 기존 코드 건드리지 않아도 자동으로 올바른 위치에 배치.
- DAG가 알아서 순서 맞춤.
**3. Refactoring**:
- 리소스를 module로 옮겨도 의존성 유지.
- Terraform이 추적.
**DAG의 한계**:
**1. Provider 간 숨은 의존성**:
IAM role이 먼저 있어야 Lambda 생성 가능:
resource "aws_iam_role" "lambda" {}
resource "aws_lambda_function" "app" {
role = aws_iam_role.lambda.arn # DAG가 감지
}
하지만 IAM role의 권한 **전파 지연** (~10초) 은 DAG가 모름. 생성 후 즉시 Lambda 호출하면 실패 가능.
**해결**:
- `sleep` provisioner (hacky).
- Provider에서 retry.
- `depends_on` + `time_sleep` 리소스.
**2. External side effects**:
Terraform 밖에서 일어나는 일 (DNS propagation, cache invalidation 등)은 DAG로 모델링 못 함.
**3. Dynamic dependencies**:
런타임에만 알 수 있는 의존성은 표현 어려움. `count`, `for_each`로 일부 해결.
**교훈**:
DAG는 **선언형 인프라 관리의 근본**이다. 모든 강력한 IaC 도구 (Terraform, Pulumi, CloudFormation)가 이 개념을 쓴다. 차이는 언어와 생태계.
DAG 덕분에 **인프라가 코드**가 된다:
- Git으로 버전 관리.
- PR로 리뷰.
- CI/CD로 자동 배포.
- 의존성 분석.
이것이 **2014년 Terraform의 혁신**이었다. 이전에는 CloudFormation 정도가 있었지만 AWS-only. Terraform이 **멀티 클라우드 + 선언형 + DAG** 를 결합해서 업계를 바꿨다.
당신이 `terraform apply`를 칠 때, 이 모든 것이 뒤에서 일어난다. 의존성 분석, 병렬 스케줄링, 실패 복구, state 추적. 이 복잡성을 **하나의 명령어로 추상화**한 것이 Terraform의 진짜 가치다.
**A.**
**State는 Terraform의 "기억"** 이다. 없으면 Terraform이 아무것도 못 한다.
**State가 하는 일**:
**1. Resource → Cloud Object 매핑**:
resource "aws_instance" "web" {
ami = "ami-..."
}
State에 저장:
{
"type": "aws_instance",
"name": "web",
"instances": [{
"attributes": {
"id": "i-0123456789abcdef0", # 실제 AWS instance ID
...
}
}]
}
다음 `terraform apply` 시 Terraform은:
- **State의 `web`이 `i-0123456789abcdef0`에 해당**을 안다.
- 이 instance를 refresh.
- Desired state와 비교.
**State 없이는**: 매번 새로 생성. 기존 리소스는 orphan이 됨.
**2. 의존성 추적**:
State에는 각 리소스의 **의존성 목록**도:
{
"dependencies": ["aws_vpc.main", "aws_subnet.public"]
}
Destroy 시 역순으로 처리.
**3. Metadata 저장**:
클라우드 API에서 안 보이는 값:
- `sensitive` 필드.
- Internal IDs.
- Computed attributes.
**4. Performance**:
매번 모든 리소스를 조회하는 대신, state의 정보 활용. Refresh는 **선택적**.
**State 관리의 위험**:
**1. 손실**:
State 잃으면 **모든 리소스를 다시 import**해야.
terraform import aws_instance.web i-0123456789
terraform import aws_vpc.main vpc-abc123
... 수십/수백 개
**악몽**.
**2. 손상**:
Corrupt JSON → Terraform이 읽지 못함. 수동 수정 어려움.
**3. Drift**:
State와 실제가 맞지 않음. 다음 apply 시 **예상 못한 변경**.
**4. 민감 정보**:
State에 **password, API key** 등이 저장됨. 유출되면 큰 문제.
**State 관리 원칙**:
**1. Remote Backend 사용**:
**절대** 로컬 state만으로 production 운영 금지.
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
**이점**:
- **공유**: 팀 멤버가 같은 state 접근.
- **백업**: S3 versioning으로 이전 state.
- **Encryption**: 미사용 시 암호화.
- **Lock**: 동시 apply 방지.
**S3 버킷 설정**:
resource "aws_s3_bucket" "tfstate" {
bucket = "my-terraform-state"
}
resource "aws_s3_bucket_versioning" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
**2. State Locking**:
**DynamoDB** (AWS):
resource "aws_dynamodb_table" "tf_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
**다른 backend**:
- GCS: 내장 locking.
- Azure: Blob leasing.
- HTTP: 커스텀 lock API.
**Lock의 중요성**:
두 사람이 동시에 apply → race condition → **state 손상**. Lock이 이를 방지.
alice: terraform apply
→ lock 획득
→ 작업 시작
bob: terraform apply (alice 작업 중)
→ lock 획득 시도
→ 실패
→ 에러: "Lock held by alice"
→ 대기 또는 중단
**3. State 파일 크기 관리**:
State가 **수십 MB**가 되면:
- Plan/apply 느림.
- 메모리 사용 증가.
- 일부 backend는 크기 제한.
**해결**:
- **State 분할**: 큰 프로젝트를 여러 개의 작은 state로.
- **Environment별**: 하나의 거대한 state 대신.
- **Service별**: VPC, EKS, RDS 각각.
**예시 구조**:
terraform/
├── network/ # VPC, subnets (별도 state)
│ └── terraform.tfstate
├── compute/ # EC2, ASG
│ └── terraform.tfstate
├── database/ # RDS
│ └── terraform.tfstate
└── monitoring/ # CloudWatch, alarms
└── terraform.tfstate
**4. State 간 참조**:
State 분리 후 값 공유:
**Data source**:
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "network/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_instance" "web" {
subnet_id = data.terraform_remote_state.network.outputs.public_subnet_id
}
**이점**: 네트워크 team과 app team이 독립 관리.
**5. Sensitive Data 관리**:
**State에 저장되는 민감 정보**:
- DB password.
- API keys.
- Certificates.
**보호**:
- **Backend encryption**.
- **Access control**: IAM 엄격.
- **Audit logging**.
**Vault / Secrets Manager 활용**:
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/db/password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
Password가 state에도 저장되지만 **Secrets Manager가 진실의 원천**.
**6. Backup**:
**S3 versioning**: 자동 이전 버전.
**수동 백업**:
aws s3 cp s3://my-state/prod/terraform.tfstate ./backup/prod-$(date +%F).tfstate
**복구**:
aws s3 cp ./backup/prod-2025-04-10.tfstate s3://my-state/prod/terraform.tfstate
**7. State Migration**:
**Backend 변경**:
기존
terraform {
backend "local" {}
}
새로
terraform {
backend "s3" { ... }
}
terraform init -migrate-state
Terraform이 자동으로 state를 새 backend로 이동.
**State 조작 명령어**:
**`terraform state list`**: 리소스 목록.
**`terraform state show <resource>`**: 리소스 상세.
**`terraform state mv`**: 리소스 이름/위치 변경.
Module로 이동
terraform state mv aws_instance.web module.compute.aws_instance.web
이름 변경
terraform state mv aws_instance.old aws_instance.new
**`terraform state rm`**: State에서 제거 (실제 리소스는 남음).
Terraform이 이 리소스 관리 안 하도록
terraform state rm aws_instance.legacy
**`terraform import`**: 기존 리소스를 state에 추가.
terraform import aws_instance.existing i-0123456789
**이런 명령어는 조심**. State 손상 위험.
**Best Practices 요약**:
1. **Remote backend 필수** (S3 + DynamoDB 표준).
2. **State encryption at rest**.
3. **State locking 활성화**.
4. **Access control 엄격** (IAM).
5. **Backup 주기적**.
6. **State 분할** (monolithic 금지).
7. **`*.tfstate*` gitignore**.
8. **Sensitive 값 external secret manager**.
9. **Audit logging**.
10. **Team에게 `terraform state` 명령어 교육**.
**실전 사고 사례**:
**사례 1: Git에 state commit**:
- 실수로 개발자가 `terraform.tfstate`를 Git에 commit.
- Repository public.
- DB password 노출.
- **해결**: Secret rotation, git history 정리, education.
**사례 2: State 손실**:
- 개발자 로컬에서 작업.
- 노트북 분실.
- Backend 없음 → state 영원히 손실.
- **복구**: 100+ 리소스 수동 import. 수 일.
**사례 3: 동시 apply**:
- Lock 없는 GCS backend.
- 두 팀이 동시 apply.
- State corruption.
- **복구**: 수동 state 수정, 업계 전문가 도움 필요.
**교훈**:
State는 Terraform의 **가장 중요한 자산**이다. Code는 Git에 있다. Cloud는 provider에 있다. **둘을 연결하는 것은 state**다.
State를 제대로 관리하지 않으면:
- 혼란.
- 장애.
- 보안 사고.
- 복구 불가능한 상황.
반면 잘 관리하면:
- 팀 협업 매끄럽게.
- 신뢰할 수 있는 인프라.
- 쉬운 troubleshooting.
- 장기적 유지보수 가능.
**"Terraform을 쓸지 말지 망설일 때 답은 간단하다: state 관리 방법을 이해했는가?"**. 이해했다면 사용하라. 이해 못 했다면 먼저 배워라.
이 글의 지식은 state 관리의 **모든 기본**을 다룬다. 하지만 실전에선 **조직의 요구**에 맞게 응용해야 한다. 작은 팀과 대기업은 다른 방법이 필요하다. 하지만 원칙은 같다:
**State를 보호하라. Backup하라. Lock하라. Encrypt하라. Audit하라**.
이 다섯 가지만 지키면 대부분의 문제를 피할 수 있다. 그리고 Terraform의 진정한 힘 — **declarative infrastructure management** — 을 안전하게 누릴 수 있다.
마치며: 선언형 인프라의 승리
핵심 정리
1. **HCL**: 선언형 DSL. 함수와 조건 지원.
2. **DAG**: 의존성 그래프. 병렬 실행 기반.
3. **State**: 진실의 원천. Remote backend 필수.
4. **Provider**: 클라우드 인터페이스. gRPC 기반.
5. **Plan/Apply**: 미리보기 + 실행.
6. **Modules**: 재사용 단위.
7. **Workspaces vs Directories**: 환경 분리.
8. **OpenTofu**: 오픈소스 fork.
실전 체크리스트
**New Terraform project**:
- [ ] `.gitignore`에 `*.tfstate*`, `.terraform/`.
- [ ] Remote backend (S3 + DynamoDB).
- [ ] State encryption.
- [ ] IAM 권한 최소화.
- [ ] Provider version pinning.
- [ ] `.terraform.lock.hcl` commit.
- [ ] Module 구조화.
- [ ] CI/CD 통합.
- [ ] Plan before apply.
- [ ] Drift detection 자동화.
Terraform이 가르쳐준 것
Terraform은 **인프라 관리의 패러다임**을 바꿨다:
**Before**: 웹 콘솔, 수동 스크립트, 정적 문서.
**After**: 코드, 버전 관리, 자동화, 재현 가능성.
이 변화는 단순한 도구 교체가 아니다. **문화의 변화**다:
- 인프라 팀 → 엔지니어링 팀.
- 수동 변경 → PR 리뷰.
- 문서 → 코드.
- 수동 확인 → 자동 테스트.
이것이 **DevOps 혁명**의 기반이다.
마지막 교훈
Terraform을 제대로 쓰려면 **내부를 이해**해야 한다:
- **DAG**: 왜 이 순서로?
- **State**: 왜 이 값이?
- **Provider**: 왜 이 에러?
- **Plan**: 왜 이 변경?
이 질문들의 답이 이 글의 지식에 있다.
당신이 다음에 `terraform apply`를 칠 때, 잠시 생각해 보자:
- HCL이 파싱된다.
- DAG가 생성된다.
- State가 refresh된다.
- Plan이 계산된다.
- Provider가 API를 호출한다.
- 병렬로 수많은 리소스가 변경된다.
- State가 업데이트된다.
이 복잡한 오케스트레이션이 **한 줄 명령**으로 작동한다. Terraform이 모든 복잡성을 숨기고 **선언적 인터페이스**만 보여준다.
이것이 **좋은 도구의 정의**다. 복잡함을 숨기되, 필요할 때 들여다볼 수 있는 것.
참고 자료
- [Terraform Documentation](https://developer.hashicorp.com/terraform)
- [OpenTofu Documentation](https://opentofu.org/docs/)
- [HashiCorp Learn: Terraform](https://developer.hashicorp.com/terraform/tutorials)
- [Terraform Up & Running (Yevgeniy Brikman)](https://www.terraformupandrunning.com/)
- [Terraform Registry](https://registry.terraform.io/)
- [terraform-aws-modules](https://github.com/terraform-aws-modules)
- [Terratest](https://terratest.gruntwork.io/)
- [Terragrunt](https://terragrunt.gruntwork.io/)
- [Atlantis: Terraform Pull Request Automation](https://www.runatlantis.io/)
- [HashiCorp Provider Development](https://developer.hashicorp.com/terraform/plugin/framework)
- [State Management Best Practices](https://developer.hashicorp.com/terraform/language/state)
현재 단락 (1/1246)
$ terraform apply