1. 簡介
總覽
在本實驗室中,您會將 Hermes Agent (由 Nous Research 提供) 的完整持續性安全執行個體部署至 Cloud Run 執行個體。您將使用 Hermes Web 資訊主頁與 AI 代理程式互動,並使用 Google Cloud Storage 備份持續性工作區。
Hermes 支援閘道模式,可做為自動調整資源配置的 Cloud Run 服務執行,但也會做為有狀態的代理程式,在啟動時掃描技能並處理背景執行作業。Cloud Run 執行個體提供可長期運作的個別定址環境,非常適合這類工作負載。
學習內容
- 準備 Cloud Storage bucket,用於保存容器狀態和設定。
- 建立自訂 Python 管理員 (
run_hermes.py) 和開機指令碼 (start_hermes.sh),處理開機初始化作業。 - 使用
gcloud beta run instances deploy部署 Hermes 代理程式。 - 存取 Hermes 資訊主頁並進行驗證。
課程內容
- 如何將 Hermes 代理程式部署至 Cloud Run 執行個體。
- 如何使用 GCSFuse 將 Cloud Storage 值區掛接至 Cloud Run 執行個體。
- 如何安全地設定 SQLite 和暫時性快取,略過 GCSFuse 檔案鎖定限制。
2. 設定和需求條件
設定 GCP 專案
- 登入 Google Cloud 控制台。
- 建立或選取 Google Cloud 專案。
- 確認 Google Cloud 專案已啟用計費功能。
開啟 Cloud Shell
從 Cloud Shell 的頂端工具列啟用 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 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
我們會將資訊主頁密碼等機密憑證儲存在 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 bucket 和設定檔
Hermes 需要掛接為 /opt/data 的永久儲存空間。我們會使用 Google Cloud Storage (GCS) bucket,並透過 Cloud Storage 磁碟區掛接來掛接該 bucket。
1. 建立 Cloud Storage bucket
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)。這個指令碼會設定 Hermes,在啟動代理程式前,將暫時資料庫鎖定儲存在容器的本機記憶體中,而非 Cloud Storage。
在本機建立 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 bucket 的根目錄:
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呼叫自訂 Python 監督程式run_hermes.py,將 SQLite 鎖定限制和快取問題從 GCS FUSE 轉移到本機 tmpfs。 --set-secrets:直接從 Secret Manager 將憑證注入環境變數。
7. 透過 Hermes 網頁版 UI 直接互動
部署完成後,您就能透過產生的 .run.app 網址存取資訊主頁。系統提示您進行驗證時,請輸入 admin 做為使用者名稱,並輸入 ${DASHBOARD_PASSWORD} 做為密碼。
與虛擬服務專員對話
您可以嘗試執行 echo "hello" 等動作,確認代理程式是否正常運作。
測試永久儲存空間
您可以詢問代理程式,測試 Google Cloud Bucket 中的永久儲存空間
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. 清除
如要避免系統向您的 Google Cloud 帳戶收取本程式碼研究室所用資源的費用,請按照下列步驟操作:
- 刪除 Cloud Run 執行個體:
gcloud beta run instances delete hermes-instance --region ${REGION} --quiet - 刪除 Secret Manager Secret:
gcloud secrets delete hermes-dashboard-password --quiet - 刪除 Cloud Storage bucket:
gcloud storage rm -r gs://${BUCKET_NAME} - 刪除專屬服務帳戶:
gcloud iam service-accounts delete ${SERVICE_ACCOUNT} --quiet
9. 結語
恭喜!您已成功在 Cloud Run 執行個體上部署 Hermes Agent 的安全全持續性執行個體,並由 Cloud Storage 提供支援!
您學到的內容
- 如何將 Hermes 代理程式部署至 Cloud Run 執行個體。
- 如何使用 GCSFuse 將 Cloud Storage 值區掛接至 Cloud Run 執行個體。
- 如何安全地設定 SQLite 和暫時性快取,略過 GCSFuse 檔案鎖定限制。