<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Paul SANTUS</title>
    <description>The latest articles on Forem by Paul SANTUS (@psantus).</description>
    <link>https://forem.com/psantus</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1338515%2F8abdaf33-0c48-4f84-aa29-7a881090986e.jpeg</url>
      <title>Forem: Paul SANTUS</title>
      <link>https://forem.com/psantus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/psantus"/>
    <language>en</language>
    <item>
      <title>Remplacez AWS Transfer Family SFTP par S3 Files + Atmoz SFTP</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Wed, 08 Apr 2026 08:37:49 +0000</pubDate>
      <link>https://forem.com/aws-builders/remplacez-aws-transfer-family-sftp-par-s3-files-atmoz-sftp-49n</link>
      <guid>https://forem.com/aws-builders/remplacez-aws-transfer-family-sftp-par-s3-files-atmoz-sftp-49n</guid>
      <description>&lt;p&gt;AWS vient de lancer l'une des fonctionnalités de stockage les plus attendues : &lt;strong&gt;S3 Files&lt;/strong&gt;. S3 Files place une interface de système de fichiers compatible EFS directement devant vos buckets S3. Quand j'ai entendu parler de cette fonctionnalité (dans le cadre du &lt;a href="https://www.linkedin.com/posts/coreystrausman_aws-s3-cloudcomputing-activity-7447375367155351552-Uuyo?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAAHqFuoBYjYsx4cq4zQ6SBklTKN3Pd_juYs" rel="noopener noreferrer"&gt;programme Community Builders&lt;/a&gt;) j'ai tout de suite pensé au cas d'usage du SFTP sur AWS.&lt;/p&gt;

&lt;p&gt;Si vous payez actuellement AWS Transfer Family pour donner à vos partenaires un accès SFTP à S3, lisez attentivement ce qui suit. Il existe désormais une alternative nettement moins chère et plus puissante.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qu'est-ce que S3 Files ?
&lt;/h2&gt;

&lt;p&gt;S3 Files crée un système de fichiers NFS haute performance adossé à un bucket S3. Voyez-le comme une couche EFS qui lit et écrit directement dans les objets S3, avec une synchronisation bidirectionnelle automatique. Tout fichier écrit via le système de fichiers apparaît comme un objet S3, et tout objet uploadé dans S3 devient visible via le système de fichiers.&lt;/p&gt;

&lt;p&gt;Les propriétés clés :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latence sub-milliseconde&lt;/strong&gt; pour les opérations fichier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synchronisation automatique&lt;/strong&gt; entre le système de fichiers et le bucket S3 (via EventBridge sous le capot)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Montable sur ECS Fargate&lt;/strong&gt;, ECS Managed Instances, EKS et EC2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protocole NFS standard&lt;/strong&gt; — pas de client spécial nécessaire côté compute (ECS/EKS le gèrent nativement)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Points d'accès&lt;/strong&gt; avec contrôle d'identité POSIX (uid/gid)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Versioning&lt;/strong&gt; requis et exploité pour la cohérence&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Le problème avec AWS Transfer Family
&lt;/h2&gt;

&lt;p&gt;AWS Transfer Family est la solution "officielle" pour exposer des endpoints SFTP adossés à S3. Ça fonctionne, mais avec des inconvénients sérieux :&lt;/p&gt;

&lt;h3&gt;
  
  
  C'est cher
&lt;/h3&gt;

&lt;p&gt;Transfer Family facture &lt;strong&gt;0,30 $/heure&lt;/strong&gt; rien que pour l'endpoint — soit &lt;strong&gt;~216 $/mois&lt;/strong&gt; avant même de transférer un seul octet. Ajoutez les coûts de transfert de données par-dessus. Pour un service que beaucoup d'équipes utilisent pour quelques dépôts de fichiers quotidiens, c'est difficile à justifier.&lt;/p&gt;

&lt;h3&gt;
  
  
  C'est une boîte noire
&lt;/h3&gt;

&lt;p&gt;Vous obtenez un endpoint SFTP, mais vous ne contrôlez pas le serveur. L'authentification personnalisée nécessite des hooks Lambda. Le logging est limité. Vous ne pouvez pas vous connecter en SSH pour débugger. Vous ne pouvez pas personnaliser le comportement du serveur SFTP, ajouter des scripts de pré/post-traitement, ou exécuter quoi que ce soit à côté.&lt;/p&gt;

&lt;h2&gt;
  
  
  La nouvelle architecture : atmoz/sftp + S3 Files sur ECS Fargate
&lt;/h2&gt;

&lt;p&gt;Voici ce que nous allons mettre en place :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0j8tr2khodyf4lw8cmds.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0j8tr2khodyf4lw8cmds.png" alt=" " width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Les composants :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bucket S3&lt;/strong&gt; avec versioning activé (requis par S3 Files)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Système de fichiers S3 Files&lt;/strong&gt; pointant vers le bucket, avec des mount targets dans votre VPC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume EFS&lt;/strong&gt; pour les clés SSH persistantes (empreinte stable entre les redémarrages et le scaling)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service ECS Fargate&lt;/strong&gt; exécutant &lt;a href="https://github.com/atmoz/sftp" rel="noopener noreferrer"&gt;atmoz/sftp&lt;/a&gt; avec le volume S3 Files monté sur &lt;code&gt;/home&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network Load Balancer&lt;/strong&gt; exposant le port 22&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enregistrement DNS&lt;/strong&gt; pour &lt;code&gt;sftp.votredomaine.com&lt;/code&gt; (optionnel)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Les fichiers uploadés via SFTP atterrissent sur le montage S3 Files → apparaissent dans S3 en quelques secondes → déclenchent les notifications S3 pour le traitement en aval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparaison des coûts
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Composant&lt;/th&gt;
&lt;th&gt;Transfer Family&lt;/th&gt;
&lt;th&gt;S3 Files + Fargate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Coût de base&lt;/td&gt;
&lt;td&gt;0,30 $/h (~216 $/mois)&lt;/td&gt;
&lt;td&gt;NLB : ~16 $/mois&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute&lt;/td&gt;
&lt;td&gt;Inclus&lt;/td&gt;
&lt;td&gt;Fargate 0.25 vCPU / 512 Mo : ~9 $/mois&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stockage&lt;/td&gt;
&lt;td&gt;Tarification S3&lt;/td&gt;
&lt;td&gt;Tarification S3 (identique)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transfert de données&lt;/td&gt;
&lt;td&gt;0,04 $/Go via SFTP&lt;/td&gt;
&lt;td&gt;Tarification NLB standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Minimum mensuel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~216 $&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~25 $&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;C'est environ &lt;strong&gt;8 fois moins cher&lt;/strong&gt; au niveau de base. Pour les cas d'usage SFTP à trafic faible à moyen (c'est-à-dire la majorité), les économies sont significatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi c'est mieux que du SFTP sur EFS
&lt;/h2&gt;

&lt;p&gt;Avant S3 Files, l'approche DIY classique consistait à monter EFS sur Fargate et faire tourner atmoz/sftp. C'est exactement ce que nous faisions. Ça marchait, mais avec une limitation fondamentale : &lt;strong&gt;vos fichiers vivaient dans EFS, pas dans S3&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Ça signifiait :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pas de notifications S3 à l'arrivée des fichiers&lt;/li&gt;
&lt;li&gt;Pas de politiques de cycle de vie S3&lt;/li&gt;
&lt;li&gt;Pas de réplication cross-region S3&lt;/li&gt;
&lt;li&gt;Pas d'accès direct aux fichiers via l'API S3&lt;/li&gt;
&lt;li&gt;Tarification EFS (0,30 $/Go pour Standard) vs S3 (0,023 $/Go)&lt;/li&gt;
&lt;li&gt;Stratégie de backup séparée nécessaire&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avec S3 Files, les données vivent dans S3. Vous bénéficiez de tout l'écosystème S3 — notifications, règles de cycle de vie, réplication, analytics, tiering Glacier — tout en ayant un système de fichiers montable pour votre serveur SFTP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traitement événementiel des fichiers
&lt;/h2&gt;

&lt;p&gt;Transfer Family et notre approche S3 Files écrivent tous deux dans S3, donc vous obtenez les mêmes capacités événementielles dans les deux cas :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Notifications S3 → SQS/SNS/Lambda&lt;/strong&gt; pour un traitement immédiat à l'arrivée d'un fichier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notifications S3 → EventBridge&lt;/strong&gt; pour des règles de routage complexes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Inventory&lt;/strong&gt; pour l'audit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Object Lock&lt;/strong&gt; pour la conformité&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Replication&lt;/strong&gt; pour répliquer les fichiers uploadés vers une autre région ou un autre compte&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La différence n'est pas dans les fonctionnalités — c'est dans le coût. Vous obtenez exactement le même pipeline événementiel S3 pour ~25 $/mois au lieu de ~216 $/mois.&lt;/p&gt;

&lt;h2&gt;
  
  
  L'implémentation Terraform
&lt;/h2&gt;

&lt;p&gt;Comme &lt;code&gt;aws_s3files_file_system&lt;/code&gt; n'est pas encore dans le provider Terraform AWS (&lt;a href="https://github.com/hashicorp/terraform-provider-aws/pull/47325" rel="noopener noreferrer"&gt;PR #47325&lt;/a&gt; ouverte et priorisée), nous gérons les ressources S3 Files via &lt;code&gt;terraform_data&lt;/code&gt; avec des provisioners &lt;code&gt;local-exec&lt;/code&gt; appelant l'AWS CLI.&lt;/p&gt;

&lt;p&gt;Les ressources clés :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Système de fichiers S3 Files — créé via AWS CLI&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"terraform_data"&lt;/span&gt; &lt;span class="s2"&gt;"s3files_file_system"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOT&lt;/span&gt;&lt;span class="sh"&gt;
      aws s3files create-file-system \
        --bucket "$BUCKET_ARN" \
        --role-arn "$ROLE_ARN" \
        --accept-bucket-warning \
        --region "$REGION"
&lt;/span&gt;&lt;span class="no"&gt;    EOT
&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Mount targets dans chaque sous-réseau privé&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"terraform_data"&lt;/span&gt; &lt;span class="s2"&gt;"s3files_mount_targets"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOT&lt;/span&gt;&lt;span class="sh"&gt;
      aws s3files create-mount-target \
        --file-system-id "$FS_ID" \
        --subnet-id "${each.value}" \
        --security-groups "$SG_ID"
&lt;/span&gt;&lt;span class="no"&gt;    EOT
&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# La task definition ECS utilise s3filesVolumeConfiguration&lt;/span&gt;
&lt;span class="nx"&gt;volume&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;sftp-home&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;s3files_volume_configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;file_system_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3files_fs_arn&lt;/span&gt;
      &lt;span class="nx"&gt;root_directory&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le code Terraform complet est disponible en tant que &lt;a href="https://registry.terraform.io/modules/psantus/s3files-sftp/aws/latest" rel="noopener noreferrer"&gt;module Terraform&lt;/a&gt;. Le provider Terraform AWS ne supporte pas encore &lt;code&gt;aws_s3files_file_system&lt;/code&gt; (&lt;a href="https://github.com/hashicorp/terraform-provider-aws/pull/47325" rel="noopener noreferrer"&gt;PR #47325&lt;/a&gt; ouverte et priorisée), donc les ressources S3 Files sont actuellement gérées via &lt;code&gt;terraform_data&lt;/code&gt; + AWS CLI. Je m'engage à mettre à jour ce module pour utiliser les ressources Terraform natives dès que le provider intégrera le support S3 Files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration IAM
&lt;/h2&gt;

&lt;p&gt;Deux rôles IAM sont nécessaires :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rôle de service S3 Files&lt;/strong&gt; — assumé par &lt;code&gt;elasticfilesystem.amazonaws.com&lt;/code&gt; pour synchroniser entre le système de fichiers et le bucket S3. Nécessite un accès S3 en lecture/écriture sur le bucket + des permissions EventBridge pour la détection des changements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rôle de tâche ECS&lt;/strong&gt; — nécessite &lt;code&gt;s3files:ClientMount&lt;/code&gt;, &lt;code&gt;s3files:ClientWrite&lt;/code&gt;, et &lt;code&gt;s3:GetObject&lt;/code&gt;/&lt;code&gt;s3:ListBucket&lt;/code&gt; sur le bucket pour des lectures optimisées.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Quand Transfer Family reste pertinent
&lt;/h2&gt;

&lt;p&gt;Pour être honnête, Transfer Family n'est pas mort pour tous les cas d'usage :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gestion managée des clés SFTP et des utilisateurs&lt;/strong&gt; — Transfer Family intègre nativement des fournisseurs d'identité (AD, authentification Lambda custom). Avec atmoz/sftp, vous gérez les utilisateurs via la configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support du protocole AS2&lt;/strong&gt; — si vous avez besoin d'AS2, Transfer Family reste la seule option managée.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FTPS&lt;/strong&gt; — Transfer Family supporte FTPS nativement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tolérance zéro aux opérations&lt;/strong&gt; — si vous ne pouvez vraiment pas gérer un conteneur, Transfer Family est entièrement managé.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mais pour la grande majorité des cas d'usage SFTP — des partenaires qui déposent des fichiers à traiter — l'approche S3 Files est moins chère, plus flexible, et offre une meilleure observabilité.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pour démarrer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prérequis :&lt;/strong&gt; Les commandes &lt;code&gt;aws s3files&lt;/code&gt; nécessitent AWS CLI v2.34.26 ou ultérieur. Vous avez également besoin de &lt;a href="https://jqlang.github.io/jq/" rel="noopener noreferrer"&gt;jq&lt;/a&gt; (utilisé par les scripts des provisioners Terraform). Mettez à jour la CLI avec &lt;code&gt;brew upgrade awscli&lt;/code&gt; ou consultez le &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;guide d'installation AWS CLI&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Créer un bucket S3 avec le versioning activé&lt;/li&gt;
&lt;li&gt;Créer un rôle IAM pour S3 Files avec les politiques de confiance et de permissions requises&lt;/li&gt;
&lt;li&gt;Créer un système de fichiers S3 Files via la console ou &lt;code&gt;aws s3files create-file-system&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Créer des mount targets dans vos sous-réseaux VPC&lt;/li&gt;
&lt;li&gt;Créer un EFS pour les clés SSH persistantes&lt;/li&gt;
&lt;li&gt;Déployer un service ECS Fargate avec atmoz/sftp, en montant S3 Files sur &lt;code&gt;/home&lt;/code&gt; et EFS sur &lt;code&gt;/etc/ssh/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Placer un NLB devant, pointer votre DNS dessus&lt;/li&gt;
&lt;li&gt;Configurer les notifications S3 sur le bucket pour le traitement en aval&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ou utilisez simplement le &lt;a href="https://registry.terraform.io/modules/psantus/s3files-sftp/aws/latest" rel="noopener noreferrer"&gt;module Terraform&lt;/a&gt; — le tout se déploie en moins de 10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test de bout en bout
&lt;/h2&gt;

&lt;p&gt;Après &lt;code&gt;terraform apply&lt;/code&gt;, le serveur SFTP est prêt en environ 8 minutes (l'essentiel du temps est consacré à la mise à disposition des mount targets S3 Files). Voici un test rapide :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Upload d'un fichier&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Hello from S3 Files SFTP!"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; test.txt
sshpass &lt;span class="nt"&gt;-p&lt;/span&gt; demo sftp &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;StrictHostKeyChecking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no &lt;span class="nt"&gt;-P&lt;/span&gt; 22 demo@&amp;lt;sftp_endpoint&amp;gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
cd upload
put test.txt
bye
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Vérifier qu'il est arrivé dans S3 (attendre ~30-60s pour la synchro)&lt;/span&gt;
aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;s3://&amp;lt;sftp_bucket_name&amp;gt;/demo/upload/test.txt -
&lt;span class="c"&gt;# Output: Hello from S3 Files SFTP!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nous avons également vérifié que les clés SSH persistent entre les redémarrages de tâches — l'empreinte du serveur reste identique après un redéploiement forcé, grâce au volume EFS monté sur &lt;code&gt;/etc/ssh/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;S3 Files comble le fossé entre système de fichiers et stockage objet d'une manière qui rend beaucoup de services AWS coûteux redondants. Pour le SFTP en particulier, la combinaison atmoz/sftp + S3 Files sur Fargate vous offre :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~8x moins cher&lt;/strong&gt; que Transfer Family&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contrôle total&lt;/strong&gt; sur le serveur SFTP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notifications S3 natives&lt;/strong&gt; pour le traitement événementiel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 comme source de vérité&lt;/strong&gt; — règles de cycle de vie, réplication, analytics fonctionnent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure as Code&lt;/strong&gt; avec Terraform (même avant le support natif du provider)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;L'époque où il fallait payer 216 $/mois minimum pour un endpoint SFTP managé est révolue pour la plupart des équipes. S3 Files est la pièce manquante qui rend le SFTP DIY sur AWS non seulement viable, mais ~8x moins cher.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>s3files</category>
      <category>sftp</category>
      <category>terraform</category>
    </item>
    <item>
      <title>AWS S3 Files just made Transfer Family SFTP obsolete for most use cases</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Wed, 08 Apr 2026 08:15:29 +0000</pubDate>
      <link>https://forem.com/aws-builders/aws-s3-files-just-made-transfer-family-sftp-obsolete-for-most-use-cases-4me</link>
      <guid>https://forem.com/aws-builders/aws-s3-files-just-made-transfer-family-sftp-obsolete-for-most-use-cases-4me</guid>
      <description>&lt;p&gt;AWS just launched one of the most impactful storage features in years: &lt;strong&gt;S3 Files&lt;/strong&gt;. It puts an EFS-compatible file system interface directly in front of your S3 buckets. When I was introduced to S3 Files (as part of &lt;a href="https://www.linkedin.com/posts/coreystrausman_aws-s3-cloudcomputing-activity-7447375367155351552-Uuyo?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAAHqFuoBYjYsx4cq4zQ6SBklTKN3Pd_juYs" rel="noopener noreferrer"&gt;AWS Community Builder program&lt;/a&gt;), I immediately thought of SFTP as the most obvious use case for my clients.&lt;/p&gt;

&lt;p&gt;If you're currently paying for AWS Transfer Family to give your partners SFTP access to S3, you should read this carefully. There's now a dramatically cheaper and more powerful alternative.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is S3 Files?
&lt;/h2&gt;

&lt;p&gt;S3 Files creates a high-performance NFS file system backed by an S3 bucket. Think of it as an EFS-like layer that reads and writes directly to S3 objects, with automatic bidirectional synchronization. Any file written through the file system appears as an S3 object, and any object uploaded to S3 becomes visible through the file system.&lt;/p&gt;

&lt;p&gt;The key properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sub-millisecond latency&lt;/strong&gt; for file operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic sync&lt;/strong&gt; between file system and S3 bucket (powered by EventBridge under the hood)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mountable on ECS Fargate&lt;/strong&gt;, ECS Managed Instances, EKS, and EC2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard NFS protocol&lt;/strong&gt; — no special client needed on the compute side (ECS/EKS handle it natively)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access points&lt;/strong&gt; with POSIX user/group enforcement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Versioning&lt;/strong&gt; required and leveraged for consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Problem with AWS Transfer Family
&lt;/h2&gt;

&lt;p&gt;AWS Transfer Family has been the "official" way to expose SFTP endpoints backed by S3. It works, but it comes with serious pain points:&lt;/p&gt;

&lt;h3&gt;
  
  
  It's expensive
&lt;/h3&gt;

&lt;p&gt;Transfer Family charges &lt;strong&gt;$0.30/hour&lt;/strong&gt; just for the endpoint — that's &lt;strong&gt;~$216/month&lt;/strong&gt; before you transfer a single byte. Add data transfer costs on top. For a service that many teams use for a handful of daily file drops, this is hard to justify.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's a black box
&lt;/h3&gt;

&lt;p&gt;You get an SFTP endpoint, but you don't control the server. Custom authentication requires Lambda hooks. Logging is limited. You can't SSH in to debug. You can't customize the SFTP server behavior, add pre/post-processing scripts, or run anything alongside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Architecture: atmoz/sftp + S3 Files on ECS Fargate
&lt;/h2&gt;

&lt;p&gt;Here's what you could run instead:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1l55rvat4vr4vxk97vbs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1l55rvat4vr4vxk97vbs.png" alt=" " width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;S3 bucket&lt;/strong&gt; with versioning enabled (required by S3 Files)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Files file system&lt;/strong&gt; pointed at the bucket, with mount targets in your VPC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EFS volume&lt;/strong&gt; for persistent SSH host keys (stable fingerprint across restarts and scaling)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ECS Fargate service&lt;/strong&gt; running &lt;a href="https://github.com/atmoz/sftp" rel="noopener noreferrer"&gt;atmoz/sftp&lt;/a&gt; with the S3 Files volume mounted at &lt;code&gt;/home&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network Load Balancer&lt;/strong&gt; exposing port 22&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS record&lt;/strong&gt; for &lt;code&gt;sftp.yourdomain.com&lt;/code&gt; (optional)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Files uploaded via SFTP land on the S3 Files mount → appear in S3 within seconds → trigger S3 event notifications for downstream processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Transfer Family&lt;/th&gt;
&lt;th&gt;S3 Files + Fargate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base cost&lt;/td&gt;
&lt;td&gt;$0.30/hr (~$216/mo)&lt;/td&gt;
&lt;td&gt;NLB: ~$16/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Fargate 0.25 vCPU / 512MB: ~$9/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;S3 pricing&lt;/td&gt;
&lt;td&gt;S3 pricing (same)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data transfer&lt;/td&gt;
&lt;td&gt;$0.04/GB over SFTP&lt;/td&gt;
&lt;td&gt;Standard NLB pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monthly minimum&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$216&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$25&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's roughly &lt;strong&gt;8x cheaper&lt;/strong&gt; at the base level. For low-to-medium traffic SFTP use cases (which is most of them), the savings are significant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Better Than EFS-Backed SFTP
&lt;/h2&gt;

&lt;p&gt;Before S3 Files, the common DIY approach was to mount EFS on Fargate and run atmoz/sftp. We did exactly this. It worked, but had a fundamental limitation: &lt;strong&gt;your files lived in EFS, not S3&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No S3 event notifications when files arrived&lt;/li&gt;
&lt;li&gt;No S3 lifecycle policies&lt;/li&gt;
&lt;li&gt;No S3 cross-region replication&lt;/li&gt;
&lt;li&gt;No direct S3 API access to the files&lt;/li&gt;
&lt;li&gt;EFS pricing ($0.30/GB for Standard) vs S3 ($0.023/GB)&lt;/li&gt;
&lt;li&gt;Separate backup strategy needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With S3 Files, the data lives in S3. You get the full S3 feature set — notifications, lifecycle rules, replication, analytics, Glacier tiering — while still having a mountable file system for your SFTP server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event-Driven File Processing
&lt;/h2&gt;

&lt;p&gt;Both Transfer Family and our S3 Files approach write to S3, so you get the same event-driven capabilities either way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3 Event Notifications → SQS/SNS/Lambda&lt;/strong&gt; for immediate processing when a file arrives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Event Notifications → EventBridge&lt;/strong&gt; for complex routing rules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Inventory&lt;/strong&gt; for auditing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Object Lock&lt;/strong&gt; for compliance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Replication&lt;/strong&gt; to replicate uploaded files to another region or account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference isn't in features — it's in cost. You get the exact same S3 event-driven pipeline for ~$25/mo instead of ~$216/mo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Terraform Implementation
&lt;/h2&gt;

&lt;p&gt;Since &lt;code&gt;aws_s3files_file_system&lt;/code&gt; isn't in the Terraform AWS provider yet (&lt;a href="https://github.com/hashicorp/terraform-provider-aws/pull/47325" rel="noopener noreferrer"&gt;PR #47325&lt;/a&gt; is open and prioritized), we manage S3 Files resources through &lt;code&gt;terraform_data&lt;/code&gt; with &lt;code&gt;local-exec&lt;/code&gt; provisioners calling the AWS CLI.&lt;/p&gt;

&lt;p&gt;The key resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# S3 Files file system — created via AWS CLI&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"terraform_data"&lt;/span&gt; &lt;span class="s2"&gt;"s3files_file_system"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOT&lt;/span&gt;&lt;span class="sh"&gt;
      aws s3files create-file-system \
        --bucket "$BUCKET_ARN" \
        --role-arn "$ROLE_ARN" \
        --accept-bucket-warning \
        --region "$REGION"
&lt;/span&gt;&lt;span class="no"&gt;    EOT
&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Mount targets in each private subnet&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"terraform_data"&lt;/span&gt; &lt;span class="s2"&gt;"s3files_mount_targets"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOT&lt;/span&gt;&lt;span class="sh"&gt;
      aws s3files create-mount-target \
        --file-system-id "$FS_ID" \
        --subnet-id "${each.value}" \
        --security-groups "$SG_ID"
&lt;/span&gt;&lt;span class="no"&gt;    EOT
&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# ECS task definition uses s3filesVolumeConfiguration&lt;/span&gt;
&lt;span class="nx"&gt;volume&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;sftp-home&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;s3files_volume_configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;file_system_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3files_fs_arn&lt;/span&gt;
      &lt;span class="nx"&gt;root_directory&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full working Terraform code is available as a &lt;a href="https://registry.terraform.io/modules/psantus/s3files-sftp/aws/latest" rel="noopener noreferrer"&gt;Terraform module&lt;/a&gt;. The Terraform AWS provider doesn't support &lt;code&gt;aws_s3files_file_system&lt;/code&gt; yet (&lt;a href="https://github.com/hashicorp/terraform-provider-aws/pull/47325" rel="noopener noreferrer"&gt;PR #47325&lt;/a&gt; is open and prioritized), so S3 Files resources are currently managed via &lt;code&gt;terraform_data&lt;/code&gt; + AWS CLI. I pledge to update this module to use native Terraform resources as soon as the provider ships S3 Files support.&lt;/p&gt;

&lt;h2&gt;
  
  
  IAM Setup
&lt;/h2&gt;

&lt;p&gt;Two IAM roles are needed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;S3 Files service role&lt;/strong&gt; — assumed by &lt;code&gt;elasticfilesystem.amazonaws.com&lt;/code&gt; to sync between the file system and S3 bucket. Needs S3 read/write on the bucket + EventBridge permissions for change detection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ECS task role&lt;/strong&gt; — needs &lt;code&gt;s3files:ClientMount&lt;/code&gt;, &lt;code&gt;s3files:ClientWrite&lt;/code&gt;, and &lt;code&gt;s3:GetObject&lt;/code&gt;/&lt;code&gt;s3:ListBucket&lt;/code&gt; on the backing bucket for optimized reads.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When Transfer Family Still Makes Sense
&lt;/h2&gt;

&lt;p&gt;To be fair, Transfer Family isn't dead for every use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Managed SFTP keys and user management&lt;/strong&gt; — Transfer Family has built-in identity provider integration (AD, Lambda custom auth). With atmoz/sftp, you manage users via config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AS2 protocol support&lt;/strong&gt; — if you need AS2, Transfer Family is still the only managed option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FTPS&lt;/strong&gt; — Transfer Family supports FTPS natively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero ops tolerance&lt;/strong&gt; — if you truly cannot manage a container, Transfer Family is fully managed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But for the vast majority of SFTP use cases — partners dropping files that need processing — the S3 Files approach is cheaper, more flexible, and gives you better observability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; The &lt;code&gt;aws s3files&lt;/code&gt; commands require AWS CLI v2.34.26 or later. You also need &lt;a href="https://jqlang.github.io/jq/" rel="noopener noreferrer"&gt;jq&lt;/a&gt; installed (used by the Terraform provisioner scripts). Update the CLI with &lt;code&gt;brew upgrade awscli&lt;/code&gt; or see &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;AWS CLI install guide&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an S3 bucket with versioning enabled&lt;/li&gt;
&lt;li&gt;Create an IAM role for S3 Files with the required trust and permissions policies&lt;/li&gt;
&lt;li&gt;Create an S3 Files file system via the console or &lt;code&gt;aws s3files create-file-system&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create mount targets in your VPC subnets&lt;/li&gt;
&lt;li&gt;Create an EFS for persistent SSH host keys&lt;/li&gt;
&lt;li&gt;Deploy an ECS Fargate service with atmoz/sftp, mounting S3 Files at &lt;code&gt;/home&lt;/code&gt; and EFS at &lt;code&gt;/etc/ssh/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Put an NLB in front, point your DNS at it&lt;/li&gt;
&lt;li&gt;Set up S3 event notifications on the bucket for downstream processing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Or just use the &lt;a href="https://registry.terraform.io/modules/psantus/s3files-sftp/aws/latest" rel="noopener noreferrer"&gt;Terraform module&lt;/a&gt; — the whole thing deploys in under 10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it end-to-end
&lt;/h2&gt;

&lt;p&gt;After &lt;code&gt;terraform apply&lt;/code&gt;, the SFTP server is ready in about 8 minutes (most of the time is S3 Files mount targets becoming available). Here's a quick test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Upload a file&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Hello from S3 Files SFTP!"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; test.txt
sshpass &lt;span class="nt"&gt;-p&lt;/span&gt; demo sftp &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;StrictHostKeyChecking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no &lt;span class="nt"&gt;-P&lt;/span&gt; 22 demo@&amp;lt;sftp_endpoint&amp;gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
cd upload
put test.txt
bye
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Verify it landed in S3 (wait ~30-60s for sync)&lt;/span&gt;
aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;s3://&amp;lt;sftp_bucket_name&amp;gt;/demo/upload/test.txt -
&lt;span class="c"&gt;# Output: Hello from S3 Files SFTP!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also verified that SSH host keys persist across task restarts — the server fingerprint stays the same after a forced redeployment, thanks to the EFS volume mounted at &lt;code&gt;/etc/ssh/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;S3 Files bridges the gap between file system and object storage in a way that makes a lot of expensive AWS services feel redundant. For SFTP specifically, the combination of atmoz/sftp + S3 Files on Fargate gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~8x lower cost&lt;/strong&gt; than Transfer Family&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full control&lt;/strong&gt; over the SFTP server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native S3 event notifications&lt;/strong&gt; for event-driven processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 as the source of truth&lt;/strong&gt; — lifecycle rules, replication, analytics all work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure as Code&lt;/strong&gt; with Terraform (even before native provider support)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The days of paying $216/month minimum for a managed SFTP endpoint are over for most teams. S3 Files is the missing piece that makes DIY SFTP on AWS not just viable, but ~8x cheaper.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>s3files</category>
      <category>terraform</category>
      <category>sftp</category>
    </item>
    <item>
      <title>Agents Bedrock AgentCore en mode VPC : attention aux coûts de NAT Gateway !</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Sat, 04 Apr 2026 08:59:14 +0000</pubDate>
      <link>https://forem.com/aws-builders/agents-bedrock-agentcore-en-mode-vpc-attention-aux-couts-de-nat-gateway--3pp4</link>
      <guid>https://forem.com/aws-builders/agents-bedrock-agentcore-en-mode-vpc-attention-aux-couts-de-nat-gateway--3pp4</guid>
      <description>&lt;p&gt;La semaine dernière, j'ai reçu une alerte d'anomalie de coûts AWS. L'alerte pointait vers mon compte de formation (où je fais mes démos et aussi mes POCs), signalant une charge inattendue de 29 $ sous — étrangement — Amazon Elastic Block Store. Le type d'utilisation racontait cependant une tout autre histoire : &lt;code&gt;NatGateway-Bytes&lt;/code&gt;. 659 Go de données avaient transité par ma NAT Gateway en six jours.&lt;/p&gt;

&lt;p&gt;J'avais récemment déployé un agent vocal sur Bedrock AgentCore Runtime en mode VPC, utilisant une NAT Gateway pour l'accès internet sortant (nécessaire pour le relais TURN WebRTC) — voir &lt;a href="https://dev.to/aws-builders/heberger-un-agent-ia-vocal-sur-aws-bedrock-agentcore-communiquant-via-webrtc-2k17"&gt;mon billet de blog ici&lt;/a&gt;. Le VPC avait été créé spécifiquement pour cet agent, donc le suspect était évident. Mais je voulais des preuves concrètes avant de tirer des conclusions. Était-ce le trafic WebRTC ? Autre chose ?&lt;/p&gt;

&lt;h2&gt;
  
  
  Début de l'investigation
&lt;/h2&gt;

&lt;p&gt;Mon premier réflexe a été de consulter les métriques CloudWatch de la NAT Gateway. La métrique &lt;code&gt;BytesOutToDestination&lt;/code&gt; (trafic du conteneur vers internet) ne montrait que 2,1 Go au total sur les six jours. Négligeable. Mais &lt;code&gt;BytesInFromDestination&lt;/code&gt; (trafic d'internet vers le conteneur à travers la NAT) racontait une tout autre histoire :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Entrant via la NAT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;26 mars&lt;/td&gt;
&lt;td&gt;6,3 Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;27 mars&lt;/td&gt;
&lt;td&gt;240,3 Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;28 mars&lt;/td&gt;
&lt;td&gt;149,1 Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29 mars&lt;/td&gt;
&lt;td&gt;149,8 Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30 mars&lt;/td&gt;
&lt;td&gt;102,3 Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31 mars&lt;/td&gt;
&lt;td&gt;15,0 Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1er avril&lt;/td&gt;
&lt;td&gt;5,4 Go (journée partielle)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ce déséquilibre entre les flux entrants et sortants plaidait contre WebRTC comme responsable du trafic.&lt;/p&gt;

&lt;p&gt;De plus, la métrique &lt;code&gt;ActiveConnectionCount&lt;/code&gt; montrait un nombre stable d'environ 90 connexions 24h/24, même quand personne n'utilisait l'agent. Le pattern horaire était remarquablement régulier — alternant entre ~850 Mo et ~430 Mo par heure, en continu.&lt;/p&gt;

&lt;p&gt;Pour en avoir le cœur net, j'ai vérifié CloudTrail pour les événements &lt;code&gt;InvokeAgentRuntime&lt;/code&gt; entre le 28 et le 30 mars. Zéro. Aucune activité utilisateur pendant la période avec le trafic le plus intense. L'agent était complètement inactif.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu5016a9y8nyzgvohurhc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu5016a9y8nyzgvohurhc.png" alt="Trafic NAT Gateway vs invocations AgentCore" width="800" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Activation des VPC Flow Logs
&lt;/h2&gt;

&lt;p&gt;J'avais besoin de voir d'où venait le trafic. J'ai activé les VPC Flow Logs (j'aurais dû le faire dès le premier jour ? Bah, c'était un POC !) sur le VPC, en les envoyant vers un groupe de logs CloudWatch, et j'ai lancé une requête Logs Insights pour identifier les plus gros consommateurs :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;totalBytes&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;srcAddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstAddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstPort&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="n"&gt;totalBytes&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;limit&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Les résultats sur une fenêtre de deux heures montraient une poignée d'adresses IP responsables de tout le trafic lourd :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    52.216.58.42 -&amp;gt;       10.0.0.144: 31175     270.1 MB
    16.15.207.229 -&amp;gt;      10.0.0.144: 62935     263.7 MB
    16.15.191.63 -&amp;gt;       10.0.0.144: 25320     263.6 MB
    52.216.12.24 -&amp;gt;       10.0.0.144: 12542     115.8 MB
    3.5.16.209 -&amp;gt;         10.0.0.144: 30762     113.4 MB
    16.15.199.52 -&amp;gt;       10.0.0.144: 49632     113.3 MB
    54.231.160.154 -&amp;gt;     10.0.0.144: 55754      29.6 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;L'adresse &lt;code&gt;10.0.0.144&lt;/code&gt; est l'IP privée de la NAT Gateway. Tout le trafic transitait depuis des IP externes, à travers la NAT, vers les ENI du conteneur AgentCore dans les sous-réseaux privés.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identification de la source
&lt;/h2&gt;

&lt;p&gt;J'avais besoin de savoir à quel service appartenaient ces IP. J'ai utilisé mon outil &lt;a href="https://does-this-ip-belong-to-aws.terracloud.fr" rel="noopener noreferrer"&gt;does-this-ip-belong-to-aws&lt;/a&gt;, qui vérifie les IP par rapport aux plages IP officielles AWS publiées sur &lt;code&gt;https://ip-ranges.amazonaws.com/ip-ranges.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Chaque IP à fort trafic correspondait à Amazon S3 en us-east-1 !&lt;/p&gt;

&lt;p&gt;Tout le trafic — jusqu'au dernier gigaoctet — était des téléchargements S3 transitant par la NAT Gateway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le correctif : S3 Gateway Endpoint
&lt;/h2&gt;

&lt;p&gt;Le correctif est simple et gratuit. Un S3 Gateway VPC Endpoint route le trafic S3 directement via le réseau AWS, contournant entièrement la NAT Gateway. Contrairement aux interface endpoints, les gateway endpoints n'ont ni frais horaires ni frais de traitement de données.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc_endpoint"&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;service_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"com.amazonaws.${var.aws_region}.s3"&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un &lt;code&gt;terraform apply&lt;/code&gt; et les coûts de transfert de données de la NAT Gateway tombent à quasi zéro.&lt;/p&gt;

&lt;p&gt;Ce qui soulève une question plus large : pourquoi ne pas toujours avoir un S3 Gateway Endpoint dans un VPC ? C'est gratuit, ça se crée en une seule ressource, et ça prévient exactement ce genre de surprise. Si vous créez des VPC avec des sous-réseaux privés et des NAT Gateways, ajoutez un S3 Gateway Endpoint par défaut. Il n'y a aucun inconvénient. Les S3 Gateway endpoints sont bons pour votre portefeuille, sinon pour votre âme.&lt;/p&gt;

&lt;h2&gt;
  
  
  La cause racine : recyclage du warm pool
&lt;/h2&gt;

&lt;p&gt;Après avoir ouvert un ticket de support, l'équipe du service Bedrock AgentCore a identifié la cause racine.&lt;/p&gt;

&lt;p&gt;AgentCore Runtime maintient un warm pool de VM pour garantir des invocations à faible latence. Chaque VM du pool télécharge l'image du conteneur depuis ECR — et ECR stocke les couches d'images dans S3. Mon image de conteneur faisait environ 435 Mo compressée.&lt;/p&gt;

&lt;p&gt;Trois facteurs se sont combinés pour produire la facture de 659 Go :&lt;/p&gt;

&lt;p&gt;Premièrement, les 21 appels API &lt;code&gt;UpdateAgentRuntime&lt;/code&gt; que j'ai effectués le 27 mars (une journée de débogage et redéploiement intensifs) ont chacun déclenché un cycle asynchrone de re-provisionnement du warm pool. Plusieurs séries de provisionnement de 10 VM, chacune téléchargeant l'image de 435 Mo, ont produit le pic de ~240 Go observé ce jour-là.&lt;/p&gt;

&lt;p&gt;Deuxièmement, le warm pool a continué à recycler les VM les jours suivants pour les garder fraîches et prêtes. Avec 10 VM téléchargeant chacune l'image périodiquement, le trafic stable de ~150 Go/jour du 28 au 30 mars est cohérent avec un recyclage régulier.&lt;/p&gt;

&lt;p&gt;Troisièmement, après environ 72 heures sans invocations, le warm pool a automatiquement réduit sa taille de 10 VM à 1 VM. Cela explique la chute de ~150 Go/jour à ~15 Go/jour le 31 mars.&lt;/p&gt;

&lt;p&gt;Le recyclage du warm pool est un comportement attendu de la plateforme — c'est ce qui permet à AgentCore de servir les requêtes avec une faible latence. Le problème était que tous ces téléchargements S3 passaient par ma NAT Gateway à 0,045 $/Go au lieu de rester sur le réseau interne AWS.&lt;/p&gt;

&lt;p&gt;Lancer autant de VM pour si peu d'invocations me semble un peu comme tirer au bazooka pour tuer une mouche ; je me demande si c'est soutenable... Cela dit, AWS a un bon historique de gestion d'activités rentables à grande échelle : qui suis-je pour juger ?&lt;/p&gt;

&lt;p&gt;Quoi qu'il en soit, l'équipe du service a promis de mettre à jour la documentation pour que pas (trop) d'utilisateurs ne se retrouvent face à ces charges (franchement) indues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Points à retenir
&lt;/h2&gt;

&lt;p&gt;Si vous utilisez Bedrock AgentCore Runtime en mode VPC, trois choses à garder en tête :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ajoutez un S3 Gateway Endpoint à votre VPC&lt;/strong&gt;. C'est gratuit et ça élimine ce qui s'est avéré être la source dominante de coûts de transfert de données de la NAT Gateway — les téléchargements d'images ECR par le warm pool. AWS a confirmé qu'ils mettent à jour leur documentation VPC pour recommander cela plus visiblement. Il n'y a véritablement aucune raison de ne pas en avoir un dans chaque VPC avec des sous-réseaux privés.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Soyez attentif à la taille de votre image de conteneur&lt;/strong&gt;. Mon image de 435 Mo, téléchargée par un warm pool de 10 VM avec recyclage régulier, a généré des centaines de gigaoctets de transfert. Réduire l'image (builds multi-étapes, moins de dépendances, base Alpine) réduit directement ce coût — même avec le endpoint S3 en place, des images plus petites signifient des démarrages à froid plus rapides.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Surveillez vos métriques NAT Gateway tôt&lt;/strong&gt;. Les métriques &lt;code&gt;BytesInFromDestination&lt;/code&gt; et &lt;code&gt;BytesOutToSource&lt;/code&gt; dans CloudWatch vous montreront si quelque chose d'inattendu se passe. Je ne m'en suis rendu compte que grâce à l'alerte d'anomalie de coûts — à ce moment-là, 29 $ avaient déjà été dépensés. Les VPC Flow Logs combinés avec CloudWatch Logs Insights ont rendu le diagnostic simple une fois que j'ai regardé.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Paul Santus est consultant cloud indépendant chez &lt;a href="https://terracloud.fr" rel="noopener noreferrer"&gt;TerraCloud&lt;/a&gt;. Il accompagne les organisations dans la construction et le déploiement d'applications IA sur AWS. Retrouvez-le sur &lt;a href="https://www.linkedin.com/in/paulsantus" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>vpc</category>
      <category>bedrock</category>
      <category>ai</category>
    </item>
    <item>
      <title>VPC-connected Bedrock AgentCore Runtime-hosted agents: beware of NAT Gateway costs!</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Fri, 03 Apr 2026 14:05:35 +0000</pubDate>
      <link>https://forem.com/aws-builders/vpc-connected-bedrock-agentcore-runtime-hosted-agents-beware-of-nat-gateway-costs-3ohh</link>
      <guid>https://forem.com/aws-builders/vpc-connected-bedrock-agentcore-runtime-hosted-agents-beware-of-nat-gateway-costs-3ohh</guid>
      <description>&lt;p&gt;Last week I received a cost anomaly alert from AWS. The alert pointed at my training account, flagging an unexpected $29 charge under — oddly enough — Amazon Elastic Block Store. The usage type, however, told a different story: &lt;code&gt;NatGateway-Bytes&lt;/code&gt;. 659 GB of data had flowed through my NAT Gateway in six days.&lt;/p&gt;

&lt;p&gt;I had recently deployed a voice agent on Bedrock AgentCore Runtime in VPC mode, using a NAT Gateway for outbound internet access (required for WebRTC TURN relay) - see &lt;a href="https://dev.to/aws-builders/switching-my-ai-voice-agent-from-websocket-to-webrtc-what-broke-and-what-i-learned-3dkn"&gt;my blog post here&lt;/a&gt;. The VPC had been created specifically for this agent, so the suspect was obvious. But I wanted ground truth before jumping to conclusions. Was it WebRTC traffic? Something else?&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting the investigation
&lt;/h2&gt;

&lt;p&gt;My first stop was CloudWatch metrics on the NAT Gateway. The &lt;code&gt;BytesOutToDestination&lt;/code&gt; metric (traffic from the container to the internet) showed only 2.1 GB total over the six days. Negligible. But &lt;code&gt;BytesInFromDestination&lt;/code&gt; (traffic from the internet into the container through the NAT) told a very different story:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Inbound through NAT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mar 26&lt;/td&gt;
&lt;td&gt;6.3 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mar 27&lt;/td&gt;
&lt;td&gt;240.3 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mar 28&lt;/td&gt;
&lt;td&gt;149.1 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mar 29&lt;/td&gt;
&lt;td&gt;149.8 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mar 30&lt;/td&gt;
&lt;td&gt;102.3 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mar 31&lt;/td&gt;
&lt;td&gt;15.0 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apr 01&lt;/td&gt;
&lt;td&gt;5.4 GB (partial)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This unbalanced metrics values between inbound and outbound flows pleaded against WebRTC as the traffic culprit. &lt;/p&gt;

&lt;p&gt;Moreover, the &lt;code&gt;ActiveConnectionCount&lt;/code&gt; metric showed a steady ~90 connections 24/7, even when nobody was using the agent. The hourly pattern was remarkably regular — alternating between ~850 MB and ~430 MB per hour, around the clock.&lt;/p&gt;

&lt;p&gt;Just to be sure, I checked CloudTrail for &lt;code&gt;InvokeAgentRuntime&lt;/code&gt; events between March 28 and March 30. Zero. No user activity at all during the period with the heaviest traffic. The agent was completely idle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu5016a9y8nyzgvohurhc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu5016a9y8nyzgvohurhc.png" alt="NAT Gateway traffic vs AgentCore invocations" width="800" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling VPC Flow Logs
&lt;/h2&gt;

&lt;p&gt;I needed to see where the traffic was coming from. I enabled VPC Flow Logs (shouldn't have it done on day 1? Nay, this was a POC workload!) on the VPC, sending them to a CloudWatch log group, and ran a Logs Insights query to identify the top talkers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;totalBytes&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;srcAddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstAddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstPort&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="n"&gt;totalBytes&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;limit&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The results over a two-hour window showed a handful of IP addresses responsible for all the heavy traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    52.216.58.42 -&amp;gt;       10.0.0.144: 31175     270.1 MB
    16.15.207.229 -&amp;gt;      10.0.0.144: 62935     263.7 MB
    16.15.191.63 -&amp;gt;       10.0.0.144: 25320     263.6 MB
    52.216.12.24 -&amp;gt;       10.0.0.144: 12542     115.8 MB
    3.5.16.209 -&amp;gt;         10.0.0.144: 30762     113.4 MB
    16.15.199.52 -&amp;gt;       10.0.0.144: 49632     113.3 MB
    54.231.160.154 -&amp;gt;     10.0.0.144: 55754      29.6 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;10.0.0.144&lt;/code&gt; address is the NAT Gateway's private IP. All the traffic was flowing from external IPs, through the NAT, to the AgentCore container ENIs in the private subnets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identifying the source
&lt;/h2&gt;

&lt;p&gt;I needed to know what service these IPs belonged to. I used my &lt;a href="https://does-this-ip-belong-to-aws.terracloud.fr" rel="noopener noreferrer"&gt;does-this-ip-belong-to-aws&lt;/a&gt; tool, which checks IPs against the official AWS IP ranges published at &lt;code&gt;https://ip-ranges.amazonaws.com/ip-ranges.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every single high-traffic IP resolved to Amazon S3 in us-east-1!&lt;/p&gt;

&lt;p&gt;All the traffic — every last gigabyte — was S3 pulls flowing through the NAT Gateway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: S3 Gateway Endpoint
&lt;/h2&gt;

&lt;p&gt;The fix is straightforward and free. An S3 Gateway VPC Endpoint routes S3 traffic directly through the AWS network, bypassing the NAT Gateway entirely. Unlike interface endpoints, gateway endpoints have no hourly charge and no data processing fee.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc_endpoint"&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;service_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"com.amazonaws.${var.aws_region}.s3"&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;terraform apply&lt;/code&gt; and the NAT Gateway data transfer cost drops to near zero.&lt;/p&gt;

&lt;p&gt;This raises a broader question: why would you ever not have an S3 Gateway Endpoint in a VPC? It's free, takes one resource to create, and prevents exactly this kind of surprise. If you're creating VPCs with private subnets and NAT Gateways, add an S3 Gateway Endpoint as a default. There's no downside. S3 Gateway endpoints are good for you wallet, if not for your soul.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root cause: warm pool recycling
&lt;/h2&gt;

&lt;p&gt;After filing a support case, the Bedrock AgentCore service team identified the root cause.&lt;/p&gt;

&lt;p&gt;AgentCore Runtime maintains a warm pool of VMs to ensure low-latency invocations. Each VM in the pool pulls the container image from ECR — and ECR stores image layers in S3. My container image was ~435 MB compressed.&lt;/p&gt;

&lt;p&gt;Three things combined to produce the 659 GB bill:&lt;/p&gt;

&lt;p&gt;First, the 21 &lt;code&gt;UpdateAgentRuntime&lt;/code&gt; API calls I made on March 27 (a day of heavy debugging and redeployment) each triggered an asynchronous warm pool re-provisioning cycle. Multiple rounds of 10-VM provisioning, each pulling the 435 MB image, produced the ~240 GB spike that day.&lt;/p&gt;

&lt;p&gt;Second, the warm pool continued recycling VMs over the following days to keep them fresh and ready. With 10 VMs each pulling the image periodically, the steady ~150 GB/day on March 28-30 is consistent with regular recycling.&lt;/p&gt;

&lt;p&gt;Third, after approximately 72 hours with no invocations, the warm pool automatically downscaled from 10 VMs to 1 VM. This explains the drop from ~150 GB/day to ~15 GB/day on March 31.&lt;/p&gt;

&lt;p&gt;The warm pool recycling is expected platform behavior — it's what makes AgentCore able to serve requests with low latency. The problem was that all those S3 pulls were routing through my NAT Gateway at $0.045/GB instead of staying on the AWS internal network.&lt;/p&gt;

&lt;p&gt;Firing these many VMs for so few invocations seems to mme like shooting a bazooka to kill a fly; I wonder how sustainable that is.. yet AWS has a good track record at operating profitable business at scale: who am I to judge? &lt;/p&gt;

&lt;p&gt;Anyway, the service team promised they'll make an update to the documentation so that not (too) many users face these (frankly) undue charges. &lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;If you're running Bedrock AgentCore Runtime in VPC mode, three things to keep in mind:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;add an S3 Gateway Endpoint to your VPC&lt;/strong&gt;. It's free and eliminates what turned out to be the dominant source of NAT Gateway data transfer costs — ECR image pulls from the warm pool. AWS has confirmed they are updating their VPC documentation to more prominently recommend this. There is genuinely no reason not to have one in every VPC with private subnets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;be mindful of container image size&lt;/strong&gt;. My 435 MB image, pulled across a 10-VM warm pool with regular recycling, generated hundreds of gigabytes of transfer. Slimming the image (multi-stage builds, fewer dependencies, Alpine base) directly reduces this cost — even with the S3 endpoint in place, smaller images mean faster cold starts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;monitor your NAT Gateway metrics early&lt;/strong&gt;. The &lt;code&gt;BytesInFromDestination&lt;/code&gt; and &lt;code&gt;BytesOutToSource&lt;/code&gt; metrics in CloudWatch will show you if something unexpected is happening. I only noticed because of the cost anomaly alert — by then, $29 had already been spent. VPC Flow Logs combined with CloudWatch Logs Insights made the diagnosis straightforward once I looked.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Paul Santus is an independent cloud consultant at &lt;a href="https://terracloud.fr" rel="noopener noreferrer"&gt;TerraCloud&lt;/a&gt;. He helps organizations build and deploy AI-powered applications on AWS. Connect with him on &lt;a href="https://www.linkedin.com/in/paulsantus" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>bedrock</category>
      <category>agentcore</category>
      <category>vpc</category>
    </item>
    <item>
      <title>Héberger un agent IA vocal sur AWS Bedrock AgentCore communiquant via WebRTC</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Fri, 27 Mar 2026 16:02:24 +0000</pubDate>
      <link>https://forem.com/aws-builders/heberger-un-agent-ia-vocal-sur-aws-bedrock-agentcore-communiquant-via-webrtc-2k17</link>
      <guid>https://forem.com/aws-builders/heberger-un-agent-ia-vocal-sur-aws-bedrock-agentcore-communiquant-via-webrtc-2k17</guid>
      <description>&lt;p&gt;Aujourd'hui j'ai migré agent vocal IA de WebSocket vers WebRTC — voici ce qui a cassé et ce que j'ai appris.&lt;/p&gt;

&lt;p&gt;Il y a quelques jours, je suis tombé sur le &lt;a href="https://darryl-ruggles.cloud/bi-directional-voice-controlled-recipe-assistant-with-nova-sonic-2/" rel="noopener noreferrer"&gt;billet de blog&lt;/a&gt; et le repo de Darryl Ruggles pour un agent vocal bidirectionnel construit avec Strands BidiAgent et Amazon Nova Sonic v2. Son travail est remarquablement bien ficelé — j'avais un assistant vocal fonctionnel sur mon laptop en une dizaine de minutes. L'agent écoute votre voix, cherche dans une base de recettes, programme des minuteurs de cuisson, consulte des données nutritionnelles et convertit des unités, le tout par conversation naturelle.&lt;/p&gt;

&lt;p&gt;La version de Darryl utilise WebSocket comme transport entre le navigateur et l'agent. Ça fonctionne bien, mais je voulais aller plus loin : passer le transport en WebRTC et déployer le tout sur Bedrock AgentCore Runtime. Ce billet couvre ce parcours — ce qui a changé, ce qui a cassé, et ce que j'en ai tiré.&lt;/p&gt;

&lt;p&gt;Vous avez rêvé de demander la recette des crêpes à un agent IA ? je l'ai fait :)&lt;br&gt;
  &lt;iframe src="https://www.youtube.com/embed/kxElgTLmIQ8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Le code source complet est disponible sur &lt;a href="https://github.com/psantus/strands-bidir-nova" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Le repo est entièrement géré par Terraform, mais vous pouvez toujours utiliser l'approche Makefile de Darryl si vous préférez garder Terraform pour l'infrastructure et le CLI pour le déploiement de l'agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi WebRTC pour un agent vocal ?
&lt;/h2&gt;

&lt;p&gt;La version WebSocket de l'agent fonctionne, alors pourquoi changer ? Plusieurs raisons m'ont poussé vers WebRTC.&lt;/p&gt;

&lt;p&gt;D'abord, la latence. WebSocket tourne sur TCP, ce qui signifie que chaque paquet est garanti d'arriver dans l'ordre. C'est parfait pour des messages de chat, mais pour de l'audio en temps réel, un seul paquet perdu bloque tout le flux pendant que TCP retransmet. WebRTC&lt;sup id="fnref1"&gt;1&lt;/sup&gt; utilise UDP — si un paquet est perdu, le flux continue. Pour une conversation vocale, un micro-glitch est bien préférable à une pause perceptible.&lt;/p&gt;

&lt;p&gt;Ensuite, le navigateur fait plus de travail. Avec WebSocket, je devais capturer l'audio du micro via &lt;code&gt;getUserMedia&lt;/code&gt;, le sous-échantillonner à 16kHz avec un &lt;code&gt;ScriptProcessorNode&lt;/code&gt;, l'encoder en base64 PCM et l'envoyer en messages JSON. Côté lecture, il fallait un &lt;code&gt;AudioWorklet&lt;/code&gt; avec un buffer circulaire pour gérer le flux audio entrant. Avec WebRTC, le navigateur gère nativement la capture audio, l'encodage (Opus) et la lecture via &lt;code&gt;RTCPeerConnection&lt;/code&gt;. Le code frontend s'en trouve considérablement simplifié.&lt;/p&gt;

&lt;p&gt;Enfin, WebRTC est prêt pour la vidéo. Les avatars IA arrivent à des latences acceptables, et WebRTC gère les pistes vidéo aussi naturellement que les pistes audio. Ajouter un flux vidéo plus tard revient simplement à ajouter une piste à la connexion existante — aucun changement d'architecture ne sera nécessaire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Petit tour d'horizon des architectures WebRTC
&lt;/h2&gt;

&lt;p&gt;Il existe deux façons fondamentalement différentes d'utiliser WebRTC, et le choix compte quand on construit un agent vocal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pair-à-pair (P2P)
&lt;/h3&gt;

&lt;p&gt;En WebRTC P2P, deux pairs se connectent directement l'un à l'autre. Pas de serveur média au milieu — l'audio circule directement du navigateur à l'agent et retour. Un serveur relais TURN&lt;sup id="fnref2"&gt;2&lt;/sup&gt; peut être nécessaire quand l'un ou les deux pairs sont derrière un NAT&lt;sup id="fnref3"&gt;3&lt;/sup&gt; (ce qui est quasi systématique en production : les clients sont derrière un routeur Internet et les agents doivent être dans un VPC privé pour accéder aux outils de l'entreprise), mais le serveur TURN ne fait que relayer les paquets sans les inspecter ni les traiter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpgd5s5k2a0ty88lt62uf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpgd5s5k2a0ty88lt62uf.png" alt="P2P WebRTC"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Basé sur des salles (SFU)
&lt;/h3&gt;

&lt;p&gt;Dans une architecture basée sur des salles de visio, un serveur média (appelé SFU&lt;sup id="fnref4"&gt;4&lt;/sup&gt; — Selective Forwarding Unit) se place au milieu. Les participants se connectent au serveur, pas entre eux. Le serveur reçoit les pistes audio/vidéo de chaque participant et les retransmet sélectivement aux autres. LiveKit, Amazon Chime SDK et Daily sont des exemples de plateformes SFU.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpbdeeofri17xb09zucwu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpbdeeofri17xb09zucwu.png" alt="Media Server WebRTC"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pour un agent vocal en 1:1, le P2P est plus simple et évite le coût et la complexité de faire tourner (ou de payer) un serveur média. J'ai opté pour le P2P avec Amazon Kinesis Video Streams (KVS) comme relais TURN managé — c'est l'approche documentée pour WebRTC sur AgentCore.&lt;/p&gt;

&lt;p&gt;J'ai envisagé les solutions basées sur des salles, mais chaque plateforme SFU nécessite son propre SDK — on ne peut pas simplement se connecter avec un &lt;code&gt;RTCPeerConnection&lt;/code&gt; standard. L'offre WebRTC d'AWS, Amazon Chime SDK, est riche en fonctionnalités (transcription, enregistrement, analytics) et nettement moins chère que les alternatives comme LiveKit ou Daily, mais elle n'offre pas encore de chemin balisé pour la communication agent-vers-salle côté serveur. C'est une fonctionnalité que j'aimerais beaucoup voir arriver, vu la qualité du reste du Chime SDK. Pour l'instant, le P2P avec KVS TURN était le chemin le plus direct. Je considérerai certainement le WebRTC en salle, mais c'est une histoire pour un autre billet.&lt;/p&gt;

&lt;h3&gt;
  
  
  La pile WebRTC : navigateur et serveur
&lt;/h3&gt;

&lt;p&gt;Côté navigateur, WebRTC est intégré nativement. L'API &lt;code&gt;RTCPeerConnection&lt;/code&gt; est disponible dans tous les navigateurs modernes — Chrome, Safari, Firefox, Edge. On crée une connexion pair, on ajoute une piste micro via &lt;code&gt;getUserMedia&lt;/code&gt;, et le navigateur gère l'encodage audio (Opus), la collecte des candidats ICE et le chiffrement DTLS. Aucune bibliothèque nécessaire.&lt;/p&gt;

&lt;p&gt;Côté serveur, c'est une autre histoire. WebRTC a été conçu pour les navigateurs, pas pour des backends Python. La bibliothèque de référence pour le WebRTC côté serveur en Python est &lt;a href="https://github.com/aiortc/aiortc" rel="noopener noreferrer"&gt;aiortc&lt;/a&gt; — une implémentation asyncio de WebRTC et ORTC. Elle gère les connexions pair, la négociation ICE et les pistes média, et utilise &lt;a href="https://github.com/PyAV-Org/PyAV" rel="noopener noreferrer"&gt;PyAV&lt;/a&gt; (bindings FFmpeg) pour le traitement des trames audio/vidéo. Elle n'est pas aussi éprouvée que le WebRTC des navigateurs, mais elle fonctionne bien et c'est ce qu'utilise aussi le code d'exemple AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture : développement local vs. déployé
&lt;/h2&gt;

&lt;p&gt;Un point que je voulais préserver du design original de Darryl est la possibilité de tout faire tourner localement pour le développement, sans aucune infrastructure cloud. La migration WebRTC maintient cela.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mode local
&lt;/h3&gt;

&lt;p&gt;En mode local, l'agent tourne sur votre machine. Le navigateur et l'agent sont sur le même réseau (ou la même machine), donc WebRTC se connecte en pair-à-pair sans avoir besoin de relais TURN. La signalisation — l'échange d'offres/réponses SDP&lt;sup id="fnref5"&gt;5&lt;/sup&gt; et de candidats ICE&lt;sup id="fnref6"&gt;6&lt;/sup&gt; — passe par le proxy du serveur de développement Vite vers le serveur FastAPI local.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ncnt6qlx7dtqo03hq4s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ncnt6qlx7dtqo03hq4s.png" alt="Local mode"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Mode hébergé sur Bedrock AgentCore
&lt;/h3&gt;

&lt;p&gt;En mode hébergé, l'agent tourne dans un conteneur Docker sur Bedrock AgentCore Runtime, attaché à un VPC via une interface réseau élastique (ENI) dans un sous-réseau privé. Le navigateur ne peut pas atteindre l'agent directement — tout le trafic média passe par un relais KVS TURN. La signalisation passe par le endpoint HTTP &lt;code&gt;/invocations&lt;/code&gt; d'AgentCore, authentifié en SigV4 via le SDK &lt;code&gt;@aws-sdk/client-bedrock-agentcore&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ofz0nt6qpa0t6umt7r1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ofz0nt6qpa0t6umt7r1.png" alt="Voice Agent deployed on AgentCore"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Le diagramme suivant, tiré de la &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-webrtc.html" rel="noopener noreferrer"&gt;documentation AWS&lt;/a&gt;, montre le réseau VPC en détail — la signalisation passe par le endpoint HTTP d'AgentCore tandis que le trafic média passe par la NAT gateway du VPC vers le relais KVS TURN :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzz12es56e4fierlr30vt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzz12es56e4fierlr30vt.png" alt="AgentCore WebRTC Architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Le point important à noter est que le code de l'agent est quasi identique entre les modes local et déployé. Le &lt;code&gt;BidiAgent&lt;/code&gt;, le &lt;code&gt;BidiNovaSonicModel&lt;/code&gt; et les quatre outils (recherche de recettes, minuteur, recherche nutritionnelle, conversion d'unités) sont totalement inchangés. La seule différence est la couche transport : en local, aiortc se connecte en P2P ; en déployé, il se connecte via KVS TURN. L'agent détecte dans quel mode il se trouve via la variable d'environnement &lt;code&gt;CONTAINER_ENV&lt;/code&gt; et configure les serveurs ICE en conséquence.&lt;/p&gt;

&lt;p&gt;Cette séparation propre a été possible grâce au protocole &lt;code&gt;BidiInput&lt;/code&gt;/&lt;code&gt;BidiOutput&lt;/code&gt; de Strands. J'ai écrit deux petites classes adaptateurs — &lt;code&gt;WebRTCBidiInput&lt;/code&gt; et &lt;code&gt;WebRTCBidiOutput&lt;/code&gt; — qui font le pont entre les pistes audio aiortc et le format d'événements attendu par BidiAgent. L'agent ne sait pas et ne se soucie pas de savoir si l'audio vient d'un WebSocket ou d'une piste WebRTC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce qu'apporte le support WebRTC de Bedrock AgentCore
&lt;/h2&gt;

&lt;p&gt;Le 20 mars 2026, AWS a &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-bedrock-webrtc/" rel="noopener noreferrer"&gt;annoncé&lt;/a&gt; le support WebRTC pour AgentCore Runtime. Je veux être honnête sur ce que cela signifie en pratique.&lt;/p&gt;

&lt;p&gt;Je n'en suis pas sûr à 100%, et je suis prêt à être corrigé, mais mon impression est que les briques de base — le mode réseau VPC, KVS TURN, le endpoint HTTP &lt;code&gt;/invocations&lt;/code&gt; — existaient tous déjà avant cette annonce. Le mode réseau VPC est disponible depuis la disponibilité générale d'AgentCore en octobre 2025. KVS TURN est une fonctionnalité de longue date de Kinesis Video Streams. Et &lt;code&gt;/invocations&lt;/code&gt; a toujours été le endpoint HTTP standard des runtimes AgentCore.&lt;/p&gt;

&lt;p&gt;Ce que la release du 20 mars ajoute, d'après ce que je comprends, c'est de la documentation officielle, du code d'exemple fonctionnel, et la déclaration explicite que WebRTC est un protocole supporté sur AgentCore Runtime. Avant cela, on pouvait techniquement assembler les mêmes pièces soi-même, mais on était seul — pas de docs, pas d'exemples, pas de garantie que ça continuerait à fonctionner.&lt;/p&gt;

&lt;p&gt;Ce qu'AgentCore apporte est réellement précieux : un hébergement de conteneurs managé avec auto-scaling, l'isolation des sessions entre utilisateurs concurrents, l'observabilité intégrée (logs CloudWatch, traces X-Ray), et aucune infrastructure à gérer au-delà du VPC. Je n'ai pas eu à configurer ECS, des load balancers ou de l'orchestration de conteneurs.&lt;/p&gt;

&lt;p&gt;Cela dit, il y a une bonne quantité de code custom. La signalisation WebRTC (échange SDP, gestion des candidats ICE), le cycle de vie de la connexion pair aiortc, le pont entre les pistes audio et BidiAgent, et la gestion des identifiants KVS TURN — tout cela est du code applicatif que j'ai écrit. AgentCore l'héberge et l'exécute, mais ne l'abstrait pas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Défis et leçons apprises
&lt;/h2&gt;

&lt;p&gt;La migration de WebSocket vers WebRTC a commencé en douceur, puis ça s'est corsé. Voici ce qui m'a fait trébucher.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compatibilité des zones de disponibilité du VPC
&lt;/h3&gt;

&lt;p&gt;AgentCore Runtime ne supporte que certaines zones de disponibilité. En us-east-1, seules &lt;code&gt;use1-az4&lt;/code&gt; (us-east-1a), &lt;code&gt;use1-az1&lt;/code&gt; (us-east-1c) et &lt;code&gt;use1-az2&lt;/code&gt; (us-east-1d) sont supportées. J'ai initialement laissé Terraform choisir les deux premières AZ automatiquement, ce qui m'a donné us-east-1a et us-east-1b. La mise à jour du runtime a échoué avec un statut cryptique &lt;code&gt;UPDATE_FAILED&lt;/code&gt;. Le vrai message d'erreur — mentionnant l'AZ non supportée — était enfoui dans le champ &lt;code&gt;failureReason&lt;/code&gt; de la réponse API, pas remonté dans l'erreur Terraform. J'ai fini par coder en dur les AZ supportées dans mon module VPC.&lt;/p&gt;

&lt;h3&gt;
  
  
  Affinité de session
&lt;/h3&gt;

&lt;p&gt;Celui-ci m'a coûté des heures. La signalisation WebRTC est une poignée de main en plusieurs étapes — le navigateur et l'agent échangent plusieurs messages pour établir une connexion. L'agent doit se souvenir de l'état de la connexion du premier message quand il traite le deuxième et le troisième. Si ces messages atterrissent sur des instances serveur différentes, l'agent n'a aucune mémoire de la poignée de main en cours et la connexion échoue.&lt;/p&gt;

&lt;p&gt;J'ai d'abord utilisé des requêtes HTTP POST signées SigV4, en supposant qu'inclure l'identifiant de session comme paramètre de requête fournirait l'affinité de routage. Ce n'était pas le cas. Les candidats ICE atterrissaient sur une instance de conteneur différente de celle qui détenait la connexion pair.&lt;/p&gt;

&lt;p&gt;La solution a été d'utiliser le SDK &lt;code&gt;@aws-sdk/client-bedrock-agentcore&lt;/code&gt; avec &lt;code&gt;InvokeAgentRuntimeCommand&lt;/code&gt; et le paramètre &lt;code&gt;runtimeSessionId&lt;/code&gt;. C'est le seul moyen fiable de s'assurer que toutes les requêtes d'une session WebRTC atteignent la même instance de conteneur. Le code d'exemple AWS utilise ce pattern aussi — je ne l'avais simplement pas remarqué au début parce que j'étais concentré sur les parties WebRTC.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filtrage des candidats SDP
&lt;/h3&gt;

&lt;p&gt;Quand l'agent crée une connexion pair à l'intérieur du VPC, aiortc génère des candidats ICE pour toutes les interfaces réseau disponibles — y compris des IP internes au VPC comme &lt;code&gt;169.254.0.2&lt;/code&gt;. Ces candidats hôtes se retrouvent dans la réponse SDP envoyée au navigateur. Le navigateur essaie consciencieusement de s'y connecter, échoue (parce qu'ils sont injoignables depuis l'Internet public), et ne se rabat sur les candidats relais qu'ensuite. Cela ajoute plusieurs secondes au temps de connexion.&lt;/p&gt;

&lt;p&gt;La solution est simple : retirer les candidats non-relais de la réponse SDP avant de la renvoyer au navigateur. En mode déployé, les seuls candidats qui peuvent fonctionner sont les candidats relais TURN, donc il n'y a aucune raison d'inclure les autres.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mode TURN uniquement
&lt;/h3&gt;

&lt;p&gt;Similaire au problème de filtrage SDP, l'instance aiortc de l'agent essaie les candidats hôtes avant les candidats relais par défaut. Comme les candidats hôtes utilisent des IP internes au VPC qui ne peuvent jamais fonctionner du point de vue du navigateur, c'est du temps perdu. Configurer aiortc pour n'utiliser que les candidats relais TURN (&lt;code&gt;turn_only=True&lt;/code&gt;) saute directement aux candidats qui fonctionnent réellement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initialisation paresseuse de KVS
&lt;/h3&gt;

&lt;p&gt;J'appelais initialement &lt;code&gt;kvs.init()&lt;/code&gt; au moment de l'import du module, protégé par un &lt;code&gt;if IS_CONTAINER&lt;/code&gt;. Ça fonctionnait bien en local mais faisait crasher le conteneur sur AgentCore. L'appel API KVS pour trouver ou créer le canal de signalisation nécessite des identifiants AWS, et au démarrage du conteneur il peut y avoir un bref délai avant que les identifiants du rôle IAM soient disponibles. Déplacer l'initialisation à la première requête réelle (init paresseuse) a résolu le crash.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comportement au démarrage à froid
&lt;/h3&gt;

&lt;p&gt;Après que le conteneur est resté inactif un moment, la première tentative de connexion WebRTC échoue parfois. Les requêtes de signalisation réussissent (AgentCore renvoie 200), mais la connexion ICE ne s'établit jamais. Je soupçonne que c'est lié au fait qu'AgentCore démarre une nouvelle instance de conteneur — les premières requêtes peuvent être traitées par une instance qui n'est pas encore complètement prête. Côté agent, j'ai explicitement mis &lt;code&gt;--workers 1&lt;/code&gt; dans la commande uvicorn pour m'assurer que toutes les requêtes au sein d'un conteneur touchent le même processus (et donc le même état de connexion pair en mémoire). Côté frontend, j'ai ajouté un mécanisme de retry : attendre que ICE atteigne l'état "connected", et si ce n'est pas le cas dans les 10 secondes, tout démonter et réessayer avec un nouvel identifiant de session. Ensemble, ces deux mesures ont rendu la connexion fiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code clé
&lt;/h2&gt;

&lt;p&gt;Je ne vais pas parcourir chaque fichier, mais voici les pièces qui font fonctionner l'intégration WebRTC.&lt;/p&gt;

&lt;p&gt;L'adaptateur &lt;code&gt;WebRTCBidiInput&lt;/code&gt; lit les trames audio de la piste aiortc, les rééchantillonne à 16kHz, et les renvoie comme événements &lt;code&gt;bidi_audio_input&lt;/code&gt; que BidiAgent comprend :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebRTCBidiInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_track&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;MediaStreamError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;StopAsyncIteration&lt;/span&gt;
        &lt;span class="n"&gt;resampled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_resampler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;pcm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;planes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resampled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bidi_audio_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;L'adaptateur &lt;code&gt;WebRTCBidiOutput&lt;/code&gt; fait l'inverse — il reçoit les événements de BidiAgent et pousse l'audio vers la piste de sortie aiortc :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebRTCBidiOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_track&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_output_track&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output_track&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bidi_audio_stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;audio_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_output_track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bidi_interruption&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_output_track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Côté frontend, le hook &lt;code&gt;useWebRTCSession&lt;/code&gt; utilise le SDK AgentCore pour la signalisation :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoke&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BedrockAgentCoreClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InvokeAgentRuntimeCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;agentRuntimeArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;runtimeSessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// assure l'affinité de session&lt;/span&gt;
    &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transformToByteArray&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le code source complet est dans le &lt;a href="https://github.com/psantus/strands-bidir-nova" rel="noopener noreferrer"&gt;repo&lt;/a&gt; — la branche &lt;code&gt;feat/webrtc&lt;/code&gt; contient la version locale uniquement, et &lt;code&gt;feat/webrtc-agentcore&lt;/code&gt; la version déployée complète avec Terraform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outillage de développement
&lt;/h2&gt;

&lt;p&gt;J'ai construit ce projet avec &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro CLI&lt;/a&gt;, l'assistant de développement IA d'Amazon. Il a géré la planification, la génération de code, le débogage et le déploiement itératif — y compris les nombreux allers-retours d'essais-erreurs avec la configuration WebRTC que ce billet décrit. Le va-et-vient entre écriture de code, déploiement, vérification des logs et correction des problèmes se prêtait naturellement à un workflow de pair-programming avec une IA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Essayez vous-même
&lt;/h2&gt;

&lt;p&gt;Pour lancer en local :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/psantus/strands-bidir-nova.git
&lt;span class="nb"&gt;cd &lt;/span&gt;strands-bidir-nova
git checkout feat/webrtc
uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make install-frontend
&lt;span class="c"&gt;# Terminal 1 :&lt;/span&gt;
make serve
&lt;span class="c"&gt;# Terminal 2 :&lt;/span&gt;
make serve-frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ouvrez &lt;code&gt;http://localhost:5173&lt;/code&gt;, cliquez sur le micro, et commencez à parler.&lt;/p&gt;

&lt;p&gt;Pour la version déployée sur AgentCore, passez sur la branche &lt;code&gt;feat/webrtc-agentcore&lt;/code&gt; et suivez le README. Vous aurez besoin d'une Knowledge Base Bedrock avec quelques recettes, d'un pool d'utilisateurs Cognito et de Docker pour construire l'image du conteneur. Un seul &lt;code&gt;terraform apply&lt;/code&gt; gère le reste.&lt;/p&gt;

&lt;p&gt;Si vous préférez commencer par la version WebSocket, le &lt;a href="https://darryl-ruggles.cloud/bi-directional-voice-controlled-recipe-assistant-with-nova-sonic-2/" rel="noopener noreferrer"&gt;billet original&lt;/a&gt; de Darryl Ruggles est le bon point de départ.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Paul Santus est consultant cloud indépendant chez &lt;a href="https://terracloud.fr" rel="noopener noreferrer"&gt;TerraCloud&lt;/a&gt;. Il accompagne les organisations dans la construction et le déploiement d'applications IA sur AWS. Retrouvez-le sur &lt;a href="https://www.linkedin.com/in/paulsantus" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;strong&gt;WebRTC&lt;/strong&gt; (Web Real-Time Communication) — Standard ouvert pour la communication audio, vidéo et données en temps réel directement entre navigateurs et appareils, utilisant un transport basé sur UDP. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;strong&gt;TURN&lt;/strong&gt; (Traversal Using Relays around NAT) — Serveur relais qui transfère le trafic média quand deux pairs ne peuvent pas se connecter directement. Les deux côtés envoient leur audio au serveur TURN, qui le relaie à l'autre côté. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;strong&gt;NAT&lt;/strong&gt; (Network Address Translation) — Mécanisme réseau qui fait correspondre des adresses IP privées à des adresses publiques. La plupart des routeurs domestiques et des VPC cloud utilisent le NAT, ce qui empêche les connexions entrantes directes. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;&lt;strong&gt;SFU&lt;/strong&gt; (Selective Forwarding Unit) — Serveur média qui reçoit les pistes audio/vidéo des participants et les retransmet sélectivement aux autres, sans mixage ni transcodage. Utilisé par LiveKit, Chime SDK, Daily, etc. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;&lt;strong&gt;SDP&lt;/strong&gt; (Session Description Protocol) — Format texte décrivant une session multimédia : codecs, adresses de transport et types de média. En WebRTC, les pairs échangent des « offres » et « réponses » SDP pour négocier la connexion. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;&lt;strong&gt;ICE&lt;/strong&gt; (Interactive Connectivity Establishment) — Protocole pour trouver le meilleur chemin réseau entre deux pairs. Il collecte des adresses candidates (locales, réflexives serveur, relais) et teste la connectivité entre elles. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>agentcore</category>
      <category>voice</category>
    </item>
    <item>
      <title>Switching my AI voice agent from WebSocket to WebRTC — what broke and what I learned</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Fri, 27 Mar 2026 15:29:19 +0000</pubDate>
      <link>https://forem.com/aws-builders/switching-my-ai-voice-agent-from-websocket-to-webrtc-what-broke-and-what-i-learned-3dkn</link>
      <guid>https://forem.com/aws-builders/switching-my-ai-voice-agent-from-websocket-to-webrtc-what-broke-and-what-i-learned-3dkn</guid>
      <description>&lt;p&gt;Switching my AI voice agent from WebSocket to WebRTC — what broke and what I learned&lt;/p&gt;

&lt;p&gt;A few weeks ago, I came across Darryl Ruggles' &lt;a href="https://darryl-ruggles.cloud/bi-directional-voice-controlled-recipe-assistant-with-nova-sonic-2/" rel="noopener noreferrer"&gt;blog post&lt;/a&gt; and accompanying repo for a bidirectional voice agent built with Strands BidiAgent and Amazon Nova Sonic v2. His work is remarkably well put together — I had a working voice assistant running on my laptop in about 10 minutes. The agent listens to your voice, searches a recipe knowledge base, sets cooking timers, looks up nutrition data, and converts units, all through natural conversation.&lt;/p&gt;

&lt;p&gt;Darryl's version uses WebSocket as the transport between the browser and the agent. It works well, but I wanted to push things further: switch the transport to WebRTC, and deploy the whole thing on Bedrock AgentCore Runtime. This post covers that journey — what changed, what broke, and what I learned along the way.&lt;/p&gt;

&lt;p&gt;But first, a short demo!&lt;br&gt;
  &lt;iframe src="https://www.youtube.com/embed/kxElgTLmIQ8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The full source code is available on &lt;a href="https://github.com/psantus/strands-bidir-nova" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. The repo is Terraform-managed end-to-end, though you can still use Darryl's Makefile approach if you prefer keeping Terraform for surrounding infrastructure and CLI calls for agent deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WebRTC for a voice agent
&lt;/h2&gt;

&lt;p&gt;The WebSocket version works, so why change it? A few reasons pushed me toward WebRTC.&lt;/p&gt;

&lt;p&gt;First, latency. WebSocket runs over TCP, which means every packet is guaranteed to arrive in order. That's great for chat messages, but for real-time audio, a single dropped packet causes the entire stream to stall while TCP retransmits. WebRTC&lt;sup id="fnref1"&gt;1&lt;/sup&gt; uses UDP under the hood — if a packet is lost, the stream keeps going. For a voice conversation, a tiny glitch is far better than a noticeable pause.&lt;/p&gt;

&lt;p&gt;Second, the browser does more of the heavy lifting. With WebSocket, I had to capture microphone audio using &lt;code&gt;getUserMedia&lt;/code&gt;, downsample it to 16kHz with a &lt;code&gt;ScriptProcessorNode&lt;/code&gt;, encode it as base64 PCM, and send it as JSON messages. On the playback side, I needed an &lt;code&gt;AudioWorklet&lt;/code&gt; with a ring buffer to handle the incoming audio stream. With WebRTC, the browser handles audio capture, encoding (Opus), and playback natively through &lt;code&gt;RTCPeerConnection&lt;/code&gt;. The frontend code got significantly simpler.&lt;/p&gt;

&lt;p&gt;Third, WebRTC is future-proof for video. AI avatar are getting there with acceptable latency, and WebRTC handles video tracks just as naturally as audio tracks. Adding a video stream later is just a matter of adding a track to the existing peer connection — no architectural change needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick primer on WebRTC architectures
&lt;/h2&gt;

&lt;p&gt;There are two fundamentally different ways to use WebRTC, and the choice matters when building a voice agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Peer-to-peer (P2P)
&lt;/h3&gt;

&lt;p&gt;In P2P WebRTC, two peers connect directly to each other. There's no media server in the middle — audio flows straight from the browser to the agent and back. A TURN&lt;sup id="fnref2"&gt;2&lt;/sup&gt; relay server may be needed when one or both peers are behind NAT&lt;sup id="fnref3"&gt;3&lt;/sup&gt; (which is almost always the case in production: clients are behind Internet Router and Agents need to be in private VPC to access company tools), but the TURN server just forwards packets without inspecting or processing them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpgd5s5k2a0ty88lt62uf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpgd5s5k2a0ty88lt62uf.png" alt="P2P WebRTC"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Room-based (SFU)
&lt;/h3&gt;

&lt;p&gt;In a room-based architecture, a media server (called an SFU&lt;sup id="fnref4"&gt;4&lt;/sup&gt; — Selective Forwarding Unit) sits in the middle. Participants connect to the server, not to each other. The server receives audio/video tracks from each participant and selectively forwards them to the others. LiveKit, Amazon Chime SDK, and Daily are examples of SFU-based platforms.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpbdeeofri17xb09zucwu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpbdeeofri17xb09zucwu.png" alt="Media Server WebRTC"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a 1:1 voice agent, P2P is simpler and avoids the cost and complexity of running (or paying for) a media server. I went with P2P using Amazon Kinesis Video Streams (KVS) as the managed TURN relay — this is the documented approach for WebRTC on AgentCore.&lt;/p&gt;

&lt;p&gt;I did consider room-based solutions, but each SFU platform requires its own SDK — you can't just connect with a standard &lt;code&gt;RTCPeerConnection&lt;/code&gt;. AWS's own WebRTC offering, Amazon Chime SDK, is feature-rich (transcription, recording, analytics) and significantly cheaper than alternatives like LiveKit or Daily, but it doesn't yet offer a paved path for server-side agent-to-room communication. That's a feature I'd love to see, given how compelling the rest of the Chime SDK is. For now, P2P with KVS TURN was the most straightforward path. I'll definitely consider in-room WebRTC, but that's a story for another post.&lt;/p&gt;

&lt;h3&gt;
  
  
  The WebRTC stack: browser and server
&lt;/h3&gt;

&lt;p&gt;On the browser side, WebRTC is built in. The &lt;code&gt;RTCPeerConnection&lt;/code&gt; API is available natively in every modern browser — Chrome, Safari, Firefox, Edge. You create a peer connection, add a microphone track via &lt;code&gt;getUserMedia&lt;/code&gt;, and the browser handles audio encoding (Opus), ICE candidate gathering, and DTLS encryption. No libraries needed.&lt;/p&gt;

&lt;p&gt;On the server side, it's a different story. WebRTC was designed for browsers, not for Python backends. The go-to library for server-side WebRTC in Python is &lt;a href="https://github.com/aiortc/aiortc" rel="noopener noreferrer"&gt;aiortc&lt;/a&gt; — an asyncio-based implementation of WebRTC and ORTC. It handles peer connections, ICE negotiation, and media tracks, and uses &lt;a href="https://github.com/PyAV-Org/PyAV" rel="noopener noreferrer"&gt;PyAV&lt;/a&gt; (FFmpeg bindings) for audio/video frame processing. It's not as battle-tested as browser WebRTC, but it works well and is what the AWS sample code uses too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: local development vs. deployed
&lt;/h2&gt;

&lt;p&gt;One thing I wanted to preserve from Darryl's original design is the ability to run everything locally for development, without any cloud infrastructure. The WebRTC migration maintains this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local mode
&lt;/h3&gt;

&lt;p&gt;In local mode, the agent runs on your machine. The browser and agent are on the same network (or the same machine), so WebRTC connects peer-to-peer without needing a TURN relay. Signaling — the exchange of SDP&lt;sup id="fnref5"&gt;5&lt;/sup&gt; offers/answers and ICE&lt;sup id="fnref6"&gt;6&lt;/sup&gt; candidates — goes through the Vite dev server proxy to the local FastAPI server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ncnt6qlx7dtqo03hq4s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ncnt6qlx7dtqo03hq4s.png" alt="Local mode"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployed mode
&lt;/h3&gt;

&lt;p&gt;In deployed mode, the agent runs inside a Docker container on Bedrock AgentCore Runtime, attached to a VPC via an elastic network interface (ENI) in a private subnet. The browser can't reach the agent directly — all media traffic flows through a KVS TURN relay. Signaling goes through AgentCore's &lt;code&gt;/invocations&lt;/code&gt; HTTP endpoint, authenticated with SigV4 via the &lt;code&gt;@aws-sdk/client-bedrock-agentcore&lt;/code&gt; SDK.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ofz0nt6qpa0t6umt7r1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ofz0nt6qpa0t6umt7r1.png" alt="Voice Agent deployed on AgentCore"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The following diagram from the &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-webrtc.html" rel="noopener noreferrer"&gt;AWS documentation&lt;/a&gt; shows how it works in terms of networking: signaling flows through AgentCore's HTTP endpoint while media traffic goes through the VPC's NAT gateway to the KVS TURN relay:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzz12es56e4fierlr30vt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzz12es56e4fierlr30vt.png" alt="AgentCore WebRTC Architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The important thing to note is that the agent code is almost identical between local and deployed modes. The &lt;code&gt;BidiAgent&lt;/code&gt;, &lt;code&gt;BidiNovaSonicModel&lt;/code&gt;, and all four tools (recipe search, timer, nutrition lookup, unit converter) are completely unchanged. The only difference is the transport layer: in local mode, aiortc connects P2P; in deployed mode, it connects through KVS TURN. The agent detects which mode it's in via the &lt;code&gt;CONTAINER_ENV&lt;/code&gt; environment variable and configures ICE servers accordingly.&lt;/p&gt;

&lt;p&gt;This clean separation was possible because of Strands' &lt;code&gt;BidiInput&lt;/code&gt;/&lt;code&gt;BidiOutput&lt;/code&gt; protocol. I wrote two small adapter classes — &lt;code&gt;WebRTCBidiInput&lt;/code&gt; and &lt;code&gt;WebRTCBidiOutput&lt;/code&gt; — that bridge aiortc audio tracks to the event format BidiAgent expects. The agent doesn't know or care whether audio is coming from a WebSocket or a WebRTC track.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Bedrock AgentCore's WebRTC support adds
&lt;/h2&gt;

&lt;p&gt;On March 20th, 2026, AWS &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-bedrock-webrtc/" rel="noopener noreferrer"&gt;announced&lt;/a&gt; WebRTC support for AgentCore Runtime. &lt;/p&gt;

&lt;p&gt;I'm not 100% sure, and am ready to stand corrected, but my impression is that the building blocks — VPC network mode, KVS TURN, the &lt;code&gt;/invocations&lt;/code&gt; HTTP endpoint — all existed before this announcement. VPC network mode has been available since AgentCore's general availability in October 2025. KVS TURN is a long-standing Kinesis Video Streams feature. And &lt;code&gt;/invocations&lt;/code&gt; has always been the standard HTTP endpoint for AgentCore runtimes.&lt;/p&gt;

&lt;p&gt;What the March 20th release adds, as far as I can tell, is official documentation, working sample code, and the explicit statement that WebRTC is a supported protocol on AgentCore Runtime. Before this, you could technically have assembled the same pieces yourself, but you'd be on your own — no docs, no samples, no guarantee it would keep working.&lt;/p&gt;

&lt;p&gt;What AgentCore does provide is genuinely valuable: managed container hosting with auto-scaling, session isolation between concurrent users, built-in observability (CloudWatch logs, X-Ray traces), and no infrastructure to manage beyond the VPC. I didn't have to set up ECS, configure load balancers, or manage container orchestration.&lt;/p&gt;

&lt;p&gt;That said, there's a fair amount of custom code involved. The WebRTC signaling (SDP exchange, ICE candidate management), the aiortc peer connection lifecycle, the audio track bridging to BidiAgent, and the KVS TURN credential management — all of that is application code that I wrote. AgentCore hosts and runs it, but doesn't abstract it away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and lessons learned
&lt;/h2&gt;

&lt;p&gt;The migration from WebSocket to WebRTC started as a smooth ride (local mode worked on first attempt!), and was not so smooth afterwards, as I tried to get it to work on Bedrock AgentCore. Here's what tripped me up.&lt;/p&gt;

&lt;h3&gt;
  
  
  VPC availability zone compatibility
&lt;/h3&gt;

&lt;p&gt;AgentCore Runtime only supports specific availability zones. In us-east-1, only &lt;code&gt;use1-az4&lt;/code&gt; (us-east-1a), &lt;code&gt;use1-az1&lt;/code&gt; (us-east-1c), and &lt;code&gt;use1-az2&lt;/code&gt; (us-east-1d) are supported. I initially let Terraform pick the first two AZs automatically, which gave me us-east-1a and us-east-1b. The runtime update failed with a cryptic &lt;code&gt;UPDATE_FAILED&lt;/code&gt; status. The actual error message — mentioning the unsupported AZ — was buried in the &lt;code&gt;failureReason&lt;/code&gt; field of the API response, not surfaced in the Terraform error. I ended up hardcoding the supported AZs in my VPC module.&lt;/p&gt;

&lt;h3&gt;
  
  
  Session affinity
&lt;/h3&gt;

&lt;p&gt;This one cost me hours. WebRTC signaling is a multi-step handshake — the browser and agent exchange several messages to establish a connection. The agent needs to remember the connection state from the first message when processing the second and third. If those messages land on different server instances, the agent has no memory of the ongoing handshake and the connection fails. &lt;/p&gt;

&lt;p&gt;I initially used raw SigV4-signed HTTP POST requests, assuming that including the session ID as a query parameter would provide routing affinity. It didn't. The ICE candidates were landing on a different container instance (?) than the one holding the peer connection.&lt;/p&gt;

&lt;p&gt;The fix was to use the &lt;code&gt;@aws-sdk/client-bedrock-agentcore&lt;/code&gt; SDK with &lt;code&gt;InvokeAgentRuntimeCommand&lt;/code&gt; and the &lt;code&gt;runtimeSessionId&lt;/code&gt; parameter. This is the only reliable way to ensure all requests for a WebRTC session reach the same container instance. The AWS sample code uses this pattern too — I just didn't notice it at first because I was focused on the WebRTC parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  SDP candidate filtering
&lt;/h3&gt;

&lt;p&gt;When the agent creates a peer connection inside the VPC, aiortc generates ICE candidates for all available network interfaces — including VPC-internal IPs like &lt;code&gt;169.254.0.2&lt;/code&gt;. These host candidates end up in the SDP answer sent to the browser. The browser dutifully tries to connect to them, fails (because they're unreachable from the public internet), and only then falls back to the relay candidates. This adds several seconds to the connection time.&lt;/p&gt;

&lt;p&gt;The fix is straightforward: strip non-relay candidates from the SDP answer before returning it to the browser. In deployed mode, the only candidates that can work are TURN relay candidates, so there's no reason to include the others.&lt;/p&gt;

&lt;h3&gt;
  
  
  TURN-only mode
&lt;/h3&gt;

&lt;p&gt;Similar to the SDP filtering issue, the agent's aiortc instance tries host candidates before relay candidates by default. Since host candidates use VPC-internal IPs that can never work from the browser's perspective, this wastes time. Configuring aiortc to only use TURN relay candidates (&lt;code&gt;turn_only=True&lt;/code&gt;) skips straight to the candidates that actually work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lazy KVS initialization
&lt;/h3&gt;

&lt;p&gt;I initially called &lt;code&gt;kvs.init()&lt;/code&gt; at module import time, guarded by an &lt;code&gt;if IS_CONTAINER&lt;/code&gt; check. This worked fine locally but caused the container to crash on AgentCore. The KVS API call to find or create the signaling channel requires AWS credentials, and during container startup there can be a brief delay before the IAM role credentials are available. Moving the initialization to the first actual request (lazy init) fixed the crash.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cold start behavior
&lt;/h3&gt;

&lt;p&gt;After the container has been idle for a while, the first WebRTC connection attempt sometimes fails. The signaling requests succeed (AgentCore returns 200), but the ICE connection never completes. I suspect this is related to AgentCore spinning up a fresh container instance — the first few requests may be handled by an instance that isn't fully warmed up. On the agent side, I explicitly set &lt;code&gt;--workers 1&lt;/code&gt; in the uvicorn command to ensure all requests within a container hit the same process (and therefore the same in-memory peer connection state). On the frontend, I added a retry mechanism: wait for ICE to reach "connected" state, and if it doesn't within 10 seconds, tear down and retry with a new session ID. Together, these made the connection reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key code
&lt;/h2&gt;

&lt;p&gt;I won't walk through every file, but here are the pieces that make the WebRTC integration work.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;WebRTCBidiInput&lt;/code&gt; adapter reads audio frames from the aiortc track, resamples them to 16kHz, and returns them as &lt;code&gt;bidi_audio_input&lt;/code&gt; events that BidiAgent understands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebRTCBidiInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_track&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;MediaStreamError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;StopAsyncIteration&lt;/span&gt;
        &lt;span class="n"&gt;resampled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_resampler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;pcm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;planes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resampled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bidi_audio_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;WebRTCBidiOutput&lt;/code&gt; adapter does the reverse — it receives events from BidiAgent and pushes audio to the aiortc output track:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebRTCBidiOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_track&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_output_track&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output_track&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bidi_audio_stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;audio_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_output_track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bidi_interruption&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_output_track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the frontend, the &lt;code&gt;useWebRTCSession&lt;/code&gt; hook uses the AgentCore SDK for signaling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoke&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BedrockAgentCoreClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InvokeAgentRuntimeCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;agentRuntimeArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;runtimeSessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// ensures session affinity&lt;/span&gt;
    &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transformToByteArray&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full source is in the &lt;a href="https://github.com/psantus/strands-bidir-nova" rel="noopener noreferrer"&gt;repo&lt;/a&gt; — the &lt;code&gt;feat/webrtc&lt;/code&gt; branch has the local-only version, and &lt;code&gt;feat/webrtc-agentcore&lt;/code&gt; has the full deployed version with Terraform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development tooling
&lt;/h2&gt;

&lt;p&gt;I built this project using &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro CLI&lt;/a&gt;, Amazon's AI development assistant. It handled the planning, code generation, debugging, and iterative deployment — including the many rounds of trial-and-error with WebRTC configuration that this post describes. The back-and-forth between writing code, deploying, checking logs, and fixing issues was a natural fit for an AI pair-programming workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;To run locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/psantus/strands-bidir-nova.git
&lt;span class="nb"&gt;cd &lt;/span&gt;strands-bidir-nova
git checkout feat/webrtc
uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make install-frontend
&lt;span class="c"&gt;# Terminal 1:&lt;/span&gt;
make serve
&lt;span class="c"&gt;# Terminal 2:&lt;/span&gt;
make serve-frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:5173&lt;/code&gt;, click the microphone, and start talking.&lt;/p&gt;

&lt;p&gt;For the deployed version on AgentCore, check out the &lt;code&gt;feat/webrtc-agentcore&lt;/code&gt; branch and follow the README. You'll need a Bedrock Knowledge Base with some recipes, a Cognito user pool, and Docker for building the container image. A single &lt;code&gt;terraform apply&lt;/code&gt; handles the rest.&lt;/p&gt;

&lt;p&gt;If you'd rather start with the WebSocket version first, Darryl Ruggles' &lt;a href="https://darryl-ruggles.cloud/bi-directional-voice-controlled-recipe-assistant-with-nova-sonic-2/" rel="noopener noreferrer"&gt;original post&lt;/a&gt; is the place to go.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Paul Santus is an independent cloud consultant at &lt;a href="https://terracloud.fr" rel="noopener noreferrer"&gt;TerraCloud&lt;/a&gt;. He helps organizations build and deploy AI-powered applications on AWS. Connect with him on &lt;a href="https://www.linkedin.com/in/paulsantus" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;strong&gt;WebRTC&lt;/strong&gt; (Web Real-Time Communication) — An open standard for real-time audio, video, and data communication directly between browsers and devices, using UDP-based transport. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;strong&gt;TURN&lt;/strong&gt; (Traversal Using Relays around NAT) — A relay server that forwards media traffic when two peers can't connect directly. Both sides send their audio to the TURN server, which relays it to the other side. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;strong&gt;NAT&lt;/strong&gt; (Network Address Translation) — A networking mechanism that maps private IP addresses to public ones. Most home routers and cloud VPCs use NAT, which prevents direct inbound connections. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;&lt;strong&gt;SFU&lt;/strong&gt; (Selective Forwarding Unit) — A media server that receives audio/video tracks from participants and selectively forwards them to others, without mixing or transcoding. Used by LiveKit, Chime SDK, Daily, etc. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;&lt;strong&gt;SDP&lt;/strong&gt; (Session Description Protocol) — A text format describing a multimedia session: codecs, transport addresses, and media types. In WebRTC, peers exchange SDP "offers" and "answers" to negotiate the connection. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;&lt;strong&gt;ICE&lt;/strong&gt; (Interactive Connectivity Establishment) — A protocol for finding the best network path between two peers. It gathers candidate addresses (local, server-reflexive, relay) and tests connectivity between them. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>agentcore</category>
      <category>webrtc</category>
      <category>bedrock</category>
      <category>ai</category>
    </item>
    <item>
      <title>re:Invent25, jour 2 : le chemin vers l'IA</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Thu, 04 Dec 2025 08:54:18 +0000</pubDate>
      <link>https://forem.com/aws-builders/reinvent25-jour-2-le-chemin-vers-lia-2l77</link>
      <guid>https://forem.com/aws-builders/reinvent25-jour-2-le-chemin-vers-lia-2l77</guid>
      <description>&lt;p&gt;Matt Garman ayant défloré la quasi-totalité des annonces hier (allant jusqu'au rythme d'une annonce de fonctionnalité toutes les 24 secondes &lt;a href="https://youtu.be/q3Sb9PemsSo?si=pXetvfDzJLfx5aOb&amp;amp;t=7028" rel="noopener noreferrer"&gt;dans un sprint de 10'&lt;/a&gt; en fin de keynote), le what's new est un peu plus vide ce matin. &lt;/p&gt;

&lt;p&gt;Mais c'est l'occasion de prendre un peu de recul et de jeter un oeil aux nombreuses sessions (toutes mise en vidéo sur le compte Youtube @awsevents). Et hier, le thème était clairement : comment aller vers l'IA ? En &lt;strong&gt;préparant vos data&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approche sémantique : S3 Vectors et Valkey
&lt;/h2&gt;

&lt;p&gt;Les données vectorielles permettent aux LLMs de faire des rapprochements sémantiques entre une question, ou une conversation, et des documents (textes, images, vidéos) de votre base documentaire.&lt;/p&gt;

&lt;p&gt;Jusqu'ici, pour utiliser ces données, il fallait utiliser une base de données dédiées, comme OpenSearch ou PostgreSQL avec pgvectors. Toute ça coûte un peu cher..&lt;/p&gt;

&lt;p&gt;Sorti en pré-version en cours d'année, S3 Vectors est maintenant un service AWS pleinement supporté, avec un gain de performance notable. Il est maintenant directement intégré avec Bedrock Knowledge base.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9o8p0epwnxfy4tuedtio.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9o8p0epwnxfy4tuedtio.png" alt="S3 Vectors is now GA"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Les agents IA peuvent donc désormais rechercher facilement des documents encodés sous forme de vecteurs. &lt;/p&gt;

&lt;p&gt;Néanmoins, les agents coûtant également cher et, dans la pratique, répondront assez souvent aux mêmes questions. De la même manière que votre appli web peut utiliser un side-cache au niveau de la BDD pour éviter de trop la solliciter, vous pouvez désormais utiliser un &lt;strong&gt;cache sémantique&lt;/strong&gt; avec Valkey&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxbln77bsh12faqx4kdzg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxbln77bsh12faqx4kdzg.png" alt="What is semantic cache"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pour savoir comment ça fonctionne, je vous partage ci-dessous une conférence à ce sujet que j'ai trouvée passionnante : "Optimize gen AI apps with semantic caching in Amazon ElastiCache (DAT451)".&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/i_lLnV6NgPg"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&gt;
  
  
  Approche tabulaire : S3 Tables et Apache Iceberg
&lt;/h2&gt;

&lt;p&gt;Dans de nombreuses entreprises, le défi reste encore de pouvoir mobiliser toute la donnée structurée pour de la BI ou du Machine Learning. &lt;/p&gt;

&lt;p&gt;Tous les services d'analytics AWS sont désormais alignés pour supporter les tables Apache Iceberg dans sa version v3 (Redshift, qui pouvait seulement les lire, peut maintenant les écrire). &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiol15t5s4y9wf8r0vm57.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiol15t5s4y9wf8r0vm57.png" alt="Support for Apache Iceberg v3"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Côté Athena : il y a de nombreuses évolutions !&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;certaines sont quasi-invisibles pour nous, mais permettent des gains de performance (et de coût) considérables : l'indexation des colonnes dans les fichiers Parquet, le fait de pousser de plus en plus de prédicats au niveau stockage (et donc éviter de récupérer et traiter en mémoire de la donnée inutile pour la requête en cours). &lt;/li&gt;
&lt;li&gt;d'autres sont de petits trucs pratiques : depuis juin, il n'y a plus besoin de gérer &lt;a href="https://aws.amazon.com/blogs/big-data/introducing-managed-query-results-for-amazon-athena/" rel="noopener noreferrer"&gt;l'emplacement S3 d'arrivée du résultat de vos requêtes Athena&lt;/a&gt;. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Parmi les évolutions qu'il me semble important de noter : &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/11/aws-glue-apache-iceberg-based-materialized-views/" rel="noopener noreferrer"&gt;l'arrivée des vues matérialisées dans Glue&lt;/a&gt; !!!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwx9yly7n82u5upl6p9gl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwx9yly7n82u5upl6p9gl.png" alt="Glue Materialised Views"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Rafraichies sur événement ou de façon planifiées, ces vues vont vous permettre de mettre en place des pipelines complexes avec des étapes intermédiaires matérialisées, tout en SQL, sans avoir à gérer la mise à jour des données. &lt;/p&gt;

&lt;p&gt;Ce talk détaille l'ensemble des évolutions sur Redshift et Athena 

  &lt;iframe src="https://www.youtube.com/embed/As1utw4zZ6M"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&gt;
  
  
  Et l'infra sous-jacente ? S3 !
&lt;/h2&gt;

&lt;p&gt;Une des questions que je me suis posées il y a quelques temps était : quelles sont les performances maximales que je peux tirer de S3 ? Si vous prenez une lambda en python et que vous faite un basique GetObject, vous aurez 70-100Mbps par secondes. Mais on peut aller &lt;strong&gt;beaucoup plus vite&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Dans ce talk Ian Mc Garry, directeur du développement logiciel sur S3, montre comment vous pouvez aller chercher une performance quasi-illimitée grâce à &lt;a href="https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/crt-based-s3-client.html" rel="noopener noreferrer"&gt;l'AWS Common Runtime&lt;/a&gt;: ça parle de "ListParts", de "Range GET" côté client, et côté back, de &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/optimizing-performance.html" rel="noopener noreferrer"&gt;préfixes&lt;/a&gt;, de directory buckets / One-Zone et de session durable (s3:CreateSession) pour gagner du temps sur l'authentification de chaque requête.&lt;br&gt;


  &lt;iframe src="https://www.youtube.com/embed/JNN5Aw5kVFI"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;Si vous souhaitez savoir comment fonctionne l'infra S3, je vous conseille deux conférences très riches :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tout d'abord l'innovation talk d'Andy Warfield, VP Storage d'AWS qui donne un aperçu des évolutions de l'infra sous-jacente au service 


  &lt;iframe src="https://www.youtube.com/embed/beWO7h7Ut44"&gt;
  &lt;/iframe&gt;


&lt;/li&gt;
&lt;li&gt;Ensuite, celui de Carl Summers qui explique comment S3 gère ses logs 

  &lt;iframe src="https://www.youtube.com/embed/NaoXnfjFQLU"&gt;
  &lt;/iframe&gt;


&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>reinvent</category>
      <category>data</category>
    </item>
    <item>
      <title>re:Invent25, jour 1 : « boom »</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Wed, 03 Dec 2025 00:51:23 +0000</pubDate>
      <link>https://forem.com/aws-builders/reinvent25-jour-1-boom--3i1h</link>
      <guid>https://forem.com/aws-builders/reinvent25-jour-1-boom--3i1h</guid>
      <description>&lt;p&gt;Je suis le "What's new" d'AWS de façon quasi quotidienne depuis plus de 7 ans. J'ai trouvé certaines éditions de re:Invent excitantes, d'autres décevantes.. mais j'ai rarement éprouvé le sentiment de submersion face à l'explosion d'annonces du jour. &lt;/p&gt;

&lt;h2&gt;
  
  
  IA : des agents autonomes
&lt;/h2&gt;

&lt;p&gt;L'annonce la plus importante de la Keynote de Matt Garman &lt;a href="https://www.youtube.com/live/rpsh_UBQY14?si=atFqMpcLjcYlcJJE&amp;amp;t=246" rel="noopener noreferrer"&gt;↗️&lt;/a&gt; est sans doute la disponibilité immédiate de 3 agents à durée de vie longue. &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3y997fi0sk3vj5bjlgrr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3y997fi0sk3vj5bjlgrr.png" alt=" " width="800" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Jusqu'ici, l'IA agentique s'appuie sur des agents qui doivent à chaque tâche redécouvrir le contexte (comment est structuré le projet, quels profils CLI sont dispo pour qu'ils accèdent aux API, ce qu'ils ont le droit de faire ou non, les choix d'architecture) ; il y a besoin d'interactions fréquentes avec le "pilote" pour les garder "on-track". &lt;strong&gt;L'agent Kiro autonome&lt;/strong&gt;&lt;a href="https://kiro.dev/blog/introducing-kiro-autonomous-agent/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt; se propose de résoudre cette limite avec des agents tournant en tâche de fond et capitalisant sur l'ensemble des tâches effectuées. Arrêtons de babysitter nos agents et commençons à les manager !&lt;/p&gt;

&lt;p&gt;Deux autres agents font leur entrée : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;l'&lt;strong&gt;agent DevOps&lt;/strong&gt; &lt;a href="https://aws.amazon.com/blogs/aws/aws-devops-agent-helps-you-accelerate-incident-response-and-improve-system-reliability-preview/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt; qui, en situation d'incident, va pouvoir proactivement analyser l'ensemble des logs, métriques, et déterminer la cause de l'incident, proposer des actions résolutives et correctives. &lt;/li&gt;
&lt;li&gt;l'&lt;strong&gt;agent de Sécurité&lt;/strong&gt; &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/aws-security-agent-preview/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt; qui va pouvoir évaluer tant le code lors de sa production (à chaque PR) que réaliser des Pentests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ces agents sont le déploiement, à l'échelle, des &lt;a href="https://www.linkedin.com/posts/paulsantus_paul-santus-lia-agentique-par-et-pour-activity-7400207024510107648-mzu8?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAAHqFuoBYjYsx4cq4zQ6SBklTKN3Pd_juYs" rel="noopener noreferrer"&gt;technologies que j'ai pu présenter&lt;/a&gt; lors du dernier AWS Poitiers User Group : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Des frameworks d'agents comme Strands permettent de donner aux LLM des "yeux" et des "mains" pour voir et agir ; &lt;/li&gt;
&lt;li&gt;Bedrock &lt;strong&gt;AgentCore&lt;/strong&gt;, service qui héberge les agents, permet maintenant &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/amazon-bedrock-agentcore-policy-evaluations-preview/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt; d'évaluer les agents (ont-ils résolu la tâche ? en faisant appel aux bons outils ?) et de contrôler rigoureusement leur exécution par des politiques exprimées en langage naturel ("tu ne feras pas une offre commerciale à un client en recouvrement") traduites dans un langage formel, Cedar.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  IA : les modèles d'AWS, ceux des tiers.. et le vôtre?
&lt;/h2&gt;

&lt;p&gt;Pour faire tourner ces agents, au début, il faut un LLM. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS vient d'ajouter &lt;strong&gt;20 modèles&lt;/strong&gt; "open-weight" (deepseek, GPT, ...) à Bedrock&lt;/li&gt;
&lt;li&gt;La firme de Seattle publie également la v2 de sa &lt;strong&gt;suite de modèles Nova&lt;/strong&gt;, avec de vraies innovations : &lt;strong&gt;Nova Omni&lt;/strong&gt; &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/amazon-nova-2-omni-preview/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt; un modèle multimodal capable de raisonnements avancés, &lt;strong&gt;Nova Sonic v2 **&lt;a href="https://aws.amazon.com/blogs/aws/introducing-amazon-nova-2-sonic-next-generation-speech-to-speech-model-for-conversational-ai/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt;, modèle speech-to-speech à basse latence qui alimente désormais **Amazon Connect&lt;/strong&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/11/amazon-connect-agentic-self-service/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt;, le centre de contact omni-canal d'AWS qui est maintenant présenté comme "staffé par des agents humains et IA", ainsi que Nova Lite et Pro.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nova Forge&lt;/strong&gt; &lt;a href="https://aws.amazon.com/blogs/aws/introducing-amazon-nova-forge-build-your-own-frontier-models-using-nova/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt; propose de partir de ces modèles et d'entraîner sur vos données (complétées par des données fournies par AWS pour que le modèle ne perde pas ses capacités généralistes) afin d'acquérir une vraie compréhension de votre métier, qu'un simple RAG ne permet pas d'obtenir.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nova Act&lt;/strong&gt; &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/build-automate-production-ui-workflows-nova-act/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt;, modèle capable sans ajout d'outil tiers, de naviguer sur le web ou d'exécuter du code. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cela sans compter AWS Transform, un service d'agents dédiés à la refonte du legacy (sortie de VM Ware, d'Oracle, de Windows... ) ou l'inclusion dans d'autres services d'agents d'upgrade (EMR permet de mettre à jour du vieux code Spark&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/apache-spark-upgrade-agent-amazon-emr/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt; et il faut s'attendre à voir la même chose sur des Lambda dans les prochains jours)&lt;/p&gt;

&lt;h2&gt;
  
  
  Une infra "IA-ready"
&lt;/h2&gt;

&lt;p&gt;De nombreux services annoncent des évolutions pour être adaptés à l'IA : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;comme API Gateway (cf. post sur Pre:Invent), Bedrock AgentCore Runtime supporte maintenant du &lt;strong&gt;streaming bidirectionnel&lt;/strong&gt; &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/bedrock-agentcore-runtime-bi-directional-streaming/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt;, &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Vector&lt;/strong&gt; &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/amazon-s3-vectors-generally-available/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt; devient généralement disponible, s'intègre avec Bedrock Knowledge bases et permet des recherches à partir de 100ms de latence (pour les recherches fréquentes) à 1s (pour les moins fréquentes) sur des milliards de vecteurs.&lt;/li&gt;
&lt;li&gt;Le &lt;strong&gt;Support&lt;/strong&gt; annonce des plans &lt;a href="https://aws.amazon.com/blogs/aws/new-and-enhanced-aws-support-plans-add-ai-capabilities-to-expert-guidance/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt; où un premier niveau de réponse par l'IA permet, à moindre coût, d'avoir un opérateur humain en moins de 30min sur un incident de prod. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Et pour les autres services ?
&lt;/h2&gt;

&lt;p&gt;Lambda connaît deux évolutions majeures :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lambda Durable Functions&lt;/strong&gt;&lt;a href="https://aws.amazon.com/blogs/aws/build-multi-step-applications-and-ai-workflows-with-aws-lambda-durable-functions/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt; permet d'interrompre en cours d'exécution, puis de reprendre des exécutions Lambda jusqu'à 1 an après (après un "sleep" ou réception d'un callback), sans payer aucun frais pendant la phase de pause. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda Managed Instances&lt;/strong&gt;&lt;a href="https://aws.amazon.com/blogs/aws/introducing-aws-lambda-managed-instances-serverless-simplicity-with-ec2-flexibility/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt; permet à ceux qui ont des charges de travail relativement stables ou des besoins de hardware spécifiques (pas encore de GPU, mais on parie que ça va venir?) de bénéficier du tarif d'EC2 avec la simplicité opérationnelle de Lambda.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Et aussi &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 Tables offre désormais une classe de stockage type &lt;strong&gt;"Intelligent-tiering"&lt;/strong&gt;&lt;a href="https://aws.amazon.com/blogs/aws/announcing-replication-support-and-intelligent-tiering-for-amazon-s3-tables/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt; qui permet de réduire fortement les coûts des tables Iceberg.&lt;/li&gt;
&lt;li&gt;Les bases de données&lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/12/database-savings-plans-savings/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt; offrent désormais des &lt;strong&gt;savings plans&lt;/strong&gt;. Mieux vaut tard que jamais !! Economies massives à la clé. 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ne75bmen7j76tqa92bw.png" alt="Un database savings plan en action" width="800" height="432"&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Storage Lens&lt;/strong&gt;&lt;a href="https://aws.amazon.com/blogs/aws/amazon-s3-storage-lens-adds-performance-metrics-support-for-billions-of-prefixes-and-export-to-s3-tables/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt; offre plus de visibilité sur l'utilisation du stockage s3 (distribution de la taille des objets, requêtes les plus coûteuses etc.)&lt;/li&gt;
&lt;li&gt;SageMaker offre désormais MLFlow en mode serverless&lt;a href="https://aws.amazon.com/blogs/aws/accelerate-ai-development-using-amazon-sagemaker-ai-with-serverless-mlflow/" rel="noopener noreferrer"&gt;↗️&lt;/a&gt;.. gratuitement ??? (&lt;em&gt;Pricing – The new serverless MLflow capability is offered at no additional cost. Note there are service limits that apply&lt;/em&gt;) à suivre.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>reinvent</category>
      <category>ai</category>
    </item>
    <item>
      <title>Pre:Invent : Semaine 1, que retenir ?</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Mon, 24 Nov 2025 09:08:41 +0000</pubDate>
      <link>https://forem.com/aws-builders/preinvent-semaine-1-que-retenir--439i</link>
      <guid>https://forem.com/aws-builders/preinvent-semaine-1-que-retenir--439i</guid>
      <description>&lt;p&gt;Les semaines qui précèdent Re:Invent sont intéressantes pour les entreprises déjà utilisatrices d'AWS, car elles regorgent d'annonces de nouvelles fonctionnalités ou d'optimisation des services existants. &lt;/p&gt;

&lt;p&gt;Ces fonctionnalités ne sont peut-être pas suffisamment "marketing" pour paraître pendant re:Invent, mais elles peuvent parfois grandement améliorer l'expérience utilisateur. &lt;/p&gt;

&lt;p&gt;Voici quelques fonctionnalités clés de cette première semaine de "pre:Invent" : &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;CloudFront offre des &lt;a href="https://aws.amazon.com/blogs/networking-and-content-delivery/introducing-flat-rate-pricing-plans-with-no-overages/" rel="noopener noreferrer"&gt;forfaits à prix fixe&lt;/a&gt;. J'en ai parlé &lt;a href="https://dev.to/aws-builders/cloudfront-de-nouveaux-plans-a-prix-fixes--a1o"&gt;ici assez longuement&lt;/a&gt;. Si votre use case tombe dans les limites prévues, vous ferez de belles économies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Le &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/11/api-gateway-response-streaming-rest-apis/" rel="noopener noreferrer"&gt;response streaming&lt;/a&gt; est désormais possible sur API Gateway (pour les API Rest). &lt;br&gt;
C'est bien sûr une fonctionnalité tirée par l'IA / les Chatbots, qui permet aux clients de recevoir le premier octet rapidement et de se voir transmettre la suite de la réponse dès que le LLM l'a calculée... mais ça pourrait avoir de bien plus larges applications&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Utilisez vos rôles IAM sur des services tiers avec OAuth! IAM supportait déjà OAuth dans le sens entrant (ça permet notamment à GitHub Actions ou GitLab CI de venir agir sur vos comptes AWS par un &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt;) ; grâce à &lt;a href="https://aws.amazon.com/blogs/aws/simplify-access-to-external-services-using-aws-iam-outbound-identity-federation/" rel="noopener noreferrer"&gt;IAM Outbound Identity Federation&lt;/a&gt;, appeler &lt;code&gt;GetWebIdentityToken&lt;/code&gt; vous donnera un token JWT, il suffira de déclarer l'issuer URL fournie par IAM sur votre fournisseur de services et celui-ci acceptera les identités IAM. Plus de raison d'avoir des secrets à durée de vie longue !&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Gérer ses permissions sur &lt;a href="https://aws.amazon.com/blogs/aws/introducing-attribute-based-access-control-for-amazon-s3-general-purpose-buckets/" rel="noopener noreferrer"&gt;S3 devient plus facile avec le support de l'ABAC (Attribute-based access control&lt;/a&gt;), c'est-à dire la gestion des permissions par les tags/étiquettes).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Mais aussi &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Grâce à l'IA, StepFunctions devient plus accessible (sans devoir apprendre le détail du domain-specific language). Du coup AWS publie &lt;a href="https://aws.amazon.com/blogs/aws/accelerate-workflow-development-with-enhanced-local-testing-in-aws-step-functions/" rel="noopener noreferrer"&gt;de l'outillage pour tester facilement votre Step Function localement&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Redshift &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/11/aws-redshift-iceberg-writes-m1/" rel="noopener noreferrer"&gt;peut maintenant écrire dans des tables Iceberg&lt;/a&gt;. L'ETL full SQL a de beaux jours devant lui.&lt;/li&gt;
&lt;li&gt;ECS lance &lt;a href="https://aws.amazon.com/blogs/aws/build-production-ready-applications-without-infrastructure-complexity-using-amazon-ecs-express-mode/" rel="noopener noreferrer"&gt;"express mode", un wizard&lt;/a&gt; qui permet de créer facilement un service à partir d'une image docker. Bien pour découvrir le service, mais de toute façon, ensuite, vous créerez tout avec de l'Infra-as-Code, n'est-ce pas ?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A bientôt !&lt;/p&gt;

</description>
      <category>aws</category>
    </item>
    <item>
      <title>CloudFront : de nouveaux plans à prix fixes !</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Mon, 24 Nov 2025 08:33:09 +0000</pubDate>
      <link>https://forem.com/aws-builders/cloudfront-de-nouveaux-plans-a-prix-fixes--a1o</link>
      <guid>https://forem.com/aws-builders/cloudfront-de-nouveaux-plans-a-prix-fixes--a1o</guid>
      <description>&lt;p&gt;« Pre:Invent » est là : le &lt;a href="https://aws.amazon.com/new/" rel="noopener noreferrer"&gt;What's New&lt;/a&gt; d'AWS s'allonge (suivez-moi ici ou &lt;a href="https://www.linkedin.com/in/paulsantus/" rel="noopener noreferrer"&gt;sur LinkedIn&lt;/a&gt; pour suivre l'actualité de re:Invent en français pendant ces quelques jours) alors que Re:Invent démarre dans 2 semaines. Parmi les annonces de ces dernières heures, l'une mérite d'être relevée : des offres CloudFront à prix fixe !&lt;/p&gt;

&lt;p&gt;Le modèle économique principal du Cloud est le pay-as-you-go : on démarre à $0 et on les frais augmentent avec l'usage réel. Mais certains clients éprouvent des difficultés à estimer leurs frais à l'avance, ou à les corréler avec leurs revenus. C'est en particulier vrai pour les données sortantes et CloudFront représente une partie non-négligeable de ces frais. &lt;/p&gt;

&lt;h2&gt;
  
  
  Des forfaits "all-inclusive" pour CloudFront
&lt;/h2&gt;

&lt;p&gt;CloudFront propose désormais 4 forfaits "tout-en-un" qui conviendront à la plupart des besoins, mais laisseront néanmoins de côté certains (cf. business case ci-dessous) : &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2r3y0waek8xasq8fvj7s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2r3y0waek8xasq8fvj7s.png" alt="CloudFront pricing" width="800" height="680"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Les plans permettent tous de mobiliser les services suivants : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudFront (CDN)&lt;/li&gt;
&lt;li&gt;WAF (plus ou moins riche fonctionnellement selon les plans) et protection DDoS&lt;/li&gt;
&lt;li&gt;Ingestion des CloudWatch Log correspondant aux services du plan
DNS (Route 53)&lt;/li&gt;
&lt;li&gt;Certificats TLS&lt;/li&gt;
&lt;li&gt;Serverless Edge Compute (CloudFront Functions)&lt;/li&gt;
&lt;li&gt;Stockage S3 (sous forme de crédits qui viendront compenser les frais S3 induits par l'activité CloudFront)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ces plans sont au nombre de quatre :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Un plan gratuit permettra de servir 1 millions de requêtes CloudFront (100 Go de traffic sortant), et incluant 5Go de stockage S3, et des fonctionnalités WAF de base&lt;/li&gt;
&lt;li&gt;Pro : pour $15 on a 10m de requêtes et 50To de trafic sortant, des fonctionnalités WAF plus avancées (protection des vulnérabilités PHP connues + SQL injection)
&lt;/li&gt;
&lt;li&gt;Business : pour $200, on a l'ensemble du WAF, 125m de requêtes (et toujours 50To de trafic sortant)&lt;/li&gt;
&lt;li&gt;Premium : pour $1000 des fonctionnalités avancées qui seront inutiles pour la plupart (mTLS) mais surtout 500m de requêtes mensuelles. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ils viennent s'ajouter aux possibilités actuelles sans les dégrader :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Il reste possible d'être en mode "pay-as -you-go".&lt;/li&gt;
&lt;li&gt;On bénéficie toujours de 1To de données sortantes par mois "always free" &lt;/li&gt;
&lt;li&gt;Les savings bundle et private pricing agreement restent accessibles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Une distribution qui dépassera sa limite verra ses performances dégradées (mais AWS ne dit pas de combien.. +1s à chaque appel? à voir avec l'expérience).. &lt;/p&gt;

&lt;p&gt;Attention néanmoins : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;il y a un certain nombre de &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/flat-rate-pricing-plan.html#pricing-plan-unsupported-features" rel="noopener noreferrer"&gt;fonctionnalités non-supportées&lt;/a&gt; par les plans CloudFront - par exemple si votre WAF utilise des règles de prévention de vol de compte (ATP), vous ne pourrez pas activer ces plans.
&lt;/li&gt;
&lt;li&gt;Comme l'indique &lt;a href="https://dev.to/aws-builders/is-flat-rate-cloudfront-worth-it-2bgn"&gt;Jason Butz ici&lt;/a&gt; il y a également des &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/flat-rate-pricing-plan.html#pricing-plan-features" rel="noopener noreferrer"&gt;limites liées à chaque Plan&lt;/a&gt;. Parmi celles-ci, à mon sens, interdire les Custom origin request rules et Custom caching roles est rédhibitoire pour les Plans Free et Pro hormis pour des sites purement statiques.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Un business case tiré d'un de mes clients
&lt;/h2&gt;

&lt;p&gt;Ces offres permettent-ils de répondre à tous les besoins : pour y répondre, regardons un de mes clients préférés (oui, vous !) , qui a 2 distributions CloudFront :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Requêtes / mois&lt;/th&gt;
&lt;th&gt;Débit / mois&lt;/th&gt;
&lt;th&gt;Coût / mois&lt;/th&gt;
&lt;th&gt;WAF / mois&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Distribution 1&lt;/td&gt;
&lt;td&gt;243m&lt;/td&gt;
&lt;td&gt;4 To&lt;/td&gt;
&lt;td&gt;$600&lt;/td&gt;
&lt;td&gt;$200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distribution 2&lt;/td&gt;
&lt;td&gt;40m&lt;/td&gt;
&lt;td&gt;7 To&lt;/td&gt;
&lt;td&gt;$600&lt;/td&gt;
&lt;td&gt;$29&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Clairement,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;La Distribution 2 pourra bénéficier d'un plan business, soit une économie directe de $429/mois. &lt;/li&gt;
&lt;li&gt;Mais pour la Distribution 1, les nouveaux Plans de CloudFront ne changeront pas la donne. Le plan à $200 n'offre pas assez de requêtes... et à 243m de requêtes (en croissance), on ne va probablement pas faire d'effort pour découper la distribution en deux (au risque qu'une des sous-partie dépasse 125m de requêtes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En conclusion les Plans à prix fixe permettront de couvrir un large éventail de clients... mais certains devront rester sur du Pay-as-you-Go. L'occasion de redécouvrir les "Savings Bundle" CloudFront (qui m'annoncent une économie de $300 mensuels sur le niveau d'usage actuel sur les distributions précitées). &lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudfront</category>
      <category>cdn</category>
      <category>finops</category>
    </item>
    <item>
      <title>Great post if you have need for custom instances (GPUs...) to host your containers</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Wed, 22 Oct 2025 20:28:21 +0000</pubDate>
      <link>https://forem.com/psantus/great-post-if-you-have-need-for-custom-instances-gpus-to-host-your-containers-8fo</link>
      <guid>https://forem.com/psantus/great-post-if-you-have-need-for-custom-instances-gpus-to-host-your-containers-8fo</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/aws-builders/lets-try-managed-ecs-instances-3fl4" class="crayons-story__hidden-navigation-link"&gt;Let's try Managed ECS Instances&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/aws-builders"&gt;
            &lt;img alt="AWS Community Builders  logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2794%2F88da75b6-aadd-4ea1-8083-ae2dfca8be94.png" class="crayons-logo__image"&gt;
          &lt;/a&gt;

          &lt;a href="/ppabis" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1318014%2F38290fbe-cb90-48fd-a738-e86d08ca25da.jpeg" alt="ppabis profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/ppabis" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Piotr Pabis
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Piotr Pabis
                
              
              &lt;div id="story-author-preview-content-2950743" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/ppabis" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1318014%2F38290fbe-cb90-48fd-a738-e86d08ca25da.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Piotr Pabis&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/aws-builders" class="crayons-story__secondary fw-medium"&gt;AWS Community Builders &lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/aws-builders/lets-try-managed-ecs-instances-3fl4" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 22 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/aws-builders/lets-try-managed-ecs-instances-3fl4" id="article-link-2950743"&gt;
          Let's try Managed ECS Instances
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/aws"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;aws&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/containers"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;containers&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ecs"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ecs&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/docker"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;docker&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/aws-builders/lets-try-managed-ecs-instances-3fl4" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/aws-builders/lets-try-managed-ecs-instances-3fl4#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            8 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




</description>
      <category>aws</category>
      <category>containers</category>
      <category>ecs</category>
      <category>docker</category>
    </item>
    <item>
      <title>AWS : une panne « mondiale » ?</title>
      <dc:creator>Paul SANTUS</dc:creator>
      <pubDate>Tue, 21 Oct 2025 13:15:23 +0000</pubDate>
      <link>https://forem.com/aws-builders/aws-une-panne-mondiale--37n3</link>
      <guid>https://forem.com/aws-builders/aws-une-panne-mondiale--37n3</guid>
      <description>&lt;p&gt;AirBnB, Slack, SnapChat par terre ! Les médias se sont fait l'écho (par exemple ici &lt;a href="https://www.lemonde.fr/pixels/article/2025/10/21/aws-le-service-cloud-d-amazon-annonce-avoir-resolu-la-panne-qui-a-touche-des-applications-dans-le-monde-entier_6648232_4408997.html" rel="noopener noreferrer"&gt;Le Monde avec l'AFP&lt;/a&gt;) d'un incident majeur touchant l'infrastructure d'AWS, parlant de « panne mondiale ». Le terme est-il approprié ? &lt;/p&gt;

&lt;p&gt;Nb : il ne s'agit pas de "défendre" AWS, ni de nier l'ampleur de l'incident, mais de donner l'opportunité aux moins "tech" de comprendre ce qui se cache derrière.&lt;/p&gt;

&lt;h1&gt;
  
  
  Quelques exemples d'impacts chez mes clients
&lt;/h1&gt;

&lt;p&gt;L'un de mes clients a toute son infrastructure sur les datacenters d'AWS en France (on parle de "région" de Paris, ou &lt;em&gt;eu-west-3&lt;/em&gt;). Il n'a juste eu aucun impact de l'incident. &lt;em&gt;Business as usual&lt;/em&gt;. Même trafic, mêmes temps de réponses, même nombre de commandes sur son site d'e-commerce.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frqiajo1or40av2bplpyn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frqiajo1or40av2bplpyn.png" alt="Trafic et temps de réponse constants" width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Un autre client a une partie de son infra à Paris et l'autre aux Etats-Unis, en Virginie du Nord (&lt;em&gt;us-east-1&lt;/em&gt;), la région en cause dans l'incident. Pendant la durée de l'incident, les envois de mails via le service SES ont échoué. Les temps de réponses ont augmenté et le nombre de messages en file d'attente traités par seconde a drastiquement baissé, le débit étant volontairement limité par AWS dans la phase de "&lt;em&gt;recovery&lt;/em&gt;" (pour ne pas submerger des machines qui démarrent sous un flux de requêtes d'autant plus volumineux que tout le monde "&lt;em&gt;retry&lt;/em&gt;").&lt;/p&gt;

&lt;p&gt;Enfin, pendant tout l'incident, j'ai de mon côté été dans l'impossibilité de mettre à jour l'infrastructure de mes clients (ce que je fais habituellement en appelant les services d'AWS par un outil appelé "Terraform").&lt;/p&gt;

&lt;h1&gt;
  
  
  Un incident régional, des impacts mondiaux
&lt;/h1&gt;

&lt;p&gt;Alors que s'est-il passé ? Sans entrer trop dans les détails techniques, AWS a eu - &lt;strong&gt;sur la région us-east-1&lt;/strong&gt; un problème d'adressage réseau (DNS) pour un de ses services, DynamoDB, une base de données ultra-performante qui est utilisée par de nombreux autres services AWS (AWS dénombrait environ 70 services impactés). &lt;/p&gt;

&lt;p&gt;Pourquoi cet incident, semble-t-il local, a t-il eu un impact si large ? Aurait-il été identique si une autre région, par exemple Europe (Milan), avait connu le même incident ?&lt;/p&gt;

&lt;p&gt;Pour comprendre cela, il faut comprendre deux notions clés : &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data-Plane vs Control-Plane&lt;/strong&gt; : pour consommer un service, je mets en place du paramétrage, c'est le &lt;em&gt;control plane&lt;/em&gt; ; ce paramétrage est ensuite utilisé par AWS pour opérer une ressource, c'est le &lt;em&gt;data plane&lt;/em&gt;. Par exemple : je démarre un cluster de base de données MySQL (&lt;em&gt;control plane&lt;/em&gt;), ce cluster sert du trafic SQL, enregistre des transactions sur des disques durs, écrit des logs (&lt;em&gt;data plane&lt;/em&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwj883sniijucbivez3yk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwj883sniijucbivez3yk.png" alt="Data plane vs Control Plane" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;L'empreinte géographique d'un service&lt;/strong&gt; : certains services AWS sont conçus pour fonctionne à l'échelle d'un data center (pour faire simple, en réalité à l'échelle d'une zone de disponibilité). C'est le cas quand je déploie une machine Linux. D'autres fonctionnent à l'échelle d'une région (de sorte qu'ils sont capable de vivre avec la perte d'un datacenter). D'autres enfin fonctionnent à l'échelle mondiale.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ces deux notions se conjuguent : par exemple IAM a son &lt;em&gt;control plane centralisé&lt;/em&gt; (je définis mes politiques d'autorisation à un seul endroit) puis la configuration est distribuée sur toutes les régions, de sorte que celles-ci peuvent appliquer ces politiques eu toute autonomie (&lt;em&gt;data plane régionalisé&lt;/em&gt;). &lt;/p&gt;

&lt;p&gt;Le tableau ci-dessous donne quelques exemples :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkwqgm9bqu4bufckw8dz0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkwqgm9bqu4bufckw8dz0.png" alt="Data plane vs Control plane : quelques exemples de services AWS" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Que s'est-il passé ?
&lt;/h1&gt;

&lt;p&gt;Il se trouve que la région &lt;em&gt;us-east-1&lt;/em&gt; sert de control plane centralisé pour un certain nombre (voire un nombre certain) de services AWS. Quand un incident majeur survient sur cette région (historiquement tous les 2 à 4 ans), cela impacte donc : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;le control plane de tous ces services&lt;/li&gt;
&lt;li&gt;le data plane des services &lt;em&gt;sur cette région&lt;/em&gt; (le data plane des services sur les autres régions est sauf, d'où le &lt;em&gt;business-as-usual&lt;/em&gt; de mon premier client).&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Quelles leçons en tirer ?
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Architecturer pour la haute-disponibilité&lt;/strong&gt; ! « &lt;em&gt;Tout échoue tout le temps, tout tombera en panne un jour&lt;/em&gt;» dit le Dr. Werner Vogels, CTO d'Amazon.  Si l'incident a eu un tel impact visible du public c'est que les opérateurs des services cités par la presse n'ont pas jugé bon (ou su) architecturer leurs services pour être résilients à la perte de la région &lt;em&gt;us-east-1&lt;/em&gt;. Cela peut-être un choix éclairé (mon client précité a choisi d'être KO si la région de Paris tombait, confiant qu'AWS saurait remonter l'infra plus rapidement que lui ne saurait mettre en oeuvre un Plan de Reprise d'Activité, avec toutefois un backup externalisé juste au cas où...)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Persister d'abord, traiter ensuite&lt;/strong&gt; : mon deuxième client ne pouvait pas envoyer ses mails... et les appels d'API pour les envoyer se faisaient au beau milieu du traitement d'une transaction. Retour d'expérience et évolution dans les prochains jours : il va désormais stocker ces tâches dans une file d'attente et la dépiler avec gestion du &lt;em&gt;retry&lt;/em&gt;, une &lt;em&gt;dead letter queue&lt;/em&gt; etc.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Voilà, vous avez maintenant quelques base sur la conception de l'infrastructure mondiale d'AWS !&lt;/p&gt;

</description>
      <category>aws</category>
      <category>infrastructure</category>
      <category>incident</category>
    </item>
  </channel>
</rss>
