Hermes-Agent in Cloud Run-Instanzen bereitstellen

1. Einführung

Übersicht

In diesem Lab stellen Sie eine vollständig persistente, sichere Instanz des Hermes-Agents (von Nous Research) in Cloud Run-Instanzen bereit. Sie interagieren mit Ihrem KI-Agenten über das Hermes-Web-Dashboard und sichern den persistenten Arbeitsbereich mit Google Cloud Storage.

Hermes unterstützt zwar einen Gateway-Modus, der als automatisch skalierender Cloud Run-Dienst ausgeführt werden kann, fungiert aber auch als zustandsbehafteter Agent, der Skills beim Start scannt und die Ausführung im Hintergrund übernimmt. Cloud Run-Instanzen bieten eine langlebige, individuell adressierbare Umgebung, die sich perfekt für diese Arbeitslast eignet.

Aufgaben

  • Bereiten Sie einen Cloud Storage-Bucket vor, in dem der Containerstatus und die Konfigurationen gespeichert werden.
  • Erstellen Sie einen benutzerdefinierten Python-Supervisor (run_hermes.py) und ein Startskript (start_hermes.sh) für die Boot-Initialisierung.
  • Stellen Sie den Hermes-Agent mit gcloud beta run instances deploy bereit.
  • Greifen Sie auf das Hermes-Dashboard zu und authentifizieren Sie sich.

Lerninhalte

  • Hermes-Agent in Cloud Run-Instanzen bereitstellen
  • Cloud Storage-Buckets mit GCSFuse für Cloud Run-Instanzen bereitstellen
  • So konfigurieren Sie SQLite und temporäre Caches sicher, um die Einschränkungen der GCSFuse-Dateisperrung zu umgehen.

2. Einrichtung und Anforderungen

GCP-Projekt einrichten

  1. Melden Sie sich in der Google Cloud Console an.
  2. Erstellen Sie ein Google Cloud-Projekt oder wählen Sie eines aus.
  3. Die Abrechnung für das Google Cloud-Projekt muss aktiviert sein.

Cloud Shell öffnen

Aktivieren Sie Google Cloud Shell über die obere Symbolleiste der Cloud Console.

Projekt festlegen und gcloud beta installieren

Legen Sie zuerst Ihr Projekt und Ihre Region als Umgebungsvariablen fest.

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

Konfigurieren Sie Ihr Projekt für gcloud.

gcloud config set project $PROJECT_ID

Prüfen Sie, ob die Komponente beta für gcloud beta run instances installiert ist:

gcloud components install beta --quiet

Ihre gcloud-Version ist auf dem neuesten Stand.

gcloud components updates

Erforderliche Google Cloud APIs aktivieren

Aktivieren Sie in Cloud Shell die Cloud Run API, die Cloud Storage API und die Secret Manager API:

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

3. Dediziertes Dienstkonto erstellen

Um das Prinzip der geringsten Berechtigung einzuhalten, erstellen Sie ein dediziertes IAM-Dienstkonto für den Hermes-Agent und gewähren Sie ihm die erforderlichen Berechtigungen zum Aufrufen von Vertex AI-Modellen:

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. Anmeldedaten in Secret Manager speichern

Wir speichern vertrauliche Anmeldedaten wie das Dashboard-Passwort in Google Cloud Secret Manager, damit Cloud Run sie beim Start sicher in den Container einfügen kann.

Generieren Sie ein sicheres zufälliges Passwort für Ihr Dashboard und speichern Sie es in 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 und Konfigurationsdateien vorbereiten

Hermes benötigt nichtflüchtigen Speicher, der als /opt/data bereitgestellt wird. Wir verwenden einen Google Cloud Storage-Bucket (GCS) und stellen ihn mit Cloud Storage-Volume-Bereitstellungen bereit.

1. Cloud Storage-Bucket erstellen

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 erstellen

Erstellen Sie eine config.yaml-Datei. Achten Sie darauf, _config_version: 12 einzufügen, damit die Konfiguration richtig geladen wird:

_config_version: 12

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

dashboard:
  enabled: true

database:
  journal_mode: delete

3. Supervisor-Script erstellen (run_hermes.py)

Cloud Storage unterstützt nicht die spezifischen Dateisperrmechanismen, die für den sicheren Betrieb von SQLite-Datenbanken erforderlich sind. Um eine Beschädigung der Datenbank zu verhindern, benötigen wir ein benutzerdefiniertes Supervisor-Skript (run_hermes.py). Dieses Skript konfiguriert Hermes so, dass die temporären Datenbanksperren des Containers im lokalen Speicher des Containers und nicht in Cloud Storage gespeichert werden, bevor der Agent gestartet wird.

run_hermes.py lokal erstellen:

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. Startskript erstellen (start_hermes.sh)

Erstellen Sie start_hermes.sh lokal.

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

5. Dateien in Cloud Storage hochladen

Kopieren Sie die Konfigurationsdateien in das Stammverzeichnis Ihres GCS-Buckets:

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

6. Hermes auf Cloud Run-Instanzen bereitstellen

Wir verwenden gcloud beta run instances deploy, um den Container bereitzustellen. Dieser Befehl enthält spezifische Konfigurationen, um bekannte Probleme mit GCSFuse und Containerlimits zu beheben.

Achten Sie darauf, dass Ihre Umgebungsvariablen (PROJECT_ID, REGION, BUCKET_NAME, SERVICE_ACCOUNT) in Ihrer aktiven Terminalsitzung exportiert werden.

Stellen Sie die Instanz bereit:

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

Wichtige Konfigurationen, die oben enthalten sind:

  • --service-account: Das dedizierte hermes-sa-Dienstkonto wird angehängt.
  • Supervisor-Scripts: start_hermes.sh ruft den benutzerdefinierten Python-Supervisor run_hermes.py auf, der SQLite-Sperrbegrenzungen und Caching-Probleme von GCS FUSE in das lokale tmpfs weiterleitet.
  • --set-secrets: Fügt Anmeldedaten direkt aus Secret Manager in Umgebungsvariablen ein.

7. Direkt über die Hermes-Web-UI interagieren

Nach der Bereitstellung können Sie über die generierte .run.app-URL auf Ihr Dashboard zugreifen. Wenn Sie zur Authentifizierung aufgefordert werden, geben Sie admin als Nutzernamen und Ihr ${DASHBOARD_PASSWORD} als Passwort ein.

Chatten Sie mit Ihrem Agent

Sie können beispielsweise echo "hello" ausprobieren, um zu bestätigen, dass der Agent funktioniert.

Nichtflüchtigen Speicher testen

Sie können den nichtflüchtigen Speicher in Ihrem Google Cloud-Bucket testen, indem Sie den Agenten

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

Anschließend können Sie in der Shell prüfen, ob die Datei geschrieben wurde, indem Sie folgenden Befehl ausführen:

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

Wenn Sie prüfen möchten, ob Ihre Chats und Dateien in neuen Cloud Run-Instanzen beibehalten werden (eine Cloud Run-Instanz hat eine kontinuierliche Laufzeit von bis zu 7 Tagen mit einer standardmäßig konfigurierten automatischen Neustartrichtlinie), können Sie den Befehl gcloud beta run instances deploy genau wie zuvor noch einmal ausführen. Anschließend werden Ihre Chatsitzungen angezeigt. Sie können Ihren Agenten

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

und „hello world“ wird angezeigt.

8. Bereinigen

So vermeiden Sie, dass Ihrem Google Cloud-Konto die in diesem Codelab verwendeten Ressourcen in Rechnung gestellt werden:

  1. Cloud Run-Instanz löschen:
    gcloud beta run instances delete hermes-instance --region ${REGION} --quiet
    
  2. Secret Manager-Secrets löschen:
    gcloud secrets delete hermes-dashboard-password --quiet
    
  3. Cloud Storage-Bucket löschen:
    gcloud storage rm -r gs://${BUCKET_NAME}
    
  4. Dediziertes Dienstkonto löschen:
    gcloud iam service-accounts delete ${SERVICE_ACCOUNT} --quiet
    

9. Fazit

Glückwunsch! Sie haben eine sichere, vollständig persistente Instanz des Hermes-Agents in Cloud Run-Instanzen bereitgestellt, die von Cloud Storage unterstützt werden.

Das haben Sie gelernt

  • Hermes-Agent in Cloud Run-Instanzen bereitstellen
  • Cloud Storage-Buckets mit GCSFuse für Cloud Run-Instanzen bereitstellen
  • So konfigurieren Sie SQLite und temporäre Caches sicher, um die Einschränkungen der GCSFuse-Dateisperrung zu umgehen.