grep

DevOps

여기어때 Secret 플랫폼 구축기 Part 3: 시크릿 저장소를 운영 가능한 상태로 만들기 — 컨테이너화부터 CI/CD, 로그 수집까지

Dana Won여기어때

2026년 2월 27일

원문에서 보기 ↗

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

앞선 두 편에서 Secrethub가 무엇인지, 어떻게 설계했는지를 다뤘습니다. 이번 글에서는 설계한 서비스를 실제로 운영 가능한 상태로 만드는 과정을 이야기하려고 합니다.

컨테이너 이미지 빌드부터 CI/CD, 그리고 Observability까지 단순히 “배포가 되는 파이프라인”이 아니라 DevOps 관점에서 관리 가능한 구조를 만들고 싶었습니다. 그 고민이 이 글의 시작입니다.

Bun 런타임 위에 구현한 Secrethub를 컨테이너로 배포하기

Secrethub는 Bun 기반으로 개발되었습니다. Bun은 빠른 실행 속도와 번들러·패키지 매니저를 내장한 JavaScript/TypeScript 런타임으로, TypeScript를 별도 컴파일 없이 바로 실행할 수 있어 개발 생산성이 높습니다.

애플리케이션을 컨테이너로 패키징 하는 과정 뿐만아니라 Bun의 빌드 방식과 의존성 관리 전략까지 함께 고민했습니다.

isolated linker — node_modules를 심볼릭 링크 없이 구성하기

bunfig.toml에는 다음 설정을 사용하고 있습니다.

[install]
linker = "isolated"

Bun의 기본 링커는 node_modules를 심볼릭 링크 방식으로 구성하는데, isolated 모드는 각 패키지를 독립적인 디렉토리에 복사하는 방식으로 설치합니다. 심볼릭 링크로 인한 경로 해석 문제를 방지하고, 컨테이너 환경에서 파일 시스템을 더 예측 가능하게 만들어줍니다.

Bun build: 빌드 결과물을 단일 파일로

AI Generated with ChatGPT

기존 Node.js 빌드 방식에서는 컴파일된 파일들이 여러 개로 나뉘고 node_modules 전체를 함께 가져가야 했습니다. 반면 Bun의 bun build는 TypeScript 소스와 모든 의존성을 정적으로 번들링해 단일 파일로 생성합니다.

즉, 컨테이너 안에는 dist/index.js 파일만 존재합니다. node_modules도, 소스 코드도 없이 번들 파일만으로 서버가 구동됩니다.

Dockerfile 멀티 스테이지 빌드

# ------- build stage ------
FROM oven/bun:1.3-slim AS build

WORKDIR /app
COPY package.json bun.lock bunfig.toml ./
COPY scripts ./scripts
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build

# ------- run stage ------
FROM oven/bun:1.3-slim

WORKDIR /app
COPY --from=build /app/dist ./dist
ENV PORT=3000
EXPOSE ${PORT}
CMD ["bun", "./dist/index.js"]

빌드 스테이지 와 실행 스테이지를 분리한 멀티 스테이지 빌드 구조입니다.

빌드 스테이지에서는 의존성 설치와 번들링을 수행하고, 실행 스테이지에는 dist/ 결과물만 복사합니다. 덕분에 최종 이미지에 소스 코드, node_modules, 빌드 도구가 포함되지 않아 이미지 크기를 크게 줄일 수 있습니다.

Secrethub CI/CD 파이프라인 구성

Secrethub의 CI/CD 파이프라인은 다음과 같은 흐름으로 구성됩니다.

GitLab Push

→ GitLab Runner: Lint / Build / Test

→ 컨테이너 이미지 빌드 & ECR Push

→ 배포 스크립트 S3 업로드

→ CodeDeploy 트리거

→ EC2: docker compose pull & up

AI Generated with ChatGPT

기존 EC2 CI/CD 구조 (AS-IS)

여기어때의 EC2 프로젝트들은 Jenkins를 중심으로 CI/CD를 운영하고 있었고, 각 팀이 배포 스크립트를 서비스에 맞게 수정해 사용하는 방식이였습니다. 이 구조는 각 서비스의 특성에 맞게 유연하게 운영할 수 있다는 장점이 있지만, 파이프라인 개선도 서비스마다 해야한다는 의미이기도 합니다. 어떤 서비스가 어떤 방식으로 배포되고 있는지 파악하려면 직접 들어가서 확인하는 방법 밖에 없다보니 전체 배포 현환을 한눈에 파악하는데 어려움이 있습니다.

즉, CI/CD는 잘 동작하고 있었지만, 중앙에서 관리하고 개선하기에는 구조적으로 확장성이 제한적인 구조였습니다.

Gitlab CI 기반 중앙 관리 구조

Secrethub 프로젝트를 진행하면서, 단순히 하나의 서비스 CI/CD를 만드는 데 그치지 않고

DevOps에서 관리 가능한 확장성있는 CI/CD 구조를 만들고자 했습니다.

DevOps팀에서는 이미 EKS 환경에서 Gitlab CI 기반의 공통 CI 템플릿 을 운영하고 있습니다.

Secrethub은 EC2프로젝트이지만 컨테이너 기반으로 운영될 예정이었기 때문에, 이 공통 템플릿을 EC2에도 녹여내는 것이 가능하였습니다. 배포 대상만 다를 뿐, CI 흐름 자체는 EKS와 동일하게 가져갈 수 있었습니다.

핵심 설계 방향은 다음과 같았습니다.

CD: CodeDeploy + Docker Compose

CD 과정은 CodeDeploy를 통해 EC2에 배포 스크립트를 전달하고, docker compose up & down 방식으로 컨테이너를 갱신하는 구조입니다.

CodeDeploy는 AWS에서 제공하는 배포 자동화 서비스로, appspec.yml에 정의된 순서에 따라 배포를 단계적으로 실행합니다. 각 단계마다 실행할 스크립트를 지정할 수 있는 데 이를 훅(hooks) 이라고 합니다. 대표적으로 ApplicationStop → BeforeInstall → AfterInstall → ApplicationStart 순서로 진행되며, 각 훅에서 컨테이너 중지, 파일 정리, 서비스 시작 등의 작업을 수행합니다.

aws 공식 페이지

기존 배포 스크립트들을 살펴보니 컨테이너 중지 로직이 BeforeInstall 훅에 들어가 있었습니다. 배포 스크립트를 조금씩 수정해가며 쓰다 보니 적절한 훅이 있음에도 관행적으로 굳어진 것으로 보였습니다. 공식 문서에 따르면 실행 중인 애플리케이션을 종료하는 작업은 ApplicationStop 훅에서, 설치 전 준비 작업은 BeforeInstall 훅에서 처리하는 것이 더 적절하다고 판단하여 해당 방식으로 수정하였습니다.

배포 스크립트 설계에서 특히 신경 쓴 부분은 운영환경에서의 독립성이였습니다. 서버가 재부팅되거나 새로운 EC2 인스턴스로 교체되는 상황에서도, 배포 스크립트 하나만으로 전체 CI/CD 프로세스가 완료될 수 있도록 하는 것을 주안점으로 뒀습니다.

파이프라인이 완성된 후, Secrethub에 적용한 구조를 기반으로 DevOps 팀내에서 운영 중인 다른 EC2 서비스들에도 동일한 파이프라인을 순차적으로 적용하였습니다.

Observability

서비스가 안정적으로 운영되기 위해서는

빌드와 배포뿐만 아니라,관측(Observability) 역시 중요한 요소입니다.

LGTM 연동을 진행하면서 가장 먼저 고민한 질문은 이것이었습니다.

“로그 수집 책임을 어디에 둘 것인가?”

우리 환경은 하나의 EC2 서버에 여러 컨테이너(여러 서비스)를 실행하는 구조였습니다.

이 환경에서 각 서비스마다 winston이나 pino 같은 로깅 라이브러리를 추가하고 OTEL 설정을 개별적으로 적용하는 방식이 과연 적절한 선택일지 고민하게 되었습니다.

Docker 레벨 로그 수집

그래서 선택한 방식은

Docker log를 Loki Driver Client를 통해 직접 LGTM으로 전송하는 구조였습니다.

Container → Docker Log → Loki Driver → LGTM (Loki)

이 방식은 애플리케이션 코드 변경 없이

컨테이너 레벨에서 로그 수집을 처리할 수 있다는 장점이 있습니다.

Loki Docker Driver란?

Grafana에서 공식 제공하는 Docker 로깅 드라이버 플러그인으로, 컨테이너의 stdout/stderr 로그를 Loki로 직접 전송할 수 있습니다.

왜 Docker 레벨 수집을 선택했는가

1️⃣ 컨테이너 레벨에서 일관된 수집

로그 수집 책임을

애플리케이션이 아닌 플랫폼(Docker 레벨) 으로 이동시켰습니다.

→ 여러 컨테이너가 동작하는 환경에서는 각 서비스 별 로깅 설정이 아닌,

Docker 레벨에서 일괄 처리하는 방식을 택했습니다.

2️⃣ 언어/프레임워크 독립적

Node.js, Java, Python 등

어떤 스택이든 동일한 방식으로 로그 수집이 가능합니다.

3️⃣ 설정 표준화 가능

Docker Compose 템플릿을 통해

로그 수집 설정을 재사용 가능한 구조로 만들 수 있습니다.

# logging.yml
services:
  lgtm:
    logging:
      driver: loki
      options:
        loki-url: ${LOKI_URL}
        mode: "non-blocking"
        loki-batch-size: "400"
        loki-external-labels: >
          job=docker,
          environment=${PROFILE},
          namespace=${PROFILE}-${ROOT_PROJECT_NAME},
          service=${PROJECT_NAME}

# docker-compose.yml
services:
  secrethub:
    extends:
      file: ../logging.yml
      service: lgtm

앞으로의 방향: EC2 → EKS

현재 여기어때 서비스의 약 40%는 EC2 기반으로 운영되고 있습니다. EKS 전환을 장기적으로 진행 중이지만, 여러 이유로 당장 전환이 쉽지 않은 서비스들이 많습니다. 이런 상황에서 선택한 전략은 CI를 먼저 표준화하는 것이었습니다. EKS 전환 시 바꿔야 할 것이 CD뿐이 되기 때문에 전환 범위가 줄어들고, GitLab CI로 통일되면 전사 배포 현황을 일관된 방식으로 파악하는 것도 가능해집니다.

Secrethub 또한, 현재는 Docker Compose 기반 EC2 환경에서 운영하고 있지만, 안정성과 확장성을 고려해 공통 EKS 클러스터로 전환할 예정입니다.

마치며

Secrethub를 Cloud Native하게 운영하기 위해 고민한 것들을 정리해보면, 결국 하나의 방향으로 수렴됩니다. 로그 수집도, CI/CD 구조도, 빌드 방식도 — 서비스 단위가 아니라 플랫폼 레벨에서 관리 가능한 구조를 만드는 것입니다. 그 방향이 앞으로의 EKS 전환과 운영 확장에도 자연스럽게 이어질 것이라 생각합니다.

끝까지 읽어주셔서 감사합니다😊