Terraform 신입 면접/실무 질문 & 핵심 정리

📚 목차

  1. State 관련
  2. 리소스 관리
  3. 모듈
  4. 변수와 출력
  5. 워크플로우
  6. 트러블슈팅
  7. 실무 시나리오

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_ID

2. 리소스 관리

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"]...
}

핵심 차이:

상황countfor_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 destroy

6. 트러블슈팅

Q: State와 실제 인프라가 다르면?

상황해결
콘솔에서 수동 삭제terraform state rm
콘솔에서 수동 생성terraform import
콘솔에서 수동 변경terraform refresh 후 코드 수정
# 콘솔에서 삭제된 리소스를 State에서 제거
terraform state rm aws_instance.deleted_one
 
# 콘솔에서 만든 리소스를 Terraform으로 가져오기
terraform import aws_instance.manual i-1234567890abcdef0

Q: 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_name

7. 실무 시나리오

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 tfplan

Q: 환경별(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.tfvars

Q: 대규모 인프라에서 apply 시간 단축?

# 특정 리소스만 적용
terraform apply -target=aws_instance.web
 
# 병렬 처리 증가 (기본 10)
terraform apply -parallelism=20

주의: -target은 임시 방편, 상시 사용 금지


📝 면접 빈출 요약

주제핵심 키워드
State매핑, Remote, Locking
count vs for_each인덱스 밀림, 키 기반
모듈재사용, 버전 관리
lifecycleprevent_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/bALBAWS 스펙상 Multi-AZ 2개 필수
public_cBastionSG 독립 관리 목적
public_dNAT Gateway퍼블릭에 위치해야 EIP 연결 가능
private_a/bBackend EC2, RDS, RedisRDS/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 대역으로만 인바운드 제한