본문으로 건너뛰기
Lab 11: 모듈 버전 관리와 레지스트리

Lab 11: 모듈 버전 관리와 레지스트리

Lab 11

로컬 모듈을 Git 소스 모듈로 전환하고, 시맨틱 버저닝 태그(v1.0.0 → v1.1.0 → v2.0.0)를 붙여가며 소비자 코드에서 안전하게 업그레이드하는 흐름을 실습합니다. 팀 규모가 커질수록 “모듈을 만드는 것"보다 “모듈의 버전을 관리하는 것"이 훨씬 어려운 문제가 됩니다.


모듈 소스의 진화 단계

모듈 소스는 보통 세 단계로 진화합니다. 로컬 경로 모듈은 버전 개념이 없어 모듈을 수정하는 순간 모든 소비자가 즉시 영향을 받습니다. Git 태그를 붙이면 소비자가 원하는 버전을 골라 쓸 수 있고, Registry로 가면 ~> 4.0 같은 버전 제약 연산자까지 사용할 수 있습니다.

    flowchart TD
    subgraph stage1["1단계: 로컬 모듈"]
        L1["source = ./modules/s3\n버전 개념 없음\n수정 즉시 모든 소비자에 반영"]
    end

    subgraph stage2["2단계: Git 소스 모듈"]
        G1["source = git::https://...?ref=v1.0.0\nGit 태그로 버전 고정\n저장소 간 재사용 가능"]
    end

    subgraph stage3["3단계: Module Registry"]
        R1["source = terraform-aws-modules/s3-bucket/aws\nversion = ~> 4.0\n버전 제약 연산자 지원"]
        R2["Private Module Registry\nTerraform Cloud / Enterprise\n조직 내부 모듈 배포 채널"]
    end

    stage1 -->|"팀 간 공유 필요"| stage2 -->|"조직 표준화"| stage3
  

시맨틱 버저닝(Semantic Versioning) 규칙

버전 변경의미예시
v1.0.0 → v1.0.1 (Patch)버그 수정, 동작 변경 없음태그 오타 수정
v1.0.0 → v1.1.0 (Minor)하위 호환되는 기능 추가선택적 변수 enable_versioning 추가
v1.0.0 → v2.0.0 (Major)Breaking Change — 하위 호환 깨짐변수 이름 변경, 필수 변수 추가
버전 고정(Pinning) 없는 Git 소스는 시한폭탄입니다: ref 없이 source = "git::https://..."만 쓰면 항상 기본 브랜치(main)의 최신 커밋을 가져옵니다. 모듈 저장소에 누군가 커밋을 푸시하는 순간, 다음 terraform init부터 소비자의 인프라 계획이 바뀔 수 있습니다. 운영 코드는 반드시 태그로 버전을 고정하세요.

이 랩이 입증하는 실무 역량

실제 LinkedIn 채용공고(JD)에서 요구하는 역량입니다.

“lead the design, development, and governance of Infrastructure as Code (IaC) using Terraform… build and maintain reusable Terraform modules, support production infrastructure changes” — Snowrelic Inc, Senior Terraform Engineer

“Terraform Enterprise, Private Module Registry, Terraform Sentinel” — TALENTRIX AI / Congensys Corp, Terraform Infrastructure Engineer

JD 요구사항이 랩에서 커버하는 내용
reusable Terraform modules 구축·유지보수S3 버킷 모듈을 별도 Git 저장소로 분리하고 입력/출력 인터페이스 설계
IaC governance시맨틱 버저닝 태그 전략과 버전 고정(pinning)으로 변경 통제
production infrastructure changes 지원v1 → v2 breaking change를 소비자가 원하는 시점에 선택적으로 수용하는 업그레이드 절차
Private Module RegistryTerraform Cloud/Enterprise의 내부 레지스트리 개념과 Registry 공개 모듈(terraform-aws-modules) 사용법

실습 파일 구성

모듈 저장소와 소비자 코드는 별도의 Git 저장소입니다. 이 랩에서는 로컬에 두 디렉터리로 만들되, 모듈 쪽만 Git 저장소로 초기화하고 태그를 붙입니다.

lab11-module-versioning/
├── s3-module/              ← 버전 태그를 붙일 모듈 저장소 (v1.0.0 → v1.1.0 → v2.0.0)
│   ├── versions.tf
│   ├── variables.tf
│   ├── main.tf
│   └── outputs.tf
└── consumer/                ← 모듈을 소비하는 루트 모듈
    ├── versions.tf
    ├── providers.tf
    ├── main.tf
    └── outputs.tf

이 랩을 실제로 실행하려면 s3-module을 먼저 로컬 Git 저장소로 만들어야 합니다: consumer/main.tf는 원격 Git 호스트나 Registry가 아니라 로컬 절대경로를 가리키는 git::file:///.../s3-module?ref=v1.0.0 소스를 사용합니다. 아래 명령을 먼저 실행해 두어야 consumerterraform init이 성공합니다.

cd s3-module
git init
git add -A
git commit -m "v1.0.0"
git tag v1.0.0

이후 랩 진행에 따라 모듈을 v1.1.0, v2.0.0으로 진화시킬 때도 동일하게 커밋 후 태그를 붙여야 합니다(아래 “실행 단계” 참고). 또한 예제의 절대경로는 이 저장소가 클론된 실제 위치에 맞게 반드시 수정해야 합니다.

consumer/main.tfrandom_string.suffix 리소스를 두고 app_bucket, registry_bucket 두 모듈의 버킷 이름 뒤에 붙입니다. S3 버킷 이름은 전 세계에서 유일해야 하므로 리터럴 이름을 그대로 쓰면 BucketAlreadyExists 오류가 발생합니다. 이 때문에 consumer/versions.tf에도 random 프로바이더가 추가로 필요합니다.

모듈 코드 — s3-module (v1.0.0)

s3-module/versions.tf

terraform {
  required_version = ">= 1.0.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

s3-module/variables.tf

# v1.0.0 인터페이스: 버킷 이름 (v2.0.0에서 name으로 개명 예정 — Breaking Change)
variable "bucket_name" {
  description = "생성할 S3 버킷 이름"
  type        = string
}

variable "tags" {
  description = "버킷에 부여할 태그"
  type        = map(string)
  default     = {}
}

s3-module/main.tf

# S3 버킷 생성 (모듈 v1.0.0)
resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name

  tags = merge(var.tags, {
    ManagedBy = "terraform"
  })
}

# v1.1.0에서 추가할 선택적 버저닝 리소스 예시 (하위 호환 Minor 업그레이드)
# variable "enable_versioning" { type = bool, default = false } 를 variables.tf에 추가한 뒤 아래 블록을 활성화합니다.
# resource "aws_s3_bucket_versioning" "this" {
#   count  = var.enable_versioning ? 1 : 0
#   bucket = aws_s3_bucket.this.id
#
#   versioning_configuration {
#     status = "Enabled"
#   }
# }
s3-module은 버전별로 별도 디렉터리를 두지 않습니다. 하나의 모듈 디렉터리를 그대로 두고 Git 태그로만 버전을 구분합니다. v1.1.0에서 추가할 리소스는 위처럼 미리 주석으로 안내만 해 두고, 실제 코드 변경은 실행 단계에서 주석을 해제하고 커밋·태그하는 방식으로 진행합니다.

s3-module/outputs.tf

output "bucket_id" {
  description = "생성된 버킷 이름"
  value       = aws_s3_bucket.this.id
}

output "bucket_arn" {
  description = "생성된 버킷 ARN"
  value       = aws_s3_bucket.this.arn
}

소비자 코드 — consumer

consumer/versions.tf

terraform {
  required_version = ">= 1.0.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.0"
    }
  }
}

consumer/providers.tf

provider "aws" {
  region = "ap-northeast-2"
}

consumer/main.tf

# 버킷 이름 중복 방지를 위한 무작위 접미사
resource "random_string" "suffix" {
  length  = 8
  special = false
  upper   = false
}

# Git 태그(v1.0.0)로 버전을 고정한 모듈 호출
# 실습에서는 로컬 Git 저장소(file://)를 사용하고,
# 실무에서는 GitHub 등 원격 저장소 URL을 사용합니다.
# ref를 v1.1.0 -> v2.0.0으로 올릴 때는 반드시 `terraform init -upgrade`가 필요합니다.
module "app_bucket" {
  source = "git::file:///절대경로/lab11-module-versioning/s3-module?ref=v1.0.0"
  # 실무 예시:
  # source = "git::https://github.com/my-org/terraform-aws-s3-module.git?ref=v1.0.0"

  bucket_name = "lab11-module-versioning-app-${random_string.suffix.result}"

  tags = {
    Name = "lab11-app"
  }
}

# Terraform Registry 공개 모듈 사용 예시 (보너스) — 버전 제약 연산자로 4.x 최신만 허용, 5.0은 차단
module "registry_bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "~> 4.0"

  bucket = "lab11-registry-example-${random_string.suffix.result}"

  tags = {
    Name = "lab11-registry"
  }
}
Git 소스 모듈(app_bucket)과 Registry 소스 모듈(registry_bucket)을 같은 파일에 나란히 둘 수 있습니다. Registry 모듈에 대한 자세한 설명은 아래 “Terraform Registry 공개 모듈 사용” 절을 참고하세요.

consumer/outputs.tf

output "bucket_id" {
  value = module.app_bucket.bucket_id
}

output "bucket_arn" {
  value = module.app_bucket.bucket_arn
}

output "registry_bucket_id" {
  value = module.registry_bucket.s3_bucket_id
}

output "registry_bucket_arn" {
  value = module.registry_bucket.s3_bucket_arn
}
git::file://도 동작 원리는 동일합니다: Terraform은 git:: 접두어를 보면 git clone을 실행하고 ref로 지정된 태그/브랜치/커밋을 체크아웃합니다. 로컬 경로든 GitHub URL이든 태그 기반 버전 고정 메커니즘은 완전히 같으므로, 네트워크 없이도 실무와 동일한 흐름을 연습할 수 있습니다.

실행 단계

모듈 저장소 초기화 및 v1.0.0 태그

cd lab11-module-versioning/s3-module

git init
git add .
git commit -m "feat: initial S3 bucket module"

# 시맨틱 버저닝 태그 부여
git tag v1.0.0
git tag   # v1.0.0 확인

소비자에서 v1.0.0 사용

cd ../consumer

# main.tf의 source에 s3-module의 실제 절대경로를 반영한 뒤
terraform init
Initializing modules...
Downloading git::file:///.../s3-module?ref=v1.0.0 for app_bucket...
- app_bucket in .terraform/modules/app_bucket
terraform apply -auto-approve

v1.1.0 — 하위 호환 기능 추가 (Minor)

모듈에 선택적 변수를 추가합니다. 기본값이 있으므로 기존 소비자는 아무것도 바꾸지 않아도 됩니다.

s3-module/variables.tf에 추가:

variable "enable_versioning" {
  description = "버킷 버저닝 활성화 여부"
  type        = bool
  default     = false
}

s3-module/main.tf에는 이미 아래 리소스가 주석으로 준비되어 있습니다. 주석을 해제해 활성화합니다(모듈 코드는 버전마다 새 디렉터리를 만들지 않고, 같은 디렉터리를 그대로 진화시킵니다):

resource "aws_s3_bucket_versioning" "this" {
  count  = var.enable_versioning ? 1 : 0
  bucket = aws_s3_bucket.this.id

  versioning_configuration {
    status = "Enabled"
  }
}
cd ../s3-module
git add .
git commit -m "feat: add optional enable_versioning variable"
git tag v1.1.0

소비자 업그레이드 — ref 변경 + init -upgrade

consumer/main.tfref만 수정합니다:

  source = "git::file:///절대경로/lab11-module-versioning/s3-module?ref=v1.1.0"

  bucket_name       = "lab11-module-versioning-app-${random_string.suffix.result}"
  enable_versioning = true   # v1.1.0  기능 사용
cd ../consumer

# ref가 바뀌면 반드시 init -upgrade로 모듈을 다시 다운로드
terraform init -upgrade
terraform plan
terraform apply -auto-approve

v2.0.0 — Breaking Change (Major)

변수 이름을 bucket_namename으로 변경합니다. 기존 소비자 코드가 그대로는 동작하지 않는 파괴적 변경입니다.

s3-module/variables.tf:

variable "name" {   # bucket_name에서 이름 변경 (BREAKING)
  description = "생성할 S3 버킷 이름"
  type        = string
}

s3-module/main.tf의 참조도 var.name으로 수정 후:

cd ../s3-module
git add .
git commit -m "feat!: rename bucket_name to name

BREAKING CHANGE: input variable bucket_name renamed to name"
git tag v2.0.0

v2.0.0 업그레이드 실패 체험 → 코드 수정 후 성공

먼저 ref=v2.0.0으로만 바꾸고 소비자 코드는 그대로 둔 채 실행해봅니다:

cd ../consumer
terraform init -upgrade
terraform plan
Error: Unsupported argument

  on main.tf line 8, in module "app_bucket":
   8:   bucket_name = "lab11-module-versioning-app-${random_string.suffix.result}"

An argument named "bucket_name" is not expected here.

Breaking change가 소비자를 어떻게 깨뜨리는지 확인했습니다. 이제 소비자 코드를 v2 인터페이스에 맞게 수정합니다:

module "app_bucket" {
  source = "git::file:///절대경로/lab11-module-versioning/s3-module?ref=v2.0.0"

  name              = "lab11-module-versioning-app-${random_string.suffix.result}"   # bucket_name → name
  enable_versioning = true

  tags = {
    Name = "lab11-app"
  }
}
terraform plan    # No changes 또는 태그 변경만 확인
terraform apply -auto-approve

예상 결과 / 검증

각 단계에서 다음을 확인할 수 있어야 합니다.

# 1. 모듈 저장소에 세 개의 버전 태그 존재
cd ../s3-module && git tag
# v1.0.0
# v1.1.0
# v2.0.0

# 2. 소비자가 어떤 버전을 쓰는지 확인
cd ../consumer
cat .terraform/modules/modules.json | python3 -m json.tool
# "Source": "git::file:///...?ref=v2.0.0"  ← ref가 그대로 기록됨 ✅

# 3. 버저닝이 실제로 켜졌는지 확인 (v1.1.0 기능)
# 버킷 이름은 random_string.suffix가 붙으므로 terraform output으로 실제 이름을 먼저 확인합니다
terraform output bucket_id
aws s3api get-bucket-versioning --bucket "$(terraform output -raw bucket_id)"
# { "Status": "Enabled" } ✅

# 4. plan이 깨끗한지 최종 확인
terraform plan
# No changes. Your infrastructure matches the configuration. ✅

핵심 검증 포인트: v2.0.0으로 ref만 올렸을 때 Unsupported argument 오류가 났다가, 소비자 코드를 수정하면 통과하는 흐름을 직접 경험했다면 이 랩의 목표를 달성한 것입니다.


Terraform Registry 공개 모듈 사용 (보너스)

직접 만들지 않아도 되는 범용 모듈은 Terraform Registry의 검증된 모듈을 사용합니다. Registry 모듈은 version 인자로 버전 제약 연산자를 지원합니다. 이 랩의 consumer/main.tf에는 위에서 이미 본 것처럼 registry_bucket 모듈이 app_bucket(Git 소스)과 나란히 정의되어 있습니다.

module "registry_bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "~> 4.0"   # 4.x의 최신 버전 사용, 5.0은 차단

  bucket = "lab11-registry-example-${random_string.suffix.result}"

  tags = {
    Name = "lab11-registry"
  }
}
연산자의미
version = "4.1.2"정확히 4.1.2만
version = "~> 4.1"4.1 이상 5.0 미만 (Minor까지 허용)
version = "~> 4.1.0"4.1.0 이상 4.2.0 미만 (Patch만 허용)
version = ">= 4.0, < 5.0"범위 지정
Private Module Registry: Terraform Cloud/Enterprise에는 조직 전용 모듈 레지스트리가 내장되어 있습니다. VCS(GitHub 등) 저장소를 연결하면 Git 태그를 푸시하는 것만으로 새 버전이 자동 배포되고, 소비자는 source = "app.terraform.io/my-org/s3-bucket/aws" + version = "~> 1.0" 형태로 사용합니다. 이 랩에서 연습한 “태그 = 버전” 전략이 그대로 Private Registry의 배포 메커니즘이 됩니다. Sentinel 정책과 결합하면 “승인된 모듈만 사용 가능” 같은 거버넌스도 강제할 수 있습니다.

실습 정리

cd lab11-module-versioning/consumer
terraform destroy -auto-approve

app_bucket(Git 소스)뿐 아니라 registry_bucket(Registry 소스) 모듈도 함께 삭제되는지 destroy 출력에서 확인하세요.


실무 포인트

ref에는 브랜치가 아닌 태그(또는 커밋 해시)를 사용하세요: ref=main은 기술적으로 동작하지만 브랜치는 계속 움직이는 포인터입니다. 태그도 강제로 옮길 수 있으므로, 감사(audit) 요구사항이 엄격한 조직은 ref=<커밋 해시>로 고정하기도 합니다.
Major 업그레이드는 반드시 CHANGELOG와 함께: v2.0.0을 릴리스할 때는 무엇이 깨지는지, 소비자가 코드를 어떻게 수정해야 하는지(마이그레이션 가이드)를 문서화해야 합니다. git tag -a v2.0.0 -m "..." 주석 태그와 저장소의 CHANGELOG.md가 최소한의 장치입니다.
모듈 업그레이드는 환경별로 점진적으로: 실무에서는 dev 환경의 ref를 먼저 올려 검증한 뒤 stage → prod 순서로 올립니다. 환경마다 소비자 코드가 분리되어 있으면(Lab 06 환경 분리 참고) 환경별로 다른 모듈 버전을 쓰는 과도기를 안전하게 운영할 수 있습니다.
모듈 개발 중에는 로컬 경로, 릴리스 후에는 태그: 모듈을 활발히 수정하는 동안 매번 커밋+태그+init -upgrade를 반복하면 느립니다. 개발 중에는 source = "../s3-module" 로컬 경로로 빠르게 반복하고, 인터페이스가 안정되면 태그를 릴리스해 Git 소스로 전환하는 것이 일반적인 워크플로입니다.

→ 다음 실습: Lab 12 Policy as Code — 배포 전 정책 검사 자동화