이번에는 MLflow-GCP 간 연동에 필요한 Dockerfile과 쉘 스크립트를 작성하고 세부 내용을 확인하는 시간을 갖도록 하겠습니다.
아무래도 모든 코드에 대한 설명을 포함하기 때문에, 분량이 조금 많은 편입니다. (절대로 칭찬해달라고 애원하는 글은 아닙니다 ㅎㅎ…)
틈 나실 때 조금씩 읽어보시는 것도 하나의 방법이 될 것 같아요
개요
연동을 위해 필요한 코드 트리 구조는 아래와 같습니다.
.
├── .dockerignore
├── .envs
│ ├── .gcp
│ └── .tracking-server
├── Makefile
├── docker
│ ├── Dockerfile
│ └── scripts
│ └── run-server.sh
├── docker-compose.yaml
├── poetry.lock
├── pyproject.toml
└── scripts
├── create-server.sh
└── startup-script.sh
•
.envs 폴더 내에는 MLflow tracking server / Artifact Registry / Artifact Store / Backend store 접근 + VM 인스턴스 생성을 위해 필요한 환경 변수를 저장한 파일이 있습니다.
•
docker 폴더 내에는 VM 인스턴스에서 실행할 Dockerfile과 MLflow tracking server에 대한 스크립트(run-server.sh)가 있습니다.
•
scripts 폴더 내에는 VM 인스턴스 생성을 수행하는 스크립트와(create-server.sh) VM 인스턴스 실행 직후 실행되는 스크립트(startup-script.sh)가 있습니다.
•
각 파일의 역할 및 세부 코드는 아래에서 설명하겠습니다.
환경변수 설정
1.
./envs/.gcp 파일
# GCP Environment variable
GCP_PROJECT_ID=e2eml-jiho
PROJECT_NAME=e2eml-jiho
DOCKER_IMAGE_NAME=${PROJECT_NAME}-mlflow
GCP_DOCKER_REGISTRY_NAME=mlflow-jiho
GCP_DOCKER_REGISTERY_URL=asia-northeast3-docker.pkg.dev/${GCP_PROJECT_ID}/${GCP_DOCKER_REGISTRY_NAME}/${DOCKER_IMAGE_NAME}
# IMAGE_NAME=<gcp-machine-image-name>
IMAGE_NAME=ubuntu-2204-jammy-v20240126
# IMAGE_PROJECT_ID=<gcp-machine-image-project-name>
IMAGE_PROJECT_ID=ubuntu-os-cloud
# Compute Instance (GCP VM Instance)
VM_NAME=${PROJECT_NAME}-mlflow
REGION=asia-northeast3
ZONE=asia-northeast3-c
LABELS=env=production,project=e2eml-jiho,task=jiho-mlflow-tracking-server
MACHINE_TYPE=n2-standard-4
NETWORK=default
SUBNET=default
Plain Text
복사
•
GCP 리소스 접근을 위한 환경변수, VM Instance에서 사용할 운영체제 및 관련 환경변수를 정의한 파일입니다.
◦
프로젝트 ID, 프로젝트 이름, 리전, Zone, Machine 타입 등 환경변수를 본인의 것에 알맞게 바꿔주세요.
2. .envs/.tracking-server 파일
# MLFlow
MLFLOW_HOST=0.0.0.0
MLFLOW_PORT=6100
# Postgres (Backend store)
# POSTGRES_CONNECTION_NAME=<SQL Instance-connection-name> (GCP -> SQL -> SQL Instance -> Connection Name)
POSTGRES_CONNECTION_NAME=e2eml-jiho:asia-northeast3:jiho-mlflow-backend-store1
# POSTGRES_USER=<Username for SQL Instance>
POSTGRES_USER=mlflow
# POSTGRES_PASSWORD_SECRET_NAME=<postgres-secret-name> (GCP -> Security -> Secret manager -> Secret name)
POSTGRES_PASSWORD_SECRET_NAME=postgresql-mlflow-password
# Private IP Address for SQL instance (GCP -> SQL -> SQL Instance -> Private IP Address)
POSTGRES_HOST=10.81.160.3
POSTGRES_PORT=5432
# POSTGRES_DATABASE_NAME=<Database name for SQL instance>
POSTGRES_DATABASE_NAME=mlflow
# ARTIFACT_STORE=gs://<bucket-name> (GCP -> Cloud Storage)
ARTIFACT_STORE=gs://e2eml-jiho-mlflow-prac
Plain Text
복사
•
본 파일은 아래의 환경 변수들을 저장하고 있습니다.
◦
MLflow 서버의 호스트 정보
◦
Backend store 및 Artifact store 접근을 위한 환경 변수
•
아래 환경변수는 반드시 자신의 것으로 바꿔주세요!
◦
POSTGRES_CONNECTION_NAME
◦
POSTGRES_USER
◦
POSTGRES_PASSWORD_SECRET_NAME
◦
POSTGRES_HOST
◦
POSTGRES_DATABASE_NAME
◦
ARTIFACT_STORE
Makefile
Makefile은 다음의 역할을 수행합니다.
•
도커 이미지를 빌드 (build)
•
도커 이미지를 Artifact Registry에 push (push)
•
GCP VM instance(Tracking server) 생성 (deploy)
코드 내용은 다음과 같습니다.
include .envs/.gcp
include .envs/.tracking-server
export
SHELL := /usr/bin/env bash
HOSTNAME := $(shell hostname)
ifeq (, $(shell which docker-compose))
DOCKER_COMPOSE_COMMAND = docker compose
else
DOCKER_COMPOSE_COMMAND = docker-compose
endif
# Returns true if the stem is a non-empty environment variable, or else raises an error.
guard-%:
@#$(or ${$*}, $(error $* is not set))
# Deploy MLFlow using GCP Cloud Run
deploy: push
./scripts/create-server.sh
# Build docker containers with docker-compose
build:
$(DOCKER_COMPOSE_COMMAND) build
# Push docker image to GCP Container Registery. Requires IMAGE_TAG to be specified.
push: guard-IMAGE_TAG build
@gcloud auth configure-docker asia-northeast3-docker.pkg.dev --quiet
@docker tag "${DOCKER_IMAGE_NAME}:latest" "$${GCP_DOCKER_REGISTRY_URL}:$${IMAGE_TAG}"
@docker push "$${GCP_DOCKER_REGISTRY_URL}:$${IMAGE_TAG}"
Makefile
복사
•
.envs 폴더에 저장되어 있는 환경변수 값을 읽어들입니다.
•
build target은 docker-compose.yaml 파일을 통해 도커 이미지를 build하는 역할을 수행합니다.
◦
./docker/Dockerfile
◦
gcloud cli, gsutils 설치
◦
poetry 설치
◦
poetry를 통한 의존성 패키지 설치
◦
MLflow 서버 구동(./docker/scripts/run-server.sh)
•
push target은 prerequisites로서, guard-IMAGE_TAG target과 build target을 지정합니다.
◦
guard-<keyword> target: make command 수행 시, <keyword>에 해당하는 인자를 넘겨주는지 확인합니다. 이때, IMAGE_TAG는 도커 이미지의 tag를 의미합니다.
◦
push recipe의 첫 번째 커맨드는 Artifact Registry에 도커 이미지를 push할 수 있도록 권한을 부여받습니다.
Docker-compose
앞서 Makefile에서 build target이 recipe로서 docker-compose.yaml 파일을 참조한다고 하였습니다.
코드를 한 번 살펴보지요!
version: "3.8"
services:
app: &app
user: root
hostname: "${HOSTNAME}"
image: "${DOCKER_IMAGE_NAME}"
container_name: mlflow-tracking-server
build:
context: .
dockerfile: ./docker/Dockerfile
env_file:
- .envs/.gcp
- .envs/.tracking-server
ipc: host
network_mode: host
init: true
volumes:
- ./:/app
Docker
복사
•
build 과정에서 ./docker/Dockerfile 을 참조하도록 설정하였습니다.
•
또한, env_file 키워드를 통해 .envs 폴더에 저장된 환경변수를 Dockerfile에서 참조할 수 있도록 하였습니다.
•
빌드되는 컨테이너 이름의 경우 mlflow-tracking-server로, 이미지 이름의 경우 ${DOCKER_IMAGE_NAME}으로 지정하였습니다. (여기서는 e2eml-jiho-mlflow)
Dockerfile
앞서 Docker-compose command를 통한 이미지 build 과정에서 Dockerfile을 참조한다고 말씀드렸지요
한 번 그 내용을 살펴봅시다.
FROM python:3.9-slim
ENV HOME=/root
ENV \
PYTHONUNBUFFERED=1 \
VIRTUAL_ENV="${HOME}/venv" \
PATH="$HOME/venv/bin:/usr/local/gcloud/google-cloud-sdk/bin/:${PATH}" \
PYTHONPATH="/app:${PYTHONPATH}" \
DEBIAN_FRONTEND="noninteractive" \
LC_ALL=C.UTF-8 \
LANG=C.UTF-8 \
BUILD_POETRY_LOCK="${HOME}/poetry.lock.build"
RUN apt-get -qq update \
&& apt-get -qq -y install git curl wget vim \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get -qq -y clean
# Install gcloud and gsutils
RUN curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-462.0.1-linux-x86_64.tar.gz \
&& mv /google-cloud-cli-462.0.1-linux-x86_64.tar.gz /tmp/google-cloud-cli-462.0.1-linux-x86_64.tar.gz
RUN mkdir -p /usr/local/gcloud \
&& tar -C /usr/local/gcloud -zxf /tmp/google-cloud-cli-462.0.1-linux-x86_64.tar.gz \
&& /usr/local/gcloud/google-cloud-sdk/install.sh \
--usage-reporting false --command-completion true --bash-completion true --path-update true --quiet
# Make sure gsutil will use the default service account
RUN echo "[GoogleCompute]\nservice_account = default" > /etc/boto.cfg
COPY ./docker/scripts/*.sh /
RUN chmod +x /*.sh \
&& HOME=/tmp \
&& pip install --no-cache-dir poetry==1.7.1 \
&& mkdir -p /app
COPY ./pyproject.toml ./*.lock /app/
WORKDIR /app
RUN python3.9 -m venv "${VIRTUAL_ENV}" \
&& pip install --upgrade pip setuptools \
&& poetry install --no-dev \
&& cp poetry.lock "${BUILD_POETRY_LOCK}" \
&& rm -rf $HOME/.cache/*
ENTRYPOINT ["/bin/bash"]
CMD ["/run-server.sh"]
EXPOSE 6100
Docker
복사
•
python:3.9-slim 이미지를 사용합니다.
•
컨테이너에서 참조할 환경변수 값을 정의합니다.
◦
PATH 환경변수에 gcloud CLI를 사용하기 위한 경로를 등록한다는 점에 주목해주세요!
•
gcloud CLI와 gstuil 도구를 다운로드하는 코드를 수행합니다.
◦
gcloud CLI: Google Cloud 리소스를 만들고 관리하기 위한 도구 모음입니다.
◦
gsutil: 명령줄에서 Cloud Storage에 액세스하는 데 사용할 수 있는 Python 애플리케이션입니다.
•
./docker/scripts/run-server.sh 스크립트를 컨테이너의 root 폴더에 복사합니다.
•
run-server.sh 스크립트를 컨테이너에서 실행할 수 있도록 권한을 변경합니다.
•
poetry 라이브러리를 설치한 후, pyproject.toml에 명시한 패키지를 설치합니다.
•
Entrypoint를 지정한 후, run-server.sh 스크립트를 구동합니다.
•
이때, 컨테이너는 6100번 포트에서 실행됩니다.
create-server.sh
Makefile에서 build와 push target을 수행한 다음, deploy target를 수행한다고 말씀드렸어요
이때, deploy target에서 ./scripts/create-server.sh 스크립트를 실행합니다.
중요한 옵션 위주로 설명을 드리도록 하겠습니다~
#!/usr/bin/env bash
set -euo pipefail
# Create VM
gcloud compute instances create "${VM_NAME}" \
--image "${IMAGE_NAME}" \
--image-project "${IMAGE_PROJECT_ID}" \
--boot-disk-auto-delete \
--labels="${LABELS}" \
--machine-type="${MACHINE_TYPE}" \
--zone="${ZONE}" \
--no-address \
--network="${NETWORK}" \
--subnet="${SUBNET}" \
--scopes https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/cloud.useraccounts.readonly,https://www.googleapis.com/auth/cloudruntimeconfig \
--project="${GCP_PROJECT_ID}" \
--metadata-from-file=startup-script=./scripts/startup-script.sh \
--metadata \
gcp_docker_registery_url="${GCP_DOCKER_REGISTERY_URL}:${IMAGE_TAG}",\
mlflow_host="${MLFLOW_HOST}",\
mlflow_port="${MLFLOW_PORT}",\
artifact_store="${ARTIFACT_STORE}",\
postgres_user="${POSTGRES_USER}",\
postgres_host="${POSTGRES_HOST}",\
postgres_port="${POSTGRES_PORT}",\
postgres_database_name="${POSTGRES_DATABASE_NAME}",\
postgres_password_secret_name="${POSTGRES_PASSWORD_SECRET_NAME}"
Shell
복사
•
VM 인스턴스를 생성하는 명령어를 수행합니다.
◦
Google Cloud Compute Engine → 여기서는 MLflow tracking server 역할
•
--no-address keyword
◦
해당 키워드를 사용할 경우, External IP(외부 접근 IP)를 허용하지 않습니다.
◦
보안 측면에서, 해당 인스턴스에 접근할 수 있는 클라이언트를 필터링하기 위함입니다.
◦
•
--metadata keyword
◦
VM 인스턴스에서 사용할 메타데이터 정보를 저장할 수 있습니다.
◦
Key=value의 형식으로 저장할 수 있습니다.
•
--metadata-from-file
◦
startup-script를 key로 startup script의 파일 경로를 value로 지정합니다.
◦
startup-script는 magic keyword로서, VM 인스턴스가 생성되고 실행될 때 자동으로 실행되는 스크립트입니다.
해당 스크립트를 실행하여 VM 인스턴스를 생성할 경우, 다음 그림과 같이 Key-value 형식의 메타데이터가 인스턴스에 등록된 것을 확인할 수 있습니다.
startup-script.sh
VM 인스턴스가 생성되고 실행될 때 startup-script 스크립트가 자동으로 수행된다고 말씀드렸습니다.
그럼, 해당 스크립트의 내용을 살펴보시죠!
#!/bin/bash
MLFLOW_HOST=$(curl --silent http://metadata.google.internal/computeMetadata/v1/instance/attributes/mlflow_host -H "Metadata-Flavor: Google")
MLFLOW_PORT=$(curl --silent http://metadata.google.internal/computeMetadata/v1/instance/attributes/mlflow_port -H "Metadata-Flavor: Google")
ARTIFACT_STORE=$(curl --silent http://metadata.google.internal/computeMetadata/v1/instance/attributes/artifact_store -H "Metadata-Flavor: Google")
POSTGRES_USER=$(curl --silent http://metadata.google.internal/computeMetadata/v1/instance/attributes/postgres_user -H "Metadata-Flavor: Google")
POSTGRES_HOST=$(curl --silent http://metadata.google.internal/computeMetadata/v1/instance/attributes/postgres_host -H "Metadata-Flavor: Google")
POSTGRES_PORT=$(curl --silent http://metadata.google.internal/computeMetadata/v1/instance/attributes/postgres_port -H "Metadata-Flavor: Google")
POSTGRES_DATABASE_NAME=$(curl --silent http://metadata.google.internal/computeMetadata/v1/instance/attributes/postgres_database_name -H "Metadata-Flavor: Google")
POSTGRES_PASSWORD_SECRET_NAME=$(curl --silent http://metadata.google.internal/computeMetadata/v1/instance/attributes/postgres_password_secret_name -H "Metadata-Flavor: Google")
GCP_DOCKER_REGISTERY_URL=$(curl --silent http://metadata.google.internal/computeMetadata/v1/instance/attributes/gcp_docker_registery_url -H "Metadata-Flavor: Google")
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
echo '=========== Downloading Docker Image ============'
gcloud auth configure-docker --quiet asia-northeast3-docker.pkg.dev
echo "GCP_DOCKER_REGISTERY_URL = ${GCP_DOCKER_REGISTERY_URL}"
time sudo docker pull "${GCP_DOCKER_REGISTERY_URL}"
sudo docker run --init --network host --ipc host --user root --hostname "$(hostname)" --privileged \
--log-driver=gcplogs \
-e POSTGRES_USER="${POSTGRES_USER}" \
-e POSTGRES_PASSWORD=$(gcloud secrets versions access latest --secret="${POSTGRES_PASSWORD_SECRET_NAME}") \
-e POSTGRES_HOST="${POSTGRES_HOST}" \
-e POSTGRES_PORT="${POSTGRES_PORT}" \
-e POSTGRES_DATABASE_NAME="${POSTGRES_DATABASE_NAME}" \
-e ARTIFACT_STORE="${ARTIFACT_STORE}" \
-e MLFLOW_HOST="${MLFLOW_HOST}" \
-e MLFLOW_PORT="${MLFLOW_PORT}" \
${GCP_DOCKER_REGISTERY_URL}
Shell
복사
•
VM 인스턴스에서는 저장된 환경변수 값을 읽기 위하여, 다음의 command를 수행할 수 있습니다.
•
curl http://metadata.google.internal/computeMetadata/v1/instance/attributes/<환경변수 이름>
◦
이때, 헤더 값을 반드시 보내야 하며, 그 값은 아래와 같습니다.
▪
"Metadata-Flavor: Google"
•
Makefile의 push target을 통해 Artifact Registry에 저장된 도커 이미지를 가져옵니다.
◦
도커 이미지를 pull하기 위하여, gcloud auth configure-docker command를 수행합니다.
◦
•
Artifact Registry에서 가져온 도커 이미지를 컨테이너에서 구동합니다.
◦
sudo docker run command
◦
이때, Backend store / MLflow host & port / Artifact store에 대한 환경변수 값을 등록합니다.
run-server.sh
startup-script 스크립트의 궁극적인 목적은 VM 인스턴스 내부에서 MLflow 서버를 구동하는 것입니다.
#!/bin/bash
set -euo pipefail
BACKEND_STORE_URI="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DATABASE_NAME}"
mlflow server \
--backend-store-uri "'${BACKEND_STORE_URI}'" \
--default-artifact-root "${ARTIFACT_STORE}" \
--host "${MLFLOW_HOST}" \
--port "${MLFLOW_PORT}"
Shell
복사
•
MLflow 서버 구동 시, Backend store와 Artifact store의 주소를 명시하는 것을 확인하실 수 있습니다.
정리
이번 포스팅에서는 MLflow-GCP 간 연동에 필요한 Dockerfile과 기타 스크립트에 대한 내용을 다루어보았습니다.
다음 포스팅에서는 실제로 MLflow-GCP 간 연동을 수행하는 시간을 가지도록 하겠습니다.
그럼 안녕히 가세요
Reference