Flask 앱을 Kubernetes로 옮길 때 체크리스트: 옛 코드 배포, OOM, 쿼터 초과

2026.10.04
현장 데이터 서비스 구축기 11편

사내 PaaS에서 돌던 Flask 서비스를 Kubernetes 플랫폼으로 옮기며 겪은 문제를 체크리스트로 만들었다. 고친 코드가 반영되지 않을 때, ML 모델을 올린 Pod가 OOM으로 죽을 때, 롤링 업데이트가 쿼터 초과로 실패할 때 확인할 항목이다.

이 글의 순서

    배경

    이슈 데이터 포털(Flask)은 시민개발자용 사내 PaaS에서 돌고 있었다. 자원을 늘려도 활용률이 낮으면 회수되고, 로그를 보거나 배포 오류를 추적하기 어려워서 개발자용 Kubernetes 플랫폼으로 옮겼다. 옮기는 동안 막힌 지점을 증상별로 정리하고, 다음에 같은 작업을 할 때 그대로 쓸 체크리스트로 만들었다.

    증상으로 찾기

    증상원인확인할 곳
    코드를 고쳤는데 옛 화면이 뜬다CI가 다른 브랜치를 보고 있다CI 트리거 브랜치
    같은 태그로 배포하면 반영이 안 된다노드에 캐시된 이미지를 쓴다이미지 태그, pull 정책
    DB 연결에서 드라이버 오류베이스 이미지에 ODBC 드라이버가 없다Dockerfile FROM
    pip install이 빌드에서 실패로컬 버전 고정이 컨테이너 Python과 충돌requirements.txt
    Pod가 반복 재시작, OOMKilled워커마다 ML 모델을 메모리에 올림gunicorn 옵션, 메모리 한도
    배포가 exceeded quota로 실패롤링 업데이트 중 Pod 하나 더 필요Namespace 할당량, 배포 전략
    IP로 접속하면 404정상. Ingress는 도메인으로 라우팅한다도메인으로 접속

    옛 코드가 계속 배포될 때

    가장 오래 헤맨 문제다. 코드를 고쳐 push했는데 배포된 화면이 그대로였다. 원인은 단순했다. CI는 prod 브랜치를 보고 있었고, 나는 master에 push하고 있었다. CI는 바뀐 게 없으니 예전 코드로 계속 이미지를 만들었다.

    bash
    # CI가 보는 브랜치로 반영
    git checkout prod
    git merge master
    git push origin prod
    
    # 또는 로컬 master를 원격 prod로 바로 push
    git push origin master:prod

    브랜치가 맞는데도 반영이 안 되면 이미지 태그를 본다. latest처럼 같은 태그를 재사용하면 노드에 캐시된 이미지를 그대로 쓸 수 있다. 커밋 해시를 태그로 쓰면 이런 문제가 원천적으로 사라진다. GitOps(ArgoCD 등)로 배포한다면 배포 설정 저장소의 이미지 태그가 실제로 바뀌었는지도 확인한다.

    Dockerfile과 requirements

    플랫폼이 주는 템플릿의 기본 베이스 이미지는 순수 Python 이미지였다. 기존 환경에서는 ODBC 드라이버가 들어간 이미지를 쓰고 있었고, DB 연결에 그 드라이버가 필요했다. 템플릿을 그대로 쓰지 말고 기존 이미지와 비교한다.

    Dockerfile
    # 템플릿 기본값 대신 기존 환경과 같은 베이스 이미지 사용
    FROM registry.example.com/python:3.10-odbc
    
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    COPY . .
    
    CMD ["gunicorn", "-w", "1", "--threads", "4", "--preload", "-b", "0.0.0.0:8080", "app:app"]

    requirements.txt는 처음에 로컬의 pip freeze 결과를 그대로 넣었다가 빌드가 깨졌다. 로컬 Python 버전에서만 맞는 버전들이 섞여 있었다. 버전 고정을 대부분 풀었고, 하나만 남겼다.

    requirements.txt
    flask
    pandas
    lightgbm
    joblib
    gunicorn
    # 학습 때와 같은 버전이어야 저장한 모델 파일을 읽을 수 있다
    scikit-learn==1.7.2

    주의pickle로 저장한 scikit-learn 모델은 학습할 때와 다른 버전에서 불러오면 경고가 나거나 결과가 달라질 수 있다. 모델 파일과 묶인 라이브러리는 버전을 고정한다.

    ML 모델을 올린 Pod가 OOM으로 죽을 때

    앱이 시작할 때 등급 분류 모델을 메모리에 올린다. gunicorn 워커를 여러 개 띄우면 워커마다 모델이 따로 올라간다. 메모리 한도 1Gi에서 Pod가 계속 OOMKilled로 재시작됐다. 같은 시기에 SSO 문제도 보고 있었는데, 재시작이 반복되면서 로그인 리다이렉트가 끝나지 않는 것처럼 보여 원인을 헷갈렸다.

    워커 여러 개

    워커 1모델 사본
    워커 2모델 사본
    워커 3모델 사본

    모델 수만큼 메모리를 쓴다.

    워커 1개 + 스레드 + preload

    워커 1모델 1벌
    스레드 4개동시 요청 처리

    모델은 한 번만 올라간다.

    --workers 1 --threads 4 --preload로 바꾸고 메모리 요청 1Gi, 한도 2Gi로 맞춰 안정됐다. --preload는 워커를 띄우기 전에 앱을 먼저 불러온다. 앱 로딩 단계에서 오류가 나면 바로 드러나는 장점도 있다. 사용자 수가 많지 않은 사내 서비스라면 워커 하나와 스레드 몇 개로 충분하다. 요청이 늘면 워커 대신 Pod 수를 늘리는 쪽이 메모리 관리가 쉽다.

    롤링 업데이트가 쿼터 초과로 실패할 때

    Namespace 메모리 할당량이 4Gi이고 이미 3Gi를 쓰는 상태에서, 메모리를 2Gi 요청하는 Pod를 배포했더니 exceeded quota로 실패했다. 롤링 업데이트는 새 Pod를 먼저 띄우고 옛 Pod를 내리기 때문에, 배포하는 순간에는 2Gi가 더 필요하다.

    방법효과대가
    요청량(requests) 낮추기배포 중 추가 Pod가 들어갈 자리가 생긴다실측보다 낮추면 다른 Pod와 자원 경합
    할당량 늘리기근본 해결자원 신청 절차
    maxSurge 0옛 Pod를 먼저 내리고 새 Pod를 띄운다Pod가 1개면 배포 중 잠깐 중단
    deployment.yaml (일부)
    spec:
      replicas: 1
      strategy:
        type: RollingUpdate
        rollingUpdate:
          maxSurge: 0          # 추가 Pod 없이 교체 (잠깐 중단 감수)
          maxUnavailable: 1
      template:
        spec:
          containers:
            - name: app
              resources:
                requests:
                  memory: "1Gi"
                limits:
                  memory: "2Gi"

    그 밖에 알아 두면 좋은 것

    • 환경변수는 배포 설정 화면의 ConfigMap으로 넣었다. 비밀번호나 시크릿 키처럼 민감한 값은 ConfigMap보다 Secret에 두는 편이 맞다.
    • 사내 전용 패키지가 로컬에 없거나 포트가 겹쳐 로컬 실행이 번거로울 때는, 차라리 플랫폼에 올리고 Pod 로그로 확인하는 게 빨랐다.
    • IP로 접속해 default backend 404가 뜨는 건 정상이다. Ingress는 Host 헤더(도메인)로 라우팅한다.

    체크리스트

    누르면 확인 표시가 된다. 이관 작업 때 위에서부터 하나씩 확인하면 된다.

    정리

    대부분의 문제는 쿠버네티스 자체보다 '기존 환경이 알아서 해 주던 것'을 새 환경에서 직접 챙겨야 해서 생겼다. 베이스 이미지, 브랜치, 메모리 구성처럼 당연하게 여기던 것부터 비교하는 게 가장 빠르다. 도메인, 인증서, SSO를 붙이는 과정은 다음 편에서 다룬다.

    이전 편10편. 비개발자를 위한 쿠버네티스: 쇼핑몰로 이해하기
    다음 편12편. SSO 무한 리다이렉트 디버깅: 로그의 'POST /' 한 줄
    #Kubernetes#Flask#gunicorn#OOM#CI/CD#ArgoCD#Docker#배포트러블슈팅

    댓글