Terraform ephemeral과 write-only, 1년 굴리고 정리한 진짜 사용 패턴

Terraform ephemeral과 write-only, 1년 굴리고 정리한 진짜 사용 패턴
작년 이맘때쯤 Terraform 1.10이 나왔고, 그때부터 ephemeral 블록과 write-only 인자를 본격적으로 우리 코드베이스에 섞기 시작했다. 1.11에서 managed resource에도 write-only argument가 들어오면서 본격적으로 "state에서 시크릿을 빼는" 작업을 했고, 지금은 1.15.3까지 와있다. 1년이 지난 지금 다시 보면, 처음에 우리가 잘못 이해하고 있던 것들이 꽤 있었다. 단순히 "state에 안 남는 변수"가 아니라, 라이프사이클 자체가 다른 객체라는 것을 운영하면서야 체감했다.
이 글은 "ephemeral이 뭔지 알려주는" 글이 아니다. 그건 hashicorp 블로그에 충분히 잘 정리돼 있다. 대신 우리가 1년간 부딪힌 함정과, 결과적으로 자리잡은 사용 패턴 두세 개를 풀어둔다. AWS provider 기준이지만 다른 provider에도 거의 그대로 적용된다.
사실 내부적으로는 "값"이 아니라 "수명"이다
처음에 ephemeral을 도입하면서 우리 팀은 이걸 "민감한 값"의 동의어처럼 받아들였다. 그게 첫 번째 오해였다. ephemeral은 값의 민감도가 아니라 값의 *수명*을 정의한다. Terraform 코어 입장에서 ephemeral 값은:
- plan 단계에서 한 번 평가된다
- apply 단계에서 다시 평가된다 (이게 핵심이다)
- plan 파일에도, state 파일에도 직렬화되지 않는다
- 결과적으로
terraform output으로도 꺼낼 수 없고, 다른 ephemeral context 밖으로 새어나갈 수 없다
여기서 "apply 단계에서 다시 평가된다"는 게 의외로 자주 트리거를 친다. 예를 들어 ephemeral "aws_secretsmanager_secret_version" 으로 비밀번호를 꺼내 RDS 비밀번호로 넣어두면, plan 시점의 시크릿 값과 apply 시점의 값이 다를 수 있다. 누군가 그 사이에 rotation을 돌리면? plan은 멀쩡히 그렸는데 apply가 다른 비밀번호로 들어간다. 우리는 이걸 한 번 봤고, 그 뒤로 rotation 직후 5분간은 apply를 자제하는 룰을 운영 규칙에 박아놨다.
코드로 보면 이런 모양이다.
ephemeral "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db.id
}
resource "aws_db_instance" "main" {
identifier = "prod-main"
engine = "postgres"
engine_version = "16.4"
instance_class = "db.r6g.xlarge"
username = "appuser"
password_wo = ephemeral.aws_secretsmanager_secret_version.db_password.secret_string
password_wo_version = var.db_password_version
}
password_wo가 write-only argument고 password_wo_version이 짝꿍이다. version 변수를 안 올리면 ephemeral 값이 바뀌어도 provider가 "보낼 이유가 없다"고 판단해서 실제 RDS에는 안 내려간다. 이게 우리 팀에서 처음으로 놓친 두 번째 함정이다.
write-only version, 이게 진짜 트리거다
*_wo 인자는 정말로 write-only다. state에 안 박힌다. 그러면 Terraform이 다음 실행 때 "지금 값이 바뀐 건지 아닌 건지"를 어떻게 알까? 모른다. 못 안다. 그래서 짝꿍으로 들어온 게 *_wo_version 인자다. 이 version만 state에 박힌다. version이 바뀌면 provider한테 "값이 바뀌었다고 가정하고 update 호출 보내라"고 시그널을 준다.
운영에서 이게 의미하는 건 단순하다. 시크릿을 회전시켰으면 version을 같이 올려야 한다. 안 그러면 Terraform은 자기가 보기엔 변한 게 없다고 판단하고 아무것도 안 한다. 우리 팀에서는 처음에 이걸 모르고 Secrets Manager에서 rotation은 잘 돌아가는데 RDS 비밀번호는 옛날 값으로 박혀있는, 정말 황당한 상태를 만든 적이 있다. 30분쯤 디버깅하고 나서야 wo_version을 안 올렸다는 걸 알았다.
지금은 이런 패턴으로 묶어 쓴다.
variable "db_password_version" {
type = number
description = "DB 비밀번호 회전 카운터. Secrets Manager rotation 후 +1"
default = 1
}
그리고 rotation Lambda가 끝나면 SSM Parameter Store에 카운터를 올리고, terraform var는 그걸 datasource로 읽어오는 식으로 묶어둔다. 자동화 안 하면 사람이 까먹는다. 한 번 까먹어 봤다.
ephemeral은 다른 ephemeral로만 흐른다
이게 처음에 진짜 헷갈렸다. ephemeral 값은 다른 ephemeral context로만 흐를 수 있다. 즉,
- ephemeral resource의 출력은 → 다른 ephemeral resource의 input으로, 또는 → managed resource의
*_wo인자로만 갈 수 있다 - 일반
locals에 못 박는다 (정확히는 박을 수는 있는데 그 local도 ephemeral이 된다) - output 블록에 못 넣는다. 굳이 넣으려면
ephemeral = true옵션을 줘야 하는데, 그러면 module 사용자가 가져다 쓸 때 똑같이 ephemeral context에서만 써야 한다
이걸 모르고 처음에는 ephemeral 데이터를 local 변수에 풀어서 for_each에서 키로 쓰는 짓을 하다가 plan부터 폭발했다. for_each의 키는 plan 시점에 known이어야 하는데, ephemeral은 그 보장이 없다. 그래서 Terraform이 친절하게 막아준다.
이 제약이 답답해 보일 수 있는데, 1년 써보니 오히려 이게 안전 장치다. 시크릿이 어디로 흘러갔는지 추적할 때 ephemeral 표식이 자동으로 따라다니니까, 누가 실수로 output에 풀거나 디버깅하다가 풀어버리는 사고를 구조적으로 막는다. provider 개발자 입장에서도 ephemeral context 안에서만 시크릿을 다루면 되니까 메모리 관리가 단순해진다.
어떤 자원에는 아직 쓰지 마라
1년이 지났는데도 모든 provider, 모든 자원이 write-only를 지원하지는 않는다. AWS provider는 그중 빠른 편이라 aws_db_instance.password_wo, aws_rds_cluster.master_password_wo, aws_iam_user_policy_attachment 일부, Secrets Manager 관련 자원 정도가 들어갔다. 반면 우리가 자주 쓰는 자원 중에:
aws_eks_cluster의 OIDC client secret 류는 여전히 그냥 attribute다- third-party provider 중 datadog, cloudflare 같은 메이저 한 곳도 token argument에 write-only를 아직 일부만 깐다
- 자체 만든 internal provider는 아예 안 깔려있을 가능성이 높다
그래서 우리 팀은 코드를 까는 순서를 이렇게 잡았다. RDS / RDS Aurora의 master password → Secrets Manager로 흘러가는 다른 secret 자원 → IAM 류. 그 외에는 우선순위를 낮춰서 provider가 지원할 때 점진적으로 옮긴다. 한 번에 다 옮기겠다는 야망은 빨리 버리는 게 좋다. provider 의존성이 발목을 잡는다.
1년 운영 끝에 자리잡은 패턴 세 개
이 정도 시점에 와서 우리 팀에 자리잡은 패턴 세 개를 정리해 둔다. 다른 팀이 시작할 때 참고할 만하다.
첫째, 시크릿의 source of truth는 Terraform 바깥에 둔다. Secrets Manager든 Vault든 좋은데, Terraform은 그걸 "흘려보내는" 도구로만 쓴다. ephemeral resource로 값을 읽고, write-only argument로 자원에 박는다. Terraform state에는 절대 안 남는다. 이 원칙을 지키면 state 파일 유출 시나리오에서 시크릿이 새지 않는다는 보장이 코드 레벨에서 강제된다.
둘째, version 카운터를 자동화한다. 사람한테 맡기면 100% 까먹는다. rotation Lambda → SSM Parameter Store → terraform data source 같은 식으로 묶어둔다. 처음에는 귀찮지만 한 번 셋업해두면 운영 부담이 크게 준다.
셋째, ephemeral이 아닌 부분의 시크릿은 별도 모듈로 격리한다. 아직 write-only 지원 안 되는 자원이 섞여 있으면 그것만 따로 모듈로 빼서, 그 모듈은 state 암호화와 접근 제어를 별도로 강하게 건다. 모든 시크릿을 한 state에 섞어두지 말 것.
아직 검증 중인 것
1.14에서 들어온 actions block이랑 list resources가 ephemeral과 어떻게 조합될지는 우리도 아직 실험 중이다. 특히 actions block에서 ephemeral 값을 어떻게 전달할 수 있는지가 흥미로운데, 본격적으로 도입해보고 다시 정리할 일이 있을 것 같다.
혹시 다른 패턴으로 운영 중인 팀이 있으면 댓글 남겨주세요. 특히 third-party provider 쓰는 곳에서 어떻게 우회하는지 궁금하다.