Aller au contenu principal
Agentic AI

Déployer un agent Python sur AWS : Runtime, Lambda, microVM

Déployer un agent Python sur AWS : Runtime, Lambda, microVM

[!NOTE] TL;DR : Pydantic AI (v2.64.0) et Strands Agents (v1.63.0) se déploient sur AWS surtout derrière AgentCore Runtime (un microVM par session) ou Lambda (image conteneur). Le Runtime isole chaque session mais facture la mémoire jusqu'à 900 s d'inactivité en V1 ; Lambda facture à la milliseconde et ne facture rien entre deux tours, mais laisse la session à votre charge. Architecture, code et coûts calculés à partir des tarifs officiels ci-dessous.

La session qui dort mais facture

Au commencement était le requirements.txt, et il était déjà en conflit.

Le sujet que j'ai voulu trancher : déployer un agent Python sur AWS, oui, mais sur quelle cible, et à quel prix facturé ?

Mon agent de tri de tickets répondait en 1,8 s en local. En production, la première réponse après 20 min d'inactivité arrivait en 2,3 s. Le compteur, lui, ne s'arrêtait jamais.

Le mécanisme est documenté : par défaut, AgentCore Runtime conserve la session 900 s après la dernière requête (c'est le paramètre idleRuntimeSessionTimeout), et en plateforme V1 la mémoire est facturée pendant tout ce temps, à 0,00945 USD par Go-heure. Une session de 1,5 Go dépense donc environ 0,0059 USD de mémoire seule pendant la queue d'inactivité. (Et c'est précisément là que tout le monde se trompe.) Ça facture quand même.

La question centrale

Ma question principale était la suivante : sur AWS, où héberger un agent Python en production, entre AgentCore Runtime, Lambda et un microVM auto-hébergé, et combien chaque cible facture-t-elle réellement à la session ?

Les deux frameworks sont sortis le même jour, le 30 octobre 2026 : pydantic-ai v2.64.0 et strands-agents v1.63.0. Strands a publié 56 versions stables en douze mois. Je voulais des réponses chiffrées, pas des préférences de goût.

Pourquoi le Lambda nu coince

On imagine spontanément que Lambda suffit, puisque l'agent n'est « que » du Python. Les limites publiées disent le contraire.

Une invocation synchrone est plafonnée à 900 s, le payload à 6 Mo en bufferisé (200 Mo en streamé), et le streaming de réponse en Python exige un Lambda Web Adapter, contrairement à Node.js. Rien n'isole de « session » : l'historique de conversation, vous le reconstruisez à chaque appel, par exemple dans DynamoDB. Le cold start touche moins de 1 % des requêtes selon AWS. Ça paraît peu. Mais l'initialisation d'une image conteneur embarquant les deux SDK se paie à chaque environnement neuf.

J'ai passé deux jours à suspecter Bedrock avant de suspecter mon propre handler. Classique.

Bien que Lambda reste imbattable pour un appel d'outil court et sans état (0,20 USD le million de requêtes, durée facturée à la milliseconde), il vous renvoie toute la complexité de session dans le code.

L'architecture cible

J'ai retenu une architecture à deux voies : le Runtime pour les conversations longues, Lambda pour les tâches courtes déterministes.

flowchart LR
  Client["Client chat ou API"] --> APIGW["API Gateway"]
  APIGW --> RT["AgentCore Runtime, endpoint DEFAULT"]
  APIGW -. "tâche courte synchrone" .-> LF["Lambda, image conteneur, SnapStart"]
  RT --> MV["microVM V2 par runtimeSessionId, snapshot, max 2 vCPU 8 Go"]
  MV --> AG["Boucle agent : Strands ou Pydantic AI"]
  AG --> GW["AgentCore Gateway, exposé en MCP"]
  AG --> CI["Code Interpreter, bac à sable"]
  AG --> MEM["AgentCore Memory"]
  GW --> TOOLS["Outils : Lambda, API externes, MCP"]
  RT --> OBS["Observability OTel vers CloudWatch"]
  RT --> ID["Identity, JWT inbound, OAuth outbound"]

Chaque runtimeSessionId (33 caractères minimum) ouvre un microVM dédié, restauré depuis un snapshot en platformVersion V2, avec au plus 2 vCPU et 8 Go. Les outils passent par AgentCore Gateway, exposé en MCP (0,005 USD les 1000 invocations), et le code exécuté par l'agent passe par le Code Interpreter, facturé 0,0895 USD par vCPU-heure. L'observabilité sort en OpenTelemetry vers CloudWatch, et l'identité en entrée se branche sur Cognito ou un autre IdP. Le conteneur vise linux/arm64, écoute sur le port 8080, répond à /invocations en POST et à /ping en GET, et est poussé sur ECR.

L'analogie qui m'a servi : la session est un appartement meublé loué à la minute. On y entre avec sa valise, on travaille, on rend les clés, et le propriétaire efface la mémoire RAM à la sortie. Aucun voisin ne voit vos dossiers, parce que les murs sont en KVM, pas en carton.

Ce que j'ai construit

Premier bloc : un agent Pydantic AI typé, avec dépendances injectées et tools MCP fournis par le Gateway du tenant.

import httpx
from pydantic import BaseModel, Field
from pydantic_ai import Agent, FunctionToolset, RunContext
from pydantic_ai.mcp import MCPToolset
class AgentDeps(BaseModel):
    model_config = {"strict": True}
    tenant_id: str = Field(..., min_length=1, description="Locataire appelant.")
    gateway_mcp_url: str = Field(..., description="Endpoint MCP exposé par AgentCore Gateway.")
class TriageOutput(BaseModel):
    model_config = {"strict": True}
    category: str = Field(..., description="incident, billing ou other.")
    confidence: float = Field(..., ge=0.0, le=1.0)
    escalate: bool = Field(..., description="Escalader vers un humain.")
local_tools = FunctionToolset()
@local_tools.tool
async def fetch_customer_tier(ctx: RunContext[AgentDeps], customer_id: str) -> str:
    """Retourne le palier du client via l'API interne."""
    async with httpx.AsyncClient(timeout=5.0) as client:
        resp = await client.get(f"https://api.internal.example/customers/{customer_id}/tier", headers={"X-Tenant": ctx.deps.tenant_id})
        resp.raise_for_status()
        return resp.text
def gateway_toolset(ctx: RunContext[AgentDeps]) -> MCPToolset:
    """Toolset dynamique : le Gateway MCP propre au tenant de la session."""
    return MCPToolset(ctx.deps.gateway_mcp_url)
agent = Agent(
    "bedrock:global.anthropic.claude-sonnet-5",
    deps_type=AgentDeps,
    output_type=TriageOutput,
    toolsets=[local_tools, gateway_toolset],
    instructions="Trie le ticket entrant et renvoie un verdict typé.",
)
deps = AgentDeps(tenant_id="acme", gateway_mcp_url="https://gw-abc123.bedrock-agentcore.eu-west-1.amazonaws.com/mcp")
result = agent.run_sync("Ticket 4821 : double prélèvement sur octobre", deps=deps)
print(result.output.category, result.output.confidence)

[!WARNING] Ne passez jamais le payload brut à l'agent sans vérifier que le champ prompt est une chaîne : un bloc toolUse forgé dans le JSON exécuterait l'outil directement, sans raisonnement du modèle.

Deuxième bloc : un agent Strands emballé pour AgentCore Runtime, avec la session persistée sur S3. Voir le guide officiel de déploiement Strands sur AgentCore.

import uuid
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
from strands.session import S3SessionManager
app = BedrockAgentCoreApp()
def build_agent(session_id: str) -> Agent:
    """Un agent neuf par session, état persisté sur S3."""
    return Agent(
        model="global.anthropic.claude-sonnet-5",
        session_manager=S3SessionManager(session_id=session_id, bucket_name="samalgo-agent-sessions"),
        callback_handler=None,
    )
@app.entrypoint
def invoke(payload: dict, context) -> dict:
    """Entrée du Runtime : port 8080, routes /invocations et /ping."""
    session_id = getattr(context, "session_id", None) or str(uuid.uuid4())
    prompt = payload.get("prompt", "")
    if not isinstance(prompt, str):
        raise ValueError("le champ prompt doit être une chaîne")
    result = build_agent(session_id)(prompt)
    return {"result": str(result), "session_id": session_id}
if __name__ == "__main__":
    app.run()

Troisième bloc : le déploiement, d'abord en CLI, puis en boto3 pour le contrôle fin (V2, timeouts de cycle de vie).

pip install bedrock-agentcore strands-agents bedrock-agentcore-starter-toolkit
agentcore configure -e agent.py --disable-memory -r eu-west-1
agentcore deploy
agentcore invoke --runtime agent --payload '{"prompt":"Bonjour"}'
import boto3
client = boto3.client("bedrock-agentcore-control", region_name="eu-west-1")
response = client.create_agent_runtime(
    agentRuntimeName="support-agent",
    roleArn="arn:aws:iam::111122223333:role/AgentRuntimeRole",
    agentRuntimeArtifact={"containerConfiguration": {"containerUri": "111122223333.dkr.ecr.eu-west-1.amazonaws.com/support-agent:latest"}},
    networkConfiguration={"networkMode": "PUBLIC"},
    platformVersion="V2",
    lifecycleConfiguration={"idleRuntimeSessionTimeout": 600, "maxLifetime": 14400},
)
print(response["agentRuntimeArn"])

Pour le pas à pas complet du déploiement Runtime, voir aussi Déployer un agent IA en production avec AgentCore.

Les chiffres en production

Voici le calcul. J'ai comparé les cibles sur un scénario unique : une session de 10 min, dont 60 s de CPU actif à 1 vCPU, avec 1,5 Go de mémoire.

  • AgentCore V1 : CPU 60 × 0,0895 / 3600 = 0,001492 USD ; mémoire facturée 1500 s (600 actives + 900 d'inactivité) × 1,5 × 0,00945 / 3600 = 0,005906 USD. Total 0,00740 USD par session, soit 7,40 USD les 1000 sessions.
  • AgentCore V2 : CPU 60 × 0,1276 / 3600 = 0,002127 USD ; la mémoire est récupérée après 120 s d'inactivité, donc environ 720 s facturées × 1,5 × 0,0169 / 3600 = 0,00506 USD. Total 0,00719 USD, soit 7,19 USD les 1000 sessions.
  • Lambda : 3 Go alloués (les SDK d'agents sont gourmands), 90 s de handler, et rien entre deux tours : 90 × 3 × 0,0000166667 = 0,0045 USD, soit 4,50 USD les 1000 sessions, plus 0,20 USD par million de requêtes.

L'exemple publié par AWS sur sa page de pricing AgentCore donne 0,006703 USD par session de 10 min en V2 (90 % de temps d'attente I/O), soit 6,703 USD pour un million de sessions par mois. Runtime seul : les tokens du modèle se facturent en plus.

La facture d'une session ressemble à un compteur électrique ancien : le disque tourne tant que la session vit, même quand l'agent attend le modèle. La plateforme V2 a installé un compteur moderne qui ralentit après 120 s d'inactivité.

{
  "type": "bar",
  "data": {
    "labels": ["AgentCore Runtime V1", "AgentCore Runtime V2", "Lambda (3 Go, 90 s)"],
    "datasets": [{ "label": "Coût pour 1000 sessions (USD)", "data": [7.40, 7.19, 4.50], "backgroundColor": ["#3b82f6", "#8b5cf6", "#22c55e"] }]
  },
  "options": {
    "responsive": true,
    "plugins": { "title": { "display": true, "text": "Coût runtime pour 1000 sessions de 10 min (60 s CPU, 1,5 Go), en USD" } },
    "scales": { "y": { "title": { "display": true, "text": "USD" } } }
  }
}
Cible Isolation Démarrage Coût du scénario Charge opérationnelle Fit framework
AgentCore Runtime V1 (microVM) microVM dédié par session, mémoire vidée à la fin boot Firecracker inférieur à 125 ms 7,40 USD / 1000 sessions nulle, service managé tout framework
AgentCore Runtime V2 idem, restauration depuis snapshot constant, indépendant de la taille d'image 7,19 USD / 1000 sessions nulle tout framework
Lambda (image conteneur) environnement isolé, pas de session cold start sous 1 % des requêtes, SnapStart sub-seconde sur Python 3.12+ 4,50 USD / 1000 sessions faible bon si l'agent reste stateless
microVM auto-hébergé (Firecracker sur EC2) microVM, overhead inférieur à 5 Mio à mesurer selon votre infra prix EC2 plus votre gestion élevée tout framework

Autres limites publiées à garder sous le coude, selon les quotas officielles AgentCore et le fonctionnement des microVMs / plateforme V2 : image Docker du Runtime limitée à 2 Go, déploiement direct à 250 Mo compressé, payload 100 Mo, requête synchrone 15 min. Streaming 60 min, job asynchrone 8 h, 5000 sessions actives par compte en us-east-1, 25 créations de session par seconde, 1000 TPS côté data plane. Côté tarifs Lambda, la mémoire va de 128 Mo à 10240 Mo, avec l'équivalent 1 vCPU atteint à 1769 Mo.

Limites et chantiers ouverts

Je n'ai pas mesuré moi-même les cold starts de cet article : le 125 ms vient de la spécification Firecracker, le sub-seconde de la documentation SnapStart, et les tarifs des pages officielles consultées le 2 novembre 2026. Les prix et les quotas bougent chaque semaine.

Ce qui pourrait être testé ensuite : un benchmark V2 contre SnapStart sur une image de 900 Mo (le wheel strands-agents 1.63.0 pèse environ 900 Ko à lui seul), et l'effet d'un idleRuntimeSessionTimeout ramené à 60 s sur la facture V1.

V2 n'existe que dans 5 régions : us-east-1, us-east-2, us-west-2, eu-west-1 et ap-northeast-1. Le stockage de session est plafonné à 1 Go. Strands déployé sur Lambda ne streame pas les réponses : pour du streaming, il faut AgentCore Runtime ou Fargate. Pydantic AI publie environ une version par semaine (v2.63.0 le 27 octobre, v2.64.0 le 30 octobre) et embarque l'exécution durable sur huit moteurs, dont AWS Lambda.

Élargissement final

Plus généralement, le choix ne se joue pas entre frameworks mais entre qui porte l'état de la conversation. Celui qui porte l'état porte aussi la facture d'inactivité : c'est la leçon que V1 facturait plein pot et que V2 corrige en récupérant la mémoire après 120 s. Choisis ton runtime comme un bail : lis la clause de préavis avant de signer, bref, merci à tous !

Si vous voulez un audit de votre déploiement agentique avant la mise en production, nos services d'architecture AWS couvrent ce chantier, ou écrivez-nous directement.


Traitement en cours...
Traitement en cours...

Veuillez patienter

Opération sécurisée