IT/IaC

Terraform ephemeral 리소스와 write-only, 이제는 도입할 때다

gfrog 2026. 8. 31. 12:14

지난주 팀 내부에서 "Terraform state에 DB 패스워드가 평문으로 남아 있다"는 이슈가 다시 도마 위에 올랐다. 사실 이 문제는 몇 년째 우리를 괴롭혀 왔다. Vault provider로 뽑아서 쓰면 state에 값이 그대로 박혔고, sensitive = true를 걸어도 state 파일 안에서는 여전히 읽을 수 있는 문자열이었다. 그때마다 "state를 S3 + KMS로 암호화해두면 되지 않냐"는 답을 반복했지만, 백엔드 암호화는 결국 저장소 문제일 뿐 값 자체를 감추지는 못한다.

Terraform 1.10에서 ephemeral 리소스가, 1.11에서 write-only 인수(argument)가 정식으로 나오면서 이 이야기의 결이 좀 달라졌다. 최근 HashiCorp 블로그에서도 두 기능을 묶어서 "관리 리소스에도 ephemeral 값을 안전하게 넘길 수 있게 됐다"고 정리해뒀는데, 이번에 팀 파일럿을 돌려보고 그 흐름을 정리한다. 결론부터 말하면, 완전한 만능은 아니지만 시크릿 관리 파이프라인의 한 계층을 걷어낼 수 있는 도구다.

ephemeral과 write-only, 뭐가 어떻게 다른가

두 개념이 헷갈리기 쉬워서 먼저 짚고 간다.

ephemeral 블록/리소스는 plan과 apply가 도는 그 순간에만 존재하는 값이다. state에도, plan 파일에도 남지 않는다. 예를 들면 이런 식이다.

ephemeral "aws_secretsmanager_secret_version" "db" {
  secret_id = "prod/orders-api/db"
}

locals {
  db_password = ephemeral.aws_secretsmanager_secret_version.db.secret_string
}

이렇게 뽑아낸 값은 리소스 인수로 넘길 수는 있지만, 그 인수가 write-only로 선언돼 있어야 state에 안 박힌다. 여기서 write-only가 등장한다.

write-only 인수는 프로바이더가 "이 값은 받아서 API에 넘기기만 하고, 내가 state에 기록하지 않겠다"고 선언한 필드다. 값 대신 _wo_version 같은 버전 번호만 state에 남는다. 우리가 값을 바꾸고 싶으면 버전 숫자를 올려서 apply를 트리거하는 구조다.

즉 조합은 이렇게 된다.

  • ephemeral 리소스로 시크릿을 꺼내오고
  • 관리 리소스의 write-only 인수로 주입한다
  • state에는 값도, 이전 값도 남지 않는다

실제로 걸어본 예제

RDS 인스턴스 패스워드 관리 코드가 우리 파일럿 대상이었다. 기존 코드는 대략 이랬다.

data "aws_secretsmanager_secret_version" "db" {
  secret_id = "prod/orders-api/db"
}

resource "aws_db_instance" "orders" {
  identifier     = "orders-prod"
  engine         = "postgres"
  instance_class = "db.r6g.large"
  username       = "orders_app"
  password       = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)["password"]
  # ...
}

data 블록으로 꺼낸 값도, password 인수에 들어간 값도 전부 state에 그대로 저장됐다. 우리는 이걸 아래처럼 바꿨다.

ephemeral "aws_secretsmanager_secret_version" "db" {
  secret_id = "prod/orders-api/db"
}

resource "aws_db_instance" "orders" {
  identifier     = "orders-prod"
  engine         = "postgres"
  instance_class = "db.r6g.large"
  username       = "orders_app"

  password_wo         = jsondecode(ephemeral.aws_secretsmanager_secret_version.db.secret_string)["password"]
  password_wo_version = 3
}

password_wo가 write-only 인수, password_wo_version은 값 변경을 강제로 트리거할 때 올리는 버전 카운터다. Secrets Manager에서 값을 로테이션하고 나서 password_wo_version을 3에서 4로 올려주면 apply가 다시 API를 호출한다. 이 흐름이 처음에는 어색했다. "왜 굳이 사람이 버전을 손으로 올려야 하나" 싶었는데, 곰곰이 생각해보면 이게 없으면 Terraform이 값 변경을 감지할 방법이 없다. 값은 이미 state에 없기 때문이다.

state를 열어봤을 때 실제로 이렇게 나온다.

"password_wo": null,
"password_wo_version": 3,

값은 사라졌다. 이걸 처음 확인했을 때 팀에서 반응이 갈렸다. "드디어" 하는 사람이 있었고, "그럼 값 로테이션 트리거를 CI에서 어떻게 걸지" 걱정하는 사람도 있었다.

도입할 때 우리가 실제로 고민한 세 가지

1. 버전 카운터를 누가 관리하나

_wo_version 값을 사람이 코드에서 손으로 올리는 건 결국 실수를 유발한다. 우리는 Secrets Manager 로테이션 이벤트를 EventBridge로 받아서 GitHub Actions 워크플로우를 트리거하고, 해당 리소스의 _wo_version 값을 sed로 +1 하는 스크립트를 붙였다. 완벽하진 않다. tf 파일에 하드코딩된 숫자가 바뀌는 게 리뷰 흐름을 좀 어색하게 만든다.

대안으로는 terraform_data에 트리거 해시를 걸고 그 해시를 _wo_version 대신 넣는 방법도 있는데, 이 경우 프로바이더가 write-only version 인수 타입으로 문자열/정수 중 뭘 받는지 확인해야 한다. AWS provider 기준으로 password_wo_version은 정수라서 우리는 결국 자동 증가 스크립트로 갔다.

2. drift 감지가 사실상 불가능해진다

값을 state에 안 남기니, 누가 콘솔에서 RDS 마스터 패스워드를 직접 바꿔도 Terraform은 절대 모른다. 이게 원래 우리가 원했던 그림이긴 하다. 그런데 반대로, 애플리케이션이 인증에 실패하기 시작하면 "Terraform에는 문제없는데 왜 안 되지?" 하는 상황이 벌어질 수 있다. 팀 내부적으로는 "시크릿 계열 값은 Terraform이 아니라 로테이션 파이프라인이 소유한다"라는 원칙을 문서로 못 박아뒀다.

3. 모든 리소스 인수가 write-only를 지원하는 건 아니다

이게 도입할 때 가장 실망스러운 부분이었다. AWS provider의 aws_db_instance.password_wo는 되지만, 우리가 쓰던 aws_rds_cluster의 마스터 패스워드는 이 글 쓰는 시점까지도 write-only 대응이 늦었다. 프로바이더 릴리즈 노트에서 _wo 접미사가 붙은 인수를 하나씩 찾아 다녀야 한다. 파일럿을 뭉텅이로 못 밀어붙이고 리소스별로 갈라서 진행한 이유가 여기에 있다.

Azure azapi 프로바이더도 최근에 ephemeral resource / write-only attribute 가이드를 별도로 냈고, 커뮤니티 프로바이더 쪽은 아직 온도차가 크다. 도입 결정 전에 자기 팀이 쓰는 프로바이더가 어디까지 지원하는지 매트릭스 뽑아보길 권한다.

그래서 지금 도입해도 되나

우리 결론은 "시크릿 하나씩 점진적으로"다. 신규 모듈은 처음부터 ephemeral + write-only로 짜고, 기존 모듈은 로테이션 파이프라인이 이미 붙어있는 시크릿부터 마이그레이션한다. state에서 값이 사라지는 건 심리적으로도 꽤 큰 변화라서, 한 번에 다 갈아엎으면 팀이 감당을 못 한다.

Terraform 1.11 릴리즈 노트를 다시 훑어보고, 자기 팀의 프로바이더 버전에서 어떤 인수가 write-only를 지원하는지 표부터 만들어보길 권한다. 그다음에 시크릿 하나 골라서 파일럿을 돌려보면, "state에 값이 없어도 apply가 도는" 감각을 잡는 데 하루면 충분하다.

혹시 이미 프로덕션에 도입해서 로테이션 트리거를 다르게 푸신 분 있으면 후기 남겨주시라. 우리도 아직 워크플로우가 예쁘지 않다.

참고

  • HashiCorp: Terraform 1.11 brings ephemeral values to managed resources with write-only arguments
  • HashiCorp Developer: Plugin Framework - Write-only Arguments

태그: terraform, IaC, DevOps, 시크릿관리, ephemeral, HashiCorp