지난주 새벽에 겪은 이야기다. 프로덕션 EKS 두 개 중 한 쪽에서 신규 파드가 시크릿을 못 읽는다는 알림이 왔다. 처음엔 대수롭지 않게 봤는데, 결국 External Secrets Operator(ESO) 전체 sync가 멈춰 있는 상태였다. 우리 팀이 ESO를 IRSA + AWS Secrets Manager 조합으로 3년 넘게 쓰고 있는데, 이런 식으로 죽는 건 처음이었다.
증상은 조용했다
관측 가능한 신호가 애매했다. kubectl get externalsecret -A 를 찍으면 상태는 죄다 SecretSyncedError 였다. 근데 컨트롤러 파드는 살아 있고, CPU도 정상이고, 리더 election 로그도 잘 남고 있었다. Prometheus에서 externalsecret_sync_calls_error 카운터만 소리 없이 올라가고 있었다. 다행히 파드에 이미 마운트된 시크릿은 유효한 상태로 붙어 있어서 실서비스 장애로는 안 번졌다. 그런데 새로 스케일 아웃되는 파드가 문제였다. Rollout이 걸리 서비스 하나가 CrashLoopBackOff 로 넘어가면서 페이지가 떴다.
솔직히 처음엔 컨트롤러 이슈로 의심하고 재시작부터 시켰다. 그 이후로 상황은 더 나빠졌다. sync 큐가 다시 처음부터 밀리면서 AWS Secrets Manager 호출이 폭주했고, 대략 3분 뒤에 ThrottlingException 이 우수수 떨어졌다.
진짜 원인은 만료된 role session
로그를 다시 뒤졌다. 컨트롤러 로그의 5분 전 시점에 이런 라인이 있었다.
{"level":"error","msg":"could not get provider client","error":"operation error STS: AssumeRoleWithWebIdentity, https response error StatusCode: 400, ExpiredTokenException: Token expired"}
여기서 잠깐. IRSA는 원래 토큰을 주기적으로 갱신해준다. 그런데 우리 클러스터에서 IRSA 방식으로 만든 ClusterSecretStore가 하필 하나 남아 있었다. 나머지는 작년에 Pod Identity로 옮겼는데, 이 하나는 리소스명이 다른 팀 컨벤션에 걸려 있어서 마이그레이션 대상에서 빠져 있었다.
문제는 이 ClusterSecretStore를 쓰는 ExternalSecret이 60개가 넘었다. 하나의 provider client가 죽으니 그 store에 물린 모든 sync가 실패했고, ESO 컨트롤러의 workqueue가 실패한 아이템을 계속 재큐잉하면서 다른 store의 sync 스루풋까지 잡아먹었다. 재시작이 상황을 악화시킨 것도 같은 이유였다. 재시작 직후 컨트롤러가 전체 리컨사일을 다시 돌면서 만료된 store의 실패 리트라이가 폭발적으로 늘어난 것이다.
응급 조치
우선 죽어 있는 ClusterSecretStore 리소스를 삭제했다. 이걸 지운 순간 workqueue가 조용해졌고, 나머지 store에 물린 ExternalSecret들이 정상 sync 상태로 돌아왔다. 삭제한 store에 물려 있던 시크릿들은 이미 만료 없이 마운트된 상태여서 새로 롤아웃하지 않는 한 문제가 없었다.
그다음 삭제한 store를 Pod Identity 방식으로 다시 만들었다. 리소스 이름은 옛날 그대로 두고, spec.provider.aws.auth 부분만 jwt 대신 새 방식으로 갈아 끼웠다. 관련 ExternalSecret 60여 개는 참조 이름이 그대로라 자동으로 다시 잡혔다.
이번에 배운 것
첫째로, ExternalSecret 상태만 보면 늦다. externalsecrets_sync_calls_error 같은 카운터에 알림을 걸어야 한다. 우리는 카운터를 스크래핑만 하고 있었지 알림은 안 걸어놨었다. rate(externalsecrets_sync_calls_error[5m]) > 0 정도로 최소한만 잡아도 새벽 알림 시점이 몇십 분은 당겨졌을 것이다.
둘째로, ClusterSecretStore는 blast radius가 생각보다 크다. 스코프가 클러스터 전역이니까 한 store가 죽으면 물린 시크릿이 다 죽는다. 팀별로 SecretStore를 나누고 있었는데, "공용" 시크릿은 하나의 ClusterSecretStore에 몰아넣고 있었던 게 이번에 아팠다. 지금은 도메인별로 셋으로 쪼갰다. 어차피 IAM 정책도 도메인별로 다르니 자연스러웠다.
셋째로, IRSA에서 Pod Identity 마이그레이션은 미루면 안 된다. IRSA 자체는 여전히 잘 돌아가지만, 우리 팀 기준에서 auth 방식이 두 개 섞여 있는 것 자체가 리스크였다. 리소스 이름 컨벤션 같은 사소한 이유로 빠뜨린 하나가 결국 파장이 커지는 걸 봤다.
남는 질문
ESO 프로젝트가 작년 봄에 유지보수자 이슈로 릴리즈가 잠깐 멎었던 걸 기억할 것이다. 그 뒤로 안정화되고 v1 API도 나왔지만, 우리 팀은 여전히 workqueue 안에서 실패 리트라이가 어떻게 스루풋을 갉아먹는지에 대해선 완전히 이해하지 못한 상태다. 다음 릴리즈에서 store별로 workqueue를 분리하는 옵션이 붙는다는 얘기도 있던데, 붙으면 아마 켤 것 같다. 그때 다시 정리해보겠다.
혹시 비슷한 경험 있으신 분, ClusterSecretStore 스코프를 어떻게 쪼개서 쓰는지 궁금하다. 우리 방법이 최선인지 아직 잘 모르겠다.
'IT > DevSecOps' 카테고리의 다른 글
| Vault vs AWS Secrets Manager, 우리 팀은 왜 하이브리드로 갔나 (0) | 2026.08.08 |
|---|---|
| Kyverno CEL 전환, 지금부터 준비하는 법 (0) | 2026.08.05 |
| External Secrets Operator vs Vault Agent Injector, 뭘 쓸까 (0) | 2026.07.29 |
| SPIRE Federation trust bundle 붙이다가 사흘 태운 이야기 (0) | 2026.07.18 |
| Trivy vs Grype, 컨테이너 스캐너 뭘 쓸까 (0) | 2026.07.11 |