Terraform 신입 면접/실무 질문 & 핵심 정리
📚 목차
1. State 관련
Q: Terraform State가 뭐고 왜 필요해요?
답변 포인트:
State = Terraform이 관리하는 인프라의 "현재 상태 기록"
┌─────────────────────────────────────────────────────────┐
│ 코드 (.tf) State (.tfstate) 실제 인프라 │
│ ─────────── ──────────────── ────────── │
│ "원하는 상태" ↔ "알고 있는 상태" ↔ "실제 상태" │
└─────────────────────────────────────────────────────────┘
왜 필요한가:
| 역할 | 설명 |
|---|---|
| 매핑 | 코드의 리소스 ↔ 실제 리소스 ID 연결 |
| 성능 | 매번 API 조회 안 해도 됨 |
| 의존성 | 리소스 간 관계 추적 |
| 협업 | 팀원들과 상태 공유 |
Q: Local State vs Remote State 차이?
Local State (기본값)
─────────────────────
파일: terraform.tfstate (로컬 디렉토리)
문제: 팀 협업 불가, 유실 위험
Remote State (실무 필수)
─────────────────────
저장소: S3, GCS, Terraform Cloud 등
장점: 팀 공유, 백업, Locking 지원
Remote State 설정 예시:
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
dynamodb_table = "terraform-locks" # Locking용
}
}Q: State Locking이 뭐예요?
동시 실행 방지 메커니즘
개발자 A: terraform apply 실행 중... 🔒 Lock 획득
개발자 B: terraform apply 시도 → ❌ 차단됨 (Lock 대기)
→ State 충돌/손상 방지
Lock 강제 해제 (긴급시):
terraform force-unlock LOCK_ID2. 리소스 관리
Q: count vs for_each 차이?
# count - 인덱스 기반 (0, 1, 2...)
resource "aws_instance" "web" {
count = 3
# aws_instance.web[0], aws_instance.web[1], aws_instance.web[2]
}
# for_each - 키 기반 (명시적 이름)
resource "aws_instance" "web" {
for_each = toset(["web1", "web2", "web3"])
# aws_instance.web["web1"], aws_instance.web["web2"]...
}핵심 차이:
| 상황 | count | for_each |
|---|---|---|
| 중간 삭제 | ❌ 인덱스 밀림 | ✅ 해당 것만 삭제 |
| 식별 | 숫자 인덱스 | 명시적 키 |
| 권장 | 동일한 리소스 N개 | 각각 구분 필요할 때 |
count 문제 예시:
# ["a", "b", "c"] → ["a", "c"] 로 변경시
# count: b 삭제 → c가 [1]로 이동 → c도 재생성됨!
# for_each: b만 깔끔하게 삭제Q: depends_on은 언제 써요?
# Terraform이 자동 감지 못하는 암묵적 의존성일 때
resource "aws_instance" "app" {
# ...
depends_on = [aws_iam_role_policy.app_policy]
# IAM 정책이 먼저 생성되어야 함 (코드에서 참조 안 해도)
}자동 감지 vs 수동 지정:
# ✅ 자동 감지됨 (depends_on 불필요)
resource "aws_instance" "web" {
subnet_id = aws_subnet.main.id # 참조로 자동 의존성
}
# ❌ 자동 감지 안됨 (depends_on 필요)
# - IAM 정책 → EC2
# - Security Group Rule → EC2
# - 외부 스크립트 의존성Q: lifecycle 블록 설명해주세요
resource "aws_instance" "web" {
# ...
lifecycle {
create_before_destroy = true # 새거 먼저 만들고 기존 삭제
prevent_destroy = true # 삭제 방지 (실수 방지)
ignore_changes = [tags] # 특정 속성 변경 무시
# Terraform 1.2+
precondition { # 생성 전 조건 확인
condition = var.env != "prod" || var.instance_type != "t2.micro"
error_message = "Prod 환경에서 t2.micro 사용 금지!"
}
}
}Q: Taint가 뭐예요? (구버전)
# 리소스를 "오염됨"으로 표시 → 다음 apply시 재생성
terraform taint aws_instance.web # 구버전
terraform apply -replace=aws_instance.web # 신버전 (권장)사용 상황:
- EC2 내부가 꼬여서 재생성 필요할 때
- 프로비저너 다시 실행하고 싶을 때
3. 모듈
Q: 모듈이 뭐고 왜 써요?
모듈 = 재사용 가능한 Terraform 코드 패키지
modules/
├── vpc/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── ec2/
└── rds/
장점:
# 반복 코드 없이 환경별 생성
module "vpc_dev" {
source = "./modules/vpc"
env = "dev"
cidr = "10.0.0.0/16"
}
module "vpc_prod" {
source = "./modules/vpc"
env = "prod"
cidr = "10.1.0.0/16"
}Q: 모듈 소스 종류?
# 로컬
module "vpc" {
source = "./modules/vpc"
}
# Git
module "vpc" {
source = "git::https://github.com/company/modules.git//vpc?ref=v1.0.0"
}
# Terraform Registry
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
}
# S3
module "vpc" {
source = "s3::https://s3.amazonaws.com/bucket/vpc.zip"
}4. 변수와 출력
Q: 변수 우선순위?
높음 ↑
─────────────────────────────
1. -var 'name=value' (CLI)
2. -var-file=file.tfvars
3. *.auto.tfvars (알파벳 순)
4. terraform.tfvars
5. 환경변수 TF_VAR_name
6. default 값
─────────────────────────────
낮음 ↓
Q: variable vs local 차이?
# variable - 외부 입력값
variable "env" {
type = string
default = "dev"
}
# local - 내부 계산/가공값
locals {
name_prefix = "${var.project}-${var.env}"
common_tags = {
Environment = var.env
ManagedBy = "Terraform"
}
}Q: 민감한 변수 관리?
# 1. sensitive 표시
variable "db_password" {
type = string
sensitive = true # plan/apply 출력에서 숨김
}
# 2. 환경변수로 주입
export TF_VAR_db_password="secret123"
# 3. Vault/SSM 연동 (권장)
data "aws_ssm_parameter" "db_password" {
name = "/prod/db/password"
}5. 워크플로우
Q: plan vs apply 차이?
terraform plan # 변경 예정 사항 미리보기 (실제 변경 X)
terraform apply # 실제 인프라 변경Plan 저장 & 적용:
terraform plan -out=tfplan # 계획 저장
terraform apply tfplan # 저장된 계획 그대로 적용 (CI/CD용)Q: refresh가 뭐예요?
terraform refresh # State를 실제 인프라와 동기화
# 사용 상황:
# - 콘솔에서 수동 변경 후
# - State와 실제가 달라졌을 때주의: Terraform 0.15.4+에서는 terraform apply -refresh-only 권장
Q: 기본 워크플로우?
# 1. 초기화
terraform init
# 2. 포맷팅 (선택)
terraform fmt
# 3. 검증
terraform validate
# 4. 계획 확인
terraform plan
# 5. 적용
terraform apply
# 6. 삭제 (필요시)
terraform destroy6. 트러블슈팅
Q: State와 실제 인프라가 다르면?
| 상황 | 해결 |
|---|---|
| 콘솔에서 수동 삭제 | terraform state rm |
| 콘솔에서 수동 생성 | terraform import |
| 콘솔에서 수동 변경 | terraform refresh 후 코드 수정 |
# 콘솔에서 삭제된 리소스를 State에서 제거
terraform state rm aws_instance.deleted_one
# 콘솔에서 만든 리소스를 Terraform으로 가져오기
terraform import aws_instance.manual i-1234567890abcdef0Q: import 사용법?
# 1. 코드 먼저 작성 (빈 껍데기라도)
resource "aws_instance" "imported" {
# 일단 비워둠
}
# 2. Import 실행
terraform import aws_instance.imported i-1234567890abcdef0
# 3. State 확인하고 코드 채우기
terraform state show aws_instance.imported
# → 출력 내용을 코드에 복사
# 4. Plan으로 확인
terraform plan # No changes 나와야 함Terraform 1.5+ Import 블록:
import {
to = aws_instance.imported
id = "i-1234567890abcdef0"
}
resource "aws_instance" "imported" {
# ...
}Q: 리소스 이름 바꿀 때?
# moved 블록 (권장, Terraform 1.1+)
moved {
from = aws_instance.old_name
to = aws_instance.new_name
}
# 또는 CLI
terraform state mv aws_instance.old_name aws_instance.new_name7. 실무 시나리오
Q: 프로덕션 실수 방지 방법?
# 1. prevent_destroy
resource "aws_db_instance" "prod" {
lifecycle {
prevent_destroy = true
}
}
# 2. deletion_protection (AWS 자체 기능)
resource "aws_db_instance" "prod" {
deletion_protection = true
}# 3. Plan 파일 사용
terraform plan -out=tfplan
# 리뷰 후
terraform apply tfplanQ: 환경별(dev/prod) 분리 방법?
방법 1: Workspace
terraform workspace new dev
terraform workspace new prod
terraform workspace select prod
terraform apply방법 2: 디렉토리 분리 (권장)
environments/
├── dev/
│ ├── main.tf
│ └── terraform.tfvars
├── stg/
└── prod/
방법 3: tfvars 파일
terraform apply -var-file=prod.tfvarsQ: 대규모 인프라에서 apply 시간 단축?
# 특정 리소스만 적용
terraform apply -target=aws_instance.web
# 병렬 처리 증가 (기본 10)
terraform apply -parallelism=20주의: -target은 임시 방편, 상시 사용 금지
📝 면접 빈출 요약
| 주제 | 핵심 키워드 |
|---|---|
| State | 매핑, Remote, Locking |
| count vs for_each | 인덱스 밀림, 키 기반 |
| 모듈 | 재사용, 버전 관리 |
| lifecycle | prevent_destroy, ignore_changes |
| Import | 기존 인프라 → Terraform 관리 |
| moved | 리소스 이름 변경, 팀 협업 |
🎯 실습 추천
# 1. EC2 생성 → 콘솔에서 삭제 → state rm
# 2. 콘솔에서 EC2 생성 → import
# 3. 리소스 이름 변경 → moved 블록
# 4. count → for_each 마이그레이션
# 5. Remote State (S3) 설정이 정도 알면 신입 기준 충분합니다! 🚀
8. 팀프로젝트 코드 기준 실전 연결 (pposiraegi-terraform-infra)
추상적인 개념을 본인이 만든 코드로 설명할 수 있어야 면접에서 자연스러움
State - 현재 구조와 개선점
현재 상태: terraform.tfstate 로컬 파일로 관리 중
pposiraegi---terraform-infra/
└── terraform.tfstate ← 로컬 State
“현재는 로컬 State라 팀 협업이 안 되는 구조입니다. 실무에서는 S3 + DynamoDB로 Remote State + Locking을 구성해야 하는데, 팀프로젝트 규모라 일단 로컬로 유지했습니다.”
개선 시 적용할 구성:
terraform {
backend "s3" {
bucket = "pposiraegi-terraform-state"
key = "prod/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
dynamodb_table = "terraform-locks"
}
}count vs for_each - 실제 사용 예시
ACM DNS 검증 레코드에서 for_each 사용:
resource "aws_route53_record" "cert_validation" {
for_each = {
for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => { ... }
}
}“
domain_validation_options가 도메인마다 동적으로 생성되는 리스트라for_each로 순회했습니다.count를 쓰면 도메인 추가/삭제 시 인덱스가 밀려서 기존 레코드가 재생성될 위험이 있어요.”
모듈 - 현재 구조와 이상적인 구조
현재: main.tf 하나에 전부 (700줄)
이상: modules/vpc, modules/ec2, modules/rds ...
“현재는 모듈화 없이 단일 파일 구조입니다. 모듈화하면 VPC/SG/EC2/RDS/CloudFront 단위로 분리해서 dev/prod 환경을 같은 모듈로 재사용할 수 있는데, 팀프로젝트 기간 내에는 빠른 구현을 우선했습니다.”
모듈화 시 구조:
modules/
├── vpc/ → VPC, 서브넷, IGW, NAT, 라우팅 테이블
├── security/ → SG 5개
├── ec2/ → Bastion + Backend
├── rds/ → DB instance + subnet group
├── redis/ → ElastiCache
└── cloudfront/ → CF + S3 + ACM + Route53
sensitive 변수 - 실제 사용
variable "db_password" {
sensitive = true # plan/apply 출력에서 마스킹
}
variable "jwt_secret" {
sensitive = true
}“
sensitive = true로 plan 출력 시 마스킹 처리했고, 실제 값은terraform.tfvars에서 주입합니다. 실무에서는 SSM Parameter Store나 Vault에서 data source로 가져오는 게 더 안전합니다.”
depends_on - 실제 사용 이유
resource "aws_instance" "backend" {
user_data = templatefile("user_data.sh", {
db_url = "jdbc:postgresql://${aws_db_instance.db.endpoint}/ecommerce"
redis_host = aws_elasticache_cluster.redis.cache_nodes[0].address
...
})
depends_on = [
aws_db_instance.db,
aws_elasticache_cluster.redis,
aws_nat_gateway.nat,
]
}“Backend EC2의
user_data에 RDS endpoint, Redis address를 변수로 주입해야 해서, 해당 리소스들이 먼저 생성 완료된 뒤에 EC2가 뜨도록depends_on을 명시했습니다.user_data안에서 쓰이는 암묵적 의존성이라 수동 지정이 필요했어요.”
lifecycle - ACM 교체 시 무중단
resource "aws_acm_certificate" "cert" {
lifecycle {
create_before_destroy = true
}
}“인증서를 교체할 때 기존 걸 먼저 삭제하면 CloudFront가 잠깐 HTTPS 불가 상태가 됩니다.
create_before_destroy로 새 인증서를 먼저 만들고 교체 후 기존 걸 삭제하게 했습니다.”
VPC 설계 의도 요약
| 서브넷 | 역할 | 이유 |
|---|---|---|
| public_a/b | ALB | AWS 스펙상 Multi-AZ 2개 필수 |
| public_c | Bastion | SG 독립 관리 목적 |
| public_d | NAT Gateway | 퍼블릭에 위치해야 EIP 연결 가능 |
| private_a/b | Backend EC2, RDS, Redis | RDS/ElastiCache subnet group은 2개 AZ 필수 |
트래픽 흐름:
인터넷 → CloudFront → ALB (public_a/b)
↓
Backend EC2 (private_a)
↓
RDS / Redis (private_a/b)
NAT Gateway 필요 이유:
- 프라이빗 서브넷은 IGW 경로 없음 (인바운드 차단)
- 아웃바운드만 허용: GitHub clone, 패키지 설치, ECR pull 등
ALB SG에 CloudFront prefix list 적용 이유:
- ALB DNS 직접 접근 시 CloudFront 우회 가능 (WAF, HTTPS 강제 무력화)
- CloudFront origin-facing IP 대역으로만 인바운드 제한