grep

DevOps

여기어때 Secret 플랫폼 구축기 Part 2: 시크릿 저장소를 전체 서비스에 적용하기까지

셀린Celine(강지윤) / DevOps팀여기어때

2026년 2월 27일

원문에서 보기 ↗

안녕하세요, 여기어때컴퍼니 DevOps팀 셀린입니다.

지난 글에서는 Secrethub를 왜 도입하게 되었는지 이야기했습니다.

이번 글에서는 그 다음 단계, “그래서 어떻게 적용했는가!”에 대해 이야기해보려고 합니다.

결과부터 말씀드리면,

Secrethub는 여기어때 EKS 환경에서 운영 중이던 모든 애플리케이션에 적용되었습니다. 🎊

이 글에서는 그 결과에 도달하기까지 어떤 선택을 했고, 어떤 기준으로 설계했는지를 차근히 풀어보겠습니다.

🧑‍💻 3줄 요약

어디부터 적용할 것인가

저희 여기어때 서비스의 배포 환경은 크게 두 가지였습니다.

EKS 환경은 이미 표준화된 CI/CD Pipeline 위에서 운영되고 있었습니다.

반면 EC2 환경은 아직 표준화 이전이라 CI/CD 구조가 팀마다 상이했고, profile 사용 방식도 다양했습니다.

저희의 1차 목표는 빠르게 모든 서비스에 적용하는 것 이었기 때문에,

더 많은 프로젝트에 적용할 수 있고 구조가 정리되어 있는 EKS 환경에서 먼저 안정성을 확보하고자 했습니다.

EKS 환경에서는 어떻게 적용할 것인가

EKS 환경에서 애플리케이션이 Secret을 사용하는 방식은 정해져 있습니다.

Kubernetes에서는 민감 정보를 Kubernetes Secret이라는 형식으로 보관하고, Pod 생성 시 이를 환경변수나 파일 형태로 전달합니다.

그럼 자연스럽게 이런 질문이 생겼습니다.

🧐 Secrethub에 저장된 값을 어떻게 Kubernetes Secret으로 가져올까?

🧐 그리고 그 값이 바뀌면 Kubernetes는 어떻게 감지할까?

🧐 민감 정보가 변경될 때마다 직접 배포해야 하나?

민감 정보는 변경될 수 있는 값이기 때문에 수동 배포로 문제를 해결하는 순간 운영 부담이 커지게 됩니다.

그래서 저희가 내린 결론은 “자동 동기화”였습니다.

Secrethub에서 값이 바뀌면 Kubernetes에서도 자동으로 반영되도록 만들고 싶었습니다.

이에 따라 Webhook 기반 구조를 고민하게 되었습니다.

그래서 선택한 구조 — External Secrets Operator(ESO)

여기서 등장한 것이 External Secrets Operator , 줄여서 ESO입니다.

ESO가 무엇인지 생소하실 텐데요,

ESO는 외부 저장소에 있는 Secret을 가져와, Kubernetes가 이해하는 Secret으로 자동 동기화해주는 컨트롤러입니다.

AI 생성 이미지 (자체 제작)

쉽게 말해, Secrethub는 금고이고

ESO는 그 금고에서 값을 꺼내 Kubernetes에 배달해주는 배달원입니다.

이 구조의 장점은 명확합니다.

애플리케이션이 Secrethub를 직접 호출하지 않아도 됩니다.

애플리케이션은 기존처럼 Kubernetes Secret만 사용하면 됩니다.

전체 구조 한눈에 정리

☝️ 로컬과 서버는 시크릿을 가져오는 방식이 다릅니다.

여기서 한 번 정리를 하고 가면 이해가 쉬워집니다.

  1. 로컬 환경 로컬에서는 애플리케이션이 직접 Secrethub를 호출합니다. CLI 로그인 후 생성된 accessToken을 기반으로 Loader 라이브러리가 API를 호출하고, 값을 Spring Environment에 주입합니다.
  2. EKS 환경 EKS 환경에서는 애플리케이션이 Secrethub를 호출하지 않습니다. ESO가 대신 Secrethub를 조회하고, 그 결과를 Kubernetes Secret으로 변환해 Pod에 전달합니다.

아래에서 구체적으로 동작 과정을 설명드리겠습니다.

External Secret Operator 적용 구조

위의 그림은 EKS 환경에서 Secrethub가 동작하는 전체 흐름입니다.

먼저, Secrethub에 접근하기 위한 API Key는 AWS Secret Manager에 저장했습니다.(①)

Secrethub 접근 키를 Secrethub 내부에 저장하면

접근하려면 키가 필요하고, 키를 얻으려면 접근해야 하는

순환 참조 문제가 발생하기 때문입니다.

따라서 다음과 같이 역할을 분리했습니다.

ClusterSecretStore는 AWS Secret Manager에 대한 접근 방식을 정의하고, ESO가 이를 기반으로 API Key를 조회합니다.(②)

이후 ESO가 Secrethub를 호출해 값을 가져오고, 이를 Kubernetes Secret으로 변환합니다.(③)

Pod는 기존 방식대로 Kubernetes Secret을 마운트해 사용합니다.(④)

실제로 아래와 같이 파일이 생성되게 됩니다.

{
  "db/username": "celine",
  "db/password": "1234",
  "apiKey": "abcde"
}

애플리케이션은 이 과정을 전혀 알지 못하고 단순히 읽기만 하면 됩니다.

External Secret Operator 도입을 위한 실패 시나리오 조사

ESO는 Secrethub에 새롭게 도입하는 구조였습니다.

따라서 단순히 동작만 확인하는 것이 아니라, 실패했을 때 시스템이 어떻게 반응해야 하는지를 먼저 정의해야 한다고 생각했습니다.

따라서 실제 적용 전에 발생 가능한 모든 실패 케이스를 정리하고 각 케이스별로 다음을 확인했습니다.

테스트한 주요 케이스

실제 Kubernetes 반응 확인

각 케이스별로 실제 ExternalSecret 리소스의 상태가 어떻게 변하는지도 확인했습니다.

사용자 요청 오류에 따른 ExternalSecret 응답

이러한 상황을 명확히 정의함으로써, 실제 운영 중 관련 오류가 발생했을 때 원인을 즉시 분류할 수 있도록 하고자 했습니다.

이를 통해 운영 중 장애가 발생했을 때, “ESO 문제인지, Secrethub 문제인지, 사용자 요청 문제인지” 근거를 가지고 설명할 수 있는 상태가 되었습니다.

Secret 저장 방식 선택 — 단일 JSON 파일 전략

Secret을 Kubernetes에 저장하는 방식도 여러 가지가 있었습니다.

가능한 저장 방식을 모두 시도해보면서 실제로 Pod에 어떻게 저장이 되는지를 확인해봤습니다.

결과적으로, 저희는 단일 JSON 파일 제공 방식을 선택했습니다.

왜 단일 JSON 파일인가?

이 방식의 장점은 명확합니다.

1. Secret 구조를 그대로 유지할 수 있다.

Secrethub에 저장된 JSON 구조를 그대로 Pod에 전달할 수 있습니다.

2. 애플리케이션은 파일 하나만 읽으면 된다.

key별로 파일이 분리되는 방식의 경우

처럼 여러 파일이 생성됩니다.

이 경우 애플리케이션은 여러 파일을 각각 읽어야 합니다.

반면 단일 JSON 파일 방식은, 파일 하나만 읽으면 모든 값을 사용할 수 있습니다.

3. GitOps 관리 비용이 줄어든다.

key를 개별로 매핑하는 방식은 Secret key가 하나 추가될 때마다 values.yaml에 다음과 같은 설정을 계속 추가해야 합니다.

spec:
  data:
    - secretKey: db-user
      remoteRef:
        key: db
        property: user

서비스마다 key가 늘어날수록 values.yaml은 점점 복잡해집니다.

반면, 단일 JSON 파일 전략은 Secret 전체를 하나의 값으로 전달 하기 때문에 key가 추가되어도, values.yaml을 수정할 필요가 없습니다.

전사 적용이라는 목표를 고려했을 때, 이 단순함이 가장 큰 장점이었습니다.

Secrethub Loader 라이브러리 적용

AI 생성 이미지 (자체 제작)

EKS에서는 ESO가 Secret을 Kubernetes Secret으로 변환해주었습니다.

하지만, 또 하나의 과제가 남아 있었습니다.

Pod에 Secret이 들어왔다는 것은 알겠으나, Spring Boot는 그 값을 어떻게 인식할 것인가?

팀마다 Spring 설정 방식이 달라서 Secret을 언제, 어떻게 불러오는지 기준이 없었습니다.

그래서, 공통 라이브러리를 제공하여 이를 해결하고자 했습니다.

Secrethub Loader는 순수 Java로 구현했습니다.

이렇게 한 이유는 단순합니다.

****플랫폼 라이브러리는 “특정 팀의 기술 스택에 종속되지 않는 것”이 중요하다고 생각했습니다.

EnvironmentPostProcessor를 활용한 자동 주입

Secrethub Loader는 Spring Boot의 EnvironmentPostProcessor를 활용해

애플리케이션 시작 시점에 Secret을 자동으로 주입하도록 설계했습니다.

Spring은 시작 시 application.yml과 환경 변수 등을 모아 설정을 구성합니다. EnvironmentPostProcessor는 이 과정에서 설정값을 하나 더 추가할 수 있는 확장 지점입니다.

이 시점에 Secret을 등록하면, @Value나 @ConfigurationProperties에서 기존 설정과 동일하게 사용할 수 있습니다.

이를 통해 다음이 가능해졌습니다.

공통 라이브러리의 가장 큰 고민: 의존성 충돌

Secrethub Loader는 여기어때 모든 프로젝트에 공통 라이브러리로 제공되는 모듈입니다.

이때 제가 가장 고민한 부분은, 공통 라이브러리의 의존성 충돌 가능성이었습니다. Secrethub Loader에서는 JSON / YAML 파싱 라이브러리가 필요했습니다. 처음에는 다음과 같은 방법도 고려했습니다.

목표는 명확했습니다.

“공통 라이브러리가 각 프로젝트의 Classpath를 오염시키면 안 된다.”

그래서 선택한 방법 — Shadow Jar

****결론적으로 Shadow 방식(Shaded Jar)을 선택했습니다.

Shadow는 라이브러리 내부 의존성을 외부에 노출하지 않도록 패키지 경로를 재작성하는 방식입니다.

com.fasterxml.jackson  →  com.secrethub.shaded.jackson

이렇게 내부에서만 사용하는 독립 네임스페이스로 변경합니다.

그 결과,

즉, 플랫폼 라이브러리가 기존 애플리케이션을 침범하지 않도록 설계했습니다.

마무리하며,

본 글에서는 Secrethub를 EKS 환경에 적용하며, External Secrets Operator 기반의 자동 동기화 구조를 설계하고, Spring Boot 공통 라이브러리를 통해 환경에 관계없이 일관된 Secret 주입 방식을 마련한 과정을 공유드렸습니다.

이번 적용을 통해 단순히 민감 정보를 중앙화한 것이 아니라, 실패 상황을 예측 가능한 상태로 정리하고, 팀마다 다른 설정 방식을 존중하면서도 일관된 동작을 보장하는 기준을 만들어갈 수 있었습니다.

특히 공통 라이브러리를 직접 설계하며 의존성 충돌을 방지하기 위한 Shadow 전략을 적용하고, PropertySource 우선순위를 검증하며 Secret이 기존 설정에 안전하게 주입되도록 구조를 다듬은 과정은 개인적으로도 큰 도전이었습니다.

전사 적용이라는 목표 아래, 단순히 “동작하는 구조”를 만드는 것이 아니라

운영 중 발생할 수 있는 실패 상황까지 정의하고,

문제가 발생했을 때 설명 가능한 구조를 만드는 데에 집중했습니다.

Secrethub는 단순한 Secret 저장소가 아니라, 플랫폼 차원에서 기준을 만드는 작업이었다고 생각합니다. 앞으로도 운영 환경에서 기준을 세우고, 반복 가능한 구조를 설계하는 DevOps 엔지니어로 성장해 나가겠습니다.

읽어주셔서 감사합니다. 🤗