External Secrets Operator vs Vault Agent Injector, 우리 팀은 결국 둘 다 쓰기로 했다
쿠버네티스에서 시크릿을 어떻게 가져올 것이냐. 이 질문은 보통 회사가 어느 정도 커지고 클러스터가 두세 개 늘어나는 시점에 한 번 크게 부딪힌다. 우리 팀도 그랬다. 작년 초까지는 그냥 kubectl create secret으로 박아 넣고 헬름 차트에 손으로 옮기는 식이었는데, 클러스터가 4개로 늘고 환경별로 시크릿 동기화 누락이 두 번쯤 사고로 이어지면서 더는 못 미루겠다는 결론이 났다.
후보는 사실상 두 개였다. External Secrets Operator(이하 ESO)와 HashiCorp의 Vault Agent Injector. 둘 다 충분히 성숙했고 사례도 많다. 그런데 막상 비교하기 시작하면 "어느 게 더 좋은가" 자체가 잘못된 질문이라는 게 금방 드러난다. 결국 우리 팀이 어떤 시크릿을 다루고 있느냐에 따라 답이 갈린다.
두 도구의 본질적 차이
가장 핵심은 시크릿이 어디로 가서 누구에게 노출되느냐다.
ESO는 외부 시크릿 저장소(AWS Secrets Manager, Vault, GCP Secret Manager 등)에서 값을 읽어와서 쿠버네티스 Secret 오브젝트로 동기화한다. 그러니까 결과물은 평범한 K8s Secret이다. 파드는 그걸 envFrom이든 볼륨 마운트든 평소 하던 대로 가져다 쓴다. 시크릿이 한 번 etcd에 머무른다는 뜻이기도 하다.
Vault Agent Injector는 다르다. Mutating Admission Webhook으로 파드가 만들어질 때 사이드카(또는 init 컨테이너)를 끼워 넣고, 그 사이드카가 Vault에서 직접 시크릿을 받아와 파드 내부 파일시스템에 떨군다. K8s Secret을 거치지 않는다. etcd에 시크릿이 남지 않는 게 보안 관점에서 큰 차이다.
# ESO 예시 - SecretStore와 ExternalSecret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-db-creds
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: app-db-creds # 결과물은 그냥 K8s Secret
data:
- secretKey: password
remoteRef:
key: secret/data/app/db
property: password
# Vault Agent Injector 예시 - 어노테이션으로 끝
apiVersion: apps/v1
kind: Deployment
spec:
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "app"
vault.hashicorp.com/agent-inject-secret-db: "secret/data/app/db"
이거 하나만 봐도 사용성에서 차이가 꽤 난다. ESO는 CRD 두세 개를 운영팀이 관리하고, Vault Agent는 어노테이션 몇 줄로 앱 개발자가 셀프서비스 가능하다.
성능, 그리고 의외의 운영 부담
처음에는 별 차이 없겠지 싶었는데 쓰다 보니 몇 가지 패턴이 드러났다.
Vault Agent Injector는 파드마다 사이드카가 붙는다. 파드 1000개면 사이드카 1000개. 메모리도 그만큼 먹고, Vault 서버로 향하는 커넥션도 그만큼 늘어난다. 우리 클러스터에서 한번은 새벽에 Vault 쪽 커넥션이 튀어서 알람이 울린 적이 있었는데, 알고 보니 디플로이먼트 롤링 업데이트가 동시에 여러 개 도는 시점이었다. 사이드카들이 일제히 토큰 갱신하느라 그런 거였다.
ESO는 컨트롤러 한두 개만 떠 있고, 시크릿 폴링도 컨트롤러가 묶어서 한다. 외부 저장소 호출 횟수가 비교가 안 되게 적다. 다만 ESO는 폴링 주기 안에서는 시크릿이 갱신되지 않는다. refreshInterval: 1h로 두면 변경된 값이 반영되는 데 최대 1시간 걸린다는 뜻이다.
여기서 한 가지 더 — 사실 요즘은 Vault Secrets Operator(VSO)라는 게 따로 있다. HashiCorp가 ESO 스타일의 동기화 방식을 자기네 진영에서 다시 만든 거라고 보면 된다. 사이드카 없이 CRD 기반으로 K8s Secret을 만들어준다. 다만 백엔드는 Vault만 지원한다는 점에서 ESO보다 좁다.
동적 시크릿이라는 결정적 분기점
여기가 진짜 갈림길이다. 동적 시크릿(dynamic secrets) — 그러니까 Vault가 매번 새로 발급하고 짧게 살다 죽는 DB 자격증명 같은 것들 — 을 쓸 거냐 말 거냐.
ESO로도 Vault dynamic secret을 동기화할 수는 있다. 그런데 동기화된 그 순간 K8s Secret이 만들어지고, TTL이 지나면 자격증명은 만료되는데 Secret 오브젝트는 그대로 남는다. 갱신은 폴링 주기에 의존한다. 짧은 TTL을 쓰는 의미가 많이 희석된다.
Vault Agent는 이 부분에서 진짜 강하다. 사이드카가 토큰 lease를 직접 관리하면서 자동 갱신하고, 만료 직전에 새 자격증명을 받아 파일을 업데이트한다. 짧은 TTL을 진짜로 짧은 TTL답게 쓸 수 있다.
# Vault Agent 템플릿 예시
template {
destination = "/vault/secrets/db-creds"
contents = <<EOH
{{- with secret "database/creds/app-role" }}
DB_USER="{{ .Data.username }}"
DB_PASS="{{ .Data.password }}"
{{- end }}
EOH
}
DB lease가 1시간이면 사이드카가 알아서 50분쯤에 갱신한다. 앱은 파일을 다시 읽기만 하면 된다. 이게 ESO 폴링 모델로는 깔끔하게 안 된다.
그래서 우리 팀의 결론
처음엔 "둘 중 하나만 골라야 운영 단순해지지 않냐"는 의견이 강했다. 그런데 시크릿을 분류해 보니 답이 나왔다.
- 정적 시크릿(API 키, OAuth 클라이언트 시크릿, S3 키 등): 거의 안 바뀌고, 여러 워크로드가 공유하고, 외부 SaaS에서 가져와야 한다 → ESO
- 동적 시크릿(DB 자격증명, 클라우드 IAM 임시 자격증명): 짧은 TTL이 중요하고, 워크로드별로 발급받아야 한다 → Vault Agent Injector
결국 둘 다 쓰기로 했다. 운영 부담이 늘어난 건 사실이지만, 한쪽으로 몰아넣었을 때 잃는 게 더 컸다. ESO만 쓰면 동적 시크릿의 의미가 죽고, Vault Agent만 쓰면 사이드카 1000개와 SaaS 시크릿 동기화 자체 구현의 부담이 커진다.
다만 새로 시작하는 작은 팀한테 똑같이 권하지는 않는다. 클러스터 하나에 워크로드 몇십 개 수준이면 ESO 하나로 충분하고, 동적 시크릿이 필요해지는 시점이 오면 그때 Vault Agent를 도입해도 늦지 않다. 처음부터 둘 다 들고 들어가면 학습 곡선이 두 배다.
혹시 다른 조합으로 운영하시는 분 있으면 어떻게 쓰시는지 댓글로 알려주세요. 특히 VSO를 메인으로 쓰는 사례가 궁금하다. 우리는 아직 검토만 하고 적용은 못 해봤다.