Cloud Run 인스턴스에 Hermes 에이전트를 배포하는 방법

1. 소개

개요

이 실습에서는 Hermes Agent (Nous Research 제공)의 완전한 영구 보안 인스턴스를 Cloud Run 인스턴스에 배포합니다. Hermes 웹 대시보드를 사용하여 AI 에이전트와 상호작용하고 Google Cloud Storage로 지속적인 작업공간을 지원합니다.

Hermes는 자동 확장 Cloud Run 서비스로 실행될 수 있는 게이트웨이 모드를 지원하는 동시에 부팅 시 기술을 검색하고 백그라운드 실행을 처리하는 상태 저장 에이전트 역할도 합니다. Cloud Run 인스턴스는 이 워크로드에 적합한 수명이 길고 개별적으로 주소를 지정할 수 있는 환경을 제공합니다.

실습할 내용

  • 컨테이너 상태와 구성을 유지할 Cloud Storage 버킷을 준비합니다.
  • 부팅 초기화를 처리하는 맞춤 Python 감독자 (run_hermes.py)와 시작 스크립트 (start_hermes.sh)를 만듭니다.
  • gcloud beta run instances deploy를 사용하여 Hermes 에이전트를 배포합니다.
  • Hermes 대시보드에 액세스하고 인증합니다.

학습할 내용

  • Hermes 에이전트를 Cloud Run 인스턴스에 배포하는 방법
  • GCSFuse를 사용하여 Cloud Storage 버킷을 Cloud Run 인스턴스에 마운트하는 방법
  • GCSFuse 파일 잠금 제한을 우회하도록 SQLite 및 임시 캐시를 안전하게 구성하는 방법

2. 설정 및 요구사항

GCP 프로젝트 설정

  1. Google Cloud 콘솔에 로그인합니다.
  2. Google Cloud 프로젝트를 만들거나 선택합니다.
  3. Google Cloud 프로젝트에 결제가 사용 설정되어 있는지 확인합니다.

Cloud Shell 열기

Cloud 콘솔의 상단 툴바에서 Google Cloud Shell을 활성화합니다.

프로젝트 설정 및 gcloud 베타 설치

먼저 프로젝트와 리전을 환경 변수로 설정합니다.

export PROJECT_ID=<YOUR_PROJECT_ID>
export REGION="us-west2"
export BUCKET_NAME="hermes-state-${PROJECT_ID}"

gcloud용 프로젝트를 구성합니다.

gcloud config set project $PROJECT_ID

gcloud beta run instances에 beta 구성요소가 설치되어 있는지 확인합니다.

gcloud components install beta --quiet

gcloud 버전이 최신 상태입니다.

gcloud components updates

필수 Google Cloud API 사용 설정

Cloud Shell에서 Cloud Run, Cloud Storage, Secret Manager API를 사용 설정합니다.

gcloud services enable \
  run.googleapis.com \
  secretmanager.googleapis.com \
  storage.googleapis.com \
  compute.googleapis.com \
  aiplatform.googleapis.com

3. 전용 서비스 계정 만들기

최소 권한의 원칙을 준수하려면 Hermes 에이전트용 전용 IAM 서비스 계정을 만들고 Vertex AI 모델을 호출하는 데 필요한 권한을 부여하세요.

export SERVICE_ACCOUNT_NAME="hermes-sa"
gcloud iam service-accounts create ${SERVICE_ACCOUNT_NAME} \
  --display-name="Hermes Service Account"

export SERVICE_ACCOUNT="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"

gcloud projects add-iam-policy-binding ${PROJECT_ID} \
  --member="serviceAccount:${SERVICE_ACCOUNT}" \
  --role="roles/aiplatform.user"

4. Secret Manager에 사용자 인증 정보 저장

Cloud Run이 부팅 시 컨테이너에 안전하게 삽입할 수 있도록 대시보드 비밀번호와 같은 민감한 사용자 인증 정보는 Google Cloud Secret Manager에 저장됩니다.

대시보드의 안전한 임의 비밀번호를 생성하고 Secret Manager에 저장합니다.

export DASHBOARD_PASSWORD=$(openssl rand -hex 16)
echo "Generated Hermes Dashboard Password: ${DASHBOARD_PASSWORD}"

echo -n "${DASHBOARD_PASSWORD}" | gcloud secrets create hermes-dashboard-password \
  --data-file=- \
  --replication-policy="automatic"

gcloud secrets add-iam-policy-binding hermes-dashboard-password \
  --member="serviceAccount:${SERVICE_ACCOUNT}" \
  --role="roles/secretmanager.secretAccessor"

5. Cloud Storage 버킷 및 구성 파일 준비

Hermes에는 /opt/data로 마운트된 영구 스토리지가 필요합니다. Google Cloud Storage (GCS) 버킷을 사용하고 Cloud Storage 볼륨 마운트를 사용하여 마운트합니다.

1. Cloud Storage 버킷 만들기

gcloud storage buckets create gs://${BUCKET_NAME} --location=${REGION}

# Grant the service account permissions to mount the bucket
gcloud storage buckets add-iam-policy-binding gs://${BUCKET_NAME} \
  --member="serviceAccount:${SERVICE_ACCOUNT}" \
  --role="roles/storage.objectAdmin"

2. config.yaml 만들기

config.yaml 파일을 만듭니다. 구성이 올바르게 로드되도록 _config_version: 12를 포함해야 합니다.

_config_version: 12

model:
  default: "google/gemini-3.8-flash"
  provider: "vertex"

dashboard:
  enabled: true

database:
  journal_mode: delete

3. 감독자 스크립트 만들기 (run_hermes.py)

Cloud Storage는 SQLite 데이터베이스가 안전하게 실행하는 데 필요한 특정 파일 잠금 메커니즘을 지원하지 않습니다. 데이터베이스 손상을 방지하려면 맞춤 '감독자' 스크립트 (run_hermes.py)가 필요합니다. 이 스크립트는 에이전트를 시작하기 전에 Cloud Storage 대신 컨테이너의 로컬 메모리에 임시 데이터베이스 잠금을 저장하도록 Hermes를 구성합니다.

run_hermes.py를 로컬로 만듭니다.

import os
import shutil
import subprocess
import sys
import threading
import time

print(
    "=== INITIALIZING HERMES SUPERVISOR ===", flush=True
)

# 1. Local Directory Setup
# Creates temporary, local folders (in /tmp) for the agent's caches and working directories.
# See more below in comment section NOTE ON CLOUD STORAGE FUSE
home_dir = "/tmp/hermes_home"
hermes_dir = os.path.join(home_dir, ".hermes")
os.makedirs(hermes_dir, exist_ok=True)
os.makedirs("/tmp/logs", exist_ok=True)
os.makedirs("/tmp/skills", exist_ok=True)
os.makedirs("/tmp/uv_cache", exist_ok=True)
os.makedirs("/tmp/cache", exist_ok=True)
os.makedirs("/opt/data/workspace", exist_ok=True)
os.makedirs("/opt/data/.hermes", exist_ok=True)

# 2. State Restoration & Database Config
# Copies your existing configurations and chat history (state.db) from Cloud Storage into the local folders. 
# It also forces the SQLite database into TRUNCATE mode, a crucial step to prevent database corruption 
# when eventually saving back to Cloud Storage. See more in section 3. Note on Cloud Storage Fuse below
if os.path.exists("/opt/data/config.yaml"):
  shutil.copy("/opt/data/config.yaml", os.path.join(hermes_dir, "config.yaml"))
  print(f"Synced config.yaml -> {hermes_dir}/config.yaml", flush=True)
elif os.path.exists("/opt/data/.hermes/config.yaml"):
  shutil.copy("/opt/data/.hermes/config.yaml", os.path.join(hermes_dir, "config.yaml"))
  print(f"Synced config.yaml from .hermes -> {hermes_dir}/config.yaml", flush=True)

if os.path.exists("/opt/data/.env"):
  shutil.copy("/opt/data/.env", os.path.join(hermes_dir, ".env"))
  print(f"Synced .env -> {hermes_dir}/.env", flush=True)
elif os.path.exists("/opt/data/.hermes/.env"):
  shutil.copy("/opt/data/.hermes/.env", os.path.join(hermes_dir, ".env"))
  print(f"Synced .env from .hermes -> {hermes_dir}/.env", flush=True)

if os.path.exists("/opt/data/.hermes/state.db"):
  shutil.copy("/opt/data/.hermes/state.db", os.path.join(hermes_dir, "state.db"))
  print(f"Synced state.db -> {hermes_dir}/state.db (restored previous chats!)", flush=True)

# 3. Note on Cloud Storage Fuse
# Cloud Storage FUSE is optimized for object storage, but is not fully POSIX compliant.
# This means GCS lacks the byte-range file locking required by active caches and default SQLite (WAL mode) 
# which SQLite depends on to prevent data collisions.
# Without these locks, SQLite experiences database corruption and blocked I/O operations.
# To ensure stability, we route these active I/O processes to local container memory (/tmp).
# See section 5 Enable Autosave below on how /tmp is uploaded to Cloud Storage.
# Read more: https://cloud.google.com/storage/docs/cloud-storage-fuse/overview#differences-and-limitations
db_path = os.path.join(hermes_dir, "state.db")
try:
  import sqlite3
  conn = sqlite3.connect(db_path)
  conn.execute("PRAGMA journal_mode=TRUNCATE;")
  conn.close()
  print("Configured SQLite database to TRUNCATE mode for direct single-file persistence", flush=True)
except Exception as e:
  print(f"Warning: Failed to configure TRUNCATE mode: {e}", flush=True)

subprocess.run(["chmod", "-R", "777", "/tmp"], check=False)

# 4. Update system environment variables
# Hermes needs to know to look at the new local /tmp folders rather than defaulting to the mounted bucket.
env = dict(os.environ)
env["HOME"] = home_dir
env["HERMES_HOME"] = hermes_dir
env["PATH"] = "/opt/hermes/.venv/bin:/opt/hermes/bin:" + env.get("PATH", "")
env["PYTHONUNBUFFERED"] = "1"
env["HERMES_STATE_PATH"] = hermes_dir
env["HERMES_SKILLS_PATH"] = "/tmp/skills"
env["UV_CACHE_DIR"] = "/tmp/uv_cache"
env["XDG_CACHE_HOME"] = "/tmp/cache"
env["SQLITE_BUSY_TIMEOUT"] = "30000"
env["HERMES_ALLOW_ROOT_GATEWAY"] = "1"
env["HERMES_WORKSPACE"] = "/opt/data/workspace"
env["HERMES_WRITE_SAFE_ROOT"] = "/opt/data"

python_bin = "/opt/hermes/.venv/bin/python3"

# 5. Enable Autosave 
# Spawn a background worker thread to watch your local database and config files every 5 seconds.
# As you chat with your agent, this worker thread automatically copies the updated database content
# back to Cloud Storage to persist it.
def sync_to_gcs_loop():
  files_to_sync = ["state.db", "config.yaml", ".env"]
  last_mtimes = {}
  
  # Initialize last_mtimes
  for f in files_to_sync:
    path = os.path.join(hermes_dir, f)
    if os.path.exists(path):
      last_mtimes[f] = os.path.getmtime(path)
    else:
      last_mtimes[f] = 0
      
  while True:
    time.sleep(5)
    for f in files_to_sync:
      src_path = os.path.join(hermes_dir, f)
      if os.path.exists(src_path):
        try:
          mtime = os.path.getmtime(src_path)
          if mtime > last_mtimes.get(f, 0):
            dst_path = os.path.join("/opt/data/.hermes", f)
            shutil.copy2(src_path, dst_path)
            last_mtimes[f] = mtime
            print(f"Auto-saved {f} to GCS volume mount", flush=True)
        except Exception as e:
          print(f"Error auto-saving {f} to GCS: {e}", flush=True)

threading.Thread(target=sync_to_gcs_loop, daemon=True).start()


# 6. Launch the Hermes Gateway (the AI backend) and the Web Dashboard (the UI)
# These are launched as parallel processes, sending logs to Cloud Run via stdout & stderr
print("=== STARTING GATEWAY IN BACKGROUND ===", flush=True)
gw = subprocess.Popen(
    [python_bin, "-m", "hermes_cli.main", "gateway", "run"],
    env=env,
    cwd="/opt/data/workspace",
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
    bufsize=1,
)

def stream_gw():
  for line in iter(gw.stdout.readline, ""):
    if line:
      print(f"[GATEWAY] {line.rstrip()}", flush=True)

threading.Thread(target=stream_gw, daemon=True).start()

print("=== STARTING DASHBOARD ON 0.0.0.0:8080 ===", flush=True)
sys.stdout.flush()

dash = subprocess.Popen(
    [
        python_bin,
        "-m",
        "hermes_cli.main",
        "dashboard",
        "--host",
        "0.0.0.0",
        "--port",
        "8080",
        "--skip-build",
    ],
    env=env,
    cwd="/opt/data/workspace",
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
    bufsize=1,
)

for line in iter(dash.stdout.readline, ""):
  if line:
    print(f"[DASHBOARD] {line.rstrip()}", flush=True)

rc = dash.wait()
print(f"DASHBOARD EXITED WITH RETURN CODE: {rc}", flush=True)

while True:
  time.sleep(10)

4. 시작 스크립트 만들기 (start_hermes.sh)

로컬에서 start_hermes.sh을 만듭니다.

#!/bin/sh
set -e
export PYTHONUNBUFFERED=1
exec python3 /opt/data/run_hermes.py

5. Cloud Storage에 파일 업로드

구성 파일을 GCS 버킷의 루트에 복사합니다.

gcloud storage cp config.yaml run_hermes.py start_hermes.sh gs://${BUCKET_NAME}/

6. Cloud Run 인스턴스에 Hermes 배포

gcloud beta run instances deploy를 사용하여 컨테이너를 배포합니다. 이 명령어에는 GCSFuse 및 컨테이너 제한과 관련된 알려진 문제를 해결하기 위한 특정 구성이 포함되어 있습니다.

환경 변수 (PROJECT_ID, REGION, BUCKET_NAME, SERVICE_ACCOUNT)가 활성 터미널 세션에서 내보내지는지 확인합니다.

인스턴스를 배포합니다.

gcloud beta run instances deploy hermes-instance \
  --image nousresearch/hermes-agent:latest \
  --service-account ${SERVICE_ACCOUNT} \
  --command "/bin/sh" \
  --args "/opt/data/start_hermes.sh" \
  --port 8080 \
  --cpu 2 \
  --memory 4Gi \
  --ingress all \
  --no-invoker-iam-check \
  --add-volume name=hermes-storage,mount-path=/opt/data,type=cloud-storage,mount-options="uid=2000;gid=2000;file-mode=0777;dir-mode=0777;implicit-dirs",bucket=$BUCKET_NAME \
  --set-secrets "HERMES_DASHBOARD_BASIC_AUTH_PASSWORD=hermes-dashboard-password:latest" \
  --set-env-vars "PYTHONUNBUFFERED=1,VERTEX_PROJECT_ID=$PROJECT_ID,VERTEX_LOCATION=global,HERMES_DASHBOARD_BASIC_AUTH_USERNAME=admin,HERMES_ALLOW_ROOT_GATEWAY=1,HERMES_WORKSPACE=/opt/data/workspace,HERMES_WRITE_SAFE_ROOT=/opt/data" \
  --region $REGION \
  --project $PROJECT_ID

위에 포함된 중요한 구성:

  • --service-account: 전용 hermes-sa 서비스 계정을 연결합니다.
  • 감독자 스크립트: start_hermes.sh는 SQLite 잠금 제한과 캐싱 문제를 GCS FUSE에서 로컬 tmpfs로 라우팅하는 맞춤 Python 감독자 run_hermes.py를 호출합니다.
  • --set-secrets: Secret Manager에서 환경 변수로 사용자 인증 정보를 직접 삽입합니다.

7. Hermes 웹 UI를 통해 직접 상호작용

배포가 완료되면 생성된 .run.app URL에서 대시보드에 액세스할 수 있습니다. 인증을 묻는 메시지가 표시되면 사용자 이름으로 admin를 입력하고 비밀번호로 ${DASHBOARD_PASSWORD}를 입력합니다.

에이전트와 채팅

echo "hello"와 같은 프롬프트를 사용하여 에이전트가 작동하는지 확인할 수 있습니다.

영구 스토리지 테스트

에이전트에게 다음을 요청하여 Google Cloud 버킷의 영구 스토리지를 테스트할 수 있습니다.

Write "hello world" to a file named hello.txt in your workspace.

그런 다음 셸에서 다음을 실행하여 파일이 작성되었는지 확인할 수 있습니다.

gcloud storage cat gs://$BUCKET_NAME/workspace/hello.txt

마지막으로 새 Cloud Run 인스턴스에서 채팅과 파일이 유지되는지 확인하려면 (Cloud Run 인스턴스는 최대 7일의 연속 실행 시간을 가지며 기본적으로 자동 재시작 정책이 구성됨) 이전과 똑같이 gcloud beta run instances deploy 명령어를 다시 실행하면 됩니다. 그러면 채팅 세션이 표시됩니다. 에이전트에게 다음과 같이 요청할 수 있습니다.

Read the contents of the file hello.txt in your workspace.

'hello world'가 표시됩니다.

8. 삭제

이 Codelab에서 사용한 리소스 비용이 Google Cloud 계정에 청구되지 않도록 하려면 다음 안내를 따르세요.

  1. Cloud Run 인스턴스 삭제:
    gcloud beta run instances delete hermes-instance --region ${REGION} --quiet
    
  2. Secret Manager 보안 비밀 삭제:
    gcloud secrets delete hermes-dashboard-password --quiet
    
  3. Cloud Storage 버킷 삭제:
    gcloud storage rm -r gs://${BUCKET_NAME}
    
  4. 전용 서비스 계정 삭제:
    gcloud iam service-accounts delete ${SERVICE_ACCOUNT} --quiet
    

9. 결론

수고하셨습니다 Cloud Storage로 지원되는 Cloud Run 인스턴스에 보안이 적용된 완전 영구적인 Hermes 에이전트 인스턴스를 배포했습니다.

학습한 내용

  • Hermes 에이전트를 Cloud Run 인스턴스에 배포하는 방법
  • GCSFuse를 사용하여 Cloud Storage 버킷을 Cloud Run 인스턴스에 마운트하는 방법
  • GCSFuse 파일 잠금 제한을 우회하도록 SQLite 및 임시 캐시를 안전하게 구성하는 방법