[IaC] Terraform으로 인프라 코드화하기

2026. 6. 13. 14:14프로젝트

Flicker 인프라가 콘솔로 너무 흩어져 있었다.

 

ALB는 어느 SG를 받는지, RDS 비번은 어디 박혀있는지

EC2 IAM Role은 누가 만들었는지 정리할 때가 됐다 싶어 Terraform 으로 코드화를 시작했다.

 

원래 Terraform 에도 관심이 있어서, 이번 작업을 재밌게 진행했던 것 같다.

 

GitHub 링크

https://github.com/sns-service/flicker-infra

 

 

IaC(Terraform) 도입 배경

정답("재현성, 협업, 버전 관리")외에도, 내가 인프라를 코드로 옮긴 이유는 아래와 같다.

  • 이전에 만든 리소스를, 오늘 보면 왜 만든지 기억이 나지 않음
  • 따라서 "이거 지워도 되나?" 라는 질문에 답을 하지 못했음

Git 로그를 통해 인프라 변경 이력(생성 목적, 관련 PR 등)을 명확히 추적할 수 있게 됐고, 한눈에 인프라 구조를 파악할 수 있어

운영 자신감을 가져갈 수 있다.

 

state 파일로 변경 이력도 모두 추적 가능하다.

 

 

디렉토리 구조 — environments vs workspaces

Terraform에서 환경 분리 패턴은 둘 중 하나이다.

# 옵션 A: environments 디렉토리
flicker-infra/
├── environments/
│   ├── dev/
│   └── prod/
└── modules/

# 옵션 B: workspaces (단일 디렉토리)
flicker-infra/
├── main.tf
└── modules/

# terraform workspace new dev / prod 로 전환

 

나는 옵션 A를 골랐는데, 이유는 아래와 같다.
envirionments 디렉토리 패턴의 장점
  • environments/dev/, environments/prod/ 형태로 디렉토리를 완전히 분리함으로써 backend.tf부터 별도로 관리할 수 있고, 환경 간 변경 사항이 완전히 격리된다. (현재는 prod만 존재하지만 확장성을 고려해 선반영)

 

  • workspaces 패턴은 동일한 state 공간을 공유하여 환경별 백엔드/리전/변수 구조가 달라질 때 관리가 불편함.

 

지금 Flicker는 prod 하나뿐이지만 디렉토리는 미리 분리했다.

 

Backend — S3 + DynamoDB

 

Terrafor State 파일을 로컬에 두면 안 되는 이유는 날아가면 끝이고 협업도 불가하기 때문이다.

또 유실 방지를 위해 원격 백엔드를 사용했다.

terraform {
  required_version = ">= 1.6"

  backend "s3" {
    bucket         = "flicker-tfstate-637423170275"
    key            = "prod/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "flicker-tfstate-lock"
    encrypt        = true
  }
}

 

이 S3 버킷과 Dynamo DB 테이블을 Terraform 으로 만들면 안된다.

Backend 가 존재해야 Terraform 이 state를 저장할 수 있는데, 그걸 만드는 Terraform 자체가 state를 어디 둘 거냐는 문제가 생긴다. 따라서 이 둘은 AWS 콘솔로 수동 생성, 모든 다른 인프라는 Terraform 이 관리한다.

 

DynamoDB (State Lock): 동시 수정으로 인한 State 충돌을 막기 위해 사용 (Partition Key는 반드시 LockID로 지정)

 

그리고 S3 버킷은 versioning 켜둬야 한다. State 파일을 손으로 잘못 건드리는 일이 있는데, versioning 있으면 한 번에 복구된다.

 

첫 모듈 - ECR

modules/ecr/
├── main.tf          # resource
├── variables.tf     # input
├── outputs.tf       # 다른 모듈에 노출
└── README.md

 

varibles.tf

variable "image_tag_mutability" {
  description = "Image tag mutability (MUTABLE or IMMUTABLE)"
  type        = string
  default     = "s"

  validation {
    condition     = contains(["MUTABLE", "IMMUTABLE"], var.image_tag_mutability)
    error_message = "image_tag_mutability must be MUTABLE or IMMUTABLE."
  }
}

validation 블록 : 잘못된 값이 들어오면 apply 자체가 거부된다. 허용되지 않은 문자열이 인프라에 반영되는 것을 코드레벨에서 차단함.

 

main.tf에서 학습한 패턴 하나. lifecycle policy를 별도 리소스로 분리.

resource "aws_ecr_repository" "this" {
  name = var.name
  ...
}

resource "aws_ecr_lifecycle_policy" "this" {
  repository = aws_ecr_repository.this.name

  policy = jsonencode({
    rules = [...]
  })
}

 

aws_ecr_lifecycle_policy가 aws_ecr_repository.this.name을 참조하니까 Terraform이 의존성을 자동 추적해서 순서를 잡는다.

lifecycle policy는 repository가 만들어진 후에 생성된다. 명시적으로 depends_on 안 써도 된다.

 

jsonencode는 HCL을 JSON 문자열로 변환하는 내장 함수. AWS lifecycle policy는 JSON 문자열을 받는데, 문자열로 직접 쓰면 escape 지옥이고 들여쓰기도 못 한다. jsonencode로 HCL 객체로 작성하면 IDE 자동 완성도 되고 가독성도 좋음.

 

콘솔에 이미 있던 ECR을 어떻게 했나

콘솔에 있던 기존 ECR 리포지토리를 삭제한 후,

terraform apply 를 통해 코드로 재생성하여 완전한 상태 관리(State List 등록 및 S3 동기화)를 시작했다.

 

결과

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:
ecr_repository_url = "********.dkr.ecr.ap-northeast-2.amazonaws.com/flicker"

 

apply 끝난 후 sate 파일을 들여다봤다.

 
terraform state list
module.ecr.aws_ecr_repository.this
module.ecr.aws_ecr_lifecycle_policy.this
 
S3 콘솔 가면 prod/terraform.tfstate 파일이 생겨있다. 열어보면 JSON이고, 방금 만든 리소스의 모든 속성이 다 기록돼 있다.
다음 plan/apply는 이 파일과 실제 AWS 상태를 비교해서 차이만 변경한다.

 

정리

  • Bootstrap (S3 + DynamoDB): 콘솔로
  • ECR 모듈: Terraform apply 완료, 운영 중
  • S3 / IAM-EC2 / Network / ALB / RDS / ElastiCache 모듈: 코드 작성 + plan 통과, apply는 다음 사이클

 

다음 글에서는 Network 모듈 — VPC, Subnet, IGW, NAT, Route Table, Security Group까지 의존성 사슬을 어떻게 그렸는지 적을 예정이다.