테라폼 실전 강좌 #6 시크릿 관리: SSM vs Secrets Manager, write-only 인수

4 분 소요

지난 편의 마지막 확인은 불편했습니다. sensitive를 붙였는데도 DB 비밀번호가 state에 평문으로 있고, tfvars에도 평문으로 있습니다. 이번 편은 이 문제를 세 단계로 풉니다. 시크릿을 둘 곳을 정하고(보관소), 테라폼이 시크릿을 다루면서도 기록에 남기지 않게 만들고(write-only), 애플리케이션이 시크릿을 받는 경로를 세웁니다(런타임 조회). 셋 중 하나라도 빠지면 어딘가에 평문이 남습니다.

보관소 선택: SSM Parameter Store vs Secrets Manager #

AWS에서 시크릿 보관소 후보는 둘입니다.

항목SSM Parameter StoreSecrets Manager
요금표준 파라미터 무료시크릿당 월 요금 + API 호출
암호화SecureString(KMS)기본 KMS 암호화
자동 로테이션없음(직접 구현)RDS 등과 통합된 로테이션 내장
어울리는 곳설정값, 소규모 시크릿로테이션이 필요한 DB 자격 증명

선택 기준은 로테이션입니다. 비밀번호를 주기적으로 자동 교체해야 하는 요구가 있으면 Secrets Manager가 그 기능을 내장하고 있고, 그 요구가 없다면 무료인 SSM SecureString으로 충분합니다. 이 실습은 비용 원칙에 따라 SSM으로 진행하되, 코드 구조는 어느 쪽이든 같으므로 옮기는 것은 어렵지 않습니다.

ephemeral과 write-only: 기록에 남지 않는 값 #

테라폼 1.10과 1.11에서 이 문제를 위한 언어 기능이 정식으로 들어왔습니다. 두 조각입니다.

  • ephemeral 리소스: plan·state에 기록되지 않는 일회성 값. ephemeral "random_password"는 실행 중에만 존재하는 비밀번호를 만듭니다.
  • write-only 인수: 값을 provider에 전달만 하고 기록하지 않는 인수. aws_db_instance의 password_wo가 그것으로, 일반 password 인수와 달리 state에 저장되지 않습니다.

둘을 조합하면 비밀번호의 전체 여정이 기록 밖에서 끝납니다.

secrets.tf
# secrets.tf (새 파일)
ephemeral "random_password" "db" {
  length  = 24
  special = false
}

resource "aws_ssm_parameter" "db_password" {
  name             = "/myapp/${var.env}/db-password"
  type             = "SecureString"
  value_wo         = ephemeral.random_password.db.result
  value_wo_version = 1
}
db.tf
# db.tf 수정
resource "aws_db_instance" "main" {
  # ... 나머지는 지난 편 그대로 ...

  password_wo         = ephemeral.random_password.db.result
  password_wo_version = 1
}

지난 편의 password = var.db_password와 variable "db_password"는 지웁니다. 이제 비밀번호는 사람이 정하지도, 파일에 적히지도 않습니다. 실행 중에 생성되어 RDS와 SSM에 전달되고 사라집니다. terraform state show aws_db_instance.main을 다시 실행해 보면 password가 보이지 않습니다.

_wo_version이라는 낯선 짝이 붙는 이유도 알아 둡니다. write-only 값은 state에 없으므로 테라폼이 값의 변경을 감지할 수 없습니다. 그래서 비밀번호를 교체하고 싶을 때는 값을 바꾸는 것이 아니라 password_wo_version을 1에서 2로 올립니다. 버전 번호의 변경이 plan에 잡혀 provider가 새 값을 전달하는 구조입니다.

애플리케이션은 런타임에 읽습니다 #

시크릿이 SSM에 안전하게 들어갔어도, 그것을 user_data로 꺼내 파일에 심으면 인스턴스 안에 평문 사본이 생기고 원점으로 돌아갑니다. 원칙은 앱이 시작할 때 보관소에서 직접 읽는 것입니다.

시크릿 조회
# 애플리케이션 시작 스크립트에서
DB_PASSWORD=$(aws ssm get-parameter \
  --name "/myapp/dev/db-password" \
  --with-decryption \
  --query Parameter.Value --output text)

이 구조의 장점은 시크릿 교체가 배포와 분리된다는 것입니다. SSM의 값을 바꾸고 앱을 재시작하면 끝이며, 이미지나 launch template을 다시 만들 필요가 없습니다. 남은 퍼즐 조각은 권한입니다. 인스턴스가 ssm:GetParameter를 호출하려면 IAM 역할이 필요한데, 지금 인스턴스에는 역할이 없습니다. 이것이 바로 다음 편의 주제이고, SSH 없는 접속(Session Manager)까지 함께 풀립니다.

남는 기록과 위험 정리 #

이 구성으로 무엇이 지워졌고 무엇이 남는지 정직하게 정리해 둡니다.

  • 지워진 것: 코드·tfvars·plan·state의 비밀번호 평문. 사람이 비밀번호를 아는 단계 자체가 없습니다.
  • 남는 것: SSM 파라미터(KMS 암호화 저장)와 RDS 내부. 이 둘은 시크릿이 있어야 할 자리이며, 접근은 IAM으로 통제합니다.
  • 여전한 원칙: 기초 #9의 state 버킷 접근 제어는 그대로 중요합니다. write-only가 DB 비밀번호를 지웠어도, state에는 여전히 인프라 구조 전체가 담겨 있기 때문입니다.

정리 #

이번 글에서 다룬 내용입니다.

  • 보관소 선택 기준은 로테이션입니다. 필요하면 Secrets Manager, 아니면 무료인 SSM SecureString으로 충분합니다
  • ephemeral 리소스는 실행 중에만 존재하는 값을 만들고, write-only 인수(password_wo, value_wo)는 값을 전달만 하고 state에 남기지 않습니다. 둘 다 테라폼 1.11의 정식 기능입니다
  • write-only 값의 교체는 값 변경이 아니라 _wo_version 증가로 트리거합니다. state에 값이 없어 변경 감지가 불가능하기 때문입니다
  • 앱은 시크릿을 런타임에 보관소에서 직접 읽습니다. user_data에 심으면 평문 사본이 되살아납니다
  • 시크릿은 이제 SSM과 RDS 안에만 존재하며, 남은 과제는 읽기 권한을 주는 IAM입니다

다음 글(#7 IAM 코드화)에서는 인스턴스 역할로 SSM 접근과 Session Manager 접속을 완성하고, GitHub Actions가 액세스 키 없이 AWS에 들어오는 OIDC 연동까지 만듭니다.

X