Passer au contenu principal
MLOps & Cloud

MLOps serverless sur AWS : architecture et facture réelle

MLOps serverless sur AWS : architecture et facture réelle

[!NOTE] TL;DR : un million de prédictions mensuelles dans un pipeline ML sans instance, déployé en Terraform, revient à environ 45 USD par mois, contre 149 USD pour un endpoint réel allumé en continu. L'angle mort n'est pas le modèle : c'est Step Functions Standard, à 25 USD le million de transitions, placé dans le chemin synchrone. Voici l'architecture, le code de chiffrage et les quotas qui décident de votre facture avant vous.

Une prédiction coûte environ 0,00004 USD. Autour d'elle, l'orchestration peut coûter 0,000075 USD. Presque le double. Personne ne chiffre cette deuxième ligne avant d'ouvrir AWS Cost Explorer.

Je le sais parce que je l'ai fait à l'envers. Sur mon premier devis, le modèle tenait dans 2 Mo et la facture, elle, tenait dans une page entière. Je présente ici PISTE, le pipeline MLOps serverless que j'ai construit depuis (réentraînement hebdomadaire, déploiement, inférence, journal de prédictions, alertes), chiffré ligne par ligne avec les tarifs AWS relevés le 30 septembre 2026.

La question posée

Ma question tenait en une phrase : comment faire tourner un pipeline ML de bout en bout sans jamais allumer une instance, sans jamais payer une machine au repos, et sans que l'orchestration devienne le premier poste de dépense ?

La réponse tient en une discipline : chiffrer chaque poste avant de choisir le service, jamais après. Tout le reste n'est que de l'assemblage. Voici comment j'ai arbitré.

Le péage d'orchestration

On imagine spontanément que la facture vient du modèle. Les chiffres disent l'inverse. Step Functions Standard facture la transition d'état à 0,000025 USD dans us-east-1 (gratuite jusqu'à 4000 par mois), soit 25 USD le million. Un chemin d'inférence à trois états, validation puis appel puis journal, qui tourne un million de fois par mois vous coûte 75 USD d'orchestration pour 40 USD de calcul.

Express change la donne. L'exécution vaut 1 USD le million, plus 0,000001042 USD par tranche de 100 ms et par tranche de 64 Mo de mémoire. Sur ce même chemin, la facture retombe à 6,21 USD.

Critère Standard Express
Facturation 0,000025 USD par transition 1 USD par million d'exécutions + durée
Sémantique d'exécution exactement une fois au moins une fois
Durée maximale 1 an 5 minutes
Débit 2000 exécutions/s 100000 exécutions/s
Histoire d'exécution conservée envoyée vers CloudWatch Logs

Au commencement était la state machine, et elle facturait déjà. Step Functions est un péage : vous ne payez pas la route, vous payez le nombre de barrières traversées. Une machine à huit états passe huit fois la barrière pour la même livraison qu'une machine à trois états, et la différence se lit sur la facture avant de se lire dans le code.

Bien que Express soit douze fois moins cher sur ce chemin, je garde Standard côté entraînement. Quatre exécutions par mois à 32 transitions, cela fait 0,0008 USD, et l'exécution exactement-une-fois vaut bien ce prix-là. À noter que depuis le 1er août 2025, la phase d'INIT de Lambda est facturée pour toutes les configurations : les cold starts sont entrés dans le Billed Duration, ce qui change les petits budgets bien plus que les grands.

PISTE, l'architecture cible

Je conçois PISTE comme un plan de voie ferroviaire : une ligne lente et lourde pour l'entraînement, une ligne rapide et légère pour l'inférence. Les deux ne se croisent qu'au niveau du registre de modèles, et ce croisement est le seul point où un incident peut toucher la production.

flowchart TB
    S3[(S3 jeux entrainement)] --> EB{{EventBridge planification}}
    EB --> SFN[Step Functions STANDARD piste-train]
    SFN -->|createTrainingJob.sync| TR[SageMaker Training ml.c5.xlarge]
    TR --> REG[Modele + EndpointConfig serverless]
    REG --> EP[Endpoint serverless MaxConcurrency 20]
    C[Client] --> L[Lambda x86 pre-traitement]
    L --> EP
    L --> DDB[(DynamoDB journal de predictions)]
    EP --> DDB
    EP --> CW[CloudWatch OverheadLatency ModelSetupTime]
    CW --> AL[Alarmes]
    AL --> SNS[SNS piste-alerts]
    SNS --> SQS[SQS file de reprise]
    SFN -.echec.-> SNS

Côté entraînement, Step Functions exploite l'intégration optimisée SageMaker : arn:aws:states:::sagemaker:createTrainingJob.sync attend la fin du job, et les actions CreateModel, CreateEndpointConfig et UpdateEndpoint sont couvertes au même titre. Détail qui coûte cher si on l'ignore : le pattern .waitForTaskToken n'est pas supporté sur ces API. Côté inférence, aucun orchestrateur dans le chemin synchrone. Une Lambda prépare la requête, l'endpoint serveless répond, DynamoDB journalise.

Les briques déployées

J'ai repris le schéma d'un pipeline d'extraction documentaire que je maintiens en production : quatre fonctions Lambda et une couche commune, une machine Step Functions déclenchée par EventBridge, des sous-réseaux privés, des groupes de logs chiffrés par KMS et la télémétrie X-Ray activée. Le paix ZIP des fonctions part de S3 plutôt que de transiter par l'API Terraform, dont le payload est limité.

# PISTE - Terraform 1.9, provider hashicorp/aws 6.x
# Orchestrateur d'entraînement : 4 exécutions par mois, facture négligeable
resource "aws_sfn_state_machine" "piste_train" {
  name     = "piste-train"
  role_arn = aws_iam_role.sfn.arn
  type     = "STANDARD"
  definition = templatefile("${path.module}/piste_train.asl.json.tpl", {
    train_role_arn = aws_iam_role.sagemaker.arn
    alert_topic    = aws_sns_topic.piste_alerts.arn
  })
}
# Journal de prédictions : mode à la demande, tarif divisé par deux le 1er nov. 2024
resource "aws_dynamodb_table" "piste_journal" {
  name         = "piste-journal"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "prediction_id"
  attribute {
    name = "prediction_id"
    type = "S"
  }
}
# Inférence synchrone : plafonné pour ne pas épuiser les 1000 unités du compte
resource "aws_lambda_function" "piste_predict" {
  function_name                  = "piste-predict"
  role                           = aws_iam_role.predict.arn
  handler                        = "handler.predict"
  runtime                        = "python3.12"
  architectures                  = ["x86_64"]
  memory_size                    = 1024
  timeout                        = 10
  reserved_concurrent_executions = 50
  snap_start {
    apply_on = "PublishedVersions"
  }
}

Les 50 unités réservées ci-dessus sortent du pool réservable (900 au maximum sur un compte à 1000 d'exécutions concurrentes, les 100 restants servant de garde-fou pour les fonctions sans réglage). Le déploiement de l'endpoint, lui, passe par CreateEndpointConfig, et ce payload mérite d'être validé avant d'être envoyé.

from typing import Literal
from pydantic import BaseModel, Field, model_validator
class ServerlessEndpointConfig(BaseModel):
    """Champ ServerlessConfig de CreateEndpointConfig, validé avant appel API."""
    model_config = {"strict": True}
    memory_size_mb: Literal[1024, 2048, 3072, 4096, 5120, 6144] = Field(default=2048, description="Seules ces six valeurs sont acceptées, par pas de 1 Go.")
    max_concurrency: int = Field(default=20, ge=1, le=200, description="Plafond par endpoint. 200 maximum, 50 endpoints par région.")
    provisioned_concurrency: int = Field(default=0, ge=0, le=200, description="Instances gardées au chaud: 0,000010 USD par seconde à 2 Go.")
    @model_validator(mode="after")
    def provisioned_within_max(self) -> "ServerlessEndpointConfig":
        if self.provisioned_concurrency > self.max_concurrency:
            raise ValueError("ProvisionedConcurrency doit rester <= MaxConcurrency, règle de l'API SageMaker")
        return self

Le chiffrage, lui, est un modèle comme un autre. Je le garde typé et validé, parce qu'une unité mal placée dans un devis se réveille trois mois plus tard dans Cost Explorer.

from pydantic import BaseModel, Field
class UsageProfile(BaseModel):
    """Charge mensuelle de référence, tout en unités par mois."""
    predictions: int = Field(default=1000000, ge=0, description="Appels d'inférence par mois.")
    inference_seconds: float = Field(default=100000.0, gt=0, description="predictions x 0,1 s de traitement.")
    payload_gb: float = Field(default=30.0, ge=0, description="Entrée plus sortie, environ 30 Ko par prédiction.")
class Tariff(BaseModel):
    """Tarifs us-east-1 relevés le 30 septembre 2026 sur les pages de prix AWS."""
    serverless_second: float = 0.00004
    serverless_gb: float = 0.016
    lambda_gb_second_x86: float = 0.0000166667
    lambda_request: float = 0.0000002
    sfn_standard_transition: float = 0.000025
    sfn_express_request: float = 0.000001
    sfn_express_100ms_64mb: float = 0.000001042
def monthly_bill(usage: UsageProfile, tariff: Tariff, states_per_run: int = 3) -> dict[str, float]:
    """Trois mises en scène du même modèle, à charge constante, en USD par mois."""
    serverless = usage.inference_seconds * tariff.serverless_second + usage.payload_gb * tariff.serverless_gb
    standard = usage.predictions * states_per_run * tariff.sfn_standard_transition
    express = usage.predictions * tariff.sfn_express_request + usage.predictions * 5 * tariff.sfn_express_100ms_64mb
    lam = usage.predictions * tariff.lambda_request + usage.inference_seconds * tariff.lambda_gb_second_x86
    return {"sagemaker_serverless": round(serverless, 2), "sfn_standard_en_ligne": round(standard, 2), "sfn_express_en_ligne": round(express, 2), "lambda_modele_leger": round(lam, 2)}

Résultats chiffrés

Ce tableau est une estimation calculée avec le script ci-dessus, pas une facture de production. Hypothèses : 1 million de prédictions par mois, 0,1 s d'inférence, 30 Ko de payload, quatre réentraînements de 30 minutes sur ml.c5.xlarge.

Poste Calcul USD / mois
SageMaker Serverless, 2048 Mo, à la demande 100000 s x 0,00004 + 30 Go x 0,016 40,48
Endpoint réel ml.c5.xlarge, 24/7 730 h x 0,204 148,92
Step Functions Standard dans le chemin synchrone 3000000 transitions x 0,000025 75,00
Step Functions Express, même chemin 1 M x 0,000001 + 5 tranches x 100 ms 6,21
Lambda x86 1024 Mo, modèle léger 0,20 de requêtes + 100000 GB-s 1,87
DynamoDB, journal 1 M d'écritures + 0,5 M de lectures 0,69
CloudWatch, 10 métriques et 5 alarmes 10 x 0,30 + 5 x 0,10 3,50
Entraînement ml.c5.xlarge 4 x 0,5 h x 0,204 0,41
{
  "type": "bar",
  "data": {
    "labels": ["Réel ml.c5.xlarge 24/7", "Step Functions Standard en ligne", "SageMaker Serverless à la demande", "Lambda x86, modèle léger"],
    "datasets": [{
      "label": "USD par mois, estimation sur 1M de prédictions",
      "data": [148.92, 75.0, 40.48, 1.87],
      "backgroundColor": ["#64748b", "#ef4444", "#3b82f6", "#10b981"]
    }]
  },
  "options": {
    "responsive": true,
    "plugins": {
      "title": { "display": true, "text": "PISTE : quatre mises en scène du même modèle" },
      "legend": { "display": false }
    },
    "scales": {
      "y": { "title": { "display": true, "text": "USD par mois (estimation)" } }
    }
  }
}

Le total PISTE, orchestration d'entraînement comprise, se pose autour de 45 USD par mois. Le même trafic sur un endpoint réel ml.c5.xlarge allumé en continu coûte 148,92 USD, soit trois fois plus, et vous facture 730 heures de grève administrative pendant lesquelles votre modèle ne prédit rien. Avec un modèle léger embarqué dans la Lambda (schéma ONNX ou arbre boosté), le calcul d'inférence tombe à 1,87 USD.

La latence a son prix caché, elle aussi. AWS a mesuré sur son propre banc environ 6 s de premier appel sur un endpoint serverless à la demande, contre environ 200 ms avec Provisioned Concurrency. Cette option coûte 0,000010 USD par seconde et par unité à 2 Go (exemples 13 et 14 de la page de prix SageMaker), ce qui revient à racheter du silence. Côté Lambda, une campagne de réglage mémoire avec aws-lambda-power-tuning coûte environ 0,0003 USD pour six valeurs testées : c'est le meilleur retour sur investissement de tout le pipeline.

Limites et protocoles restants

Le serveless inférence accepte 4 Mo de payload et 60 s de traitement, contre 25 Mo côté réel. Au-delà, PISTE casse et il faut basculer sur un endpoint réel ou du batch transform. Les quotas régionaux méritent une vérification avant tout design : à Paris, la concurrence totale des endpoints serverless est de 500 par compte, contre 1000 dans les grandes régions, avec 200 au maximum par endpoint et 50 endpoints par région. Lambda plafonne à 1000 exécutions concurrentes par compte et à 10000 requêtes par seconde, avec un ajout de 1000 environnements par tranche de 10 s.

Reste à savoir si mon estimation du chemin Express tient sous charge réelle : le modèle suppose 0,5 s d'exécution et moins de 64 Mo, deux plafonds que le premier pic dépassera sans doute. Cela pourrait être facilement testé avec le protocole du SageMaker Serverless Inference Benchmarking Toolkit (paramètre cold_start_delay à 600 s, métrique ModelSetupTime au CloudWatch), puis comparé à la métrique OverheadLatency en production. Deux réserves enfin : les tarifs cités viennent de us-east-1 et l'écart avec eu-west-3 reste à vérifier, et je n'ai pas encore isolé la part des INIT facturés depuis août 2025 dans mon propre trafic.

Ce qui survit à l'exemple

Plus généralement, le MLOps serverless ne s'achète pas au prix du modèle mais au prix des transitions entre services. Chaque barrière traversée facturée, chaque quota régional et chaque milliseconde d'initialisation décident de la marge, bien avant le choix de l'algorithme. Chiffrez les transitions avant de choisir l'orchestrateur, et la facture restera un détail d'implémentation !

Quand tout est éteint, il reste l'addition, et personne ne l'a jamais mise en veille. Si vous voulez que je chiffre votre architecture avant que Cost Explorer ne le fasse à votre place, jetez un œil à mes offres ou écrivez-moi. Pour deux lectures complémentaires sur le coût d'inférence et les pipelines de données sous contrainte mémoire, voir SageMaker async inference : le piège du scale-to-zero et Pipeline fMRI : de BIDS à l'API de recherche en mémoire bornée.

Quelques sources, si vous voulez vérifier mes chiffres : les tarifs Step Functions et la page des prix Lambda, l'intégration SageMaker de Step Functions, la documentation des endpoints serverless, l'annonce de la facturation de l'INIT et aws-lambda-power-tuning.


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

Veuillez patienter

Opération sécurisée