Aller 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 pipeline MLOps serverless qui traite 1 million de prédictions par mois, réentraînement hebdomadaire compris, revient à environ 47 USD par mois (tarifs us-east-1 relevés le 3 novembre 2026), contre 148,92 USD pour un endpoint réel allumé en continu. Le poste que personne ne chiffre n'est pas le modèle, c'est l'orchestration : Step Functions Standard facture 75 USD le million d'appels sur un chemin à trois états, contre 1,52 USD en Express. La discipline qui tient la facture : aucun orchestrateur dans le chemin synchrone, Standard réservé à l'entraînement, chaque poste chiffré avant le choix du service.

Une prédiction coûte environ 0,00004 USD. Autour d'elle, l'orchestration peut coûter 0,000075 USD, presque le double, et cette deuxième ligne n'apparaît dans aucun devis avant que Cost Explorer ne l'exhibe. Mon pipeline MLOps serverless absorbe un million de prédictions mensuelles sans une seule instance allumée, et sa facture totale tourne autour de 47 USD par mois, réentraînement compris, contre 148,92 USD pour un endpoint réel ml.c5.xlarge jamais éteint.

Je le sais parce que j'ai d'abord fait l'inverse. 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 3 novembre 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 dans une discipline, pas dans une technologie : 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 0,000025 USD par transition d'état dans us-east-1 (4000 transitions gratuites 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 coûte 75 USD d'orchestration pour 40 USD de calcul. Les tarifs Step Functions sont publics. Ils se lisent rarement avant l'architecture.

Express change la donne. L'exécution vaut 1 USD le million, plus 0,0000001042 USD par tranche de 100 ms et par tranche de 64 Mo de mémoire (0,00001667 USD par GB-seconde, arrondi au-dessus). Sur ce même chemin à trois états, en supposant 0,5 s d'exécution sous 64 Mo, la facture retombe à 1,52 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 90 jours 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 qu'Express soit près de cinquante fois moins cher sur ce chemin, je garde Standard côté entraînement. Quatre exécutions par mois à 32 transitions représentent 0,003 USD, et la sémantique exactement-une-fois vaut bien ce prix pour un job SageMaker. À noter aussi que depuis le 1er août 2025, la phase d'INIT de Lambda est facturée pour les fonctions ZIP en runtime managé, comme l'annonce le billet AWS Compute : les cold starts sont entrés dans la Billed Duration, ce qui pénalise les petits budgets bien plus que les grands. SnapStart (disponible pour Python 3.12 et plus) atténue le coup, puisque la reprise depuis un snapshot court-circuite la phase d'INIT à chaque invocation.

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 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, pas plus que sur Express d'ailleurs. Côté inférence, aucun orchestrateur dans le chemin synchrone. Une Lambda prépare la requête, l'endpoint serverless répond, DynamoDB journalise.

Les briques déployées

J'ai repris le squelette d'un pipeline d'extraction documentaire que je maintiens en production : quatre fonctions Lambda et une couche partagée, une machine Step Functions déclenchée par EventBridge, des sous-réseaux privés, des groupes de logs chiffrés par KMS, la télémétrie X-Ray activée. Le paquet 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 exécutions concurrentes, les 100 restants servant de garde-fou aux 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 3 novembre 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.0000001042
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 : un 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 3 M de transitions x 0,000025 75,00
Step Functions Express, même chemin 1 M x 0,000001 + 5 tranches de 100 ms x 0,0000001042 1,52
Lambda x86 1024 Mo, prétraitement 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
Orchestration d'entraînement, Standard 4 x 32 transitions x 0,000025 0,003
Total PISTE, scénario serverless somme des lignes applicables 46,96
{
  "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 47 USD par mois. Le même trafic sur un endpoint réel ml.c5.xlarge allumé en continu coûte 148,92 USD, soit plus de trois fois, 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 même à 1,87 USD.

La latence a son prix caché, elle aussi. À froid, un endpoint serverless à la demande doit charger le modèle avant de répondre : sur le banc du Benchmarking Toolkit officiel, ce premier appel se compte en secondes. Une unité Provisioned Concurrency répond en quelques centaines de millisecondes, et elle coûte 0,000010 USD par seconde à 2 Go (exemples 13 et 14 de la page de prix SageMaker) : c'est littéralement du silence que l'on rachète. Côté Lambda, une campagne de réglage mémoire avec aws-lambda-power-tuning se paie en fractions de centime pour six valeurs testées. C'est le meilleur retour sur investissement du pipeline.

Limites et protocoles restants

Le serverless d'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, de l'asynchrone (1 Go, une heure) 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, avec une montée en charge par paliers de 10 s que le premier pic de trafic rappellera à votre budget.

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 (métrique ModelSetupTime dans 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, 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 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.

Questions fréquentes

Step Functions Standard ou Express pour un pipeline ML ?

Standard pour l'entraînement (0,000025 USD par transition, sémantique exactement-une-fois, durée jusqu'à un an), Express pour tout ce qui tourne en ligne (1 USD le million d'exécutions plus la durée). Sur un chemin synchrone à trois états et un million d'appels par mois, Standard coûte 75 USD contre 1,52 USD en Express.

Combien coûtent réellement un million de prédictions par mois en serverless ?

Environ 47 USD par mois : SageMaker Serverless Inference à 2048 Mo pour 40,48 USD, la Lambda de prétraitement pour 1,87 USD, le journal DynamoDB pour 0,69 USD, la supervision CloudWatch pour 3,50 USD et quatre réentraînements pour 0,41 USD. Un endpoint réel ml.c5.xlarge jamais éteint coûte 148,92 USD pour le même trafic.

Quels quotas peuvent casser une architecture serverless sur AWS ?

Chaque endpoint serverless plafonne à 200 invocations concurrentes, avec 50 endpoints maximum par région, et la concurrence totale du compte vaut 500 à Paris contre 1000 dans les grandes régions. Le payload est limité à 4 Mo et le traitement à 60 s, contre 25 Mo côté réel.

La facturation de l'INIT Lambda change-t-elle les petits budgets ?

Oui, depuis le 1er août 2025 la phase d'initialisation des fonctions ZIP en runtime managé est incluse dans la Billed Duration. L'impact reste faible si les cold starts sont rares, mais sur une facture de 47 USD par mois, chaque INIT facturé se voit.

Quand faut-il abandonner le serverless pour un endpoint réel ?

Dès que le trafic devient soutenu (l'instance payée 24/7 redevient compétitive), que le payload dépasse 4 Mo, ou que la latence de cold start en secondes est incompatible avec le contrat de service. Une unité Provisioned Concurrency, à 0,000010 USD par seconde à 2 Go, dépanne, mais elle réintroduit une facture d'attente.

Préparé par Vector, l'agent de rédaction de SamAlgo, augmenté et édité à partir des travaux de recherche, du code et des publications de SamAlgo.


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

Veuillez patienter

Opération sécurisée