🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Kubernetes로 ML 모델 배포하기 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 11. 30. · 21 Views

Kubernetes로 ML 모델 배포하기 완벽 가이드

머신러닝 모델을 Kubernetes 환경에서 안정적으로 배포하고 운영하는 방법을 배웁니다. 컨테이너화부터 스케일링까지 실무에서 바로 적용할 수 있는 핵심 개념을 다룹니다.


목차

  1. ML_모델_컨테이너화
  2. Flask_모델_서빙_API
  3. Kubernetes_Deployment_작성
  4. Service로_로드밸런싱
  5. Liveness와_Readiness_프로브
  6. ConfigMap과_Secret_관리
  7. HPA로_자동_스케일링
  8. 롤링_업데이트와_롤백
  9. PersistentVolume으로_모델_저장
  10. 모니터링과_로깅_구축

1. ML 모델 컨테이너화

김개발 씨는 드디어 첫 번째 머신러닝 모델을 완성했습니다. 로컬 환경에서는 완벽하게 동작하는데, 이걸 어떻게 서버에 배포해야 할까요?

선배 박시니어 씨가 웃으며 말합니다. "먼저 모델을 컨테이너에 담아야 해요."

컨테이너화란 ML 모델과 그 실행 환경을 하나의 독립된 패키지로 묶는 것입니다. 마치 이사할 때 물건을 박스에 담아 운반하는 것처럼, 모델도 컨테이너라는 박스에 담아야 어디서든 동일하게 실행됩니다.

Docker를 사용하면 "내 컴퓨터에서는 되는데..."라는 악몽에서 벗어날 수 있습니다.

다음 코드를 살펴봅시다.

# Dockerfile - ML 모델 컨테이너화
FROM python:3.9-slim

# 작업 디렉토리 설정
WORKDIR /app

# 의존성 먼저 복사하여 캐시 활용
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 모델 파일과 서빙 코드 복사
COPY model.pkl .
COPY serve.py .

# 모델 서빙 포트 노출
EXPOSE 8080

# 컨테이너 시작 시 실행할 명령
CMD ["python", "serve.py"]

김개발 씨는 입사 6개월 차 ML 엔지니어입니다. 몇 주간 공들여 만든 추천 모델이 드디어 정확도 95%를 달성했습니다.

기쁜 마음으로 배포를 요청했더니 인프라팀에서 연락이 왔습니다. "모델 파일만 주시면 안 되고요, 실행 환경 정보가 필요해요." 알고 보니 김개발 씨의 로컬 환경에는 특정 버전의 scikit-learn과 numpy가 설치되어 있었습니다.

서버 환경과 버전이 달라서 모델이 제대로 로드되지 않았던 것입니다. 박시니어 씨가 다가와 설명해 주었습니다.

"이런 문제를 해결하려면 컨테이너화가 필요해요." 그렇다면 컨테이너란 정확히 무엇일까요? 쉽게 비유하자면, 컨테이너는 마치 택배 박스와 같습니다.

물건을 박스에 담으면 어디로 보내든 내용물이 안전하게 보호됩니다. 박스 안에 완충재도 함께 넣으면 더욱 안심이 되겠지요.

마찬가지로 ML 모델을 컨테이너에 담으면, 모델 파일뿐 아니라 Python 버전, 라이브러리 버전, 설정 파일까지 모두 함께 포장됩니다. 컨테이너 기술이 없던 시절에는 어땠을까요?

개발자들은 서버에 직접 접속해서 라이브러리를 하나씩 설치해야 했습니다. 서버마다 환경이 조금씩 달라서 "A 서버에서는 되는데 B 서버에서는 안 돼요"라는 문제가 빈번했습니다.

더 큰 문제는 여러 모델이 서로 다른 라이브러리 버전을 요구할 때였습니다. 한 서버에서 여러 모델을 운영하기가 거의 불가능했습니다.

바로 이런 문제를 해결하기 위해 Docker가 등장했습니다. Docker를 사용하면 각 모델이 독립된 환경에서 실행됩니다.

모델 A는 scikit-learn 0.24를, 모델 B는 1.0을 사용해도 전혀 충돌하지 않습니다. 무엇보다 개발 환경과 운영 환경이 완벽하게 동일해집니다.

위의 Dockerfile을 한 줄씩 살펴보겠습니다. 먼저 FROM 지시어로 기반 이미지를 선택합니다.

python:3.9-slim은 가벼운 Python 이미지입니다. WORKDIR로 작업 디렉토리를 설정하고, COPY로 필요한 파일들을 복사합니다.

여기서 requirements.txt를 먼저 복사하는 것이 중요합니다. 이렇게 하면 의존성이 변경되지 않은 경우 Docker 캐시를 활용할 수 있습니다.

실제 현업에서는 이미지 크기 최적화도 중요합니다. 불필요한 파일을 제외하기 위해 .dockerignore 파일을 작성합니다.

또한 멀티 스테이지 빌드를 활용하면 최종 이미지 크기를 크게 줄일 수 있습니다. 모델 파일이 수백 MB에 달하는 경우가 많으므로, 이미지 최적화는 배포 속도에 직접적인 영향을 미칩니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 것을 한 컨테이너에 담으려는 것입니다.

학습 코드, 전처리 코드, 서빙 코드를 모두 하나에 넣으면 이미지가 비대해집니다. 서빙용 컨테이너에는 정말 필요한 것만 담아야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언대로 Dockerfile을 작성하고 이미지를 빌드했습니다.

이제 어느 서버에서든 동일하게 모델이 실행됩니다. "이제 쿠버네티스에 올릴 준비가 됐네요!"

실전 팁

💡 - requirements.txt에 정확한 버전을 명시하세요 (예: scikit-learn==1.0.2)

  • .dockerignore로 불필요한 파일 제외하여 이미지 크기 최적화
  • 보안을 위해 root가 아닌 일반 사용자로 실행하도록 설정

2. Flask 모델 서빙 API

컨테이너는 만들었는데, 외부에서 어떻게 모델을 호출할 수 있을까요? 김개발 씨가 고민하자 박시니어 씨가 말합니다.

"REST API를 만들어야 해요. Flask로 간단하게 시작해 보죠."

모델 서빙이란 학습된 모델을 API 형태로 제공하여 외부에서 예측 요청을 받을 수 있게 하는 것입니다. 마치 레스토랑의 주방과 홀 사이에 있는 서빙 창구처럼, 모델과 사용자 사이를 연결하는 인터페이스입니다.

Flask를 사용하면 몇 줄의 코드로 이 창구를 만들 수 있습니다.

다음 코드를 살펴봅시다.

# serve.py - Flask 모델 서빙 API
from flask import Flask, request, jsonify
import pickle
import numpy as np

app = Flask(__name__)

# 모델 로드 (앱 시작 시 한 번만)
with open('model.pkl', 'rb') as f:
    model = pickle.load(f)

@app.route('/health', methods=['GET'])
def health():
    return jsonify({'status': 'healthy'})

@app.route('/predict', methods=['POST'])
def predict():
    data = request.json
    features = np.array(data['features']).reshape(1, -1)
    prediction = model.predict(features)
    return jsonify({'prediction': prediction.tolist()})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

김개발 씨의 추천 모델은 이제 컨테이너 안에 들어 있습니다. 하지만 컨테이너를 실행해도 외부에서 모델을 사용할 방법이 없었습니다.

모델 파일을 직접 로드해서 사용하려면 같은 환경이 필요하니까요. 박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다.

"웹 서비스처럼 만들면 돼요. 사용자가 HTTP 요청을 보내면, 우리 모델이 예측 결과를 응답하는 거죠." 이것이 바로 모델 서빙의 핵심입니다.

비유하자면, 모델 서빙은 마치 통역사와 같습니다. 외국어를 모르는 사람이 통역사에게 말을 전하면, 통역사가 번역해서 상대방에게 전달하고 답변을 다시 번역해 줍니다.

마찬가지로 사용자가 JSON 형태로 데이터를 보내면, 서빙 API가 이를 모델이 이해하는 형태로 변환하고, 예측 결과를 다시 JSON으로 응답합니다. 모델 서빙 없이는 어떤 문제가 있을까요?

각 클라이언트가 직접 모델 파일을 가지고 있어야 합니다. 모델이 업데이트될 때마다 모든 클라이언트를 수정해야 합니다.

게다가 모델 로딩에 시간이 걸려서 응답이 느려질 수 있습니다. Flask를 사용하면 이 문제가 깔끔하게 해결됩니다.

위 코드에서 가장 중요한 부분은 모델 로딩 위치입니다. with open 구문이 함수 밖에 있는 것을 주목하세요.

이렇게 하면 서버 시작 시 한 번만 모델을 로드합니다. 매 요청마다 모델을 로드하면 응답 시간이 크게 늘어납니다.

/health 엔드포인트도 눈여겨보세요. 이것은 단순히 서버가 살아있는지 확인하는 용도입니다.

별것 아닌 것 같지만, Kubernetes에서는 이 헬스체크가 매우 중요합니다. Kubernetes가 이 엔드포인트를 주기적으로 호출해서 컨테이너 상태를 확인하기 때문입니다.

/predict 엔드포인트는 실제 예측을 수행합니다. 클라이언트가 POST 요청으로 features를 보내면, 이를 numpy 배열로 변환하고 모델의 predict 메서드를 호출합니다.

결과를 JSON으로 변환해서 응답합니다. 여기서 reshape(1, -1)은 단일 샘플을 2차원 배열로 만들어 줍니다.

scikit-learn 모델은 2차원 입력을 기대하기 때문입니다. 실무에서는 에러 처리도 중요합니다.

입력 데이터가 잘못된 형식일 수 있고, 모델이 예상치 못한 값을 만날 수도 있습니다. try-except로 감싸고 적절한 에러 메시지를 반환해야 합니다.

또한 입력 데이터 검증도 필요합니다. 하지만 Flask만으로는 한계가 있습니다.

Flask의 내장 서버는 개발용입니다. 운영 환경에서는 Gunicorn이나 uWSGI와 함께 사용해야 합니다.

고성능이 필요한 경우 FastAPI나 TensorFlow Serving 같은 전문 도구를 고려해 보세요. 김개발 씨는 serve.py를 작성하고 테스트해 보았습니다.

curl 명령으로 요청을 보내니 예측 결과가 깔끔하게 돌아왔습니다. "이제 정말 서비스 같아졌네요!"

실전 팁

💡 - 모델은 전역 변수로 한 번만 로드하세요

  • 운영 환경에서는 Gunicorn과 함께 사용 (gunicorn -w 4 serve:app)
  • 입력 데이터 검증과 에러 처리를 반드시 추가하세요

3. Kubernetes Deployment 작성

Docker 이미지도 만들었고 API도 완성했습니다. 이제 진짜 Kubernetes에 배포할 차례입니다.

김개발 씨가 처음 보는 YAML 파일 앞에서 당황하자, 박시니어 씨가 천천히 설명을 시작합니다.

Deployment는 Kubernetes에서 애플리케이션을 배포하고 관리하는 핵심 리소스입니다. 마치 공장의 생산 라인 관리자처럼, 몇 개의 컨테이너를 실행할지, 문제가 생기면 어떻게 복구할지를 정의합니다.

YAML 파일 하나로 복잡한 배포 과정을 자동화할 수 있습니다.

다음 코드를 살펴봅시다.

# ml-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ml-model-server
  labels:
    app: ml-model
spec:
  replicas: 3
  selector:
    matchLabels:
      app: ml-model
  template:
    metadata:
      labels:
        app: ml-model
    spec:
      containers:
      - name: ml-container
        image: myregistry/ml-model:v1.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "512Mi"
            cpu: "250m"
          limits:
            memory: "1Gi"
            cpu: "500m"

김개발 씨는 Kubernetes 클러스터 앞에 서 있습니다. Docker 이미지는 레지스트리에 올렸고, 이제 이것을 클러스터에서 실행해야 합니다.

하지만 어디서부터 시작해야 할지 막막했습니다. 박시니어 씨가 빈 YAML 파일을 열며 말했습니다.

"Kubernetes에서는 모든 것을 선언적으로 정의해요. 원하는 상태를 적어두면, Kubernetes가 알아서 그 상태를 유지해 주죠." 이것이 Deployment의 핵심 철학입니다.

비유하자면, Deployment는 마치 레스토랑의 매니저와 같습니다. "테이블 3개에 항상 웨이터가 있어야 해"라고 지시하면, 매니저가 알아서 사람을 배치합니다.

웨이터 한 명이 아프면 다른 사람을 불러오고, 손님이 많아지면 인원을 늘립니다. Deployment도 마찬가지로 "Pod 3개를 항상 유지해"라고 선언하면 Kubernetes가 알아서 관리합니다.

YAML 파일이 처음에는 복잡해 보이지만, 구조를 알면 간단합니다. apiVersionkind는 이 리소스가 무엇인지 정의합니다.

apps/v1의 Deployment라는 뜻입니다. metadata에는 이름과 레이블을 적습니다.

레이블은 나중에 리소스를 찾고 연결하는 데 사용됩니다. spec 부분이 실제 설정입니다.

replicas: 3은 동일한 Pod를 3개 실행하라는 의미입니다. 하나가 죽어도 나머지 둘이 트래픽을 처리하고, Kubernetes가 자동으로 새 Pod를 만들어 3개를 유지합니다.

selector는 어떤 Pod를 관리할지 지정합니다. app: ml-model 레이블이 붙은 Pod들을 이 Deployment가 관리합니다.

template은 Pod의 설계도입니다. 여기서 컨테이너 이미지, 포트, 리소스 제한 등을 정의합니다.

resources 부분을 주목하세요. requests는 최소 보장 리소스이고, limits는 최대 사용 가능 리소스입니다.

ML 모델은 메모리를 많이 사용하므로 이 설정이 특히 중요합니다. 실무에서 리소스 설정은 신중해야 합니다.

requests를 너무 높게 잡으면 클러스터 자원이 낭비됩니다. 너무 낮게 잡으면 스케줄링이 어려워집니다.

limits를 초과하면 컨테이너가 강제 종료될 수 있습니다. 처음에는 모니터링을 통해 실제 사용량을 파악하고 점진적으로 조정하는 것이 좋습니다.

흔히 하는 실수 중 하나는 레이블을 잘못 설정하는 것입니다. selector의 레이블과 template의 레이블이 일치해야 합니다.

불일치하면 Deployment가 Pod를 찾지 못해 계속 새 Pod를 만들어 버립니다. 또한 이미지 태그에 latest를 사용하면 어떤 버전이 배포되었는지 추적이 어렵습니다.

김개발 씨는 YAML 파일을 작성하고 kubectl apply -f ml-deployment.yaml 명령을 실행했습니다. 잠시 후 kubectl get pods를 치니 3개의 Pod가 Running 상태로 떠 있었습니다.

"와, 정말 3개가 실행되고 있어요!"

실전 팁

💡 - 이미지 태그는 latest 대신 구체적인 버전(v1.0.0)을 사용하세요

  • 리소스 requests/limits는 모니터링 후 실제 사용량 기반으로 설정
  • 레이블은 일관된 명명 규칙을 정해서 사용하세요

4. Service로 로드밸런싱

Pod가 3개 실행되고 있지만, 외부에서 어떻게 접근할 수 있을까요? IP 주소가 3개인데 어떤 것을 사용해야 하나요?

김개발 씨의 질문에 박시니어 씨가 답합니다. "Service를 만들면 하나의 주소로 모든 Pod에 접근할 수 있어요."

Service는 여러 Pod를 하나의 네트워크 엔드포인트로 묶어주는 추상화 계층입니다. 마치 대표 전화번호처럼, 하나의 주소로 전화하면 내부적으로 적절한 담당자에게 연결해 줍니다.

Pod가 죽고 살아나도 Service 주소는 변하지 않아 안정적인 접근이 가능합니다.

다음 코드를 살펴봅시다.

# ml-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: ml-model-service
spec:
  selector:
    app: ml-model
  type: LoadBalancer
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
---
# 내부 통신용 ClusterIP Service
apiVersion: v1
kind: Service
metadata:
  name: ml-model-internal
spec:
  selector:
    app: ml-model
  type: ClusterIP
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 8080

김개발 씨는 kubectl get pods -o wide 명령으로 Pod들의 IP를 확인했습니다. 10.244.1.5, 10.244.2.3, 10.244.1.8.

세 개의 서로 다른 IP가 있었습니다. "이 중에 어떤 IP로 요청을 보내야 하죠?" 박시니어 씨가 고개를 저었습니다.

"그 IP들은 언제든 바뀔 수 있어요. Pod가 재시작되면 새 IP를 받거든요.

직접 사용하면 안 됩니다." 이것이 바로 Service가 필요한 이유입니다. 비유하자면, Service는 마치 콜센터의 대표번호와 같습니다.

고객은 1588-xxxx 하나만 알면 됩니다. 실제로 어떤 상담원이 전화를 받을지는 고객이 알 필요가 없습니다.

상담원이 퇴사하고 새 사람이 와도 대표번호는 변하지 않습니다. Service도 마찬가지로, 하나의 고정된 주소 뒤에서 여러 Pod로 트래픽을 분산합니다.

Service의 selector가 핵심입니다. app: ml-model이라는 레이블을 가진 모든 Pod를 찾아서 연결합니다.

Deployment에서 정의한 레이블과 일치해야 합니다. 새 Pod가 생기면 자동으로 Service에 등록되고, Pod가 사라지면 자동으로 제거됩니다.

type 필드로 Service 종류를 지정합니다. ClusterIP는 기본값으로, 클러스터 내부에서만 접근 가능합니다.

다른 서비스에서 ML 모델을 호출할 때 사용합니다. LoadBalancer는 클라우드 환경에서 외부 IP를 할당받아 인터넷에서 접근할 수 있게 합니다.

NodePort는 각 노드의 특정 포트를 열어 외부 접근을 허용합니다. porttargetPort의 차이를 이해해야 합니다.

port는 Service가 노출하는 포트입니다. 클라이언트는 이 포트로 요청합니다.

targetPort는 실제 Pod의 컨테이너가 리스닝하는 포트입니다. 예제에서는 외부에서 80번 포트로 요청하면 Pod의 8080번 포트로 전달됩니다.

실무에서는 보통 두 종류의 Service를 만듭니다. 외부 사용자를 위한 LoadBalancer Service와 내부 마이크로서비스 간 통신을 위한 ClusterIP Service입니다.

내부 서비스는 ml-model-internal:8080 같은 DNS 이름으로 접근할 수 있습니다. Kubernetes가 자동으로 DNS를 관리해 줍니다.

주의할 점은 LoadBalancer 비용입니다. 클라우드에서 LoadBalancer를 만들면 별도 비용이 발생합니다.

여러 서비스가 있다면 Ingress를 사용해 하나의 LoadBalancer로 여러 서비스를 노출하는 것이 경제적입니다. 김개발 씨는 Service를 배포하고 kubectl get svc 명령을 실행했습니다.

EXTERNAL-IP 컬럼에 IP가 할당되어 있었습니다. 그 IP로 요청을 보내니 모델 예측이 정상적으로 돌아왔습니다.

"이제 어디서든 이 IP로 접근하면 되는 거군요!"

실전 팁

💡 - 내부 통신에는 ClusterIP를, 외부 노출에는 LoadBalancer나 Ingress를 사용

  • Service 이름은 DNS로 사용되므로 명확하고 일관된 명명 규칙 적용
  • 프로덕션에서는 LoadBalancer 대신 Ingress 사용을 권장

5. Liveness와 Readiness 프로브

배포한 지 며칠 후, 김개발 씨의 모델 서버가 응답하지 않는다는 알림이 왔습니다. 확인해 보니 Pod는 Running 상태인데 실제로는 먹통이었습니다.

"Running인데 왜 응답을 안 하죠?" 박시니어 씨가 말합니다. "프로브를 설정하지 않아서 그래요."

프로브는 Kubernetes가 컨테이너의 상태를 확인하는 방법입니다. Liveness Probe는 컨테이너가 살아있는지, Readiness Probe는 트래픽을 받을 준비가 되었는지 확인합니다.

마치 의사가 환자의 맥박과 의식을 확인하는 것처럼, 프로브는 컨테이너의 건강 상태를 지속적으로 모니터링합니다.

다음 코드를 살펴봅시다.

# 프로브가 추가된 Deployment
spec:
  containers:
  - name: ml-container
    image: myregistry/ml-model:v1.0
    ports:
    - containerPort: 8080
    # 컨테이너가 살아있는지 확인
    livenessProbe:
      httpGet:
        path: /health
        port: 8080
      initialDelaySeconds: 30
      periodSeconds: 10
      failureThreshold: 3
    # 트래픽을 받을 준비가 되었는지 확인
    readinessProbe:
      httpGet:
        path: /health
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 5
      failureThreshold: 3
    # 시작 시 초기화 완료 확인
    startupProbe:
      httpGet:
        path: /health
        port: 8080
      failureThreshold: 30
      periodSeconds: 10

김개발 씨는 당황스러웠습니다. kubectl get pods를 치면 분명히 Running이라고 나옵니다.

하지만 curl로 요청을 보내면 타임아웃이 발생했습니다. 어떻게 이런 일이 가능한 걸까요?

박시니어 씨가 설명했습니다. "Kubernetes가 보는 Running은 컨테이너 프로세스가 살아있다는 것뿐이에요.

프로세스 안에서 무한 루프에 빠지거나 메모리가 부족해서 응답을 못 해도, 프로세스 자체는 살아있으니까 Running으로 보이는 거죠." 이 문제를 해결하는 것이 프로브입니다. 비유하자면, 프로브는 마치 야간 경비원의 순찰과 같습니다.

단순히 문이 닫혀 있는지 확인하는 것이 아니라, 실제로 문을 열어보고 안에 문제가 없는지 확인합니다. 응답이 없으면 즉시 조치를 취합니다.

세 가지 프로브가 있고, 각각 역할이 다릅니다. Liveness Probe는 "이 컨테이너가 살아있나?"를 확인합니다.

실패하면 Kubernetes가 컨테이너를 재시작합니다. 데드락에 빠지거나 무한 루프에 걸린 경우를 탐지합니다.

Readiness Probe는 "이 컨테이너가 요청을 처리할 준비가 되었나?"를 확인합니다. 실패하면 Service의 엔드포인트에서 해당 Pod를 제외합니다.

재시작하지 않고 트래픽만 차단합니다. Startup Probe는 시작 시점에만 동작합니다.

ML 모델처럼 초기화에 시간이 오래 걸리는 경우 유용합니다. Startup Probe가 성공할 때까지 다른 프로브는 비활성화됩니다.

설정 값들의 의미를 살펴보겠습니다. initialDelaySeconds는 컨테이너 시작 후 첫 프로브까지 대기 시간입니다.

periodSeconds는 프로브 간격입니다. failureThreshold는 몇 번 실패해야 조치를 취할지 정합니다.

3으로 설정하면 3번 연속 실패해야 재시작됩니다. ML 모델 서빙에서 특히 주의할 점이 있습니다.

모델 파일이 크면 로딩에 수십 초가 걸릴 수 있습니다. initialDelaySeconds를 충분히 주지 않으면, 아직 모델 로딩 중인데 프로브가 실패해서 재시작되는 악순환에 빠집니다.

startupProbe를 사용하면 이 문제를 우아하게 해결할 수 있습니다. 또 하나 중요한 것은 /health 엔드포인트의 구현입니다.

단순히 200을 반환하는 것보다, 실제로 모델이 로드되었는지 확인하는 것이 좋습니다. 모델 객체가 None이 아닌지, 간단한 예측을 수행할 수 있는지 체크하면 더 정확한 상태 확인이 가능합니다.

김개발 씨는 프로브를 추가하고 다시 배포했습니다. 이번에는 모델 로딩이 완료되기 전까지 트래픽이 들어오지 않았고, 문제가 생기면 자동으로 재시작되었습니다.

"이제야 안심이 되네요!"

실전 팁

💡 - ML 모델은 로딩 시간이 길므로 startupProbe 사용을 권장

  • health 엔드포인트에서 실제 모델 상태를 확인하세요
  • failureThreshold는 너무 낮으면 일시적 지연에도 재시작, 너무 높으면 장애 대응 지연

6. ConfigMap과 Secret 관리

김개발 씨는 모델 버전을 바꿀 때마다 Docker 이미지를 새로 빌드해야 했습니다. 설정 파일 하나 바꾸는데 30분이 걸립니다.

"더 좋은 방법이 없을까요?" 박시니어 씨가 ConfigMap을 소개합니다.

ConfigMap은 설정 데이터를 컨테이너와 분리하여 관리하는 Kubernetes 리소스입니다. Secret은 비밀번호나 API 키 같은 민감한 정보를 안전하게 저장합니다.

마치 호텔 객실의 금고처럼, 중요한 것은 따로 보관하고 필요할 때만 꺼내 씁니다.

다음 코드를 살펴봅시다.

# ml-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: ml-model-config
data:
  MODEL_VERSION: "v2.0"
  BATCH_SIZE: "32"
  LOG_LEVEL: "INFO"
---
apiVersion: v1
kind: Secret
metadata:
  name: ml-model-secrets
type: Opaque
data:
  # base64 인코딩된 값
  DB_PASSWORD: cGFzc3dvcmQxMjM=
  API_KEY: c2VjcmV0LWFwaS1rZXk=
---
# Deployment에서 사용
spec:
  containers:
  - name: ml-container
    envFrom:
    - configMapRef:
        name: ml-model-config
    - secretRef:
        name: ml-model-secrets

김개발 씨의 ML 파이프라인은 점점 복잡해지고 있었습니다. 개발 환경에서는 배치 크기 16, 운영 환경에서는 32를 사용합니다.

데이터베이스 주소도 환경마다 다릅니다. 매번 이미지를 따로 빌드하자니 관리가 너무 어려웠습니다.

박시니어 씨가 해결책을 제시했습니다. "설정을 코드에서 분리하세요.

Kubernetes의 ConfigMap과 Secret을 사용하면 됩니다." 이것은 12 Factor App의 중요한 원칙 중 하나입니다. 비유하자면, ConfigMap은 마치 리모컨과 같습니다.

TV 본체를 분해하지 않고도 채널과 볼륨을 바꿀 수 있습니다. 마찬가지로 이미지를 다시 빌드하지 않고도 설정을 변경할 수 있습니다.

ConfigMap에는 일반적인 설정을 저장합니다. 모델 버전, 배치 크기, 로그 레벨 같은 것들입니다.

YAML의 data 섹션에 키-값 쌍으로 정의합니다. 이 값들은 컨테이너에 환경변수로 주입됩니다.

Secret은 민감한 정보를 위한 것입니다. 데이터베이스 비밀번호, API 키, 인증 토큰 같은 것들입니다.

ConfigMap과 비슷하지만 몇 가지 차이가 있습니다. 값은 base64로 인코딩해야 합니다.

etcd에 저장될 때 암호화될 수 있습니다. kubectl get으로 조회해도 값이 숨겨집니다.

주의하세요. base64는 암호화가 아닙니다.

누구나 디코딩할 수 있습니다. 진정한 보안을 위해서는 etcd 암호화를 활성화하거나, HashiCorp Vault 같은 외부 시크릿 관리 도구를 사용해야 합니다.

컨테이너에 주입하는 방법은 두 가지입니다. envFrom을 사용하면 모든 키-값이 환경변수가 됩니다.

코드에서 os.environ["MODEL_VERSION"]으로 접근합니다. volumeMounts를 사용하면 파일로 마운트됩니다.

설정 파일 형태가 필요한 경우 유용합니다. 실무에서 자주 하는 실수가 있습니다.

Secret을 Git에 커밋하는 것입니다. base64로 인코딩되어 있어서 안전하다고 착각하지만, 전혀 그렇지 않습니다.

Secret YAML 파일은 절대 버전 관리에 포함하면 안 됩니다. 대신 Sealed Secrets나 External Secrets Operator를 사용하세요.

김개발 씨는 ConfigMap을 적용하고 환경별로 다른 설정을 사용할 수 있게 되었습니다. 이제 배치 크기를 바꿀 때 kubectl apply 한 번이면 됩니다.

"이미지 빌드 기다리는 시간이 없어져서 너무 좋아요!"

실전 팁

💡 - Secret 값은 base64 인코딩 필수 (echo -n 'password' | base64)

  • Secret YAML은 절대 Git에 커밋하지 마세요
  • 설정 변경 후 Pod 재시작이 필요하면 kubectl rollout restart 사용

7. HPA로 자동 스케일링

출시 후 사용자가 급증했습니다. 평소에는 Pod 3개로 충분한데, 피크 타임에는 응답이 느려집니다.

김개발 씨가 수동으로 replicas를 늘리고 있자 박시니어 씨가 말합니다. "그거 자동화할 수 있어요."

**HPA(Horizontal Pod Autoscaler)**는 부하에 따라 자동으로 Pod 수를 조절하는 Kubernetes 리소스입니다. 마치 에스컬레이터가 사람이 많으면 빨리 돌고 적으면 천천히 도는 것처럼, CPU나 메모리 사용량에 따라 Pod를 늘리거나 줄입니다.

다음 코드를 살펴봅시다.

# ml-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: ml-model-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: ml-model-server
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 10
        periodSeconds: 60

김개발 씨의 추천 모델은 대성공이었습니다. 하지만 성공에는 책임이 따릅니다.

매일 오후 7시면 트래픽이 평소의 5배로 치솟았습니다. 김개발 씨는 알람이 울릴 때마다 kubectl scale 명령을 실행해야 했습니다.

어느 날 영화를 보다가 알람이 왔습니다. "이러다 주말도 없겠어요." 박시니어 씨가 HPA를 알려주었습니다.

HPA는 자동 스케일링의 핵심입니다. 비유하자면, HPA는 마치 스마트 에어컨의 자동 모드와 같습니다.

방이 더워지면 강풍으로, 시원해지면 약풍으로 자동 조절됩니다. 사람이 일일이 리모컨을 누를 필요가 없습니다.

HPA도 마찬가지로 부하가 높아지면 Pod를 늘리고, 낮아지면 줄입니다. 설정을 자세히 살펴보겠습니다.

scaleTargetRef는 어떤 Deployment를 스케일링할지 지정합니다. minReplicasmaxReplicas는 범위를 정합니다.

아무리 부하가 낮아도 2개는 유지하고, 아무리 높아도 10개를 넘지 않습니다. metrics가 스케일링 기준입니다.

averageUtilization: 70은 전체 Pod의 평균 CPU 사용률이 70%를 넘으면 스케일 아웃하라는 의미입니다. 여러 메트릭을 동시에 지정할 수 있고, 하나라도 기준을 넘으면 스케일링이 발동합니다.

ML 워크로드에서 특별히 고려할 점이 있습니다. 추론 작업은 CPU보다 메모리를 많이 사용하는 경우가 많습니다.

특히 대형 모델을 로드하면 메모리가 먼저 부족해집니다. 따라서 메모리 기반 스케일링 메트릭도 함께 설정하는 것이 좋습니다.

behavior 섹션은 스케일링 속도를 조절합니다. 스케일 다운이 너무 빠르면 위험합니다.

트래픽이 잠시 줄었다가 다시 올라올 수 있기 때문입니다. stabilizationWindowSeconds: 300은 5분간 안정적으로 낮은 상태를 유지해야 줄인다는 의미입니다.

Percent: 10은 한 번에 10%만 줄인다는 것입니다. HPA가 제대로 동작하려면 resources.requests가 설정되어 있어야 합니다.

HPA는 requests 대비 실제 사용량의 비율을 계산합니다. requests가 없으면 기준이 없어서 스케일링이 동작하지 않습니다.

앞서 Deployment에서 설정한 requests가 여기서 중요해집니다. 흔한 실수 중 하나는 metrics-server를 설치하지 않는 것입니다.

HPA가 메트릭을 수집하려면 metrics-server가 클러스터에 설치되어 있어야 합니다. kubectl top pods 명령이 동작하지 않는다면 metrics-server가 없는 것입니다.

김개발 씨는 HPA를 적용한 후 편안한 저녁을 보낼 수 있게 되었습니다. kubectl get hpa를 확인하니 TARGETS 컬럼에 현재 사용률이 표시되고, 부하에 따라 REPLICAS가 자동으로 변하고 있었습니다.

실전 팁

💡 - ML 워크로드는 CPU와 메모리 메트릭을 함께 설정하세요

  • 스케일 다운 속도는 보수적으로 설정 (너무 빠르면 불안정)
  • metrics-server 설치 필수, kubectl top pods로 확인

8. 롤링 업데이트와 롤백

새 버전의 모델을 배포해야 합니다. 하지만 서비스를 중단할 수는 없습니다.

김개발 씨가 걱정하자 박시니어 씨가 말합니다. "Kubernetes는 무중단 배포를 기본으로 지원해요."

롤링 업데이트는 Pod를 하나씩 순차적으로 교체하여 서비스 중단 없이 배포하는 전략입니다. 마치 달리는 기차의 바퀴를 하나씩 교체하는 것처럼, 서비스는 계속 운행하면서 새 버전으로 전환합니다.

문제가 발생하면 즉시 이전 버전으로 롤백할 수 있습니다.

다음 코드를 살펴봅시다.

# 롤링 업데이트 전략이 포함된 Deployment
spec:
  replicas: 4
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    spec:
      containers:
      - name: ml-container
        image: myregistry/ml-model:v2.0
---
# 배포 및 롤백 명령어
# kubectl apply -f ml-deployment.yaml
# kubectl rollout status deployment/ml-model-server
# kubectl rollout history deployment/ml-model-server
# kubectl rollout undo deployment/ml-model-server

김개발 씨의 새 모델 v2.0은 정확도가 크게 향상되었습니다. 당장 배포하고 싶지만, 지금 서비스에는 수천 명의 사용자가 접속해 있습니다.

"배포하는 동안 에러가 나면 어쩌죠?" 박시니어 씨가 안심시켰습니다. "롤링 업데이트를 쓰면 사용자는 전혀 눈치채지 못해요." 롤링 업데이트의 원리는 간단합니다.

비유하자면, 마치 식당의 주방 교대와 같습니다. 새 요리사가 오면 기존 요리사가 바로 퇴근하지 않습니다.

새 요리사가 적응할 때까지 함께 일하다가, 준비가 되면 한 명씩 교대합니다. 손님은 요리가 끊기지 않고 계속 나옵니다.

strategy 섹션이 이것을 제어합니다. maxSurge: 1은 업데이트 중에 원래 개수보다 최대 1개 더 만들 수 있다는 의미입니다.

4개 Pod가 있다면 최대 5개까지 허용됩니다. maxUnavailable: 0은 항상 모든 Pod가 사용 가능해야 한다는 의미입니다.

하나도 중단되지 않고 새 버전으로 전환됩니다. 업데이트 과정을 단계별로 살펴보면 이렇습니다.

먼저 새 버전의 Pod 1개가 생성됩니다. Readiness Probe를 통과하면 트래픽을 받기 시작합니다.

그러면 구버전 Pod 1개가 종료됩니다. 이 과정이 모든 Pod가 교체될 때까지 반복됩니다.

ML 모델 배포에서 특히 주의할 점이 있습니다. 모델 로딩에 시간이 오래 걸린다면, 새 Pod가 준비되는 동안 기존 Pod에 부하가 집중될 수 있습니다.

Readiness Probe를 정확히 설정해서 모델이 완전히 로드된 후에만 트래픽을 받도록 해야 합니다. 롤백은 문제 발생 시 생명줄입니다.

새 버전에서 버그가 발견되었다면 kubectl rollout undo를 실행합니다. Kubernetes는 이전 버전의 ReplicaSet을 기억하고 있다가 즉시 복원합니다.

kubectl rollout history로 배포 이력을 확인할 수 있습니다. 실무에서는 배포 전 테스트도 중요합니다.

Canary 배포를 고려해 보세요. 전체 트래픽의 일부만 새 버전으로 보내서 테스트하고, 문제가 없으면 전체로 확대합니다.

Istio 같은 서비스 메시를 사용하면 더 세밀한 트래픽 제어가 가능합니다. 김개발 씨는 이미지 태그를 v2.0으로 바꾸고 kubectl apply를 실행했습니다.

kubectl rollout status로 진행 상황을 지켜보니, Pod가 하나씩 교체되고 있었습니다. 모니터링 대시보드를 보니 에러율은 0%, 응답 시간도 정상이었습니다.

"정말 무중단으로 배포됐어요!"

실전 팁

💡 - maxUnavailable: 0으로 설정하면 항상 100% 가용성 유지

  • 배포 전 kubectl rollout status로 진행 상황 모니터링
  • 문제 발생 시 즉시 kubectl rollout undo로 롤백

9. PersistentVolume으로 모델 저장

모델 파일이 점점 커지고 있습니다. 500MB짜리 모델을 이미지에 포함시키니 빌드도 느리고 저장소 용량도 부담됩니다.

"모델 파일을 따로 관리할 수 없을까요?" 박시니어 씨가 PV를 소개합니다.

**PersistentVolume(PV)**은 Pod의 생명주기와 독립적인 스토리지입니다. **PersistentVolumeClaim(PVC)**으로 스토리지를 요청하면 Kubernetes가 적절한 PV를 할당합니다.

마치 클라우드 드라이브처럼, Pod가 재시작되어도 데이터가 사라지지 않습니다.

다음 코드를 살펴봅시다.

# ml-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ml-model-storage
spec:
  accessModes:
    - ReadOnlyMany
  resources:
    requests:
      storage: 10Gi
  storageClassName: standard
---
# Deployment에서 볼륨 마운트
spec:
  containers:
  - name: ml-container
    image: myregistry/ml-model:v2.0
    volumeMounts:
    - name: model-volume
      mountPath: /models
      readOnly: true
  volumes:
  - name: model-volume
    persistentVolumeClaim:
      claimName: ml-model-storage

김개발 씨의 최신 딥러닝 모델은 무려 2GB나 됩니다. 이걸 Docker 이미지에 넣으니 빌드에 20분, 푸시에 30분이 걸렸습니다.

코드 한 줄 고치는데 1시간이 걸리는 셈입니다. 더 큰 문제는 Pod가 여러 개일 때였습니다.

"각 Pod에 2GB 모델이 따로 저장되면... 30개 Pod면 60GB?" 박시니어 씨가 해결책을 알려주었습니다.

PersistentVolume은 공유 스토리지의 핵심입니다. 비유하자면, PV는 마치 회사의 공용 파일 서버와 같습니다.

직원 각자의 컴퓨터에 파일을 저장하는 대신, 공용 서버에 저장하면 누구나 접근할 수 있습니다. 직원이 바뀌어도 파일은 그대로입니다.

PV도 마찬가지로, Pod가 재시작되어도 데이터는 유지됩니다. PV와 PVC의 관계를 이해해야 합니다.

PV는 실제 스토리지입니다. 클러스터 관리자가 미리 생성해 둡니다.

PVC는 스토리지 요청입니다. 개발자가 "10GB 스토리지가 필요해요"라고 요청하면, Kubernetes가 조건에 맞는 PV를 찾아 연결합니다.

accessModes가 중요합니다. **ReadWriteOnce(RWO)**는 하나의 노드에서만 읽고 쓸 수 있습니다.

**ReadOnlyMany(ROX)**는 여러 노드에서 읽기만 가능합니다. **ReadWriteMany(RWX)**는 여러 노드에서 읽고 쓸 수 있습니다.

ML 모델은 읽기만 하므로 ROX가 적합합니다. 모델을 PV에 저장하는 방법은 여러 가지입니다.

가장 간단한 것은 초기화 컨테이너로 S3에서 다운로드하는 것입니다. 또는 CI/CD 파이프라인에서 모델을 PV에 복사할 수 있습니다.

대규모 환경에서는 모델 레지스트리(MLflow, BentoML)를 사용하기도 합니다. 주의할 점이 있습니다.

모든 스토리지가 ROX를 지원하는 것은 아닙니다. AWS EBS는 RWO만 지원합니다.

여러 Pod에서 같은 모델을 공유하려면 EFS나 NFS 같은 네트워크 스토리지가 필요합니다. 클라우드별로 지원하는 accessMode를 확인하세요.

스토리지 성능도 고려해야 합니다. 모델 로딩 시간은 스토리지 I/O에 영향을 받습니다.

SSD 기반 스토리지가 HDD보다 훨씬 빠릅니다. 콜드 스타트 시간이 중요하다면 스토리지 클래스 선택에 신경 써야 합니다.

김개발 씨는 모델을 PV로 분리한 후 Docker 이미지 크기가 100MB로 줄었습니다. 빌드와 배포가 눈에 띄게 빨라졌습니다.

무엇보다 모델 업데이트가 이미지 재빌드 없이 가능해졌습니다.

실전 팁

💡 - ML 모델은 ReadOnlyMany로 설정하여 여러 Pod에서 공유

  • 클라우드별 지원 accessMode 확인 필수 (EBS는 RWO만 지원)
  • 초기화 컨테이너로 S3에서 모델 다운로드하는 패턴도 고려

10. 모니터링과 로깅 구축

서비스가 안정적으로 운영되는 것 같았습니다. 그런데 어느 날 사용자로부터 "예측 결과가 이상해요"라는 문의가 왔습니다.

김개발 씨는 어디서부터 확인해야 할지 막막했습니다. "로그를 봐야 하는데, 어디서 보죠?"

모니터링은 시스템의 상태와 성능을 실시간으로 관찰하는 것입니다. 로깅은 발생한 이벤트를 기록하여 나중에 분석할 수 있게 합니다.

마치 자동차의 계기판과 블랙박스처럼, 현재 상태를 보여주고 문제 발생 시 원인을 추적할 수 있게 합니다.

다음 코드를 살펴봅시다.

# prometheus-metrics.py - 모델 서빙에 메트릭 추가
from flask import Flask, request, jsonify
from prometheus_client import Counter, Histogram, generate_latest
import time

app = Flask(__name__)

# 메트릭 정의
PREDICTION_COUNT = Counter(
    'ml_predictions_total',
    'Total predictions',
    ['model_version', 'status']
)
PREDICTION_LATENCY = Histogram(
    'ml_prediction_latency_seconds',
    'Prediction latency',
    buckets=[0.1, 0.5, 1.0, 2.0, 5.0]
)

@app.route('/predict', methods=['POST'])
def predict():
    start_time = time.time()
    try:
        result = model.predict(request.json['features'])
        PREDICTION_COUNT.labels('v2.0', 'success').inc()
        return jsonify({'prediction': result})
    finally:
        PREDICTION_LATENCY.observe(time.time() - start_time)

@app.route('/metrics')
def metrics():
    return generate_latest()

김개발 씨는 Pod 로그를 뒤지고 있었습니다. kubectl logs로 확인하니 로그가 너무 많아서 찾기가 어려웠습니다.

게다가 Pod가 재시작되면 로그가 사라집니다. "이래서는 문제를 추적할 수가 없어요." 박시니어 씨가 모니터링 시스템을 소개했습니다.

"Prometheus와 Grafana를 설치해 봐요. 그리고 애플리케이션에서 메트릭을 노출해야 해요." 모니터링로깅은 운영의 두 축입니다.

비유하자면, 모니터링은 마치 건강검진과 같습니다. 정기적으로 수치를 측정하고 기록합니다.

혈압이 갑자기 오르면 경고를 울립니다. 로깅은 마치 일기장과 같습니다.

매일 무슨 일이 있었는지 기록해 두면, 나중에 문제의 원인을 추적할 수 있습니다. ML 서빙에서 특히 중요한 메트릭이 있습니다.

**예측 횟수(prediction count)**는 얼마나 많이 사용되는지 보여줍니다. **예측 지연 시간(latency)**은 응답이 얼마나 빠른지 알려줍니다.

**에러율(error rate)**은 실패하는 요청의 비율입니다. 이 세 가지는 반드시 측정해야 합니다.

위 코드에서 prometheus_client 라이브러리를 사용합니다. Counter는 단조 증가하는 값입니다.

예측 횟수처럼 늘어나기만 하는 메트릭에 적합합니다. Histogram은 분포를 측정합니다.

지연 시간이 0.1초 이하가 몇 퍼센트인지, 1초 이상이 몇 퍼센트인지 알 수 있습니다. /metrics 엔드포인트가 핵심입니다.

Prometheus가 이 엔드포인트를 주기적으로 스크래핑합니다. 수집된 메트릭은 Prometheus에 저장되고, Grafana로 시각화합니다.

대시보드에서 실시간으로 지연 시간 그래프를 볼 수 있습니다. 로깅도 구조화해야 합니다.

단순히 print로 출력하는 대신, JSON 형식으로 로그를 남기세요. 타임스탬프, 요청 ID, 모델 버전 등을 포함합니다.

ELK 스택(Elasticsearch, Logstash, Kibana)이나 Loki로 중앙 집중식 로그 관리를 구축하면 검색과 분석이 쉬워집니다. ML 특유의 모니터링 포인트도 있습니다.

입력 데이터 분포가 학습 데이터와 다르지 않은지 확인해야 합니다. 이것을 데이터 드리프트라고 합니다.

예측 결과 분포도 모니터링합니다. 갑자기 특정 클래스만 예측한다면 문제가 있는 것입니다.

김개발 씨는 Prometheus와 Grafana를 설치하고 대시보드를 구성했습니다. 이제 실시간으로 예측 지연 시간을 볼 수 있습니다.

문제의 원인도 금방 찾았습니다. 특정 시간대에 입력 데이터 크기가 급증해서 지연이 발생한 것이었습니다.

실전 팁

💡 - 최소한 예측 횟수, 지연 시간, 에러율은 반드시 측정

  • 로그는 JSON 형식으로 구조화하여 검색 가능하게
  • ML 특유의 데이터 드리프트, 예측 분포 모니터링도 고려

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Kubernetes#MLOps#Docker#ModelServing#Deployment#Data Science

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.