IT/IaC

Terraform 1.10 ephemeral resources로 시크릿 상태 노출 줄이기

gfrog 2026. 6. 19. 18:45
SMALL

Terraform 쓰면서 가장 찜찜한 게 뭐였냐고 물으면, 솔직히 나는 terraform.tfstate에 시크릿이 평문으로 박히는 거였다. RDS 마스터 패스워드, API 토큰, OAuth 클라이언트 시크릿. data 블록으로 Secrets Manager에서 가져온 값조차 state에 그대로 기록된다.

그동안은 state 백엔드(S3 + KMS)에 의존하거나, write-only 패턴을 억지로 끼워넣거나, 아예 Terraform 밖에서 처리하는 식으로 우회했다. 그런데 작년 말 1.10에서 나온 ephemeral resources가 1년 좀 넘게 실무에서 굴려보니 꽤 쓸 만하다. 최근 6월 기준 AWS, Azure, Vault, Kubernetes, random, 그리고 GCP 프로바이더까지 지원이 거의 다 들어왔다. 우리 팀에서 어떻게 도입했는지 정리해본다.

ephemeral이 풀어주는 문제

핵심 한 줄: plan과 state에 절대 기록되지 않는다. apply가 도는 순간에만 메모리에 존재한다.

기존 data source와 비교하면 차이가 명확하다.

# 기존 방식 - state에 시크릿이 박힌다
data "aws_secretsmanager_secret_version" "db" {
  secret_id = "prod/db/master"
}

resource "aws_db_instance" "main" {
  password = data.aws_secretsmanager_secret_version.db.secret_string
}

terraform show로 state 까보면 secret_string이 그대로 보인다. KMS로 암호화돼있다고 해도, state 파일을 읽을 수 있는 사람은 결국 평문을 본다.

# ephemeral 방식
ephemeral "aws_secretsmanager_secret_version" "db" {
  secret_id = "prod/db/master"
}

resource "aws_db_instance" "main" {
  password = ephemeral.aws_secretsmanager_secret_version.db.secret_string
}

aws_db_instance.password는 원래도 sensitive라 state에 마스킹되지만, ephemeral은 한 단계 더 나아간다. 데이터 자체가 plan/state 어디에도 직렬화되지 않는다. apply 끝나면 메모리에서 증발한다.

실제 적용 패턴

패턴 1: 프로바이더 인증

가장 깔끔하게 적용되는 곳이다. Vault에서 동적 AWS 크레덴셜 받아서 AWS 프로바이더를 인증하는 케이스.

ephemeral "vault_aws_access_credentials" "creds" {
  backend = "aws"
  role    = "platform-deploy"
}

provider "aws" {
  region     = "ap-northeast-2"
  access_key = ephemeral.vault_aws_access_credentials.creds.access_key
  secret_key = ephemeral.vault_aws_access_credentials.creds.secret_key
  token      = ephemeral.vault_aws_access_credentials.creds.security_token
}

기존에는 data로 받은 동적 크레덴셜이 state에 박혀서, lease가 만료된 뒤에도 state에 흔적이 남았다. 이제는 깔끔하다.

패턴 2: write-only 인자와 짝

ephemeral만 도입해도 시크릿을 일부 자원의 attribute로 넘기면 state에 기록될 수 있다. 그래서 1.11부터 들어온 write-only 인자(_wo 접미사)와 같이 써야 완성된다.

ephemeral "random_password" "db" {
  length  = 32
  special = true
}

resource "aws_db_instance" "main" {
  identifier              = "prod-main"
  engine                  = "postgres"
  password_wo             = ephemeral.random_password.db.result
  password_wo_version     = 1   # 회전할 때 이 숫자 증가
}

password_wo는 write-only라서 plan diff에 안 보이고 state에도 안 박힌다. 비밀번호 회전이 필요하면 password_wo_version을 1에서 2로 바꿔주면 다음 apply에서 새 패스워드가 적용된다. 처음 보면 좀 어색한데, "값 자체는 어디에도 저장 안 함, 대신 버전 숫자로 변경 추적"이라는 모델이다.

패턴 3: 임시 파일에 쓰고 바로 폐기

쿠버네티스 kubeconfig 같은 거. apply 중에만 필요하고 state에 남기면 안 되는 값.

ephemeral "aws_eks_cluster_auth" "this" {
  name = aws_eks_cluster.main.name
}

provider "kubernetes" {
  host                   = aws_eks_cluster.main.endpoint
  cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data)
  token                  = ephemeral.aws_eks_cluster_auth.this.token
}

토큰이 만료될 때마다 state에 stale한 값이 박혀서 매번 plan에 diff가 떴던 게 사라진다.

도입할 때 부딪힌 것들

작년 12월부터 단계적으로 옮겼는데, 매끄럽지만은 않았다.

locals에서 가공이 안 된다. ephemeral 값은 locals에 담을 수 없다. 정확히는 ephemeral context 안에서만 참조 가능하다. data로 받아서 locals로 가공하는 패턴을 쓰던 곳은 구조를 갈아엎어야 했다.

output으로 못 내보낸다. module output에서 ephemeral 값을 그대로 넘기려면 output에도 ephemeral = true를 명시해야 하고, 이걸 받는 쪽도 ephemeral context여야 한다. module 인터페이스를 다시 그려야 했다.

프로바이더 버전 의존성. AWS 프로바이더 5.83 이상, Vault 4.5 이상이 필요했다. 우리는 AWS 5.50에 묶여있었는데, 다른 리소스 호환성 검증하느라 PoC가 2주쯤 밀렸다.

drift detection에서 묘하게 걸린다. aws_db_instance의 password_wo는 plan에 안 뜨지만, 만약 누가 콘솔에서 패스워드를 바꿨다면 Terraform은 그걸 감지하지 못한다. 시크릿 회전을 콘솔/CLI/Terraform 어디서 하든 일원화하는 정책이 먼저 필요했다.

안 옮긴 것들

다 옮기지는 않았다. 운영 중인 ECS task definition의 환경변수 시크릿 참조 같은 건 그대로 뒀다. 이건 어차피 secretsmanager: ARN 참조로 들어가서 평문이 state에 없다. 굳이 ephemeral로 바꿔서 얻을 게 없었다.

기준은 단순하다. 평문 시크릿이 state에 들어가던 곳만 옮겼다. 어차피 ARN/ID만 있던 곳은 건드리지 않았다.

그래서 권할 만한가

data "aws_secretsmanager_secret_version" 같이 평문을 state에 박는 패턴을 쓰고 있다면, 거의 무조건 옮길 만하다. 보안 감사 때 받던 지적 절반은 사라졌다.

다만 1.10 이전 버전에 묶여있거나 프로바이더 업그레이드가 막혀있으면 좀 기다려야 한다. 그리고 모듈 인터페이스 변경이 동반되니, 큰 모노레포라면 도입 PR 크기가 생각보다 커진다는 점은 미리 각오하자.

다음에는 ephemeral과 OpenTofu의 state_encryption을 같이 쓰는 구성도 정리해보려고 한다. 둘이 겹치는 것 같지만 보호 범위가 미묘하게 다르다.

BIG