테라폼 실전 강좌 #6 시크릿 관리: SSM vs Secrets Manager, write-only 인수
지난 편의 마지막 확인은 불편했습니다. sensitive를 붙였는데도 DB 비밀번호가 state에 평문으로 있고, tfvars에도 평문으로 있습니다. 이번 편은 이 문제를 세 단계로 풉니다. 시크릿을 둘 곳을 정하고(보관소), 테라폼이 시크릿을 다루면서도 기록에 남기지 않게 만들고(write-only), 애플리케이션이 시크릿을 받는 경로를 세웁니다(런타임 조회). 셋 중 하나라도 빠지면 어딘가에 평문이 남습니다.
보관소 선택: SSM Parameter Store vs Secrets Manager #
AWS에서 시크릿 보관소 후보는 둘입니다.
| 항목 | SSM Parameter Store | Secrets 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 (새 파일)
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 수정
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 연동까지 만듭니다.