Cloud Run インスタンスに Hermes エージェントをデプロイする方法

1. はじめに

概要

このラボでは、Hermes エージェント(Nous Research 製)の完全に永続的で安全なインスタンスを Cloud Run インスタンスにデプロイします。Hermes ウェブ ダッシュボードを使用して AI エージェントを操作し、Google Cloud Storage で永続ワークスペースをバックアップします。

Hermes は、自動スケーリング Cloud Run サービスとして実行できる Gateway モードをサポートしていますが、起動時にスキルをスキャンし、バックグラウンド実行を処理するステートフル エージェントとしても機能します。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 beta をインストールする

まず、プロジェクトとリージョンを環境変数として設定します。

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 APIs を有効にする

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 に認証情報を保存する

ダッシュボード パスワードなどの機密性の高い認証情報は Google Cloud Secret Manager に保存されるため、Cloud Run は起動時にコンテナに安全に挿入できます。

ダッシュボードの安全なランダム パスワードを生成し、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 サービス アカウントを関連付けます。
  • Supervisor スクリプト: 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 とエフェメラル キャッシュを安全に構成する方法。