← Ascendy EN

infra

시크릿을 자가복구시키는 데 정작 중요했던 건 '도구'가 아니라 '어디서 도느냐'였다

· Ascendy Engineering


TL;DR

소스 노트. infra 팀 인테이크를 정제한 글이다. 촉발 인시던트의 구체와 열린 운영 follow-up은 제외하고(Class A), 클러스터·레지스트리·CI 식별자는 익명화했다 — 글은 일반 설계-결정 수준에만 머문다. 같은 인프라 결정·삽질 결의 분산하라는 제약이 배포를 막았다, 알람 3개, 근원은 하나와 이어진다.

차트가 안 만든 시크릿은, 차트가 안 고친다

private 컨테이너 이미지를 쓰면 클러스터가 레지스트리에서 이미지를 받기 위해 pull-secret(쿠버네티스 imagePullSecrets, dockerconfigjson 타입)이 필요하다. 그런데 이 시크릿은 흔히 Helm 차트가 아니라 차트 에서 만들어진다.

그 말은 — 차트가 그 객체를 소유하지 않는다. 정리 스크립트, 네임스페이스 작업, 사람 실수… 어떤 이유로든 그게 out-of-band로 사라지면, helm upgrade를 다시 돌려도 차트는 그걸 되살리지 않는다. 자기가 만든 게 아니니까. 그리고 다음 이미지 pull이나 pod 재시작이 ImagePullBackOff로 깨진다.

그래서 과제는 분명했다. 이 시크릿을 자가복구(self-heal)시키자. 흥미로운 건 어떻게였다.

”제대로 된 답”으로 직행할 뻔했다

처음엔 망설임 없이 “플랫폼 엔지니어링의 정답”으로 갔다 — External Secrets Operator(ESO) + 클라우드 Secrets Manager. 시크릿의 진실을 클러스터 밖 저장소에 두고, 클러스터 안 컨트롤러가 그걸 계속 reconcile하는, 멋지고 표준적인 그림.

구현 직전, operator가 물었다.

“굳이 그거 써? 그냥 CI secret 쓰면 안 돼?”

그 한 마디가 설계를 멈춰 세웠다. 그리고 다시 따져보니, 더 단순한 쪽이 맞았다.

결정적인 건 ‘도구’가 아니라 ‘source와 엔진의 위치’였다

이 문제의 핵심 위협은 **“삭제자를 모를 수 있다”**는 것이었다. out-of-band 삭제는 출처가 다양해서, “누가 지웠는지 찾아서 막는다”만으론 부족하다 — 누가 지우든 자동으로 복구돼야 한다.

그러면 자연히 질문은 이렇게 간다. 복구의 source(진실의 원천)가 어디에 있는가? 클러스터 에 있으면, 클러스터 안에서의 삭제 한 방에 source까지 같이 날아갈 수 있으니까.

이 렌즈로 두 옵션을 보면:

[ESO]  외부 저장소 ──(pull)──▶ [클러스터 안: 오퍼레이터 + CRD + RBAC + 부트스트랩 cred] ──▶ Secret
                                ^^^^^^^ reconcile 엔진이 클러스터 안 ^^^^^^^
[CI]   CI secret ──(워크플로가 읽음 · 클러스터 밖)──(push: kubectl apply)──▶ Secret
       ^^^ source 밖 ^^^         ^^^ 엔진도 밖 ^^^

여기서 기술적 제약 하나가 선택을 갈랐다. GitHub Actions secret은 값을 읽는 API가 없다 — 값은 워크플로 런 안에서만 노출된다. 그래서 ESO의 pull 백엔드로는 애초에 못 쓴다. “GitHub secret을 ESO에 물린다”는 성립하지 않고, “워크플로가 secret을 받아 cluster에 민다”(push)만 가능하다.

결론은 의외로 분명했다. CI push가 ESO보다 한 면 더 단순하고 강했다. 같은 핵심 속성(source가 밖)을 주면서, reconcile 엔진까지 클러스터 밖에 두고, 이미 있는 배포 워크플로 + 클러스터 자격증명을 재사용해서 새로 늘어나는 surface가 거의 0이며, 부트스트랩 chicken-egg가 없다.

“무거운 정답”이 아니라 “이 문제에 맞는 답”이 이긴 케이스였다.

그래도 — 과하게 가지 않기

CD 리뷰에서 중요한 규율이 하나 나왔다. 이 push 모델을 모든 시크릿으로 확장하지 말 것.

앱·런타임 시크릿을 전부 워크플로로 밀기 시작하면, CI가 점점 비밀관리 플랫폼처럼 비대해지고 접근권·감사·rotation이 사방에 퍼진다. 그래서 push 모델은 pull-secret 하나의 self-heal로만 채택하고, “여러 시크릿을 통합 관리할 필요가 실제로 커지면 그때 ESO”로 — ESO를 버린 게 아니라 미뤄둔 옵션으로 남겼다. 도구를 버린 게 아니라, 적용 범위를 정직하게 그은 것이다.

robustness ≠ immunity

push 모델도 “넣고 잊는” 게 아니다. CI 러너, 클러스터 자격증명, 워크플로 자체가 망가지면 self-heal도 멈춘다. 그래서 정확한 표현은 **“source of truth를 클러스터의 삭제 도메인에서 분리한다”**이지, “삭제 불가능하게 만든다”가 아니다.

그래서 설계에 탐지 알람을 필수 박았다 — 시크릿이 없으면 다음 배포를 기다리지 않고 즉시 알리도록. 자동복구가 1선, 탐지 알람이 백스톱. (구현에선 시크릿 값이 로그·argv에 안 새도록 env→임시파일(umask 077)→--from-file, apply 출력 억제, 실패해도 임시파일을 지우는 trap까지 — 작은 것들이 모여 안전을 만든다.)

가져갈 것


저작·인용: 이 글은 Ascendy Engineering이 작성했으며 출처 표기 시 재인용 가능합니다. 잘못된 정보를 발견하면 GitHub 이슈로 알려주세요.


Tags: kubernetes, secrets, self-healing, design-decision, over-engineering, war-story