Déployer l'agent Hermes sur des instances Cloud Run

1. Introduction

Présentation

Dans cet atelier, vous allez déployer une instance sécurisée et entièrement persistante de l'agent Hermes (de Nous Research) sur des instances Cloud Run. Vous interagirez avec votre agent d'IA à l'aide du tableau de bord Web Hermes et sauvegarderez son espace de travail persistant avec Google Cloud Storage.

Bien qu'Hermes soit compatible avec un mode passerelle pouvant s'exécuter en tant que service Cloud Run à autoscaling, il sert également d'agent avec état qui analyse les compétences au démarrage et gère l'exécution en arrière-plan. Les instances Cloud Run fournissent un environnement durable et adressable individuellement, parfaitement adapté à cette charge de travail.

Objectifs de l'atelier

  • Préparez un bucket Cloud Storage pour conserver l'état et les configurations du conteneur.
  • Créez un superviseur Python personnalisé (run_hermes.py) et un script de démarrage (start_hermes.sh) pour gérer l'initialisation du démarrage.
  • Déployez l'agent Hermes à l'aide de gcloud beta run instances deploy.
  • Accédez au tableau de bord Hermes et authentifiez-vous.

Points abordés

  • Découvrez comment déployer l'agent Hermes sur des instances Cloud Run.
  • Comment installer des buckets Cloud Storage sur des instances Cloud Run à l'aide de GCSFuse.
  • Configurer de manière sécurisée les caches éphémères et SQLite pour contourner les limites de verrouillage de fichiers GCSFuse.

2. Préparation

Configurer un projet GCP

  1. Connectez-vous à la console Google Cloud.
  2. Créez ou sélectionnez un projet Google Cloud.
  3. Assurez-vous que la facturation est activée pour votre projet Google Cloud.

Ouvrir Cloud Shell

Activez Google Cloud Shell depuis la barre d'outils supérieure de la console Cloud.

Définir le projet et installer gcloud beta

Tout d'abord, définissez votre projet et votre région en tant que variables d'environnement.

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

Configurez ensuite votre projet pour gcloud.

gcloud config set project $PROJECT_ID

Assurez-vous que le composant beta est installé pour gcloud beta run instances :

gcloud components install beta --quiet

Votre version de gcloud est à jour.

gcloud components updates

Activer les API Google Cloud requises

Dans Cloud Shell, activez les API Cloud Run, Cloud Storage et Secret Manager :

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

3. Créer un compte de service dédié

Pour respecter le principe du moindre privilège, créez un compte de service IAM dédié pour l'agent Hermes et accordez-lui les autorisations nécessaires pour appeler les modèles 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. Stocker les identifiants dans Secret Manager

Nous stockerons les identifiants sensibles, comme le mot de passe du tableau de bord, dans Google Cloud Secret Manager afin que Cloud Run puisse les injecter de manière sécurisée dans le conteneur au moment du démarrage.

Générez un mot de passe aléatoire sécurisé pour votre tableau de bord et stockez-le dans 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. Préparer le bucket Cloud Storage et les fichiers de configuration

Hermes a besoin d'un stockage persistant installé en tant que /opt/data. Nous allons utiliser un bucket Google Cloud Storage (GCS) et l'installer à l'aide des installations de volume Cloud Storage.

1. Créer 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. Créer config.yaml

Créez un fichier config.yaml. Veillez à inclure _config_version: 12 pour vous assurer que la configuration est correctement chargée :

_config_version: 12

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

dashboard:
  enabled: true

database:
  journal_mode: delete

3. Créer le script du superviseur (run_hermes.py)

Cloud Storage n'est pas compatible avec les mécanismes de verrouillage de fichiers spécifiques dont les bases de données SQLite ont besoin pour fonctionner de manière sécurisée. Pour éviter toute corruption de la base de données, nous avons besoin d'un script de "superviseur" personnalisé (run_hermes.py). Ce script configure Hermes pour qu'il stocke ses verrous de base de données temporaires dans la mémoire locale du conteneur au lieu de Cloud Storage avant de démarrer l'agent.

Créez run_hermes.py en local :

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. Créer le script de démarrage (start_hermes.sh)

Créez start_hermes.sh en local.

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

5. Importer des fichiers dans Cloud Storage

Copiez les fichiers de configuration à la racine de votre bucket GCS :

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

6. Déployer Hermes sur des instances Cloud Run

Nous utilisons gcloud beta run instances deploy pour déployer le conteneur. Cette commande inclut des configurations spécifiques pour résoudre les problèmes connus liés à GCSFuse et aux limites de conteneurs.

Assurez-vous que vos variables d'environnement (PROJECT_ID, REGION, BUCKET_NAME, SERVICE_ACCOUNT) sont exportées dans votre session de terminal active.

Déployez l'instance :

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

Configurations importantes incluses ci-dessus :

  • --service-account : attache le compte de service hermes-sa dédié.
  • Scripts du superviseur : start_hermes.sh appelle le superviseur Python personnalisé run_hermes.py qui redirige les problèmes de limites de verrouillage et de mise en cache SQLite de GCS FUSE vers tmpfs local.
  • --set-secrets : injecte les identifiants directement depuis Secret Manager dans les variables d'environnement.

7. Interagir directement via l'interface utilisateur Web Hermes

Une fois déployé, vous pouvez accéder à votre tableau de bord à l'URL .run.app générée. Lorsque vous êtes invité à vous authentifier, saisissez admin comme nom d'utilisateur et votre ${DASHBOARD_PASSWORD} comme mot de passe.

Discuter avec votre agent

Vous pouvez essayer des commandes comme echo "hello" pour vérifier que l'agent fonctionne.

Tester le stockage persistant

Vous pouvez tester le stockage persistant dans votre bucket Google Cloud en demandant à l'agent

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

Dans votre shell, vous pouvez vérifier que le fichier a été écrit en exécutant la commande suivante :

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

Enfin, pour vérifier que vos discussions et vos fichiers sont conservés dans les nouvelles instances Cloud Run (une instance Cloud Run peut s'exécuter en continu pendant sept jours maximum, avec une règle de redémarrage automatique configurée par défaut), vous pouvez réexécuter la commande gcloud beta run instances deploy exactement comme avant. Vos sessions de chat s'affichent alors. Vous pouvez également demander à votre agent

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

et vous verrez "hello world".

8. Effectuer un nettoyage

Pour éviter que les ressources utilisées dans cet atelier de programmation soient facturées sur votre compte Google Cloud :

  1. Supprimez l'instance Cloud Run :
    gcloud beta run instances delete hermes-instance --region ${REGION} --quiet
    
  2. Supprimer les secrets Secret Manager :
    gcloud secrets delete hermes-dashboard-password --quiet
    
  3. Supprimer le bucket Cloud Storage :
    gcloud storage rm -r gs://${BUCKET_NAME}
    
  4. Supprimer le compte de service dédié :
    gcloud iam service-accounts delete ${SERVICE_ACCOUNT} --quiet
    

9. Conclusion

Félicitations ! Vous avez déployé une instance sécurisée et entièrement persistante de l'agent Hermes sur des instances Cloud Run soutenues par Cloud Storage.

Ce que vous avez appris

  • Découvrez comment déployer l'agent Hermes sur des instances Cloud Run.
  • Comment installer des buckets Cloud Storage sur des instances Cloud Run à l'aide de GCSFuse.
  • Configurer de manière sécurisée les caches éphémères et SQLite pour contourner les limites de verrouillage de fichiers GCSFuse.