LLM 엔지니어링19장 · MLOps 와 LLMOps, 워크플로 자동화9 / 23← 이전목차다음 →MetaContents

19장. MLOps 와 LLMOps, 워크플로 자동화

Docker 심화: 모델과 데이터는 밖에 둔다

체인점의 매장 인테리어는 어느 지점이나 같지만, 식재료는 매일 새로 들어옵니다. 식재료를 벽에 붙여 인테리어 공사를 다시 할 수는 없습니다. Docker 에서도 이미지(인테리어)에는 코드와 패키지만 넣고, 자주 바뀌는 모델과 색인(식재료)은 볼륨으로 밖에서 넣습니다.

이미지에 넣을 것, 볼륨으로 넣을 것, 환경 변수로 넣을 것의 세 가지를 견준 비교 그림

그림의 왼쪽 카드는 이미지에 굽는 것, 가운데는 볼륨으로 붙이는 것, 오른쪽은 실행할 때 환경 변수로 넣는 것입니다. 모델이 새 버전이 되면 이미지를 다시 빌드하지 않고 볼륨의 파일만 바꾸고 컨테이너를 다시 띄웁니다.

무엇을 어디에 두나

것두는 곳까닭
코드, 패키지이미지판이 같으면 어디서나 같게 돈다
모델 파일, 검색 색인볼륨크고 자주 바뀐다. 이미지가 가벼워진다
모델 버전, 주소, 비밀 키환경 변수같은 이미지로 개발·운영을 나눈다. 비밀이 이미지에 남지 않는다
상태 확인healthcheck모델을 다 올렸는지 확인한 뒤 요청을 보낸다

compose 로 묶은 꼴

아래는 이 PC 에서 실행하지 않은 안내 코드입니다. 17장의 compose 에 볼륨, 환경 변수, 상태 확인, GPU 를 더한 꼴입니다.

출력
services:
  rag-api:
    image: library-rag:3
    ports: ["8000:8000"]
    environment:
      INDEX_DIR: /srv/index          # 색인을 찾을 곳
      MODEL_ALIAS: champion          # 레지스트리의 어느 별칭을 쓸지
    volumes:
      - ./out_index:/srv/index:ro    # 밤마다 바뀌는 색인. 읽기만 한다
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')"]
      interval: 10s
      retries: 5
    deploy:
      resources:
        reservations:
          devices: [{driver: nvidia, count: 1, capabilities: [gpu]}]

예상 출력(실행하지 않고 유추한 예시)입니다.

출력
docker compose up -d
 ✔ Container library-rag-api-1  Started
docker compose ps
NAME                 STATUS
library-rag-api-1    Up 약 30초 (healthy)

코드는 경로를 환경 변수로 받는다

이미지 안의 코드는 색인과 모델이 어디 있는지 환경 변수로 받습니다. 없으면 기본값을 씁니다.

Python
import os

# 환경 변수가 없으면 개발 PC 의 기본값을 쓴다
INDEX_DIR = os.environ.get("INDEX_DIR", "out_index")
MODEL_ALIAS = os.environ.get("MODEL_ALIAS", "champion")
print("색인:", INDEX_DIR, "· 모델 별칭:", MODEL_ALIAS)

코드에 나오는 함수

코드쉬운 뜻
os.environenvironment(환경) 변수를 담은 사전 같은 것이다. 프로그램을 띄울 때 바깥에서 넣어 준 값이 들어 있다
os.environ.get("INDEX_DIR", "out_index")INDEX_DIR 이 있으면 그 값을, 없으면 기본값 out_index 를 쓴다
INDEX_DIR, MODEL_ALIAS색인 폴더(index directory)와 모델 별칭(model alias)을 받는 환경 변수 이름이다. 위 compose 의 environment: 에 적은 이름과 같다

이 코드가 뜻하는 것

같은 차를 빌려 타는 렌터카를 떠올려 봅니다. 차는 누가 빌려도 같고, 타는 사람이 내비게이션에 목적지를 넣습니다. 아무것도 넣지 않으면 내비게이션은 "집"으로 안내합니다. 여기서 차가 이미지, 넣는 목적지가 환경 변수, "집"이 기본값입니다.

줄하는 일쉽게 말하면
INDEX_DIR = os.environ.get(…)INDEX_DIR 을 찾고, 없으면 out_index 를 쓴다목적지가 없으면 집으로
MODEL_ALIAS = os.environ.get(…)MODEL_ALIAS 를 찾고, 없으면 champion 을 쓴다같은 방식
print(…)정해진 두 값을 보여 준다내비게이션 화면을 본다

이 PC 에서 그냥 실행하면 두 환경 변수가 없어 색인: out_index · 모델 별칭: champion 이 나옵니다. 위 compose 로 띄운 컨테이너 안에서는 INDEX_DIR 이 /srv/index 로 들어오므로 코드를 고치지 않아도 그 폴더를 씁니다. 비유와 다른 점은, 내비게이션 목적지는 달리는 중에도 바꿀 수 있지만 컨테이너의 환경 변수는 띄울 때 정해지므로 바꾸려면 컨테이너를 다시 띄운다는 것입니다.

작게 줄여서 눈으로 보기

환경 변수가 있을 때와 없을 때를 한 번에 봅니다. 값은 이 파이썬 안에서만 잠깐 넣었다가 지웁니다.

Python
import os

os.environ["MODEL_ALIAS"] = "challenger"                  # 이 파이썬 안에서만 잠깐 넣어 본다
print(os.environ.get("MODEL_ALIAS", "champion"))          # 있으면 넣은 값
del os.environ["MODEL_ALIAS"]
print(os.environ.get("MODEL_ALIAS", "champion"))          # 없으면 기본값
출력
challenger
champion
결과읽는 법
challenger환경 변수가 있으면 기본값 대신 그 값을 쓴다. compose 의 MODEL_ALIAS: champion 을 challenger 로 바꿔 띄운 것과 같은 효과다
champion지운 뒤에는 다시 기본값이 쓰인다

코드 안에서 os.environ 을 고치면 지금 돌고 있는 파이썬과 거기서 띄운 프로그램에만 적용됩니다. 창을 닫으면 사라집니다.

주의할 점

  • 볼륨을 :ro(읽기만)로 붙이면 서비스가 실수로 색인을 덮어쓰지 못합니다. 색인은 갱신 작업만 씁니다.
  • 컨테이너가 떠 있는 동안 볼륨의 색인 파일을 고쳐 쓰면, 읽는 도중의 서비스가 반쯤 쓴 파일을 볼 수 있습니다. 새 판을 다른 폴더에 다 쓴 뒤 가리킴만 바꿉니다(10번째 절).
← 이전 절19장 · Docker 심화: 이미지 층과 캐시다음 절 →19장 · 워크플로와 DAG