Model2Prod IaC Academy Blog Jouer gratuitement
← Tous les articles

Blog Model2Prod

Déployer un modèle sur SageMaker avec Terraform

Servez un modèle d'IA sur un endpoint AWS SageMaker avec Terraform : rôle IAM, modèle, configuration et endpoint, avec le code complet expliqué.

L'équipe Model2Prod7 min de lecture

Votre modèle tourne dans un notebook. Pour qu'une application l'interroge, il doit être servi derrière un endpoint : une adresse HTTPS stable, sur des machines dimensionnées pour l'inférence. Sur AWS, c'est le rôle de SageMaker. Et pour que ce déploiement soit reproductible, relu et versionné, on le décrit avec Terraform plutôt qu'en cliquant dans la console.

Ce guide déploie un modèle déjà entraîné (ici Mistral 7B) sur un endpoint SageMaker temps réel. Vous aurez à la fin un projet Terraform complet, prêt à être versionné dans Git, et vous saurez éviter les erreurs qui font échouer la plupart des premiers déploiements.

Ce que vous allez construire

Cinq ressources AWS, créées dans cet ordre :

  1. un rôle IAM que SageMaker endosse pour travailler ;
  2. une politique qui donne à ce rôle le strict nécessaire : lire les artefacts du modèle, télécharger l'image du conteneur, écrire ses journaux ;
  3. un modèle SageMaker : quelle image de conteneur, avec quels artefacts ;
  4. une configuration d'endpoint : sur quel type de machine, combien d'instances ;
  5. l'endpoint lui-même, l'adresse que votre application appellera.

Ce qu'il faut avant de commencer

  • les artefacts du modèle (model.tar.gz) dans un bucket S3 ;
  • l'adresse de l'image de conteneur d'inférence adaptée à votre framework, par exemple une image PyTorch ou Hugging Face fournie par AWS ;
  • Terraform 1.5 ou plus récent, et un accès AWS configuré (aws configure ou variables d'environnement) ;
  • un quota suffisant pour le type d'instance choisi (voir les pièges plus bas).

1. Organiser le projet

Un dossier, cinq fichiers. Terraform lit tous les fichiers .tf du dossier : les séparer ne sert qu'à vous y retrouver.

sagemaker-mistral/
├── versions.tf    # version de Terraform et du provider AWS
├── variables.tf   # ce qui change d'un environnement à l'autre
├── iam.tf         # rôle et permissions de SageMaker
├── main.tf        # modèle, configuration, endpoint
└── outputs.tf     # ce que Terraform affiche à la fin

2. Le provider et les variables

On fixe la version du provider pour que le même code donne le même résultat dans six mois :

# versions.tf
terraform {
  required_version = ">= 1.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = var.region
}

Tout ce qui change entre la recette et la production passe en variables, jamais en dur dans les ressources :

# variables.tf
variable "region" {
  type    = string
  default = "eu-west-1"
}

variable "models_bucket" {
  description = "Bucket S3 qui contient les artefacts des modèles"
  type        = string
}

variable "model_prefix" {
  description = "Dossier du modèle dans le bucket"
  type        = string
  default     = "mistral-7b"
}

variable "inference_image" {
  description = "Image de conteneur d'inférence (adresse ECR complète)"
  type        = string
}

variable "instance_type" {
  type    = string
  default = "ml.g5.xlarge"
}

3. Le rôle IAM, avec le moindre privilège

SageMaker a besoin d'une identité. On crée un rôle que seul le service SageMaker peut endosser :

# iam.tf
resource "aws_iam_role" "sagemaker" {
  name = "sagemaker-inference-mistral"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Action    = "sts:AssumeRole"
      Principal = { Service = "sagemaker.amazonaws.com" }
    }]
  })
}

Beaucoup de tutoriels lui attachent ensuite la politique gérée AmazonSageMakerFullAccess. C'est pratique, mais beaucoup trop large pour un endpoint d'inférence. Donnez-lui seulement ce dont il a besoin :

data "aws_iam_policy_document" "inference" {
  statement {
    sid       = "LireLeModele"
    actions   = ["s3:GetObject"]
    resources = ["arn:aws:s3:::${var.models_bucket}/${var.model_prefix}/*"]
  }

  statement {
    sid = "TelechargerLImage"
    actions = [
      "ecr:GetAuthorizationToken",
      "ecr:BatchCheckLayerAvailability",
      "ecr:GetDownloadUrlForLayer",
      "ecr:BatchGetImage",
    ]
    resources = ["*"]
  }

  statement {
    sid = "EcrireLesJournaux"
    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
      "cloudwatch:PutMetricData",
    ]
    resources = ["*"]
  }
}

resource "aws_iam_role_policy" "inference" {
  name   = "inference"
  role   = aws_iam_role.sagemaker.id
  policy = data.aws_iam_policy_document.inference.json
}

La lecture S3 est limitée au dossier de ce modèle : si le rôle était un jour détourné, il ne donnerait accès à rien d'autre.

4. Le modèle

Le modèle associe une image de conteneur et des artefacts. Remarquez qu'on ne lui donne pas de nom : Terraform en génère un unique, ce qui évite un conflit le jour où le modèle doit être remplacé.

# main.tf
resource "aws_sagemaker_model" "mistral" {
  execution_role_arn = aws_iam_role.sagemaker.arn

  primary_container {
    image          = var.inference_image
    model_data_url = "s3://${var.models_bucket}/${var.model_prefix}/model.tar.gz"
  }

  depends_on = [aws_iam_role_policy.inference]
}

Le depends_on garantit que les permissions existent avant que SageMaker ne tente de lire le modèle. Sans lui, la création peut échouer au premier apply, puis réussir au second, ce qui est le pire des comportements.

5. La configuration d'endpoint et l'endpoint

resource "aws_sagemaker_endpoint_configuration" "mistral" {
  name_prefix = "mistral-7b-"

  production_variants {
    variant_name           = "principal"
    model_name             = aws_sagemaker_model.mistral.name
    initial_instance_count = 1
    instance_type          = var.instance_type
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_sagemaker_endpoint" "mistral" {
  name                 = "mistral-7b"
  endpoint_config_name = aws_sagemaker_endpoint_configuration.mistral.name
}

Deux détails font toute la différence :

  • une configuration d'endpoint ne se modifie pas : changer le type d'instance oblige à en créer une nouvelle. Avec name_prefix, chaque nouvelle configuration reçoit un nom unique ;
  • create_before_destroy crée la nouvelle configuration avant de supprimer l'ancienne. L'endpoint bascule de l'une à l'autre sans interruption de service, puis l'ancienne est détruite.

L'endpoint, lui, garde un nom fixe : c'est l'adresse stable que votre application connaît.

6. Les sorties

# outputs.tf
output "endpoint_name" {
  value = aws_sagemaker_endpoint.mistral.name
}

output "endpoint_arn" {
  value = aws_sagemaker_endpoint.mistral.arn
}

7. Planifier, puis appliquer

terraform init
terraform fmt
terraform validate
terraform plan -out=tfplan
terraform apply tfplan

Relisez toujours le plan avant l'apply : il annonce exactement ce qui sera créé, modifié ou détruit. Ici, il doit annoncer Plan: 5 to add, 0 to change, 0 to destroy. Enregistrer le plan dans un fichier (-out) garantit que l'apply exécute exactement ce que vous avez relu.

La création de l'endpoint prend plusieurs minutes, le temps de démarrer l'instance et de charger le modèle.

8. Tester l'endpoint

Une fois l'endpoint au statut InService, interrogez-le avec la CLI AWS. Le format du corps dépend du conteneur ; pour un conteneur Hugging Face de génération de texte :

aws sagemaker-runtime invoke-endpoint \
  --endpoint-name mistral-7b \
  --content-type application/json \
  --cli-binary-format raw-in-base64-out \
  --body '{"inputs": "Explique Terraform en une phrase.", "parameters": {"max_new_tokens": 80}}' \
  reponse.json

cat reponse.json

Dans votre application, utilisez le SDK AWS de votre langage avec la même opération, InvokeEndpoint.

Les pièges à éviter

  • Le quota d'instances à zéro. Sur un compte récent, le quota des instances GPU pour les endpoints vaut souvent 0. L'apply échoue alors avec une erreur ResourceLimitExceeded. Demandez une augmentation dans Service Quotas (« ml.g5.xlarge for endpoint usage ») avant de déployer.
  • Un endpoint au statut Failed. Le conteneur n'a pas démarré : artefacts introuvables, image incompatible, mémoire insuffisante. Les journaux sont dans CloudWatch, groupe /aws/sagemaker/Endpoints/mistral-7b.
  • Un nom fixe sur la configuration. Sans name_prefix ni create_before_destroy, la moindre modification échoue sur un conflit de nom, ou coupe le service le temps du remplacement.
  • L'endpoint oublié. Il est facturé à l'heure tant qu'il existe, même sans aucune requête. terraform destroy le supprime proprement, avec tout le reste.
  • Des valeurs en dur. Image, bucket et type d'instance en variables : le même code sert en recette (petite instance) et en production.

Questions fréquentes

Combien coûte un endpoint SageMaker ?

Un endpoint temps réel est facturé à l'heure, par instance, tant qu'il existe. Une instance GPU comme ml.g5.xlarge coûte de l'ordre d'un dollar et demi de l'heure selon la région, soit plus de 1 000 dollars par mois si elle reste allumée en permanence. Consultez la page de tarification SageMaker pour votre région, et détruisez les endpoints de test après usage.

Quelle différence entre un endpoint temps réel et un endpoint serverless ?

Un endpoint temps réel garde des instances allumées en continu : réponse rapide, coût fixe. Un endpoint serverless démarre à la demande et n'est facturé qu'à l'usage, mais il ne propose pas de GPU et le premier appel après une période d'inactivité est plus lent. Pour un LLM comme Mistral 7B, le temps réel sur GPU reste la norme.

Pourquoi Terraform recrée-t-il la configuration d'endpoint à chaque modification ?

Parce que SageMaker ne permet pas de modifier une configuration existante. Terraform crée donc une nouvelle configuration, met l'endpoint à jour pour l'utiliser, puis supprime l'ancienne. Avec name_prefix et create_before_destroy, cette opération se fait sans interruption.

Peut-on déployer un modèle Hugging Face sans construire son propre conteneur ?

Oui. AWS fournit des images prêtes à l'emploi pour Hugging Face, dont une dédiée à la génération de texte. On indique alors le modèle à charger par des variables d'environnement dans le bloc primary_container (environment = { HF_MODEL_ID = "mistralai/Mistral-7B-Instruct-v0.3" }), sans artefact model.tar.gz.

Attention aux modèles en accès restreint. Mistral 7B Instruct v0.3 est en accès libre (vérifié le 31 octobre 2026), mais d'autres, comme Llama de Meta ou Gemma de Google, demandent d'accepter leurs conditions sur la page du modèle, puis de passer un jeton Hugging Face au conteneur : HUGGING_FACE_HUB_TOKEN = var.hf_token dans environment, avec une variable déclarée sensitive = true. Sans jeton, le téléchargement est refusé et l'endpoint passe au statut Failed ; l'erreur n'apparaît que dans les journaux CloudWatch. Vérifiez toujours la page du modèle avant de déployer, et utilisez un jeton en lecture seule : il est stocké dans l'état Terraform et visible dans la configuration du modèle SageMaker.

Comment supprimer l'endpoint et arrêter la facturation ?

Lancez terraform destroy dans le dossier du projet : Terraform supprime l'endpoint, sa configuration, le modèle et le rôle, dans l'ordre inverse de leur création. Vérifiez ensuite dans la console SageMaker qu'il ne reste aucun endpoint actif.

Pour aller plus loin

Ce déploiement est l'une des missions du Sprint AWS de Model2Prod. Vous l'écrivez dans un terminal Terraform simulé, vous lancez le plan et l'apply, et vous voyez le résultat, sans compte AWS ni facture. La suite du parcours enchaîne avec un chatbot serverless sur Bedrock, puis les mêmes cas d'usage sur Google Cloud et Azure.

Le chapitre 1 est gratuit. Pour débloquer le Sprint AWS et les chapitres suivants, voyez l'offre fondateur : un paiement unique, accès à vie.

  • terraform
  • aws
  • sagemaker
  • mlops