포스트

AWS ML CI/CD와 CDC 파이프라인 구축 (1) ML 코드 작성과 로컬 검증

uv 프로젝트 초기화부터 학습 코드 작성과 로컬 검증, SageMaker 실행 규약의 도커 재현까지 AWS에 올리기 전에 진행한 작업을 정리합니다.

AWS ML CI/CD와 CDC 파이프라인 구축 (1) ML 코드 작성과 로컬 검증

AWS ML CI/CD와 CDC 파이프라인 구축 프로젝트 시리즈의 1편입니다. 프로젝트 소개는 0편에 있습니다.

이번 편은 2026-07-23부터 2026-07-24까지 진행한 작업 기록입니다. AWS 리소스를 하나도 만들지 않은 상태에서 ML 코드를 작성하고, SageMaker가 학습 컨테이너를 실행하는 규약을 로컬 docker로 재현해 검증했습니다. 이 과정에서 sklearn 버전 불일치로 모델 로드가 깨지는 버그를 하나 잡았고, 마지막에 git 저장소를 만들어 GitHub Flow로 작업 사이클을 잡았습니다.

uv로 프로젝트 초기화

의존성 관리는 uv를 사용했습니다. ml-cicd 디렉터리를 만들고 초기화했습니다.

1
2
3
4
mkdir ml-cicd && cd ml-cicd
uv init                                  # pyproject.toml 생성
uv add joblib pandas scikit-learn        # 런타임 의존성, uv.lock 생성
uv add --dev pytest ruff                 # 개발 의존성

uv를 선택한 이유는 lock 파일입니다. 로컬 개발 환경, CI의 테스트 단계, 학습 컨테이너가 전부 같은 uv.lock으로 환경을 재조립합니다. “로컬에서는 되는데 CI에서 안 됨” 문제를 lockfile 단일화로 차단하는 구조입니다.

ML 코드 3분할

학습 코드는 세 개의 스크립트로 나눴습니다. 이후 SageMaker Pipeline의 스텝 하나에 스크립트 하나가 대응됩니다.

1
2
3
4
ml-cicd/src/preprocess.py   # 분석 DB(또는 --synthetic 합성 데이터)에서 train/val/test 분할
ml-cicd/src/train.py        # GradientBoosting 학습, model.joblib 저장
ml-cicd/src/evaluate.py     # test 분할로 AUC 산출, evaluation.json 저장
ml-cicd/tests/unit/         # 단위 테스트 (피처 생성, 학습 왕복)

preprocess.py는 분석용 DB를 읽는 것이 기본이지만, DB 없이도 돌릴 수 있게 --synthetic 옵션으로 합성 데이터를 생성하는 경로를 넣었습니다. 로컬 검증과 단위 테스트가 전부 이 옵션에 의존합니다.

train.py는 경로를 인자로 받되, 인자가 없으면 SM_CHANNEL_TRAINSM_MODEL_DIR 환경변수를 기본값으로 사용합니다. 로컬에서는 인자로, SageMaker에서는 환경변수로 같은 코드가 동작하게 하기 위한 구조입니다.

로컬 실행 검증

테스트, 전처리, 학습, 평가를 순서대로 로컬에서 실행했습니다.

1
2
3
4
5
uv run pytest tests/unit                                          # 단위 테스트
uv run python src/preprocess.py --synthetic --output-dir local-run  # 데이터 생성
uv run python src/train.py --train-dir local-run/train --model-dir local-run/model
uv run python src/evaluate.py --model-dir local-run/model \
  --test-dir local-run/test --output-dir local-run/eval            # AUC 0.8929

평가 결과 AUC 0.8929가 나왔습니다. 이 수치는 뒤의 컨테이너 검증에서 기준값으로 쓰입니다. 같은 데이터와 같은 코드라면 어디서 돌려도 같은 수치가 나와야 하기 때문입니다.

학습 컨테이너 빌드

docker/train.Dockerfile을 작성했습니다. python:3.12-slim 베이스에 uv 바이너리만 공식 이미지에서 복사하고, pyproject.tomluv.lock을 소스보다 먼저 COPY해서 코드만 바뀐 빌드에서는 의존성 레이어가 캐시되게 했습니다. 의존성 설치는 uv sync --frozen --no-dev로, lock 파일 그대로를 재조립합니다.

1
2
3
4
5
docker build -t ml-training:local -f docker/train.Dockerfile .
docker run --rm \
  -v "$PWD/local-run/train:/data/train" \
  -v "$PWD/local-run/model-docker:/data/model" \
  ml-training:local --train-dir /data/train --model-dir /data/model

컨테이너 안에서도 학습이 완료되고 model.joblib이 생성되는 것을 확인했습니다.

SageMaker 실행 규약을 docker로 재현

SageMaker는 커스텀 학습 컨테이너를 고정된 규약으로 실행합니다.

  • 데이터를 /opt/ml/input/data/<채널>/ 경로에 마운트합니다.
  • SM_* 환경변수(SM_CHANNEL_TRAIN, SM_MODEL_DIR 등)를 주입합니다.
  • docker run <image> train 형태로 train 위치 인자 하나만 전달합니다.
  • 종료 후 /opt/ml/model의 내용을 tar.gz로 수거합니다.

이 규약은 고정 스펙이므로 AWS 계정 없이 docker의 -v-e 옵션만으로 동일 조건을 재현할 수 있습니다. 스크립트가 규약을 지키는지 세 건을 검증했습니다.

검증 1: train

SageMaker가 호출하는 방식 그대로 실행했습니다.

1
2
3
4
5
6
docker run --rm \
  -v ./local-run/train:/opt/ml/input/data/train \
  -v ./out:/opt/ml/model \
  -e SM_CHANNEL_TRAIN=/opt/ml/input/data/train \
  -e SM_MODEL_DIR=/opt/ml/model \
  ml-training:local train                        # out/model.joblib 생성 = 통과

마지막의 train이 SageMaker가 덧붙이는 위치 인자입니다. train.pyparse_known_args로 이 인자를 무시하도록 작성했기 때문에 그대로 통과합니다.

검증 2: preprocess

전처리는 SageMaker Processing으로 실행할 계획이었고, 처음 설계에서는 SageMaker가 제공하는 sklearn 1.2 컨테이너를 쓰기로 했습니다. 그 환경을 python:3.10-slim에 sklearn 1.2.2를 설치하는 방식으로 근사 재현했습니다.

1
2
3
4
5
6
7
docker run --rm \
  -v ./proc:/opt/ml/processing \
  -v ./src:/opt/ml/processing/input/code \
  python:3.10-slim bash -c "
    pip install -q scikit-learn==1.2.2 pandas numpy &&
    python /opt/ml/processing/input/code/preprocess.py --synthetic --commit-sha local"
# proc/{train,validation,test}/*.csv 생성 = 통과

검증 3: evaluate, 여기서 버그 발견

SageMaker는 학습 결과를 model.tar.gz로 수거해 평가 스텝에 전달하므로, 검증 1의 산출물을 tar로 묶어 같은 sklearn 1.2 환경에서 평가를 실행했습니다.

1
2
3
4
5
6
7
8
9
10
mkdir -p eval/model && tar -czf eval/model/model.tar.gz -C out model.joblib
docker run --rm \
  -v ./src:/code \
  -v ./eval/model:/opt/ml/processing/model \
  -v ./proc/test:/opt/ml/processing/test \
  -v ./eval/out:/opt/ml/processing/evaluation \
  python:3.10-slim bash -c "
    pip install -q 'numpy==1.26.4' 'scikit-learn==1.2.2' 'pandas==2.0.3' joblib &&
    python /code/evaluate.py"
# 실패: sklearn 1.9로 저장한 모델을 1.2가 unpickle 못 함

학습 이미지의 sklearn은 1.9인데, 평가에 쓰려던 SageMaker 제공 컨테이너는 sklearn 1.2입니다. 1.9로 저장한 model.joblib을 1.2가 열지 못해 Trying to unpickle estimator ... from version 1.9.0 when using 1.2.2 오류가 났습니다. 로컬 재현이 아니었다면 실제 SageMaker 실행에서 터졌을 버그입니다.

수정과 재검증

수정은 세 가지입니다.

  1. pipelines/pipeline.py에서 평가 스텝의 프로세서를 SKLearnProcessor에서 ScriptProcessor로 바꿔, 평가를 학습과 같은 커스텀 이미지에서 실행하게 했습니다.
  2. DB 접속 정보용 AnalyticsDbUri 파라미터와 ANALYTICS_DB_URI 환경변수 주입을 추가했습니다.
  3. evaluate.pytar.extractallfilter="data"를 추가했습니다.

학습 이미지로 평가를 다시 실행했습니다.

1
2
3
4
5
6
docker build -q -t ml-training:local -f docker/train.Dockerfile .
docker run --rm --entrypoint python \
  -v ./eval/model:/opt/ml/processing/model \
  -v ./proc/test:/opt/ml/processing/test \
  -v ./eval/out:/opt/ml/processing/evaluation \
  ml-training:local /opt/ml/code/src/evaluate.py   # AUC 0.8929 = 통과

AUC 0.8929로 로컬 실행 결과와 일치했습니다.

배운 것을 정리하면, joblib으로 직렬화한 모델은 저장한 sklearn 버전과 같은 버전에서만 로드가 보장됩니다. “저장하는 환경 = 읽는 환경”을 이미지 차원에서 맞추는 것이 해법이고, 그래서 이 프로젝트의 평가 스텝은 학습과 같은 이미지를 사용합니다.

git 저장소와 GitHub Flow

2026-07-24에 git 저장소를 만들고 GitHub private 저장소에 연결했습니다.

1
2
3
4
5
6
git init -b main
# .gitignore 작성: .venv, __pycache__, local-run/, out/, proc/, eval/ 등 제외
git add . && git commit -m "Initial commit"

git remote add origin https://github.com/<계정>/<저장소>.git
git push -u origin main

이후 모든 변경은 GitHub Flow 사이클로 진행했습니다.

1
2
3
4
5
6
git switch -c feature/ml-infra      # 브랜치 생성
git add . && git commit -m "..."
git push -u origin feature/ml-infra # 원격에 브랜치 push
# GitHub 웹에서 PR 생성, Files changed 확인 후 Merge
git switch main && git pull         # 병합 결과 수신
git branch -d feature/ml-infra      # 브랜치 정리

핵심은 main을 항상 동작하는 상태로 유지하는 것입니다. 다음 편에서 만들 CI/CD 파이프라인은 main 병합을 트리거로 전체 파이프라인을 실행하므로, “main에 들어온 코드 = 검증 완료”라는 전제가 성립해야 합니다. CI/CD 개념 자체는 CI/CD 기초 시리즈에 따로 정리했습니다.


여기까지가 AWS 비용 0원으로 진행한 구간입니다. 학습 코드, 컨테이너, SageMaker Pipeline 정의, buildspec까지 저장소 쪽 준비는 전부 끝났고, 다음 편에서는 이것을 실행할 AWS 리소스(ECR, S3, IAM, CodeConnections, CodeBuild)를 콘솔에서 수동으로 구성합니다.

다음 글: AWS ML CI/CD와 CDC 파이프라인 구축 (2) AWS 인프라 구성

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.