Hermes Agent روی EKS: از docker-compose تا Helm

ai-agents aws eks kubernetes llm helm devops

تو یه کارگاه دوروزه داخلی رفتیم سراغ Hermes Agent تا ببینیم آیا می‌شه ازش به عنوان یه پلتفرم هوش مصنوعی خودهاست استفاده کرد. چیزی که با یه docker-compose ساده شروع شد، با یه Helm Chart سفارشی، یه پایپلاین ECR روی EKS، و کلی احترام برای طراحی agent loop تیم Nous Research تموم شد. اینجا کل مسیر رو از کانتینر محلی تا Kubernetes توضیح می‌دم.

Hermes چیه؟

Hermes یه فریم‌ورک متن‌باز برای ساخت ایجنت‌های هوش مصنوعی‌ه که Nous Research، یه مؤسسه تحقیقاتی مستقل در حوزه هوش مصنوعی، توسعه‌اش داده. ویژگی اصلیش یه حلقه یادگیری بسته (closed learning loop) ‌ه: ایجنت از تجربه‌هاش skill می‌سازه، اون skill ها رو در استفاده‌های بعدی بهتر می‌کنه، یه حافظه پایدار از تو در طول session ها نگه می‌داره، و می‌تونه سرچ بزنه تو مکالمات گذشته‌اش. به هیچ مدل خاصی هم گره نخورده. می‌شه نشونش داد به OpenRouter، AWS Bedrock یا هر endpoint سازگار با OpenAI، و با hermes model مدل رو عوض کرد.

یه نکته مهم رو قبلش بگم: وقتی می‌گیم «خودبهبود»، ذهن آدم می‌ره سمت Reinforcement Learning. اما Hermes مدل زیرین رو fine-tune یا train نمی‌کنه. هیچ gradient update ای اتفاق نمی‌افته. بهبود کاملاً روی لایه context engineering اتفاق می‌افته: skill ها به صورت prompt های ساختاریافته ذخیره می‌شن، حافظه متن کوریت‌شده‌ایه که تو system context inject می‌شه، و پروفایل کاربر اطلاعاتی جمع می‌کنه که نحوه پاسخ دادن ایجنت رو شکل می‌ده. می‌شه گفت یه رویکرد استنتاج نیمه‌نظارت‌شده‌ست (semi-supervised inference): یه انسان هنوز در حلقه‌ست و اعتبارسنجی می‌کنه چی باید بمونه، ولی ایجنت خودش رو نج می‌ده که چیزهای مفید رو ذخیره کنه و تعریف skill هاش رو بهتر کنه.

از نظر معماری، Hermes به صورت دو پروسه اجرا می‌شه:

  • Gateway: حلقه اصلی ایجنت به علاوه یه API server سازگار با OpenAI روی پورت 8642. مکالمه‌ها، tool ها و skill ها اینجا اجرا می‌شن؛ messaging bridge (تلگرام، Slack، Discord و…) هم اینجا وصل می‌شه.
  • Dashboard: یه وب UI روی پورت 9119 برای مرور مکالمات، مدیریت skill ها و حافظه، و مانیتورینگ ایجنت.

هر دو پروسه دایرکتوری HERMES_HOME یکسانی دارن (~/.hermes محلی، /opt/data تو کانتینر).

مرحله اول: راه‌اندازی محلی با docker-compose

ریپو upstream یه docker-compose.yml آماده برای این دو سرویس ارائه می‌ده. چند چیز از اول توجه رو جلب می‌کنه:

services:
  gateway:
    build: .
    image: hermes-agent
    container_name: hermes
    restart: unless-stopped
    network_mode: host
    volumes:
      - ~/.hermes:/opt/data
    environment:
      - HERMES_UID=${HERMES_UID:-10000}
      - HERMES_GID=${HERMES_GID:-10000}
      # برای expose کردن API server کامنت‌ها رو بردار:
      # - API_SERVER_HOST=0.0.0.0
      # - API_SERVER_KEY=${API_SERVER_KEY}
    command: ["gateway", "run"]

  dashboard:
    image: hermes-agent
    container_name: hermes-dashboard
    restart: unless-stopped
    network_mode: host
    depends_on:
      - gateway
    volumes:
      - ~/.hermes:/opt/data
    environment:
      - HERMES_UID=${HERMES_UID:-10000}
      - HERMES_GID=${HERMES_GID:-10000}
    command: ["dashboard", "--host", "127.0.0.1", "--no-open"]

الگوی HERMES_UID / HERMES_GID رو یه مرحله init از s6-overlay داخل image مدیریت می‌کنه: کانتینر به عنوان root شروع می‌کنه، user داخلی hermes رو از طریق gosu/usermod به UID هاست map می‌کنه، و بعد قبل از شروع هر سرویسی از سطح دسترسی root پایین میاد. اینطوری فایل‌هایی که زیر /opt/data ساخته می‌شن روی هاست قابل خوندن و نوشتن هستن.

Dashboard به طور پیش‌فرض به 127.0.0.1 bind می‌شه. دلیلش تو کامنت‌هاست: dashboard کلیدهای API رو ذخیره می‌کنه و لایه احراز هویت نداره. برای دسترسی ریموت، SSH tunnel راه درسته (ssh -L 9119:localhost:9119)، نه 0.0.0.0 روی یه شبکه مشترک.

سریع‌ترین شروع:

HERMES_UID=$(id -u) HERMES_GID=$(id -g) docker compose up -d

مرحله دوم: build image و push به ECR

برای deployment روی EKS، image باید تو رجیستری ECR خصوصی ما باشه.

# احراز هویت در ECR
aws ecr get-login-password --region eu-central-1 \
  | docker login --username AWS --password-stdin \
    <account-id>.dkr.ecr.eu-central-1.amazonaws.com

# build از Dockerfile upstream
docker build -t hermes-agent .

# tag و push
docker tag hermes-agent \
  <account-id>.dkr.ecr.eu-central-1.amazonaws.com/hermes-agent:latest
docker push \
  <account-id>.dkr.ecr.eu-central-1.amazonaws.com/hermes-agent:latest

Dockerfile upstream از یه multi-stage build استفاده می‌کنه: یه build stage وابستگی‌های Python رو با uv نصب می‌کنه، و runtime stage ایجنت رو روی یه base image سبک با s6-overlay از قبل نصب‌شده پکیج می‌کنه.

مرحله سوم: manifest کوبرنتیز

Namespace و Secret

apiVersion: v1
kind: Namespace
metadata:
  name: hermes-agent

---
apiVersion: v1
kind: Secret
metadata:
  name: hermes-secrets
  namespace: hermes-agent
type: Opaque
data:
  API_SERVER_KEY: "<مقدار base64 شده تصادفی>"

API_SERVER_KEY درخواست‌های API server سازگار با OpenAI گیت‌وی رو احراز هویت می‌کنه. قبل از apply کردن manifest باید بسازیش:

kubectl create secret generic hermes-secrets \
  --from-literal=API_SERVER_KEY="$(openssl rand -base64 32)" \
  -n hermes-agent --dry-run=client -o yaml | kubectl apply -f -

هیچ‌وقت یه کلید واقعی رو تو فایل YAML کامیت نکن. در پروداکشن، External Secrets Operator یا ابزار مشابهی که secret رو از secrets manager سازمانی بگیره رو توصیه می‌کنم.

Persistent Volumes

هر دو deployment PVC جداگانه دارن. ReadWriteOnce معادل EBS GP3 ه. یه node در آن واحد می‌تونه mountش کنه، که برای یه replica کافیه. برای scale کردن gateway به صورت افقی باید ReadWriteMany (EFS) داشته باشی. ولی برای یه ایجنت با state مکالمه‌ای، یه replica منطقی‌ترین انتخابه.

ServiceAccount با IRSA

apiVersion: v1
kind: ServiceAccount
metadata:
  name: hermes-agent
  namespace: hermes-agent
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/hermes-agent-role

annotation eks.amazonaws.com/role-arn نقطه ورود IRSA (IAM Roles for Service Accounts) ه. اگه cluster داری Pod Identity Agent یا OIDC provider رو configure شده باشه، هر pod که از این ServiceAccount استفاده کنه، به طور خودکار credential های کوتاه‌مدت برای hermes-agent-role بهش inject می‌شه (نه AWS key ثابت، نه اشتراک‌گذاری instance profile).

تو کیس ما، IAM role دسترسی bedrock:InvokeModel و bedrock:InvokeModelWithResponseStream روی مدل‌های مورد نظر رو داره. اینطوری gateway می‌تونه مستقیم با AWS Bedrock برای inference کار کنه. کلید API هیچ provider ثالثی لازم نیست. اگه Bedrock استفاده نمی‌کنی، annotation رو حذف کن و کلیدهای provider رو به عنوان متغیرهای محیطی اضافی از طریق Secret پاس بده.

Deployment گیت‌وی

دو متغیر محیطی مهم‌ترن:

env:
  - name: API_SERVER_HOST
    value: "0.0.0.0"
  - name: API_SERVER_KEY
    valueFrom:
      secretKeyRef:
        name: hermes-secrets
        key: API_SERVER_KEY
        optional: true

API_SERVER_HOST: "0.0.0.0" سرور API رو روی همه interface های داخل pod باز می‌کنه تا Service کوبرنتیز بتونه ترافیک روت کنه. API_SERVER_KEY از Secret خونده می‌شه و هیچ‌وقت تو Deployment spec ظاهر نمی‌شه.

Deployment داشبورد

args: ["dashboard", "--host", "0.0.0.0", "--no-open", "--insecure"]

سه flag که هر کدوم یه مشکل جداگانه حل می‌کنن:

  • --host 0.0.0.0: دلیلش همونه که گیت‌وی: اگه dashboard فقط روی loopback گوش بده، ClusterIP Service نمی‌تونه بهش برسه.
  • --no-open: رفتار «مرورگر رو هنگام شروع باز کن» رو غیرفعال می‌کنه که توی کانتینر headless یا بی‌صدا fail می‌شه.
  • --insecure: این مهم‌ترینه. HTTP server داخلی dashboard به طور پیش‌فرض enforce می‌کنه که درخواست‌ها با FQDN مشخصی بیان. بدون --insecure، هر درخواستی از ALB (با Host header متفاوت) رد می‌شه. پشت یه load balancer که TLS و کنترل دسترسی رو مدیریت می‌کنه، --insecure یه عقب‌نشینی امنیتی نیست.

Services و Ingress

هر دو deployment یه ClusterIP Service دارن. Ingress ترافیک رو اینطوری route می‌کنه:

  • / → داشبورد
  • /api/ → گیت‌وی (API داخلی)
  • /v1/ → گیت‌وی (endpoint سازگار با OpenAI)

Ingress به عنوان alb.ingress.kubernetes.io/scheme: internal تنظیم شده و تو یه ALB group داخلی قرار داره. هیچ‌وقت از اینترنت عمومی قابل دسترس نیست. SSL redirect روی لایه ALB با یه policy حداقل TLS 1.2 enforce می‌شه.

انتخاب مدل و هزینه توکن

تو کارگاه از Claude Sonnet 4.6 (از طریق AWS Bedrock) استفاده کردیم نه Opus. دلیلش ساده‌ست: Opus تقریباً 5 برابر Sonnet هزینه داره و برای کارهایی که بهش سپردیم (code review، نوشتن اسکریپت، خلاصه کردن مستندات)، Sonnet خوب از پسشون برومد. Hermes این تغییر رو ساده می‌کنه: hermes model اجازه می‌ده provider و مدل رو mid-session عوض کنی بدون اینکه context از دست بره. تو دو روز فقط یه بار نیاز شد بریم سراغ Opus، اونم برای یه task چند مرحله‌ای پیچیده.

مرحله چهارم: Helm Chart

وقتی manifest خام ثابت شد، استخراجش کردیم تو یه Helm Chart:

caruso-hermes/
├── Chart.yaml
├── values.yaml
└── templates/
    ├── _helpers.tpl
    ├── namespace.yaml
    ├── secret.yaml
    ├── pvc.yaml
    ├── serviceaccount.yaml
    ├── deployment-gateway.yaml
    ├── deployment-dashboard.yaml
    ├── service-gateway.yaml
    ├── service-dashboard.yaml
    └── ingress.yaml

values.yaml پارامترهایی رو expose می‌کنه که واقعاً بین محیط‌ها فرق دارن: image tag، resource request/limit، اندازه storage، IRSA role ARN، ingress hostname، و اینکه namespace و secret توسط chart مدیریت بشن یا خارج از اون. بدون chart یا باید چندین نسخه از manifest نگه داری، یا find-and-replace تو CI. با chart یه helm upgrade --install با یه فایل values اختصاصی هر محیط کافیه.

secret.create: false تو values این امکان رو می‌ده که مدیریت Secret رو از chart بگیری و External Secrets Operator یا Vault رو جاش بذاری. این رویکردیه که تو پروداکشن ازش استفاده می‌کنیم.

بعد از دو روز

Hermes رو deploy کردیم، با یه مدل Bedrock-backed پیکربندی کردیم، و چند سناریوی PoC رو رفتیم: نوشتن و اجرای shell script، خلاصه کردن مستندات داخلی، و هماهنگ کردن task های چند مرحله‌ای از طریق قابلیت subagent spawning. حلقه یادگیری همونطور که تبلیغ شده کار کرد. skill هایی که روز اول بهش یاد دادیم، روز دوم بدون اینکه prompt بزنیم تو context ظاهر شدن.

چیزی که نرسیدیم بررسی کنیم: ادغام کامل messaging gateway، cron scheduler، trajectory compression برای تولید داده‌های آموزشی، و ادغام‌های عمیق‌تر با MCP server. Hermes سطح گسترده‌ای داره که یه کارگاه دوروزه به زور خراشش می‌ده.

با داده‌های پروداکشن معنادار برمی‌گردم: اینکه تجمع skill ها در طول هفته‌ها چطور نگه می‌داره، آیا سیستم حافظه به کوریشن فعال نیاز داره، و پروفایل هزینه واقعی Bedrock در مقیاس بزرگ‌تر چقدره. اینجا خیلی چیزه که ارزش بررسی درست داره — به زودی برمی‌گردم.