본문으로 이동
Kim Jongwon
← 전체 프로젝트

Kubernetes 실행 | 개인 프로젝트 | 단독 설계와 구현 | 2026.02.02 - 2026.03.12

Kubernetes-based Stock Backtesting Platform

백테스트 1건을 Kubernetes Job 1개로 실행하는 플랫폼

오래 걸리는 백테스트 계산을 Kubernetes Job으로 분리하고, run_id 하나로 요청부터 결과까지 추적했습니다.

실행 단위

1 Job

백테스트 1건 = Kubernetes Job 1개

추적 키

run_id

웹, Worker 로그와 MySQL 결과 연결

부하 측정

+58%

동시 요청 20건 처리량 (로컬 측정 뒤 설정 조정)

AAPL과 RSI 전략으로 실행한 백테스트가 완료되고 수익률, 샤프 지수, 최대 낙폭 같은 결과 지표가 표시된 화면
백테스트 실행 결과 화면

실행 흐름

요청 하나가 결과까지 도달하는 경로

웹은 요청과 조회만 맡고, 계산, 상태 저장과 모니터링은 각각 다른 곳이 맡습니다.

요청

Flask Web

요청 접수와 상태 조회

계산

Kubernetes Job

요청마다 Job을 만들어 계산

상태

MySQL

대기 → 실행 → 완료 상태와 결과 저장

모니터링

Grafana

Job 실행 수와 요청 빈도

설계 배경

계산을 웹 밖으로 뺀 이유

오래 걸리는 백테스트 계산이 웹 요청 처리를 붙잡고 있었습니다. 계산을 웹 밖으로 빼고, 실행마다 상태와 결과를 따라갈 수 있는 구조가 필요했습니다.

직접 한 일

  • 설계, 구현, 배포, 검증 전 과정을 혼자 수행했습니다.
  • 백테스트 1건을 Kubernetes Job 1개로 실행하는 구조를 설계했습니다.
  • 기존 백테스트 엔진 코드는 고치지 않고 감싸는 방식으로 Worker에 붙였습니다.

결과

  • MSP 과정 개인 프로젝트 우수상
  • 웹과 계산 실행을 분리해 긴 계산이 웹을 막지 않게 함
  • run_id 하나로 요청부터 결과까지 추적

실행 구조

웹 밖에서 계산을 실행하는 구조

웹, Worker Job, DB, Grafana를 run_id 하나로 잇는 실행 구조입니다.

  • Flask 웹은 요청 접수와 결과 조회만 맡고, 백테스트 계산은 요청마다 새로 만드는 Kubernetes Job(Worker)이 맡습니다.
  • 각 실행에 run_id를 붙이고, 실행 조건과 이미지 태그를 함께 남겨 같은 실행을 다시 재현할 수 있게 했습니다.
  • MySQL에 대기, 실행, 완료 상태와 결과를 저장해, 웹과 Worker가 같은 기준으로 상태를 확인합니다.
  • CI가 이미지를 올리고 배포 설정 변경 PR을 만들면, 사람이 검토해 병합한 뒤 Argo CD가 반영합니다. 실행 지표는 Prometheus, Grafana로 확인합니다.
Kubernetes JobsPython/FlaskMySQLArgo CDGrafana

판단 과정

선택한 이유와 확인한 결과

Celery 대신 Kubernetes Job을 고른 이유

이미 쓰고 있는 Kubernetes에서 요청마다 Pod를 따로 띄울 수 있어 메시지 브로커를 따로 운영하지 않아도 됐습니다. 성공 결과를 조회하면 웹이 Job 삭제를 요청하고, 실패한 Job은 로그를 볼 수 있게 24시간 남긴 뒤 TTL로 정리합니다. 대신 Pod가 뜨는 시간이 걸리고, 우선순위 제어는 직접 만들어야 합니다.

설계와 검증 근거 보기 ↗

AI 코딩 도구도 같은 규칙으로 일하게 한 방법

개발에 AI 코딩 도구를 썼지만, 엔진 수정 금지, API 형식 유지, run_id 로깅, 이미지 태그 고정 같은 규칙 10가지를 CLAUDE.md에 적고, AGENTS.md에는 작업을 멈춰야 하는 조건을 정해 두었습니다. AI가 만든 변경은 테스트 83개를 CI(pytest)로 돌려 확인한 뒤 반영했습니다.

설계와 검증 근거 보기 ↗

동시 요청을 넣어 보니 병목은 결과 조회였음

로컬 kind 클러스터에 저장소 매니페스트를 올리고 요청을 20건 동시에 보냈습니다. 계산 Job은 1초 안팎이었지만, 결과 조회가 조회마다 차트를 다시 그려 한 건에 1.6~1.9초씩 웹 워커 1개를 붙잡았습니다. 웹 워커를 3개로 늘리자 메모리 한도 512Mi를 넘어 OOMKilled로 6번 재시작했고, 웹 워커 2개, CPU 한도 1코어, 메모리 한도 1Gi 구성에서 처리량이 분당 27.7건에서 43.7건(+58%)으로 늘어, 이 구성을 매니페스트에 반영했습니다(PR #4). 조회마다 차트를 다시 그리지 않게 바꾸는 것이 다음 과제입니다.

설계와 검증 근거 보기 ↗

성공과 실패를 모두 확인

2026-03-04 요청 한 건을 처음부터 끝까지 실행해, 같은 run_id로 대기 → 실행 → 완료 상태와 MySQL 저장을 확인했습니다. Job 생성에 실패하면 요청이 대기 상태에 남지 않고 실패로 바뀌는지도 테스트했습니다.

설계와 검증 근거 보기 ↗

실제 화면

아키텍처와 실행 화면

최종 아키텍처, 백테스트 화면, Grafana 지표가 같은 실행 흐름을 보여 줍니다.

GitHub Actions, Argo CD, Ingress, Flask Web, Kubernetes Worker Job, MySQL, Prometheus와 Grafana를 포함한 최종 아키텍처
최종 아키텍처: CI부터 Worker Job, 모니터링까지 원본 보기
주식 백테스트 플랫폼 대시보드 화면
백테스트 실행 화면 원본 보기
Prometheus와 Grafana 기반 실행 지표 대시보드
Grafana 실행 지표 대시보드 원본 보기
백테스트 결과의 매매 타점과 포트폴리오 분석 화면
결과 분석 화면 원본 보기
MSP 과정 개인 프로젝트 우수상 상장
MSP 과정 개인 프로젝트 우수상 원본 보기

핵심 코드

요청마다 Job을 만드는 코드

요청의 run_id를 Job 이름과 라벨에 넣어, 로그와 결과를 같은 ID로 찾을 수 있게 했습니다.

Kubernetes Job 생성 코드

launchers/job_launcher.py

metadata = client.V1ObjectMeta(
    name=build_job_name(_stringify(run_payload.get("run_id"))),
    namespace=self.namespace,
    labels={
        "app": "worker",
        "run_id": _stringify(run_payload.get("run_id")),
    },
)

container = client.V1Container(
    name="worker",
    image=self.worker_image,
    image_pull_policy="IfNotPresent",
    command=["python", "worker.py"],
    env=env,
    env_from=[
        client.V1EnvFromSource(
            config_map_ref=client.V1ConfigMapEnvSource(name=self.configmap_name)
        ),
        client.V1EnvFromSource(
            secret_ref=client.V1SecretEnvSource(name=self.secret_name)
        ),
    ],
)

직접 확인하기

코드와 자료

아래 링크에서 코드, 설계 문서, 실행 기록을 직접 확인할 수 있습니다.

맡은 범위

  • 개인 프로젝트: 설계, 웹, Worker 코드, Kubernetes 매니페스트, CI, Argo CD, 모니터링 설정을 모두 직접 작성했습니다.

범위와 한계: 동시 요청 처리량은 로컬 kind 클러스터 1노드에서 한 번씩 잰 값이며 운영 수치가 아닙니다. 가장 나았던 구성(웹 워커 2개, CPU 1코어, 메모리 1Gi)은 저장소 매니페스트에 반영했고, Worker가 중간에 멈춘 모든 경우의 복구는 아직 검증하지 않았습니다.