추론을 위한 작업자 풀로 Ollama를 호스팅하는 방법

1. 소개

개요

이 Codelab에서는 이벤트 기반 비동기 AI 처리 파이프라인을 빌드하는 방법을 알아봅니다. Cloud Run 작업자 풀에서 Ollama를 사용하여 오픈소스 모델을 배포합니다. 작업자 풀은 Pub/Sub 주제에서 메시지를 가져와 gemma3:4b 모델을 사용하여 처리합니다.

학습할 내용

  • Pub/Sub 풀 구독과 함께 작업자 풀을 사용하는 방법
  • Ollama를 사용하여 작업자 풀로 추론하는 방법

2. 시작하기 전에

API 사용 설정

이 Codelab을 시작하기 전에 다음을 실행하여 다음 API를 사용 설정하세요.

gcloud services enable run.googleapis.com \
    cloudbuild.googleapis.com \
    artifactregistry.googleapis.com \
    pubsub.googleapis.com \
    storage.googleapis.com

3. 설정 및 요구사항

필수 리소스를 설정하려면 다음 단계를 따르세요.

  1. 이 Codelab의 환경 변수를 설정합니다.
export PROJECT_ID=<YOUR_PROJECT_ID>
export REGION=<YOUR_REGION>

export BUCKET_NAME=$PROJECT_ID-gemma3-4b
export SERVICE_ACCOUNT_NAME=ollama-worker-sa
export SERVICE_ACCOUNT_EMAIL=${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com
export TOPIC_NAME=ollama-prompts
export SUBSCRIPTION_NAME=ollama-prompts-sub
export AR_REPO_NAME=ollama-worker-repo
export PULL_MSG_IMAGE_NAME=pubsub-pull-msg
export OLLAMA_IMAGE_NAME=ollama-coordinator
  1. 작업자 풀의 서비스 계정 만들기
gcloud iam service-accounts create ${SERVICE_ACCOUNT_NAME} \
  --display-name="Ollama Worker Service Account"
  1. SA에 Pub/Sub 액세스 권한 부여
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
  --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
  --role="roles/pubsub.subscriber"
  1. 작업자 풀 이미지의 AR 저장소 만들기
gcloud artifacts repositories create ${AR_REPO_NAME} \
  --repository-format=docker \
  --location=${REGION}
  1. PubSub 주제 및 구독 만들기
gcloud pubsub topics create $TOPIC_NAME
gcloud pubsub subscriptions create $SUBSCRIPTION_NAME --topic $TOPIC_NAME

4. GCS에서 모델 다운로드 및 호스팅

빌드 프로세스 중에 컨테이너 내에서 모델을 직접 가져오는 대신(느리고 비효율적일 수 있음) Ollama CLI를 사용하여 로컬 머신으로 모델을 가져온 다음 모델 파일을 GCS 버킷에 업로드합니다. 그러면 작업자 풀이 이 버킷을 마운트하여 모델에 액세스합니다.

  1. 로컬 머신에 Ollama 설치:

다음 명령어를 실행하여 Linux에 Ollama를 설치합니다. 다른 운영체제의 경우 Ollama 웹사이트를 참고하세요.

curl -fsSL https://ollama.com/install.sh | sh
  1. Ollama 서비스를 시작하고 모델을 가져옵니다.

먼저 백그라운드에서 Ollama 서비스를 시작합니다.

ollama serve &
ollama pull gemma3:4b
  1. GCS 버킷 만들기:

이전에 설정한 BUCKET_NAME 환경 변수를 사용하여 GCS 버킷을 만듭니다.

gsutil mb gs://${BUCKET_NAME}
  1. 모델 파일을 GCS 버킷에 업로드합니다.

Ollama는 ~/.ollama/models 디렉터리에 모델 파일을 저장합니다. 이 디렉터리의 콘텐츠를 GCS 버킷에 업로드합니다. 이렇게 하면 다운로드한 모든 모델이 복사됩니다.

gsutil -m cp -r ~/.ollama/models/* gs://${BUCKET_NAME}/
  1. SA에 Cloud Storage 버킷에 대한 액세스 권한 부여
gcloud storage buckets add-iam-policy-binding gs://${BUCKET_NAME} \
     --member=serviceAccount:${SERVICE_ACCOUNT_EMAIL} \
     --role=roles/storage.objectViewer

5. Cloud Run 작업 만들기

Cloud Run 작업은 다음 두 컨테이너를 사용합니다.

  • ollama-coordinator - ollama를 호스팅하고 gemma 3 4B 모델을 제공하는 데 사용
  • pubsub-pull-msg - pubsub 구독에서 가져와 ollama-coordinator 컨테이너에 메시지를 전달

먼저 ollama-coordinator 컨테이너를 만듭니다.

  1. Codelab의 상위 디렉터리를 만듭니다.
mkdir codelab-ollama-wp
cd codelab-ollama-wp
  1. ollama-coordinator 컨테이너의 디렉터리 만들기
mkdir ollama-coordinator
cd ollama-coordinator
  1. 다음 콘텐츠로 Dockerfile을 만듭니다.
# Use the official Ollama image as a base image
FROM ollama/ollama

# Expose the port that Ollama listens on
EXPOSE 11434

# Set the entrypoint to start the Ollama server
ENTRYPOINT ["ollama", "serve"]
  1. ollama 컨테이너 빌드
gcloud builds submit --tag ${REGION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO_NAME}/${OLLAMA_IMAGE_NAME} --timeout=20m

다음으로 pubsub-pull-msg 컨테이너를 만듭니다.

  1. pubsub-pull-msg 컨테이너의 디렉터리 만들기
cd ..
mkdir pubsub-pull-msg
cd pubsub-pull-msg
  1. Dockerfile를 만드는 방법
# Use the official Python image as a base image
FROM python:3.9-slim

# Set the working directory in the container
WORKDIR /app

# Copy the requirements file into the container
COPY requirements.txt .

# Install the required Python packages
RUN pip install --no-cache-dir -r requirements.txt

# Copy the Python script into the container
COPY main.py .

# Set the entrypoint to run the Python script
CMD ["python", "main.py"]
  1. 다음 콘텐츠로 requirements.txt 파일을 만듭니다.
google-cloud-pubsub
requests
  1. 다음 콘텐츠로 main.py 파일을 만듭니다.
import os
import sys
import requests
import json
from google.cloud import pubsub_v1

# --- Main Application Logic ---
print("--- Sidecar container script started ---")

# --- Environment and Configuration ---
project_id = os.environ.get("PROJECT_ID")
subscription_name = os.environ.get("SUBSCRIPTION_NAME")
ollama_api_url = "http://localhost:11434/api/generate"

if not project_id or not subscription_name:
    print("FATAL: PROJECT_ID and SUBSCRIPTION_NAME must be set.")
    sys.exit(1)

print(f"PROJECT_ID: {project_id}")
print(f"SUBSCRIPTION_NAME: {subscription_name}")

def callback(message):
    """Processes a single Pub/Sub message."""
    print(f"Received message ID: {message.message_id}")
    try:
        prompt = message.data.decode("utf-8")
        print(f"Decoded prompt: '{prompt}'")
        
        data = {"model": "gemma3:4b", "prompt": prompt, "stream": False}
        
        print("Sending request to Ollama...")
        response = requests.post(ollama_api_url, json=data, timeout=300)
        response.raise_for_status()
        
        print("Successfully received response from Ollama.")
        ollama_response = response.json()
        print(f"Ollama response: {json.dumps(ollama_response)[:200]}...")

        message.ack()
        print(f"Message {message.message_id} acknowledged.")

    except requests.exceptions.RequestException as e:
        print(f"Error calling Ollama API: {e}")
        message.nack()
        print(f"Message {message.message_id} not acknowledged.")
    except Exception as e:
        print(f"An unexpected error occurred in callback: {e}")
        message.nack()
        print(f"Message {message.message_id} not acknowledged.")

def main():
    """Starts the Pub/Sub subscriber."""
    subscriber = pubsub_v1.SubscriberClient()
    subscription_path = subscriber.subscription_path(project_id, subscription_name)
    
    streaming_pull_future = subscriber.subscribe(subscription_path, callback=callback)
    print(f"Subscribed to {subscription_path}. Listening for messages...")

    try:
        # .result() will block indefinitely.
        streaming_pull_future.result()
    except Exception as e:
        print(f"A fatal error occurred in the subscriber: {e}")
        streaming_pull_future.cancel()
        streaming_pull_future.result()

if __name__ == "__main__":
    main()
  1. 이제 pubsub-pull-msg 컨테이너를 빌드합니다.
gcloud builds submit --tag ${REGION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO_NAME}/${PULL_MSG_IMAGE_NAME}

6. 작업 배포 및 실행

이 단계에서는 yaml 파일을 배포하여 Cloud Run 작업을 만듭니다.

루트 폴더로 이동하여 yaml 파일을 만듭니다.

cd ..
  1. 다음 콘텐츠로 worker-pool.template.yaml 파일을 만듭니다.
apiVersion: run.googleapis.com/v1
kind: WorkerPool
metadata:
  name: codelab-ollama-wp
  labels:
    cloud.googleapis.com/location: europe-west1
  annotations:
    run.googleapis.com/launch-stage: BETA
    run.googleapis.com/scalingMode: manual
    run.googleapis.com/manualInstanceCount: '1'
    run.googleapis.com/gcs-fuse-mounter-enabled: "true"
spec:
  template:
    metadata:
      annotations:
        run.googleapis.com/gpu: "1"
        run.googleapis.com/gpu-zonal-redundancy-disabled: 'true'        
    spec:
      serviceAccountName: ${SERVICE_ACCOUNT_EMAIL}
      nodeSelector:
        run.googleapis.com/accelerator: nvidia-l4
      volumes:
      - name: gcs-bucket
        csi:
          driver: gcsfuse.run.googleapis.com
          readOnly: true
          volumeAttributes: 
            bucketName: ${BUCKET_NAME}
      containers:
      - image: ${REGION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO_NAME}/${PULL_MSG_IMAGE_NAME}
        name: pubsub-pull-msg
        env:
        - name: PROJECT_ID
          value: ${PROJECT_ID}
        - name: SUBSCRIPTION_NAME
          value: "ollama-prompts-sub"
        - name: PYTHONUNBUFFERED
          value: "1"
        resources:
          limits:
            cpu: '1'
            memory: 1Gi
      - image: ${REGION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO_NAME}/${OLLAMA_IMAGE_NAME}
        name: ollama-coordinator
        env:
        - name: OLLAMA_MODELS
          value: /mnt/models
        volumeMounts:
        - name: gcs-bucket
          mountPath: /mnt/models
        resources:
          limits:
            cpu: '6'
            nvidia.com/gpu: '1'
            memory: 16Gi

그런 다음 전체 이미지 URL을 정의하고 sed를 사용하여 템플릿 파일의 변수를 대체하여 최종 worker-pool.yaml를 만듭니다.

sed -e "s|\${SERVICE_ACCOUNT_EMAIL}|${SERVICE_ACCOUNT_EMAIL}|g" \
     -e "s|\${BUCKET_NAME}|${BUCKET_NAME}|g" \
     -e "s|\${PULL_MSG_IMAGE_NAME}|${PULL_MSG_IMAGE_NAME}|g" \
     -e "s|\${OLLAMA_IMAGE_NAME}|${OLLAMA_IMAGE_NAME}|g" \
     -e "s|\${PROJECT_ID}|${PROJECT_ID}|g" \
     -e "s|\${REGION}|${REGION}|g" \
     -e "s|\${AR_REPO_NAME}|${AR_REPO_NAME}|g" \
     worker-pool.template.yaml > worker-pool.yaml

이제 배포할 수 있습니다.

gcloud beta run worker-pools replace worker-pool.yaml

테스트

gcloud pubsub topics publish ${TOPIC_NAME} --message="What is 1 + 1?"

그런 다음 로그를 확인합니다. 잠시 기다리거나 Cloud Console 작업자 풀 페이지로 이동하여 로그를 실시간으로 확인할 수 있습니다.

gcloud alpha run worker-pools logs read "codelab-ollama-wp" --limit 10

다음과 같은 메시지가 표시됩니다.

Ollama response: {"model": "gemma3:4b", "created_at": "2025-11-06T23:48:39.572079369Z", "response": "1 + 1 = 2\n", ...

7. 축하합니다.

축하합니다. Codelab을 완료했습니다.

Cloud Run 문서를 검토하는 것이 좋습니다.

학습한 내용

  • Pub/Sub 풀 구독과 함께 Cloud Run 작업자 풀을 사용하는 방법
  • Ollama를 사용하여 Cloud Run 작업자 풀로 추론하는 방법

8. 삭제

이 튜토리얼에서 사용된 리소스 비용이 Google Cloud 계정에 청구되지 않도록 하려면 리소스가 포함된 프로젝트를 삭제하거나 프로젝트를 유지하고 개별 리소스를 삭제하세요.

프로젝트 삭제

비용이 청구되지 않도록 하는 가장 쉬운 방법은 튜토리얼에서 만든 프로젝트를 삭제하는 것입니다.

프로젝트를 삭제하는 방법은 다음과 같습니다.

  1. Google Cloud 콘솔에서 리소스 관리 페이지로 이동합니다.
  2. 프로젝트 목록에서 삭제할 프로젝트를 선택하고 삭제를 클릭합니다.
  3. 대화상자에서 프로젝트 ID를 입력하고 종료를 클릭하여 프로젝트를 삭제합니다.

개별 리소스 삭제

개별 리소스를 삭제하려면 다음 명령어를 실행합니다.

  1. Cloud Run 작업자 풀을 삭제합니다.
gcloud beta run worker-pools delete codelab-ollama-wp --region ${REGION}
  1. GCS 버킷을 삭제합니다.
gsutil -m rm -r gs://${BUCKET_NAME}
  1. Pub/Sub 구독 및 주제를 삭제합니다.
gcloud pubsub subscriptions delete ${SUBSCRIPTION_NAME}
gcloud pubsub topics delete ${TOPIC_NAME}
  1. Artifact Registry 저장소를 삭제합니다.
gcloud artifacts repositories delete ${AR_REPO_NAME} --location=${REGION} --quiet
  1. 서비스 계정을 삭제합니다.
gcloud iam service-accounts delete ${SERVICE_ACCOUNT_EMAIL} --quiet

로컬 파일 정리

로컬 파일을 정리하려면 다음 단계를 따르세요.

  1. 로컬 Ollama 서비스 중지:ollama serve &로 Ollama를 시작한 경우 프로세스 ID (PID)를 찾은 다음 kill 명령어를 사용하여 중지할 수 있습니다.
    # Find the process ID of the Ollama server
    pgrep ollama
    
    # Replace <PID> with the actual process ID obtained from the previous command
    kill <PID>
    
  2. 다운로드한 모델을 삭제합니다.
rm -rf ~/.ollama/models
  1. Ollama를 제거합니다.

Ollama 웹사이트의 안내에 따라 로컬 머신에서 Ollama를 제거합니다.