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)코드는 경로를 환경 변수로 받는다
이미지 안의 코드는 색인과 모델이 어디 있는지 환경 변수로 받습니다. 없으면 기본값을 씁니다.
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.environ | environment(환경) 변수를 담은 사전 같은 것이다. 프로그램을 띄울 때 바깥에서 넣어 준 값이 들어 있다 |
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 로 들어오므로 코드를 고치지 않아도 그 폴더를 씁니다. 비유와 다른 점은, 내비게이션 목적지는 달리는 중에도 바꿀 수 있지만 컨테이너의 환경 변수는 띄울 때 정해지므로 바꾸려면 컨테이너를 다시 띄운다는 것입니다.
작게 줄여서 눈으로 보기
환경 변수가 있을 때와 없을 때를 한 번에 봅니다. 값은 이 파이썬 안에서만 잠깐 넣었다가 지웁니다.
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번째 절).