1. Introduzione
Panoramica
In questo lab, eseguirai il deployment di un'istanza sicura e completamente persistente dell'agente Hermes (di Nous Research) nelle istanze Cloud Run. Interagirai con il tuo agente AI utilizzando la dashboard web di Hermes e il suo spazio di lavoro persistente con Google Cloud Storage.
Sebbene Hermes supporti una modalità gateway che potrebbe essere eseguita come servizio Cloud Run con scalabilità automatica, funge anche da agente stateful che esegue la scansione delle skill all'avvio e gestisce l'esecuzione in background. Le istanze Cloud Run forniscono un ambiente di lunga durata e indirizzabile individualmente, perfetto per questo workload.
In questo lab proverai a:
- Prepara un bucket Cloud Storage per rendere persistenti lo stato e le configurazioni del container.
- Crea un supervisore Python personalizzato (
run_hermes.py) e uno script di avvio (start_hermes.sh) per gestire l'inizializzazione dell'avvio. - Esegui il deployment dell'agente Hermes utilizzando
gcloud beta run instances deploy. - Accedi alla dashboard Hermes e autenticati.
Obiettivi didattici
- Come eseguire il deployment dell'agente Hermes nelle istanze Cloud Run.
- Come montare i bucket Cloud Storage sulle istanze Cloud Run utilizzando GCSFuse.
- Come configurare in modo sicuro SQLite e le cache effimere per aggirare le limitazioni del blocco dei file di GCSFuse.
2. Configurazione e requisiti
Configurazione del progetto Google Cloud
- Accedi alla console Google Cloud.
- Crea o seleziona un progetto Google Cloud.
- Assicurati che la fatturazione sia attivata per il tuo progetto Google Cloud.
Apri Cloud Shell
Attiva Google Cloud Shell dalla barra degli strumenti in alto della console Google Cloud.
Imposta il progetto e installa gcloud beta
Innanzitutto, imposta il progetto e la regione come variabili di ambiente.
export PROJECT_ID=<YOUR_PROJECT_ID>
export REGION="us-west2"
export BUCKET_NAME="hermes-state-${PROJECT_ID}"
e configura il progetto per gcloud.
gcloud config set project $PROJECT_ID
Assicurati che il componente beta sia installato per gcloud beta run instances:
gcloud components install beta --quiet
E la tua versione di gcloud è aggiornata.
gcloud components updates
Abilita le API Google Cloud richieste
In Cloud Shell, abilita le API Cloud Run, Cloud Storage e Secret Manager:
gcloud services enable \
run.googleapis.com \
secretmanager.googleapis.com \
storage.googleapis.com \
compute.googleapis.com \
aiplatform.googleapis.com
3. Crea service account dedicato
Per rispettare il principio del privilegio minimo, crea un service account IAM dedicato per l'agente Hermes e concedigli le autorizzazioni necessarie per richiamare i modelli 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. Archivia le credenziali in Secret Manager
Archivieremo le credenziali sensibili, come la password del dashboard, in Google Cloud Secret Manager in modo che Cloud Run possa inserirle in modo sicuro nel container al momento dell'avvio.
Genera una password casuale sicura per la dashboard e archiviala 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. Prepara il bucket Cloud Storage e i file di configurazione
Hermes richiede l'archiviazione permanente montata come /opt/data. Utilizzeremo un bucket Google Cloud Storage (GCS) e lo monteremo utilizzando i montaggi dei volumi Cloud Storage.
1. Crea un bucket 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. Crea config.yaml
Crea un file config.yaml. Assicurati di includere _config_version: 12 per garantire che la configurazione venga caricata correttamente:
_config_version: 12
model:
default: "google/gemini-3.8-flash"
provider: "vertex"
dashboard:
enabled: true
database:
journal_mode: delete
3. Crea lo script del supervisore (run_hermes.py)
Cloud Storage non supporta i meccanismi di blocco dei file specifici necessari per l'esecuzione sicura dei database SQLite. Per evitare il danneggiamento del database, abbiamo bisogno di uno script "supervisore" personalizzato (run_hermes.py). Questo script configura Hermes per archiviare i blocchi temporanei del database nella memoria locale del container anziché in Cloud Storage prima di avviare l'agente.
Crea run_hermes.py localmente:
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. Crea lo script di avvio (start_hermes.sh)
Crea start_hermes.sh localmente.
#!/bin/sh
set -e
export PYTHONUNBUFFERED=1
exec python3 /opt/data/run_hermes.py
5. Carica i file su Cloud Storage
Copia i file di configurazione nella radice del bucket GCS:
gcloud storage cp config.yaml run_hermes.py start_hermes.sh gs://${BUCKET_NAME}/
6. Esegui il deployment di Hermes sulle istanze Cloud Run
Utilizziamo gcloud beta run instances deploy per eseguire il deployment del container. Questo comando include configurazioni specifiche per risolvere problemi noti relativi a GCSFuse e ai limiti dei container.
Assicurati che le variabili di ambiente (PROJECT_ID, REGION, BUCKET_NAME, SERVICE_ACCOUNT) siano esportate nella sessione del terminale attiva.
Esegui il deployment dell'istanza:
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
Configurazioni importanti incluse sopra:
--service-account: collega il service accounthermes-sadedicato.- Supervisor Scripts:
start_hermes.shchiama il supervisore Python personalizzatorun_hermes.pyche indirizza i limiti di blocco e i problemi di memorizzazione nella cache di SQLite da GCS FUSE a tmpfs locale. --set-secrets: inserisce le credenziali direttamente da Secret Manager nelle variabili di ambiente.
7. Interagire direttamente tramite la UI web di Hermes
Una volta eseguito il deployment, puoi accedere alla dashboard all'URL .run.app generato. Quando ti viene chiesto di autenticarti, inserisci admin come nome utente e ${DASHBOARD_PASSWORD} come password.
Chatta con il tuo agente
Puoi provare a dire echo "hello" per verificare che l'agente funzioni.
Testare l'archiviazione permanente
Puoi testare l'archiviazione permanente nel tuo bucket Google Cloud chiedendo all'agente
Write "hello world" to a file named hello.txt in your workspace.
Nella shell, puoi verificare che il file sia stato scritto eseguendo
gcloud storage cat gs://$BUCKET_NAME/workspace/hello.txt
Infine, per verificare che le chat e i file vengano mantenuti nelle nuove istanze Cloud Run (un'istanza Cloud Run ha un runtime continuo fino a 7 giorni, con la policy di riavvio automatico configurata per impostazione predefinita), puoi eseguire di nuovo il comando gcloud beta run instances deploy esattamente come prima. A questo punto vedrai le tue sessioni di chat. Puoi anche chiedere al tuo agente
Read the contents of the file hello.txt in your workspace.
e vedrai "hello world".
8. Elimina
Per evitare che al tuo account Google Cloud vengano addebitati costi relativi alle risorse utilizzate in questo codelab:
- Elimina l'istanza Cloud Run:
gcloud beta run instances delete hermes-instance --region ${REGION} --quiet - Elimina i secret di Secret Manager:
gcloud secrets delete hermes-dashboard-password --quiet - Elimina il bucket Cloud Storage:
gcloud storage rm -r gs://${BUCKET_NAME} - Elimina service account dedicato:
gcloud iam service-accounts delete ${SERVICE_ACCOUNT} --quiet
9. Conclusione
Complimenti! Hai eseguito correttamente il deployment di un'istanza sicura e completamente persistente dell'agente Hermes su istanze Cloud Run supportate da Cloud Storage.
Che cosa hai imparato
- Come eseguire il deployment dell'agente Hermes nelle istanze Cloud Run.
- Come montare i bucket Cloud Storage sulle istanze Cloud Run utilizzando GCSFuse.
- Come configurare in modo sicuro SQLite e le cache temporanee per aggirare le limitazioni del blocco dei file di GCSFuse.