S3 POC · PrivateLink Gateway
POC · AWS · S3-compatible · PrivateLink

Passerelle S3 privée entre baleine et gros chat

Construire une passerelle S3-compatible entre deux comptes AWS réellement distincts : baleine, le compte consommateur, et gros chat, le compte propriétaire du bucket. Le compte baleine utilise un endpoint S3 custom via PrivateLink, sans droit IAM sur gros chat ; gros chat conserve le bucket réel, l'accès IAM interne et la capacité de couper l'accès rapidement.

🐈 gros chat contrôle 🐋 baleine consomme ☁️ AWS S3 🔒 PrivateLink 🚢 ECS Fargate ⚖️ NLB interne 🧰 rclone serve s3 🧪 AWS CLI
Public
Architecte Data · Cloud · Sécurité
Niveau
Avancé
Région AWS
eu-west-3 · Paris
Livrables
HTML + commandes CLI
01 / 25

Contexte, point de départ et contraintes

Ce POC a été réalisé avec deux comptes AWS distincts, ce qui permet de tester un vrai scénario inter-comptes : gros chat possède le bucket S3 et expose une passerelle privée ; baleine consomme cette passerelle sans jamais recevoir de droits IAM sur le bucket.

Le vrai compromis architectural

Ce POC ne cherche pas à prouver que cette approche est supérieure au modèle natif S3/IAM dans tous les cas. Il explore un pattern précis : exposer un bucket S3 d’un compte propriétaire (gros chat) à un compte consommateur (baleine) sans donner à ce dernier de droit IAM direct sur le bucket.

Le compromis est assumé : on déplace l’autorisation d’accès depuis le data-layer natif AWS/S3/IAM vers une passerelle réseau et applicative exposée en privé via PrivateLink. Cela renforce le contrôle côté propriétaire, mais introduit un composant intermédiaire à opérer et une perte d’attribution CloudTrail native directe à l’identité AWS du consommateur.

Le bénéfice opérationnel recherché est double : baleine reste un client S3 standard, agnostique au modèle d’autorisation interne de gros chat, et gros chat évite de maintenir une bucket policy ou des grants S3/IAM spécifiques par consommateur. Le contrat exposé devient un endpoint privé S3-compatible, tandis que l’autorisation réelle reste centralisée côté propriétaire.

🐈

Compte propriétaire

gros chat héberge le bucket réel mab-documents, ECS Fargate, le NLB interne et l'Endpoint Service PrivateLink.

🐋

Compte consommateur

baleine héberge le VPC Endpoint PrivateLink et le client S3 standard utilisé pour lire les objets.

🧪

Mode POC

La construction a été faite en CLI pour comprendre chaque brique. Quand j'aurai le temps, je referai cette version proprement en Terraform ;).

Contrainte initialeRéponse dans le POC
baleine ne doit recevoir aucun droit IAM sur gros chatLe vrai accès S3 est porté exclusivement par le Task Role ECS Fargate côté gros chat.
baleine ne doit pas connaître les credentials AWS de gros chatLe client reçoit uniquement des credentials applicatifs S3-compatible propres à la passerelle.
baleine ne doit jamais toucher directement S3 dans gros chatLe trafic passe par un endpoint custom PrivateLink, puis NLB, puis Fargate/rclone.
Le client doit rester standardLa validation utilise aws s3 ls et aws s3 cp avec --endpoint-url.
Le flux doit rester privéLe chemin réseau utilise PrivateLink entre les deux comptes, sans exposition publique de la passerelle.
Pas de composant local permanentLa passerelle tourne directement sur ECS Fargate côté gros chat, pas sur un poste local.
Ce document conserve volontairement les noms pédagogiques baleine et gros chat, ainsi que le nom du bucket mab-documents et les trois fichiers de test. Les IDs de comptes, ARNs, endpoints réels et secrets sont anonymisés ou représentés sous forme de variables.
02 / 25

Lecture rapide : qui fait quoi ?

Pour comprendre le POC sans se perdre dans les commandes, il faut d'abord séparer les responsabilités. gros chat construit et contrôle la passerelle ; baleine ne fait que la consommer comme un endpoint S3 privé.

🐈 gros chat · propriétaire et opérateur

1Possède le bucket réel mab-documents et les fichiers fifi.txt, loulou.txt, riri.txt.
2Crée le rôle IAM de la task Fargate, seul autorisé à lire le bucket S3.
3Déploie rclone serve s3 gateway: sur ECS Fargate, derrière un NLB interne.
4Expose le NLB via un Endpoint Service PrivateLink et autorise explicitement le compte baleine.
5Fournit à baleine des credentials applicatifs, puis peut couper l'accès en arrêtant ECS, en révoquant PrivateLink ou en rotant ces credentials.

🐋 baleine · consommateur uniquement

1Crée un Interface VPC Endpoint vers le service PrivateLink publié par gros chat.
2Configure son client S3 avec un --endpoint-url privé et les credentials applicatifs fournis.
3Liste et lit le bucket logique mab-documents via aws s3 ls et aws s3 cp.
4Peut copier les fichiers sur son disque local, par exemple dans ~/mab-documents.
5Ne reçoit aucun rôle IAM dans gros chat, aucun accès direct au S3 réel, aucun secret AWS de gros chat.
ActionÀ faire côté gros chatÀ faire côté baleine
Publier le serviceFargate + NLB interne + Endpoint Service PrivateLinkRien
Autoriser le consommateurmodify-vpc-endpoint-service-permissions vers le compte baleineCréer l'Interface Endpoint
Lire les objetsTask Role Fargate lit le bucket réelClient S3 lit le bucket logique via endpoint custom
Couper l'accèsArrêt ECS, révocation PrivateLink ou rotation credentialsSubit la coupure ; ne peut pas contourner par IAM S3
03 / 25

Schéma pédagogique du flux

Ce schéma résume le POC de manière visuelle et fidèle : baleine consomme un service S3-compatible privé, mais l'accès réel au bucket reste entièrement porté par gros chat. Les CIDR et adresses IP ont été volontairement anonymisés pour garder le document partageable sans exposer de détails d'adressage inutiles.

Schéma détaillé du POC S3 privé inter-comptes via PrivateLink
Schéma détaillé du POC : baleine consomme la passerelle privée, gros chat conserve le bucket et l’accès IAM réel. Ce choix déplace l’autorisation effective vers la passerelle contrôlée par le propriétaire. La flèche PrivateLink représente le flux applicatif baleine → gros chat. Le S3 Gateway Endpoint reste une connexion privée interne au VPC gros chat vers S3, pas une passerelle applicative exposée à baleine.

Comment lire le schéma

  1. À droite, le client baleine (EC2 de test ou application) utilise un client S3 standard, des credentials applicatifs locales et une --endpoint-url privée.
  2. Le trafic entre alors dans un Interface Endpoint PrivateLink déployé dans le VPC baleine.
  3. PrivateLink transporte ensuite ce flux vers le compte gros chat, sans passage par Internet.
  4. Dans gros chat, le trafic arrive sur un NLB interne, puis est distribué au service ECS Fargate qui exécute rclone serve s3 gateway:.
  5. La task Fargate authentifie les credentials applicatifs, puis utilise son Task Role ECS pour lire le bucket S3 réel mab-documents.
  6. Les objets reviennent ensuite vers le client baleine par le même chemin privé.

Ce qu'il faut retenir

  • Le bucket reste dans gros chat : il n'est jamais déplacé ni partagé directement avec baleine.
  • Pas d'IAM croisé : baleine ne reçoit aucun droit AWS sur le bucket.
  • Pas de credential AWS gros chat côté baleine : seules les credentials applicatives de la passerelle sont utilisées par le client.
  • Le rôle du service Fargate est central : c'est lui qui possède l'autorisation réelle de lecture sur mab-documents.
  • Les endpoints internes sont indispensables : S3 Gateway Endpoint, ECR API, ECR DKR et CloudWatch Logs permettent au service Fargate de fonctionner sans NAT.
  • Le point subtil rclone : le remote combine expose mab-documents comme bucket logique au travers de la passerelle.
04 / 25

Identités du POC : construction vs fonctionnement réel

Le lecteur n'a pas besoin d'avoir suivi les micro-étapes pas à pas pour comprendre cette partie. Il faut simplement distinguer deux moments : la phase de construction du POC, où l'on crée l'infrastructure, et la phase de fonctionnement, où la passerelle tourne et où baleine lit les objets.

Point de clarification. Les users admin temporaires ne font pas partie de l'architecture cible. Ils ont seulement servi de « tournevis » pour créer rapidement les ressources AWS pendant le POC. Une fois le POC construit, le flux de lecture baleine → gros chat → S3 ne dépend pas de ces users admin.

Phase 1 — Construction du POC

User temporaire gros chat
Créé pour exécuter les commandes CLI qui construisent la partie propriétaire : IAM, ECR, VPC, ECS/Fargate, NLB et Endpoint Service PrivateLink.
À supprimer ou remplacer par un rôle d'administration cadré après le POC.
User temporaire baleine
Créé pour exécuter les commandes CLI qui construisent la partie consommatrice : Security Group, Interface Endpoint, instance EC2 de test et rôle SSM.
À supprimer ou remplacer par un rôle/pipeline d'infrastructure plus propre.

Phase 2 — Fonctionnement de la passerelle

Credentials applicatifs
<LOCAL_ACCESS_KEY> / <LOCAL_SECRET_KEY>, fournis à baleine pour s'authentifier auprès de la passerelle S3-compatible.
Ce ne sont pas des credentials AWS : ils ne donnent aucun droit IAM dans gros chat.
Task Role Fargate
Identité AWS attachée à la task ECS Fargate dans gros chat.
C'est la seule identité AWS qui lit réellement s3://mab-documents.
Execution Role ECS
Rôle technique utilisé par ECS/Fargate pour tirer l'image depuis ECR et écrire les logs CloudWatch.
Il ne sert pas à lire le bucket métier ; il sert à faire démarrer correctement le conteneur.
Phrase à retenir : les users admin temporaires servent à construire le décor. Au runtime, baleine s'authentifie auprès de la passerelle avec des credentials applicatifs ; la passerelle lit le bucket avec le Task Role Fargate côté gros chat.
05 / 25

Pourquoi ce POC

Le besoin est simple : permettre à une application du compte baleine de lire des objets depuis un bucket S3 détenu par gros chat, tout en évitant une délégation IAM directe vers le bucket.

Ce que le pattern remplace. Dans une architecture AWS native, l’accès inter-comptes à S3 peut être géré par des policies IAM, des bucket policies, des rôles assumés ou des points d’accès S3. Ici, le POC explore volontairement une autre option : ne pas déléguer de droit IAM direct au compte consommateur et faire porter l’accès réel par une passerelle contrôlée par le propriétaire du bucket.

Ce que l’on gagne

  • le bucket reste intégralement contrôlé par gros chat ;
  • baleine ne reçoit aucun rôle IAM ni secret AWS du compte propriétaire ;
  • le consommateur utilise un client S3 standard avec un endpoint privé ;
  • la coupure peut être centralisée côté propriétaire : arrêt ECS, révocation PrivateLink ou rotation des credentials applicatifs.

Ce que l’on paie

  • une passerelle supplémentaire à déployer, superviser et durcir ;
  • une attribution CloudTrail S3 qui se concentre sur le rôle de la passerelle, pas sur l’identité AWS finale de baleine ;
  • des credentials applicatifs à gérer et à rotater ;
  • une disponibilité de la passerelle à traiter sérieusement avant toute production.

Le pattern testé consiste à interposer une passerelle S3-compatible dans le compte propriétaire. Le client conserve ses outils standards — AWS CLI, SDK S3, endpoint custom — mais ne parle plus directement à AWS S3. Le flux réseau passe par PrivateLink, puis par un NLB interne, puis par une task ECS Fargate qui exécute rclone serve s3.

Résultat obtenu. Depuis baleine, le client a listé et copié les fichiers fifi.txt, loulou.txt et riri.txt du bucket mab-documents sans disposer de droits IAM sur ce bucket.
06 / 25

Bucket S3 réel et fichiers de test

Le bucket réel reste dans gros chat. Il s'appelle mab-documents et contient volontairement trois petits fichiers texte, suffisants pour valider le listing et la lecture d'objets via la passerelle.

Bucket réelFichierContenu validéUsage dans le test
mab-documentsfifi.txtFifi mange une figueTest GetObject avec aws s3 cp ... -
mab-documentsloulou.txtFichier texte de testValidation du listing et de la copie récursive
mab-documentsriri.txtFichier texte de testValidation du listing et de la copie récursive
Vérification initiale côté gros chat
aws --profile groschat s3 ls s3://mab-documents

# Résultat attendu
# 2026-05-28 ... fifi.txt
# 2026-05-28 ... loulou.txt
# 2026-05-28 ... riri.txt

Côté baleine, ces fichiers ne sont pas visibles dans un bucket S3 local. Ils peuvent en revanche être lus via l'endpoint PrivateLink, puis copiés sur le disque local d'une instance ou d'une application cliente, par exemple dans ~/mab-documents.

07 / 25

Architecture cible

L'architecture sépare clairement l'identité du consommateur, le transport réseau privé et l'identité AWS qui accède réellement au bucket.

Vue d'ensemble anonymiséeCompte A = baleine · Compte B = gros chat
PrivateLink
TCP 8080
ZoneComposantRôleInformation conservée
baleineApplication / AWS CLIClient S3 standard avec endpoint customEndpoint privé + credentials locales
baleineInterface EndpointEntrée PrivateLink vers gros chatDNS privé VPC
gros chatNLB interneRépartition TCP vers FargatePas d'exposition Internet
gros chatECS FargateHéberge la passerelle S3-compatibleTask Role IAM
gros chatS3Stockage réelmab-documents
08 / 25

Pourquoi choisir rclone pour cette passerelle

Le choix de rclone concerne uniquement l’implémentation du POC : il fournit rapidement un serveur S3-compatible léger, conteneurisable et capable de s'appuyer sur l'identité AWS de la task Fargate via env_auth = true.

Ce que rclone apporte au POC

  • Interface S3 côté client : baleine peut utiliser AWS CLI ou un SDK S3 avec un simple --endpoint-url.
  • Pas de clés AWS côté baleine : les credentials locales ne sont pas des credentials IAM AWS.
  • Task Role Fargate : côté gros chat, env_auth = true permet à rclone d'utiliser la chaîne d'identité AWS de la task.
  • Image simple : un Dockerfile minimal suffit pour lancer rclone serve s3.
  • Mapping maîtrisé : le remote combine permet d'exposer mab-documents comme bucket logique.

Ce que rclone ne promet pas seul

  • Ce n'est pas un service AWS S3 managé : il faut tester les opérations nécessaires une par une.
  • La compatibilité doit être validée pour ListObjectsV2, GetObject, gros fichiers, range requests et éventuellement multipart.
  • Les credentials locales doivent être externalisées et rotatées avant toute industrialisation.
  • Le mapping racine/bucket est subtil : servir directement le bucket réel ne suffisait pas pour aws s3 ls s3://mab-documents.
Décision validée par le test. Avec gateway: et le remote combine, baleine liste fifi.txt, loulou.txt et riri.txt, puis copie fifi.txt sur son disque local.
09 / 25

Glossaire pédagogique

🔒

PrivateLink

Permet d'exposer un service d'un VPC à un autre compte sans passer par Internet, via un endpoint réseau privé.

⚖️

NLB

Load balancer niveau 4, adapté au TCP brut. Ici il transmet le trafic S3-compatible vers la task Fargate.

🚢

Fargate

Exécution serverless de conteneurs ECS : pas d'instance EC2 à gérer, mais des objets logiques ECS à déclarer.

🧰

rclone serve s3

Expose un backend rclone sous forme d'API S3-compatible. C'est le cœur de la passerelle.

🧩

combine remote

Remote rclone utilisé pour faire apparaître le bucket réel comme un bucket logique côté client S3.

🪪

Task Role

Identité IAM interne à gros chat. Elle lit le bucket réel, sans jamais être donnée à baleine.

10 / 25

Concepts ECS / Fargate à comprendre avant les commandes

Une grande partie de la difficulté du POC vient du vocabulaire ECS. Fargate est bien serverless : on ne crée pas de machines EC2 à administrer. En revanche, on décrit plusieurs objets logiques pour dire à AWS quoi lancer, où le lancer, avec quels droits et comment le garder disponible.

ConceptDéfinition simpleDans ce POC
Cluster ECSConteneur logique qui regroupe des services et des tasks. Ce n'est pas un cluster de serveurs EC2 dans notre cas.rclone-s3-gateway-cluster
Task DefinitionLa recette de lancement du conteneur : image Docker, CPU, mémoire, port, logs, rôles IAM.Elle dit de lancer l'image rclone-s3-gateway:latest sur le port 8080.
TaskUne exécution concrète d'une Task Definition. C'est le conteneur réellement démarré par Fargate.Une task exécute rclone serve s3 gateway:.
Service ECSLe superviseur : il garde le nombre voulu de tasks en vie et redémarre une task si elle tombe.desired-count = 1 signifie : garder une passerelle active.
Desired / Running / PendingÉtat du service : combien de tasks sont demandées, combien tournent, combien sont en démarrage.État final attendu : Desired=1, Running=1, Pending=0.
Execution RoleRôle technique utilisé par ECS pour tirer l'image ECR et écrire les logs CloudWatch.ecsTaskExecutionRole
Task RoleRôle applicatif disponible dans le conteneur. C'est lui qui donne à rclone le droit de lire S3.rclone-s3-gateway-task-role avec s3:ListBucket et s3:GetObject.
Target Group IPGroupe de cibles du NLB. En Fargate, on cible les IP privées des tasks, pas des instances EC2.Target Group TCP 8080, type ip.
NLB internePoint d'entrée réseau privé côté gros chat. Il relaie le TCP vers les tasks Fargate.Listener TCP 8080 vers le Target Group.
Endpoint ServiceService PrivateLink exposé par gros chat à d'autres comptes.Il expose le NLB à baleine.
Interface EndpointEndpoint créé côté baleine pour consommer le service PrivateLink.Il fournit un DNS privé utilisable par l'application baleine.
Lecture mentale utile : Task Definition = recette, Task = conteneur en cours d'exécution, Service = gardien qui maintient la task en vie, Fargate = moteur serverless qui exécute la task sans EC2 à gérer.
Lecture d'état du service ECS · sortie à interpréter
# Desired : nombre de tasks demandées par le service.
# Running : nombre de tasks réellement en fonctionnement.
# Pending : nombre de tasks en cours de démarrage.
# Si Desired=1, Running=1, Pending=0, la passerelle est stable.
aws --profile groschat ecs describe-services   --cluster rclone-s3-gateway-cluster   --services rclone-s3-gateway-service   --query 'services[0].{Desired:desiredCount,Running:runningCount,Pending:pendingCount,Status:status}'   --output table
11 / 25

IAM et séparation des rôles

Attribution CloudTrail : limite volontaire du pattern

Dans ce POC, le bucket S3 réel est lu par le Task Role ECS de la passerelle côté gros chat. Les événements CloudTrail S3 côté propriétaire attribuent donc l’accès à cette identité AWS de passerelle.

Cela signifie que l’on perd l’attribution native directe par identité AWS consommatrice. Si plusieurs consommateurs utilisent la même passerelle, CloudTrail ne suffit plus à lui seul pour distinguer finement l’utilisateur ou l’application appelante côté baleine.

Pour une version industrialisée, cette perte doit être compensée par des logs applicatifs de passerelle : identifiant de client applicatif, bucket logique, clé objet, opération, timestamp, résultat, source réseau et corrélation avec les logs NLB/CloudWatch.

Le point central du POC est la séparation entre l'authentification côté client et l'autorisation AWS réelle.

FluxIdentité utiliséePortée
baleine → passerelleCredentials locales non AWSAuthentifier le client auprès de rclone
passerelle → S3 gros chatECS Task RoleLire mab-documents
baleine → S3 AWSAucuneAucun accès direct au bucket
Policy minimale du Task Role · anonymisée
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListBucket",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::mab-documents"
    },
    {
      "Sid": "ReadObjects",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::mab-documents/*"
    }
  ]
}
POC vs production. Des users administrateurs temporaires ont pu être utilisés pour accélérer la construction. En production, il faut remplacer ces identités par des rôles/policies minimales et supprimer toutes les clés longues durées non nécessaires.
12 / 25

Transmission des credentials à baleine

Les credentials transmis à baleine ne sont pas des credentials AWS. Ce sont des credentials applicatifs S3-compatible propres à la passerelle rclone serve s3.

Ce que gros chat transmet

  • Une access_key applicative.
  • Une secret_key applicative.
  • L'endpoint PrivateLink à utiliser côté baleine.
  • Le nom du bucket logique exposé : mab-documents.

Ce qu'il ne faut pas transmettre

  • Aucune clé AWS du compte gros chat.
  • Aucun rôle IAM ou droit direct sur le bucket réel.
  • Aucun secret en clair dans un mail, un dépôt Git ou une image Docker industrialisée.

Pour un POC, la paire local-access-key / local-secret-key a été utilisée pour aller vite. En environnement réel, gros chat devrait déposer ces informations dans un coffre-fort de secrets ou les transmettre par un canal sécurisé équivalent. Le mail peut annoncer l'existence de l'accès et pointer vers l'entrée du coffre-fort, mais il ne doit pas contenir le secret en clair.

Exemple de secret côté baleine · anonymisé
aws --profile baleine secretsmanager create-secret   --name poc/s3-gateway/groschat/mab-documents   --secret-string '{
    "AWS_ACCESS_KEY_ID": "<LOCAL_ACCESS_KEY>",
    "AWS_SECRET_ACCESS_KEY": "<LOCAL_SECRET_KEY>",
    "ENDPOINT_URL": "http://<PRIVATE_LINK_DNS>:8080",
    "REGION": "eu-west-3",
    "BUCKET": "mab-documents"
  }'
La logique de sécurité est simple : baleine s'authentifie auprès de la passerelle ; la passerelle, elle, lit S3 avec le Task Role Fargate dans gros chat.
13 / 25

Configuration rclone

Le point subtil du POC : rclone serve s3 considère les répertoires à la racine du remote comme des buckets. Servir directement mabaws:mab-documents expose les fichiers à la racine et ne donne pas le comportement attendu avec aws s3 ls s3://mab-documents.

La correction consiste à créer un remote combine appelé gateway, qui mappe explicitement mab-documents vers le bucket réel.

rclone.conf
[mabaws]
type = s3
provider = AWS
env_auth = true
region = eu-west-3
location_constraint = eu-west-3

[gateway]
type = combine
upstreams = mab-documents=mabaws:mab-documents
Dockerfile
FROM rclone/rclone:latest

COPY rclone.conf /config/rclone/rclone.conf

EXPOSE 8080

ENTRYPOINT ["rclone"]
CMD ["serve", "s3", "gateway:", "--addr", ":8080", "--auth-key", "<LOCAL_ACCESS_KEY>,<LOCAL_SECRET_KEY>", "--vfs-cache-mode", "off", "-vv"]
TentativeCommande serveurRésultat observéConclusion
1rclone serve s3 mabaws:NoSuchBucket avec aws s3 ls s3://mab-documentsLa racine du remote S3 ne donnait pas le bucket logique attendu.
2rclone serve s3 mabaws:mab-documentsListing racine vide, fichiers non exposés comme bucket.Les fichiers à la racine ne sont pas vus comme un bucket par le serveur S3 rclone.
3rclone serve s3 gateway:Listing et lecture OK depuis baleine.Le remote combine expose mab-documents comme bucket logique.
Configuration validée. Les logs Fargate ont confirmé le démarrage avec rclone serve s3 gateway: puis la création du backend mabaws:mab-documents.
14 / 25

Image Docker rclone : ce qui est vraiment déployé

La configuration rclone.conf ne suffit pas seule : elle est embarquée dans une image Docker, puis cette image est poussée dans ECR côté gros chat. ECS Fargate ne lance pas directement un fichier de configuration ; il lance un conteneur construit à partir de cette image.

Cycle de vie de l'imagelocal → ECR gros chat → Fargate

🧾 1. Fichiers locaux

rclone.conf décrit les remotes mabaws et gateway. Le Dockerfile décrit comment lancer rclone serve s3.

🐳 2. Build Docker

On construit l'image rclone-s3-gateway:latest à partir de l'image officielle rclone/rclone:latest.

📦 3. Push ECR

L'image est taguée puis poussée dans le repository ECR rclone-s3-gateway du compte gros chat.

🚢 4. Task Definition

La task definition ECS référence l'image ECR : <ACCOUNT_GROSCHAT>.dkr.ecr.eu-west-3.amazonaws.com/rclone-s3-gateway:latest.

🔁 5. Déploiement

Après chaque rebuild/push, on force un nouveau déploiement ECS pour que Fargate tire la nouvelle image.

✅ 6. Logs

CloudWatch confirme la commande réellement lancée : rclone serve s3 gateway:.

Dockerfile de la passerelle
FROM rclone/rclone:latest

COPY rclone.conf /config/rclone/rclone.conf

EXPOSE 8080

ENTRYPOINT ["rclone"]
CMD ["serve", "s3", "gateway:", "--addr", ":8080", "--auth-key", "<LOCAL_ACCESS_KEY>,<LOCAL_SECRET_KEY>", "--vfs-cache-mode", "off", "-vv"]
Build, tag et push ECR
docker build -t rclone-s3-gateway:latest .

docker tag rclone-s3-gateway:latest   <ACCOUNT_GROSCHAT>.dkr.ecr.eu-west-3.amazonaws.com/rclone-s3-gateway:latest

aws --profile groschat ecr get-login-password --region eu-west-3   | docker login       --username AWS       --password-stdin <ACCOUNT_GROSCHAT>.dkr.ecr.eu-west-3.amazonaws.com

docker push <ACCOUNT_GROSCHAT>.dkr.ecr.eu-west-3.amazonaws.com/rclone-s3-gateway:latest
Redéploiement Fargate après push
aws --profile groschat ecs update-service   --cluster rclone-s3-gateway-cluster   --service rclone-s3-gateway-service   --force-new-deployment

aws --profile groschat ecs wait services-stable   --cluster rclone-s3-gateway-cluster   --services rclone-s3-gateway-service
Point de vigilance. Tant que le tag latest est réutilisé, ECS ne sait pas magiquement qu'une nouvelle image a été poussée. Il faut forcer un nouveau déploiement, ou mieux en production : versionner l'image avec un tag immuable ou un digest sha256.
Dans le POC, les credentials applicatifs sont encore dans la commande de démarrage du conteneur pour aller vite. En version durcie, ils doivent être injectés depuis Secrets Manager, Parameter Store SecureString ou un coffre-fort interne, puis passés au démarrage de rclone.
15 / 25

Déploiement Fargate côté gros chat

Correction de reproductibilité. Le runbook ne doit pas pointer vers rclone-s3-gateway:1 en dur. À chaque appel de register-task-definition, ECS crée une nouvelle révision. Le runbook capture donc l'ARN retourné dans TASK_DEF_ARN, puis utilise cette variable pour créer ou mettre à jour le service.

La passerelle tourne dans ECS Fargate. Le cluster ECS est un conteneur logique : aucune instance EC2 n'est gérée. La task Fargate est placée dans des subnets privés et utilise des VPC Endpoints AWS pour ECR, CloudWatch Logs et S3.

ECR

Repository rclone-s3-gateway, image Docker poussée avec le tag latest.

CloudWatch Logs

Log group /ecs/rclone-s3-gateway, stream par task ECS.

Task Definition

Mode awsvpc, compatibilité FARGATE, port container 8080.

Service ECS

Desired count à 1, relance automatique en cas d'arrêt de la task.

Target Group

Type ip, protocole TCP, health check TCP sur le port 8080.

Extrait de task definition · anonymisé
{
  "family": "rclone-s3-gateway",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::<ACCOUNT_GROSCHAT>:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::<ACCOUNT_GROSCHAT>:role/rclone-s3-gateway-task-role",
  "containerDefinitions": [
    {
      "name": "rclone-s3-gateway",
      "image": "<ACCOUNT_GROSCHAT>.dkr.ecr.eu-west-3.amazonaws.com/rclone-s3-gateway:latest",
      "portMappings": [{ "containerPort": 8080, "protocol": "tcp" }]
    }
  ]
}
17 / 25

Validation fonctionnelle

Limite POC — credentials applicatifs dans SSM Run Command. Dans les blocs de test, les variables LOCAL_ACCESS_KEY et LOCAL_SECRET_KEY sont injectées directement dans les commandes envoyées par SSM. C'est acceptable pour un POC jetable avec des credentials applicatifs non AWS, mais ces paramètres peuvent rester visibles dans l'historique Run Command. Une version durcie devra éviter ce passage en clair : Secrets Manager, SSM Parameter Store SecureString, injection contrôlée côté instance ou mécanisme d'identité applicative plus robuste.

Une instance EC2 temporaire a été lancée dans le VPC baleine, sans clé SSH, pilotée via AWS Systems Manager Session Manager. Elle a servi de client de test interne au VPC.

Test réseau TCP 8080
timeout 5 bash -c 'cat < /dev/null > /dev/tcp/<DNS_PRIVATELINK_BALEINE>/8080' \
  && echo OK || echo KO
Résultat : OK. Le chemin réseau PrivateLink jusqu'au service rclone est fonctionnel.
Listing du bucket via endpoint custom
AWS_ACCESS_KEY_ID=<LOCAL_ACCESS_KEY> \
AWS_SECRET_ACCESS_KEY=<LOCAL_SECRET_KEY> \
AWS_EC2_METADATA_DISABLED=true \
aws s3 ls s3://mab-documents \
  --endpoint-url http://<DNS_PRIVATELINK_BALEINE>:8080 \
  --region eu-west-3
Sortie obtenue
2026-05-28 19:46:23         21 fifi.txt
2026-05-28 19:46:23         20 loulou.txt
2026-05-28 19:46:24         20 riri.txt
Lecture d'un objet
AWS_ACCESS_KEY_ID=<LOCAL_ACCESS_KEY> \
AWS_SECRET_ACCESS_KEY=<LOCAL_SECRET_KEY> \
AWS_EC2_METADATA_DISABLED=true \
aws s3 cp s3://mab-documents/fifi.txt - \
  --endpoint-url http://<DNS_PRIVATELINK_BALEINE>:8080 \
  --region eu-west-3
Résultat
Fifi mange une figue

Copie des fichiers sur le disque de baleine

Une fois la passerelle validée, les fichiers peuvent être copiés sur le disque local de l'instance cliente côté baleine.

Copie récursive vers le disque local
mkdir -p ~/mab-documents

AWS_ACCESS_KEY_ID=<LOCAL_ACCESS_KEY> \
AWS_SECRET_ACCESS_KEY=<LOCAL_SECRET_KEY> \
AWS_EC2_METADATA_DISABLED=true \
aws s3 cp s3://mab-documents ~/mab-documents \
  --recursive \
  --endpoint-url http://<DNS_PRIVATELINK_BALEINE>:8080 \
  --region eu-west-3

ls -l ~/mab-documents
cat ~/mab-documents/fifi.txt
18 / 25

Ce que le POC prouve, et ce qu'il ne prouve pas encore

La validation a été volontairement progressive : DNS, TCP, réponse HTTP, listing S3, lecture d'objet, puis copie locale côté baleine.

✅ Ce qui est prouvé

  • baleine résout le DNS PrivateLink privé dans son VPC.
  • Le port 8080 est joignable jusqu'au NLB puis Fargate.
  • aws s3 ls s3://mab-documents liste bien fifi.txt, loulou.txt et riri.txt.
  • aws s3 cp s3://mab-documents/fifi.txt - lit le contenu Fifi mange une figue.
  • Les fichiers peuvent être copiés sur le disque local côté baleine.
  • baleine ne possède aucun droit IAM sur le bucket réel.

🧭 Ce qui reste à tester

  • Fichiers volumineux, streaming, range requests et reprise de téléchargement.
  • Multipart si un jour l'écriture est envisagée.
  • TLS de bout en bout ou terminaison TLS propre côté NLB / applicatif.
  • Externalisation et rotation des credentials applicatifs.
  • Restriction des users admin temporaires et durcissement IAM.
  • Supervision, alerting, logs structurés et version Terraform.
Test 1getent hosts <DNS_PRIVATELINK> : le DNS privé retourne une IP du VPC baleine.
Test 2/dev/tcp/<DNS>/8080 : la connectivité TCP vers la passerelle est validée.
Test 3curl -i : la passerelle répond comme une API S3-compatible.
Test 4aws s3 ls et aws s3 cp : le client S3 standard fonctionne avec endpoint custom.
Complément / cadrage honnête

Ce qui a réellement été réalisé, et ce qui relève du durcissement

Cette section évite de mélanger le POC effectivement déroulé avec les recommandations d'amélioration. Le POC a validé un flux privé inter-comptes fonctionnel ; les éléments ci-dessous marqués À prévoir ou Production sont des recommandations de durcissement, pas des choses prétendues comme déjà réalisées.

Formulation honnête du résultat

La conclusion du POC n’est pas : “ce pattern est naturellement meilleur que le partage IAM/S3 natif”. La conclusion est plus précise : ce pattern fonctionne techniquement pour exposer un accès S3-compatible privé sans donner de droit IAM direct au compte consommateur, mais il transforme un sujet d’autorisation S3/IAM en sujet d’exploitation de passerelle.

L’intérêt positif du pattern n’est donc pas seulement d’éviter un grant direct sur le bucket : il est aussi d’offrir à baleine une interface S3 stable et standard, pendant que gros chat garde un point de contrôle centralisé sans multiplier les policies S3 par consommateur.

C’est donc un pattern à réserver aux contextes où la conservation du contrôle côté propriétaire, l’absence de droit IAM direct du consommateur sur le bucket et la simplicité d’exploitation côté consommateur priment sur l’attribution native CloudTrail par identité AWS consommatrice.

ÉlémentStatut dans ce POCCommentaire
Deux comptes AWS distinctsRÉALISÉgros chat possède le bucket et la passerelle ; baleine consomme via PrivateLink.
Flux PrivateLink privéRÉALISÉLe flux applicatif est bien initié par baleine vers le service exposé par gros chat.
Client S3 standardRÉALISÉValidation avec aws s3 ls et aws s3 cp via --endpoint-url.
Bucket logique mab-documentsRÉALISÉLe remote rclone combine expose le bucket réel comme bucket logique côté client.
Lecture des fichiers de testRÉALISÉfifi.txt, loulou.txt et riri.txt ont été listés/copés depuis baleine.
Absence de droit IAM direct pour baleineRÉALISÉLa lecture S3 réelle est portée par le Task Role ECS côté gros chat.
HTTP sur port 8080RÉALISÉ POCAcceptable pour la démonstration privée ; à remplacer par HTTPS avant tout usage durable.
Credentials applicatifs en clair dans le lancement rcloneRÉALISÉ POCChoix de simplicité POC. Ce n'est pas une pratique de production.
Secrets Manager / SSM SecureStringÀ PRÉVOIRÀ utiliser pour sortir les secrets de la task definition et de la ligne de commande.
TLS / certificat / DNS privé proprePRODUCTIONÀ ajouter si la passerelle devient un service pérenne.
Tests gros fichiers / range / multipart / chargeÀ PRÉVOIRNon validé par ce POC minimal ; nécessaire avant industrialisation.
RÉALISÉ

Validation démontrée

Le POC démontre la faisabilité du pattern : baleine appelle une passerelle S3-compatible privée, gros chat conserve le bucket et le contrôle IAM, et le client reste un client S3 standard.

À PRÉVOIR

Améliorations non encore réalisées

Les recommandations ci-dessous ne doivent pas être lues comme déjà implémentées. Elles servent à indiquer comment transformer le POC en solution plus robuste.

19 / 25

Couverture du déroulé pas à pas : du terminal au runbook reproductible

Cette section sert de pont entre le récit pédagogique et le runbook autonome. Elle ne renvoie plus à des numéros d’étapes internes à notre construction initiale : pour un lecteur externe, l’important est de comprendre les grandes familles d’actions à réaliser, dans quel compte AWS elles se déroulent, et quel point de contrôle permet de savoir que l’on peut continuer.

Le POC a bien été construit progressivement en terminal, avec de nombreux essais, vérifications et corrections. Cette page ne demande pas au lecteur de connaître ce déroulé historique. Elle regroupe maintenant ces actions en blocs opérationnels : préparer, construire côté gros chat, exposer via PrivateLink, consommer côté baleine, valider, puis nettoyer.
Bloc reproductibleCompte concernéCe que le lecteur doit faireCheckpoint attenduPoint pédagogique à retenir
Préparer l’environnement CLI Local + deux comptes Installer les prérequis, configurer deux profils AWS isolés, vérifier l’identité avec sts get-caller-identity. Les profils groschat et baleine répondent correctement. On évite de polluer le profil AWS par défaut ; le POC reste isolé.
Préparer le bucket de test gros chat Vérifier ou créer mab-documents, puis y placer fifi.txt, loulou.txt et riri.txt. aws s3 ls s3://mab-documents affiche les trois fichiers. Le bucket réel reste toujours côté gros chat.
Créer les rôles IAM utiles gros chat Créer l’Execution Role ECS et le Task Role Fargate avec lecture minimale sur mab-documents. Les deux rôles existent et la policy S3 est attachée au Task Role. Le Task Role est la seule identité AWS qui lit réellement S3.
Construire l’image Docker rclone Local + gros chat Créer rclone.conf, créer le Dockerfile, builder l’image, la taguer et la pousser dans ECR. Un digest ECR est retourné pour l’image rclone-s3-gateway:latest. Fargate lance une image Docker ; il ne lance pas un fichier rclone.conf isolé.
Créer le réseau privé gros chat gros chat Créer VPC, subnets, route table et Security Groups nécessaires à ECS, aux endpoints AWS et au NLB. Le VPC et les deux subnets sont disponibles. La passerelle est privée ; elle n’a pas besoin d’être exposée à Internet.
Créer les endpoints AWS internes gros chat Créer le Gateway Endpoint S3 et les Interface Endpoints ECR API, ECR DKR et CloudWatch Logs. Les endpoints d’interface sont available. Sans ces endpoints, une task Fargate privée ne peut pas tirer l’image ECR ni écrire ses logs sans NAT.
Déployer la passerelle sur Fargate gros chat Créer le cluster ECS, le log group, la task definition, puis le service ECS Fargate. Le service atteint Desired=1, Running=1, Pending=0. Fargate est serverless, mais ECS reste décrit par des objets logiques : cluster, task definition, task et service.
Publier la passerelle via NLB gros chat Créer le Target Group de type ip, le NLB interne et le listener TCP 8080. Le NLB est available et le service ECS est attaché au Target Group. Avec Fargate, le Target Group doit cibler les IP privées des tasks.
Créer le service PrivateLink gros chat Créer l’Endpoint Service PrivateLink sur le NLB et autoriser le compte baleine. Le service PrivateLink est Available. Autoriser arn:aws:iam::<ACCOUNT_BALEINE>:root signifie autoriser le compte, pas donner un accès à l’utilisateur root.
Créer l’Interface Endpoint consommateur baleine Créer le Security Group et l’Interface VPC Endpoint vers le service PrivateLink publié par gros chat. L’endpoint côté baleine est available et fournit un DNS privé. Ce DNS est privé au VPC baleine ; il ne se teste pas depuis le poste local.
Créer un client de test dans baleine baleine Lancer une petite EC2 temporaire pilotée par SSM, sans clé SSH, pour tester depuis le VPC. SSM indique PingStatus=Online. Les tests réseau doivent être exécutés depuis une ressource située dans le VPC baleine.
Valider la chaîne réseau baleine Tester la résolution DNS PrivateLink, le port TCP 8080, puis une réponse HTTP brute avec curl. Le DNS renvoie une IP privée, le test TCP retourne OK, et curl obtient une réponse de la passerelle. Ces tests valident le réseau, mais pas encore la compatibilité S3 complète.
Valider le comportement S3 baleine Utiliser aws s3 ls et aws s3 cp avec l’endpoint custom et les credentials applicatifs. Le listing affiche fifi.txt, loulou.txt, riri.txt, puis fifi.txt est lu correctement. Le client utilise une interface S3 standard, mais sans droit IAM sur le vrai bucket.
Comprendre et corriger le mapping rclone gros chat Utiliser le remote combine gateway: pour exposer mab-documents comme bucket logique. Les logs Fargate montrent rclone serve s3 gateway:. Les erreurs NoSuchBucket ont montré que le problème venait du mapping rclone, pas de PrivateLink.
Copier les fichiers côté client baleine Copier récursivement le bucket logique vers un répertoire local, par exemple ~/mab-documents. Les fichiers existent sur le disque local du client de test. Les fichiers ne sont pas créés dans un bucket S3 côté baleine ; ils sont simplement copiés sur le disque du client.
Prévoir la coupure et le nettoyage gros chat + baleine Documenter l’arrêt d’urgence, la révocation, la rotation des credentials et le nettoyage des ressources coûteuses. Après nettoyage, plus de Fargate, NLB, VPC Endpoints, EC2, EIP, ECR ni logs du POC. gros chat doit garder la capacité de couper rapidement l’accès.
Conclusion. Cette section n’est pas un historique numéroté de notre travail : c’est une checklist de reproductibilité. Le runbook autonome qui suit reprend ces blocs dans l’ordre, avec des variables, des commandes et des checkpoints.
20 / 25

Commandes commentées : comprendre ce que l'on fait

Cette section ne remplace pas le runbook complet : elle le rend plus lisible. Les commandes ci-dessous sont volontairement commentées en français pour expliquer les concepts et le rôle de chaque option importante.

0. Contexte CLI isolé : éviter de polluer le profil AWS par défaut

Création d'un environnement AWS CLI dédié au POC
# On crée un répertoire séparé pour ce POC.
# Objectif : ne pas modifier ~/.aws/config ni ~/.aws/credentials utilisés au quotidien.
mkdir -p ~/.aws-poc-s3-gateway

# Fichier de configuration isolé : deux profils, un par compte AWS.
# groschat = compte propriétaire du bucket et de la passerelle.
# baleine  = compte consommateur de la passerelle.
cat > ~/.aws-poc-s3-gateway/config <<'EOF'
[profile groschat]
region = eu-west-3
output = json

[profile baleine]
region = eu-west-3
output = json
EOF

# Fichier de credentials isolé.
# Les access keys dédiées au POC seront mises ici, pas dans le profil par défaut.
touch ~/.aws-poc-s3-gateway/credentials
chmod 600 ~/.aws-poc-s3-gateway/credentials

# Petit fichier à sourcer dans le terminal pour dire à AWS CLI d'utiliser ces fichiers isolés.
cat > ~/aws-poc-s3-gateway.env <<'EOF'
export AWS_CONFIG_FILE=$HOME/.aws-poc-s3-gateway/config
export AWS_SHARED_CREDENTIALS_FILE=$HOME/.aws-poc-s3-gateway/credentials
export AWS_PAGER=""
EOF

# Activation dans le terminal courant.
source ~/aws-poc-s3-gateway.env

1. IAM côté gros chat : séparer rôle technique et rôle applicatif

Création du Task Role applicatif
# Trust policy : autorise ECS Tasks à assumer ce rôle.
# Sans cette relation de confiance, Fargate ne pourrait pas fournir ce rôle au conteneur.
cat > /tmp/trust-ecs-task.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "ecs-tasks.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}
EOF

# Task Role = rôle applicatif visible par le conteneur.
# C'est ce rôle que rclone utilise indirectement avec env_auth = true.
aws --profile groschat iam create-role   --role-name rclone-s3-gateway-task-role   --assume-role-policy-document file:///tmp/trust-ecs-task.json

# Policy minimale : rclone doit seulement lister le bucket et lire les objets.
# Il n'a pas besoin de PutObject, DeleteObject ou d'un accès à d'autres buckets.
cat > /tmp/rclone-read-mab-documents-policy.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListBucket",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::mab-documents"
    },
    {
      "Sid": "ReadObjects",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::mab-documents/*"
    }
  ]
}
EOF

aws --profile groschat iam put-role-policy   --role-name rclone-s3-gateway-task-role   --policy-name ReadOnlyMabDocuments   --policy-document file:///tmp/rclone-read-mab-documents-policy.json
Création de l'Execution Role ECS
# Execution Role = rôle technique d'ECS.
# Il sert à tirer l'image depuis ECR et à écrire les logs dans CloudWatch.
# Il ne doit pas être confondu avec le Task Role qui lit le bucket S3.
aws --profile groschat iam create-role   --role-name ecsTaskExecutionRole   --assume-role-policy-document file:///tmp/trust-ecs-task.json

aws --profile groschat iam attach-role-policy   --role-name ecsTaskExecutionRole   --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

2. Image Docker rclone : ce qui part vraiment dans Fargate

Configuration rclone commentée
# Remote mabaws : accès au vrai S3 AWS côté gros chat.
# env_auth = true signifie : ne pas mettre de clés AWS dans le fichier.
# rclone récupère ses credentials via le Task Role Fargate.
[mabaws]
type = s3
provider = AWS
env_auth = true
region = eu-west-3
location_constraint = eu-west-3

# Remote gateway : racine artificielle exposée par rclone serve s3.
# La ligne ci-dessous crée un bucket logique mab-documents côté client.
# Ce bucket logique pointe vers le vrai bucket AWS mabaws:mab-documents.
[gateway]
type = combine
upstreams = mab-documents=mabaws:mab-documents
Dockerfile commenté
# Image officielle rclone.
FROM rclone/rclone:latest

# On embarque la configuration rclone dans l'image.
# Pour un POC c'est simple ; en production, on préférerait monter/injecter la config.
COPY rclone.conf /config/rclone/rclone.conf

# Le conteneur écoute en TCP sur 8080.
EXPOSE 8080

# ENTRYPOINT fixe le binaire lancé.
ENTRYPOINT ["rclone"]

# CMD lance un serveur S3-compatible.
# gateway: est le remote combine qui expose mab-documents comme bucket logique.
# --auth-key protège la passerelle avec une paire access_key/secret_key applicative.
# Ces credentials ne sont pas des credentials AWS.
CMD ["serve", "s3", "gateway:", "--addr", ":8080", "--auth-key", "<LOCAL_ACCESS_KEY>,<LOCAL_SECRET_KEY>", "--vfs-cache-mode", "off", "-vv"]

3. Réseau gros chat : pourquoi tous ces VPC Endpoints ?

Endpoints AWS privés nécessaires à une task Fargate sans Internet
# La task Fargate est dans des subnets privés, sans NAT ni Internet Gateway.
# Elle a donc besoin d'endpoints privés pour contacter les services AWS nécessaires.

# Endpoint Gateway S3 : permet à rclone d'accéder au bucket S3 sans sortir sur Internet.
aws --profile groschat ec2 create-vpc-endpoint   --vpc-id <VPC_GROSCHAT>   --service-name com.amazonaws.eu-west-3.s3   --vpc-endpoint-type Gateway   --route-table-ids <ROUTE_TABLE_GROSCHAT>

# Endpoint Interface ECR API : permet à ECS/Fargate de parler à l'API ECR.
aws --profile groschat ec2 create-vpc-endpoint   --vpc-id <VPC_GROSCHAT>   --service-name com.amazonaws.eu-west-3.ecr.api   --vpc-endpoint-type Interface   --subnet-ids <SUBNET_GROSCHAT_A> <SUBNET_GROSCHAT_B>   --security-group-ids <SG_ENDPOINTS_AWS>   --private-dns-enabled

# Endpoint Interface ECR DKR : permet de tirer les layers Docker de l'image.
aws --profile groschat ec2 create-vpc-endpoint   --vpc-id <VPC_GROSCHAT>   --service-name com.amazonaws.eu-west-3.ecr.dkr   --vpc-endpoint-type Interface   --subnet-ids <SUBNET_GROSCHAT_A> <SUBNET_GROSCHAT_B>   --security-group-ids <SG_ENDPOINTS_AWS>   --private-dns-enabled

# Endpoint Interface CloudWatch Logs : permet au conteneur d'envoyer ses logs.
aws --profile groschat ec2 create-vpc-endpoint   --vpc-id <VPC_GROSCHAT>   --service-name com.amazonaws.eu-west-3.logs   --vpc-endpoint-type Interface   --subnet-ids <SUBNET_GROSCHAT_A> <SUBNET_GROSCHAT_B>   --security-group-ids <SG_ENDPOINTS_AWS>   --private-dns-enabled

4. Service ECS : lancer et maintenir la passerelle

Création du service Fargate commentée
# create-service demande à ECS de maintenir une task active.
# --desired-count 1 : on veut exactement une passerelle en fonctionnement.
# --launch-type FARGATE : pas d'EC2 à gérer.
# networkConfiguration : la task reçoit une ENI privée dans les subnets indiqués.
# assignPublicIp=DISABLED : pas d'IP publique sur la task.
aws --profile groschat ecs create-service   --cluster rclone-s3-gateway-cluster   --service-name rclone-s3-gateway-service   --task-definition "$TASK_DEF_ARN"   --desired-count 1   --launch-type FARGATE   --network-configuration "awsvpcConfiguration={subnets=[<SUBNET_GROSCHAT_A>,<SUBNET_GROSCHAT_B>],securityGroups=[<SG_ECS_TASK>],assignPublicIp=DISABLED}"

# Après ajout du NLB, on rattache le Target Group au service.
# ECS enregistrera automatiquement l'IP privée de la task dans le Target Group.
aws --profile groschat ecs update-service   --cluster rclone-s3-gateway-cluster   --service rclone-s3-gateway-service   --load-balancers targetGroupArn=<TG_ARN>,containerName=rclone-s3-gateway,containerPort=8080   --force-new-deployment

5. PrivateLink : autoriser baleine puis consommer le service

Côté gros chat : exposer et autoriser
# gros chat expose son NLB comme Endpoint Service PrivateLink.
# --no-acceptance-required simplifie le POC : la connexion n'a pas besoin d'être acceptée manuellement.
aws --profile groschat ec2 create-vpc-endpoint-service-configuration   --network-load-balancer-arns <NLB_ARN>   --no-acceptance-required

# gros chat autorise le compte baleine à créer un Interface Endpoint vers ce service.
# :root signifie ici "le compte AWS baleine", pas l'utilisateur root utilisé en console.
aws --profile groschat ec2 modify-vpc-endpoint-service-permissions   --service-id <VPCE_SERVICE_ID>   --add-allowed-principals arn:aws:iam::<ACCOUNT_BALEINE>:root
Côté baleine : créer l'Interface Endpoint
# baleine crée un Interface Endpoint dans son VPC.
# Le DNS retourné par AWS sera privé au VPC baleine.
# --no-private-dns-enabled : on ne remplace pas le DNS AWS S3 global ; on utilise un endpoint explicite.
aws --profile baleine ec2 create-vpc-endpoint   --vpc-id <VPC_BALEINE>   --service-name com.amazonaws.vpce.eu-west-3.<VPCE_SERVICE_ID>   --vpc-endpoint-type Interface   --subnet-ids <SUBNET_BALEINE_A> <SUBNET_BALEINE_B>   --security-group-ids <SG_VPCE_BALEINE>   --no-private-dns-enabled

6. Tests depuis baleine : SSM, DNS, TCP, S3

Pourquoi tester via SSM ?
# Le DNS PrivateLink est privé au VPC baleine.
# Depuis ton PC local, il peut ne pas se résoudre ou ne pas être joignable.
# On lance donc les commandes depuis une EC2 temporaire dans le VPC baleine,
# pilotée par Systems Manager Session Manager, sans clé SSH.

# Test DNS : doit retourner une IP privée 172.31.x.x côté baleine.
getent hosts <DNS_PRIVATELINK_BALEINE>

# Test TCP : vérifie que le port 8080 répond jusqu'à rclone.
timeout 5 bash -c 'cat < /dev/null > /dev/tcp/<DNS_PRIVATELINK_BALEINE>/8080'   && echo OK || echo KO

# Test HTTP brut : une erreur XML S3 est acceptable ici.
# Elle prouve que le service répond, même si curl ne signe pas correctement la requête S3.
curl -i --max-time 10 http://<DNS_PRIVATELINK_BALEINE>:8080/ | head -40
Test S3 final côté baleine
# Credentials applicatifs transmis à baleine.
# Ce ne sont pas des clés AWS et elles ne donnent aucun droit IAM sur gros chat.
export AWS_ACCESS_KEY_ID=<LOCAL_ACCESS_KEY>
export AWS_SECRET_ACCESS_KEY=<LOCAL_SECRET_KEY>
export AWS_EC2_METADATA_DISABLED=true

# Listing du bucket logique exposé par la passerelle.
aws s3 ls s3://mab-documents   --endpoint-url http://<DNS_PRIVATELINK_BALEINE>:8080   --region eu-west-3

# Lecture d'un objet : le contenu vient du vrai bucket S3 dans gros chat.
aws s3 cp s3://mab-documents/fifi.txt -   --endpoint-url http://<DNS_PRIVATELINK_BALEINE>:8080   --region eu-west-3

# Copie des fichiers sur le disque local de l'instance cliente baleine.
mkdir -p ~/mab-documents
aws s3 cp s3://mab-documents ~/mab-documents   --recursive   --endpoint-url http://<DNS_PRIVATELINK_BALEINE>:8080   --region eu-west-3
21 / 25

Runbook CLI autonome : reproduire le POC de bout en bout

Correction de robustesse du runbook. Les tests côté baleine ne doivent pas utiliser un simple sleep 3 après ssm send-command. L'agent SSM peut répondre plus lentement, surtout au premier appel. Le runbook utilise donc aws ssm wait command-executed avant de lire le résultat avec get-command-invocation.
Helper robuste pour exécuter une commande côté baleine via SSM
run_baleine() {
  local COMMENT="$1"
  local COMMAND="$2"

  COMMAND_ID=$(aws --profile baleine ssm send-command \
    --instance-ids "$INSTANCE_TEST_BALEINE" \
    --document-name "AWS-RunShellScript" \
    --comment "$COMMENT" \
    --parameters "commands=[\"$COMMAND\"]" \
    --query 'Command.CommandId' \
    --output text)

  aws --profile baleine ssm wait command-executed \
    --command-id "$COMMAND_ID" \
    --instance-id "$INSTANCE_TEST_BALEINE"

  aws --profile baleine ssm get-command-invocation \
    --command-id "$COMMAND_ID" \
    --instance-id "$INSTANCE_TEST_BALEINE" \
    --query '{Status:Status,Output:StandardOutputContent,Error:StandardErrorContent}' \
    --output json
}

Cette version du runbook est pensée pour un lecteur externe qui n'a pas suivi la construction pas à pas. Elle centralise les variables, indique dans quel compte lancer chaque bloc, stocke les IDs retournés par AWS et ajoute des checkpoints après les étapes critiques.

Vérification de reproductibilité : les points bloquants classiques sont maintenant explicités dans le runbook — ordre de création des profils, création éventuelle du bucket de test, attente du NLB, endpoints AWS internes, sélection dynamique des AZ PrivateLink côté baleine et checkpoints de validation.
Important. Le runbook est volontairement pédagogique et orienté POC. Il crée des users de construction temporaires avec des droits larges pour aller vite. Avant industrialisation, remplacer ces users par des rôles/policies minimales, externaliser les credentials applicatifs dans un coffre de secrets, ajouter TLS et traduire l'ensemble en Terraform.
BlocCompteButSortie attendue
Pré-requisLocalAWS CLI v2, Docker, région, variables.Commandes disponibles.
Profilsgros chat / baleineCréer deux identités de construction et deux profils isolés.sts get-caller-identity fonctionne pour les deux comptes.
Bucketgros chatVérifier mab-documents et les fichiers de test.fifi.txt, loulou.txt, riri.txt.
IAM + Dockergros chatCréer les rôles Fargate et l'image rclone dans ECR.Digest ECR disponible.
Réseau + Endpointsgros chatCréer VPC privé, subnets, SG, endpoints S3/ECR/Logs.Endpoints available.
ECS + NLBgros chatLancer la passerelle Fargate derrière un NLB interne.Service ECS Running = 1, target healthy.
PrivateLinkgros chat / baleineExposer le NLB, autoriser baleine, créer l'Interface Endpoint consommateur.DNS PrivateLink côté baleine.
ValidationbaleineTester DNS, TCP, HTTP, aws s3 ls, aws s3 cp.Lecture de fifi.txt.
Runbook exécutable et commenté
Pré-requis locaux
# À vérifier sur le poste qui pilote le POC.
# Le runbook évite les alias : toutes les commandes utilisent explicitement
# --profile groschat ou --profile baleine.

aws --version
# attendu : AWS CLI v2

docker version
# attendu : Docker fonctionnel, car l'image rclone est construite localement

# Optionnel, mais pratique pour générer des secrets applicatifs de test
openssl version

# Région utilisée dans le POC
export REGION=eu-west-3
export AWS_PAGER=""
Variables centrales du runbook
# Fichier de variables réutilisable entre les blocs.
# Important : à ce stade, on ne récupère pas encore les IDs de comptes.
# Les commandes sts seront lancées après la configuration des profils groschat et baleine.

mkdir -p ~/poc-s3-gateway/rclone-s3-gateway
cat > ~/poc-s3-gateway/poc-vars.env <<'EOF'
export REGION=eu-west-3
export BUCKET_NAME=mab-documents
export GROSCHAT_PROFILE=groschat
export BALEINE_PROFILE=baleine
export AWS_PAGER=""
EOF

source ~/poc-s3-gateway/poc-vars.env
cat ~/poc-s3-gateway/poc-vars.env
CloudShell gros chat : créer un user de construction temporaire
# À lancer dans AWS CloudShell du compte gros chat.
# Ce user sert uniquement de "tournevis" pour construire le POC.
# Il ne fait pas partie de l'architecture cible.

aws sts get-caller-identity

aws iam create-user \
  --user-name poc-s3-gateway-admin

aws iam attach-user-policy \
  --user-name poc-s3-gateway-admin \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

aws iam create-access-key \
  --user-name poc-s3-gateway-admin

# Conserver AccessKeyId et SecretAccessKey dans un coffre temporaire sécurisé.
# Ne pas les coller dans un dépôt Git ni dans une page publique.
CloudShell baleine : créer un user de construction temporaire
# À lancer dans AWS CloudShell du compte baleine.
# Même logique : user temporaire de construction, pas identité cible.

aws sts get-caller-identity

aws iam create-user \
  --user-name poc-s3-gateway-client-admin

aws iam attach-user-policy \
  --user-name poc-s3-gateway-client-admin \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

aws iam create-access-key \
  --user-name poc-s3-gateway-client-admin

# Conserver AccessKeyId et SecretAccessKey de baleine.
Poste local : contexte AWS CLI isolé
# On n'utilise pas le profil AWS par défaut.
# Les credentials du POC sont stockés dans un dossier dédié.

mkdir -p ~/.aws-poc-s3-gateway

cat > ~/.aws-poc-s3-gateway/config <<'EOF'
[profile groschat]
region = eu-west-3
output = json

[profile baleine]
region = eu-west-3
output = json
EOF

touch ~/.aws-poc-s3-gateway/credentials
chmod 600 ~/.aws-poc-s3-gateway/credentials

cat > ~/aws-poc-s3-gateway.env <<'EOF'
export AWS_CONFIG_FILE=$HOME/.aws-poc-s3-gateway/config
export AWS_SHARED_CREDENTIALS_FILE=$HOME/.aws-poc-s3-gateway/credentials
export AWS_PAGER=""
EOF

source ~/aws-poc-s3-gateway.env
source ~/poc-s3-gateway/poc-vars.env

# Renseigner les clés obtenues dans CloudShell gros chat.
aws configure set aws_access_key_id "<ACCESS_KEY_GROSCHAT>" --profile groschat
aws configure set aws_secret_access_key "<SECRET_KEY_GROSCHAT>" --profile groschat

# Renseigner les clés obtenues dans CloudShell baleine.
aws configure set aws_access_key_id "<ACCESS_KEY_BALEINE>" --profile baleine
aws configure set aws_secret_access_key "<SECRET_KEY_BALEINE>" --profile baleine

# Checkpoint : les deux profils doivent pointer vers deux comptes différents.
aws --profile groschat sts get-caller-identity
aws --profile baleine sts get-caller-identity

# Maintenant seulement, on récupère et on persiste les IDs de comptes.
export GROSCHAT_ACCOUNT_ID=$(aws --profile groschat sts get-caller-identity --query Account --output text)
export BALEINE_ACCOUNT_ID=$(aws --profile baleine sts get-caller-identity --query Account --output text)

cat >> ~/poc-s3-gateway/poc-vars.env <<EOF
export GROSCHAT_ACCOUNT_ID=$GROSCHAT_ACCOUNT_ID
export BALEINE_ACCOUNT_ID=$BALEINE_ACCOUNT_ID
EOF

cat ~/poc-s3-gateway/poc-vars.env
Checkpoint 1 : bucket de test côté gros chat
source ~/aws-poc-s3-gateway.env
source ~/poc-s3-gateway/poc-vars.env

# Le bucket réel doit exister côté gros chat.
# Si le bucket existe déjà, cette commande réussit.
# S'il n'existe pas, le bloc le crée en eu-west-3.
if ! aws --profile groschat s3api head-bucket --bucket "$BUCKET_NAME" 2>/dev/null; then
  aws --profile groschat s3api create-bucket     --bucket "$BUCKET_NAME"     --region "$REGION"     --create-bucket-configuration LocationConstraint="$REGION"
fi

# Créer ou remettre les fichiers de test minimaux.
printf 'Fifi mange une figue
' > /tmp/fifi.txt
printf 'Loulou lit le livre
' > /tmp/loulou.txt
printf 'Riri rit encore
' > /tmp/riri.txt

aws --profile groschat s3 cp /tmp/fifi.txt s3://$BUCKET_NAME/fifi.txt
aws --profile groschat s3 cp /tmp/loulou.txt s3://$BUCKET_NAME/loulou.txt
aws --profile groschat s3 cp /tmp/riri.txt s3://$BUCKET_NAME/riri.txt

# Sortie attendue : fifi.txt, loulou.txt, riri.txt
aws --profile groschat s3 ls s3://$BUCKET_NAME
Bloc 2 : IAM côté gros chat
source ~/aws-poc-s3-gateway.env
source ~/poc-s3-gateway/poc-vars.env

# Trust policy commune aux rôles ECS Task.
cat > /tmp/trust-ecs-task.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "ecs-tasks.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

# Task Role applicatif : c'est ce rôle qui lit vraiment le bucket S3.
aws --profile groschat iam create-role \
  --role-name rclone-s3-gateway-task-role \
  --assume-role-policy-document file:///tmp/trust-ecs-task.json

cat > /tmp/rclone-read-mab-documents-policy.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListBucket",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::mab-documents"
    },
    {
      "Sid": "ReadObjects",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::mab-documents/*"
    }
  ]
}
EOF

aws --profile groschat iam put-role-policy \
  --role-name rclone-s3-gateway-task-role \
  --policy-name ReadOnlyMabDocuments \
  --policy-document file:///tmp/rclone-read-mab-documents-policy.json

# Execution Role technique : permet à ECS/Fargate de tirer l'image ECR
# et d'écrire les logs CloudWatch.
aws --profile groschat iam create-role \
  --role-name ecsTaskExecutionRole \
  --assume-role-policy-document file:///tmp/trust-ecs-task.json

aws --profile groschat iam attach-role-policy \
  --role-name ecsTaskExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

export TASK_ROLE_ARN=$(aws --profile groschat iam get-role --role-name rclone-s3-gateway-task-role --query 'Role.Arn' --output text)
export EXEC_ROLE_ARN=$(aws --profile groschat iam get-role --role-name ecsTaskExecutionRole --query 'Role.Arn' --output text)

cat >> ~/poc-s3-gateway/poc-vars.env <<EOF
export TASK_ROLE_ARN=$TASK_ROLE_ARN
export EXEC_ROLE_ARN=$EXEC_ROLE_ARN
EOF

echo "$TASK_ROLE_ARN"
echo "$EXEC_ROLE_ARN"
Bloc 3 : image Docker rclone et ECR côté gros chat
source ~/aws-poc-s3-gateway.env
source ~/poc-s3-gateway/poc-vars.env
cd ~/poc-s3-gateway/rclone-s3-gateway

# Credentials applicatifs de la passerelle.
# Ce ne sont pas des credentials AWS.
export LOCAL_ACCESS_KEY="local-$(openssl rand -hex 8)"
export LOCAL_SECRET_KEY="$(openssl rand -hex 24)"

cat >> ~/poc-s3-gateway/poc-vars.env <<EOF
export LOCAL_ACCESS_KEY=$LOCAL_ACCESS_KEY
export LOCAL_SECRET_KEY=$LOCAL_SECRET_KEY
EOF

# rclone remote AWS : env_auth=true signifie que rclone utilise le Task Role Fargate.
# gateway: combine expose mab-documents comme bucket logique côté client S3.
cat > rclone.conf <<'EOF'
[mabaws]
type = s3
provider = AWS
env_auth = true
region = eu-west-3
location_constraint = eu-west-3

[gateway]
type = combine
upstreams = mab-documents=mabaws:mab-documents
EOF

# Pour le POC, les credentials applicatifs sont injectés dans la commande.
# En production, les sortir du Dockerfile et passer par Secrets Manager / variables ECS.
cat > Dockerfile <<EOF
FROM rclone/rclone:latest

COPY rclone.conf /config/rclone/rclone.conf

EXPOSE 8080

ENTRYPOINT ["rclone"]
CMD ["serve", "s3", "gateway:", "--addr", ":8080", "--auth-key", "$LOCAL_ACCESS_KEY,$LOCAL_SECRET_KEY", "--vfs-cache-mode", "off", "-vv"]
EOF

aws --profile groschat ecr create-repository \
  --repository-name rclone-s3-gateway \
  --image-scanning-configuration scanOnPush=true \
  --encryption-configuration encryptionType=AES256

export ECR_URI=$(aws --profile groschat ecr describe-repositories \
  --repository-names rclone-s3-gateway \
  --query 'repositories[0].repositoryUri' \
  --output text)

cat >> ~/poc-s3-gateway/poc-vars.env <<EOF
export ECR_URI=$ECR_URI
EOF

docker build -t rclone-s3-gateway:latest .
docker tag rclone-s3-gateway:latest "$ECR_URI:latest"

aws --profile groschat ecr get-login-password --region "$REGION" \
  | docker login --username AWS --password-stdin "$GROSCHAT_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com"

docker push "$ECR_URI:latest"

aws --profile groschat ecr describe-images \
  --repository-name rclone-s3-gateway \
  --image-ids imageTag=latest \
  --query 'imageDetails[0].imageDigest' \
  --output text
Bloc 4 : réseau privé côté gros chat
source ~/aws-poc-s3-gateway.env
source ~/poc-s3-gateway/poc-vars.env

# VPC dédié au POC côté gros chat.
export VPC_GROSCHAT=$(aws --profile groschat ec2 create-vpc \
  --cidr-block 10.42.0.0/16 \
  --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=poc-s3-gateway-vpc}]' \
  --query 'Vpc.VpcId' \
  --output text)

aws --profile groschat ec2 modify-vpc-attribute --vpc-id "$VPC_GROSCHAT" --enable-dns-support
aws --profile groschat ec2 modify-vpc-attribute --vpc-id "$VPC_GROSCHAT" --enable-dns-hostnames

export SUBNET_GROSCHAT_A=$(aws --profile groschat ec2 create-subnet \
  --vpc-id "$VPC_GROSCHAT" \
  --cidr-block 10.42.1.0/24 \
  --availability-zone eu-west-3a \
  --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=poc-s3-gateway-subnet-a}]' \
  --query 'Subnet.SubnetId' \
  --output text)

export SUBNET_GROSCHAT_B=$(aws --profile groschat ec2 create-subnet \
  --vpc-id "$VPC_GROSCHAT" \
  --cidr-block 10.42.2.0/24 \
  --availability-zone eu-west-3b \
  --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=poc-s3-gateway-subnet-b}]' \
  --query 'Subnet.SubnetId' \
  --output text)

export RTB_GROSCHAT=$(aws --profile groschat ec2 create-route-table \
  --vpc-id "$VPC_GROSCHAT" \
  --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=poc-s3-gateway-rt-private}]' \
  --query 'RouteTable.RouteTableId' \
  --output text)

export RTB_ASSOC_A=$(aws --profile groschat ec2 associate-route-table \
  --route-table-id "$RTB_GROSCHAT" \
  --subnet-id "$SUBNET_GROSCHAT_A" \
  --query 'AssociationId' \
  --output text)

export RTB_ASSOC_B=$(aws --profile groschat ec2 associate-route-table \
  --route-table-id "$RTB_GROSCHAT" \
  --subnet-id "$SUBNET_GROSCHAT_B" \
  --query 'AssociationId' \
  --output text)

export SG_ECS=$(aws --profile groschat ec2 create-security-group \
  --group-name rclone-s3-gateway-ecs-sg \
  --description "Security group for rclone S3 gateway ECS tasks" \
  --vpc-id "$VPC_GROSCHAT" \
  --query 'GroupId' \
  --output text)

aws --profile groschat ec2 authorize-security-group-ingress \
  --group-id "$SG_ECS" \
  --protocol tcp \
  --port 8080 \
  --cidr 10.42.0.0/16

cat >> ~/poc-s3-gateway/poc-vars.env <<EOF
export VPC_GROSCHAT=$VPC_GROSCHAT
export SUBNET_GROSCHAT_A=$SUBNET_GROSCHAT_A
export SUBNET_GROSCHAT_B=$SUBNET_GROSCHAT_B
export RTB_GROSCHAT=$RTB_GROSCHAT
export RTB_ASSOC_A=$RTB_ASSOC_A
export RTB_ASSOC_B=$RTB_ASSOC_B
export SG_ECS=$SG_ECS
EOF
Bloc 5 : endpoints AWS internes côté gros chat
source ~/aws-poc-s3-gateway.env
source ~/poc-s3-gateway/poc-vars.env

# Security Group des Interface Endpoints AWS internes.
export SG_VPCE_GROSCHAT=$(aws --profile groschat ec2 create-security-group \
  --group-name poc-s3-gateway-vpce-sg \
  --description "Security group for VPC interface endpoints" \
  --vpc-id "$VPC_GROSCHAT" \
  --query 'GroupId' \
  --output text)

# La task Fargate doit pouvoir joindre ces endpoints en HTTPS.
aws --profile groschat ec2 authorize-security-group-ingress \
  --group-id "$SG_VPCE_GROSCHAT" \
  --protocol tcp \
  --port 443 \
  --source-group "$SG_ECS"

# S3 Gateway Endpoint : accès privé à S3 sans NAT.
export VPCE_S3=$(aws --profile groschat ec2 create-vpc-endpoint \
  --vpc-id "$VPC_GROSCHAT" \
  --service-name com.amazonaws.eu-west-3.s3 \
  --vpc-endpoint-type Gateway \
  --route-table-ids "$RTB_GROSCHAT" \
  --tag-specifications 'ResourceType=vpc-endpoint,Tags=[{Key=Name,Value=poc-s3-gateway-s3-endpoint}]' \
  --query 'VpcEndpoint.VpcEndpointId' \
  --output text)

# ECR API : API d'authentification et métadonnées ECR.
export VPCE_ECR_API=$(aws --profile groschat ec2 create-vpc-endpoint \
  --vpc-id "$VPC_GROSCHAT" \
  --service-name com.amazonaws.eu-west-3.ecr.api \
  --vpc-endpoint-type Interface \
  --subnet-ids "$SUBNET_GROSCHAT_A" "$SUBNET_GROSCHAT_B" \
  --security-group-ids "$SG_VPCE_GROSCHAT" \
  --private-dns-enabled \
  --tag-specifications 'ResourceType=vpc-endpoint,Tags=[{Key=Name,Value=poc-s3-gateway-ecr-api-endpoint}]' \
  --query 'VpcEndpoint.VpcEndpointId' \
  --output text)

# ECR DKR : registry Docker privé pour tirer les layers.
export VPCE_ECR_DKR=$(aws --profile groschat ec2 create-vpc-endpoint \
  --vpc-id "$VPC_GROSCHAT" \
  --service-name com.amazonaws.eu-west-3.ecr.dkr \
  --vpc-endpoint-type Interface \
  --subnet-ids "$SUBNET_GROSCHAT_A" "$SUBNET_GROSCHAT_B" \
  --security-group-ids "$SG_VPCE_GROSCHAT" \
  --private-dns-enabled \
  --tag-specifications 'ResourceType=vpc-endpoint,Tags=[{Key=Name,Value=poc-s3-gateway-ecr-dkr-endpoint}]' \
  --query 'VpcEndpoint.VpcEndpointId' \
  --output text)

# CloudWatch Logs : logs du conteneur rclone.
export VPCE_LOGS=$(aws --profile groschat ec2 create-vpc-endpoint \
  --vpc-id "$VPC_GROSCHAT" \
  --service-name com.amazonaws.eu-west-3.logs \
  --vpc-endpoint-type Interface \
  --subnet-ids "$SUBNET_GROSCHAT_A" "$SUBNET_GROSCHAT_B" \
  --security-group-ids "$SG_VPCE_GROSCHAT" \
  --private-dns-enabled \
  --tag-specifications 'ResourceType=vpc-endpoint,Tags=[{Key=Name,Value=poc-s3-gateway-logs-endpoint}]' \
  --query 'VpcEndpoint.VpcEndpointId' \
  --output text)

# Attendre que les endpoints interface soient disponibles.
aws --profile groschat ec2 wait vpc-endpoint-available \
  --vpc-endpoint-ids "$VPCE_ECR_API" "$VPCE_ECR_DKR" "$VPCE_LOGS"

cat >> ~/poc-s3-gateway/poc-vars.env <<EOF
export SG_VPCE_GROSCHAT=$SG_VPCE_GROSCHAT
export VPCE_S3=$VPCE_S3
export VPCE_ECR_API=$VPCE_ECR_API
export VPCE_ECR_DKR=$VPCE_ECR_DKR
export VPCE_LOGS=$VPCE_LOGS
EOF
Bloc 6 : ECS Fargate côté gros chat
source ~/aws-poc-s3-gateway.env
source ~/poc-s3-gateway/poc-vars.env

aws --profile groschat ecs create-cluster \
  --cluster-name rclone-s3-gateway-cluster

aws --profile groschat logs create-log-group \
  --log-group-name /ecs/rclone-s3-gateway

cat > /tmp/rclone-s3-gateway-taskdef.json <<EOF
{
  "family": "rclone-s3-gateway",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "$EXEC_ROLE_ARN",
  "taskRoleArn": "$TASK_ROLE_ARN",
  "containerDefinitions": [
    {
      "name": "rclone-s3-gateway",
      "image": "$ECR_URI:latest",
      "essential": true,
      "portMappings": [
        { "containerPort": 8080, "protocol": "tcp" }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/rclone-s3-gateway",
          "awslogs-region": "eu-west-3",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ]
}
EOF

TASK_DEF_ARN=$(aws --profile groschat ecs register-task-definition \
  --cli-input-json file:///tmp/rclone-s3-gateway-taskdef.json \
  --query 'taskDefinition.taskDefinitionArn' \
  --output text)

echo "TASK_DEF_ARN=$TASK_DEF_ARN" | tee -a ~/poc-s3-gateway/poc-vars.env

# Checkpoint : l'ARN de task definition doit être retourné.
aws --profile groschat ecs describe-task-definition \
  --task-definition rclone-s3-gateway \
  --query 'taskDefinition.taskDefinitionArn' \
  --output text
Bloc 7 : NLB, Target Group et service ECS
source ~/aws-poc-s3-gateway.env
source ~/poc-s3-gateway/poc-vars.env

export TG_ARN=$(aws --profile groschat elbv2 create-target-group \
  --name rclone-s3-gateway-tg \
  --protocol TCP \
  --port 8080 \
  --target-type ip \
  --vpc-id "$VPC_GROSCHAT" \
  --health-check-protocol TCP \
  --health-check-port 8080 \
  --query 'TargetGroups[0].TargetGroupArn' \
  --output text)

export NLB_ARN=$(aws --profile groschat elbv2 create-load-balancer \
  --name rclone-s3-gateway-nlb \
  --type network \
  --scheme internal \
  --subnets "$SUBNET_GROSCHAT_A" "$SUBNET_GROSCHAT_B" \
  --query 'LoadBalancers[0].LoadBalancerArn' \
  --output text)

# Attendre que le NLB soit disponible avant de créer le listener et le service ECS.
aws --profile groschat elbv2 wait load-balancer-available \
  --load-balancer-arns "$NLB_ARN"

export LISTENER_ARN=$(aws --profile groschat elbv2 create-listener \
  --load-balancer-arn "$NLB_ARN" \
  --protocol TCP \
  --port 8080 \
  --default-actions Type=forward,TargetGroupArn="$TG_ARN" \
  --query 'Listeners[0].ListenerArn' \
  --output text)

# Créer directement le service ECS avec son Target Group.
aws --profile groschat ecs create-service \
  --cluster rclone-s3-gateway-cluster \
  --service-name rclone-s3-gateway-service \
  --task-definition "$TASK_DEF_ARN" \
  --desired-count 1 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[$SUBNET_GROSCHAT_A,$SUBNET_GROSCHAT_B],securityGroups=[$SG_ECS],assignPublicIp=DISABLED}" \
  --load-balancers targetGroupArn="$TG_ARN",containerName=rclone-s3-gateway,containerPort=8080

aws --profile groschat ecs wait services-stable \
  --cluster rclone-s3-gateway-cluster \
  --services rclone-s3-gateway-service

# Checkpoint : Desired=1, Running=1, Pending=0.
aws --profile groschat ecs describe-services \
  --cluster rclone-s3-gateway-cluster \
  --services rclone-s3-gateway-service \
  --query 'services[0].{Desired:desiredCount,Running:runningCount,Pending:pendingCount,Status:status}' \
  --output table

# Checkpoint : la cible doit être healthy.
aws --profile groschat elbv2 describe-target-health \
  --target-group-arn "$TG_ARN" \
  --query 'TargetHealthDescriptions[*].{Target:Target.Id,Port:Target.Port,State:TargetHealth.State}' \
  --output table

cat >> ~/poc-s3-gateway/poc-vars.env <<EOF
export TG_ARN=$TG_ARN
export NLB_ARN=$NLB_ARN
export LISTENER_ARN=$LISTENER_ARN
EOF
Bloc 8 : Endpoint Service PrivateLink côté gros chat
source ~/aws-poc-s3-gateway.env
source ~/poc-s3-gateway/poc-vars.env

# Exposer le NLB interne comme service PrivateLink.
export VPCE_SERVICE_NAME=$(aws --profile groschat ec2 create-vpc-endpoint-service-configuration \
  --network-load-balancer-arns "$NLB_ARN" \
  --no-acceptance-required \
  --query 'ServiceConfiguration.ServiceName' \
  --output text)

# Le service name ressemble à : com.amazonaws.vpce.eu-west-3.vpce-svc-xxxx
export VPCE_SERVICE_ID=${VPCE_SERVICE_NAME##*.}

# Autoriser le compte baleine à créer un endpoint consommateur.
aws --profile groschat ec2 modify-vpc-endpoint-service-permissions \
  --service-id "$VPCE_SERVICE_ID" \
  --add-allowed-principals arn:aws:iam::${BALEINE_ACCOUNT_ID}:root

# Checkpoint : ServiceState doit être Available.
aws --profile groschat ec2 describe-vpc-endpoint-service-configurations \
  --filters "Name=service-name,Values=$VPCE_SERVICE_NAME" \
  --query 'ServiceConfigurations[0].{ServiceId:ServiceId,ServiceName:ServiceName,State:ServiceState,AcceptanceRequired:AcceptanceRequired}' \
  --output table

cat >> ~/poc-s3-gateway/poc-vars.env <<EOF
export VPCE_SERVICE_NAME=$VPCE_SERVICE_NAME
export VPCE_SERVICE_ID=$VPCE_SERVICE_ID
EOF
Bloc 9 : Interface Endpoint consommateur côté baleine
source ~/aws-poc-s3-gateway.env
source ~/poc-s3-gateway/poc-vars.env

# Utiliser le VPC par défaut de baleine si disponible.
# Pour un environnement sans VPC par défaut, remplacer ce bloc par la sélection d'un VPC existant.
export VPC_BALEINE=$(aws --profile baleine ec2 describe-vpcs   --filters "Name=isDefault,Values=true"   --query 'Vpcs[0].VpcId'   --output text)

if [ "$VPC_BALEINE" = "None" ] || [ -z "$VPC_BALEINE" ]; then
  echo "Aucun VPC par défaut trouvé côté baleine. Créer ou sélectionner un VPC client avant de continuer." >&2
  exit 1
fi

export CIDR_VPC_BALEINE=$(aws --profile baleine ec2 describe-vpcs   --vpc-ids "$VPC_BALEINE"   --query 'Vpcs[0].CidrBlock'   --output text)

# Récupérer les AZ disponibles pour le service PrivateLink vu depuis baleine.
# C'est plus robuste que de supposer eu-west-3a / eu-west-3b en dur.
export SERVICE_AZ_1=$(aws --profile baleine ec2 describe-vpc-endpoint-services   --service-names "$VPCE_SERVICE_NAME"   --query 'ServiceDetails[0].AvailabilityZones[0]'   --output text)

export SERVICE_AZ_2=$(aws --profile baleine ec2 describe-vpc-endpoint-services   --service-names "$VPCE_SERVICE_NAME"   --query 'ServiceDetails[0].AvailabilityZones[1]'   --output text)

export SUBNET_BALEINE_A=$(aws --profile baleine ec2 describe-subnets   --filters "Name=vpc-id,Values=$VPC_BALEINE" "Name=availability-zone,Values=$SERVICE_AZ_1"   --query 'Subnets[0].SubnetId'   --output text)

export SUBNET_BALEINE_B=$(aws --profile baleine ec2 describe-subnets   --filters "Name=vpc-id,Values=$VPC_BALEINE" "Name=availability-zone,Values=$SERVICE_AZ_2"   --query 'Subnets[0].SubnetId'   --output text)

if [ "$SUBNET_BALEINE_A" = "None" ] || [ "$SUBNET_BALEINE_B" = "None" ]; then
  echo "Subnets baleine introuvables dans les AZ du service PrivateLink. Créer deux subnets compatibles avant de continuer." >&2
  exit 1
fi

export SG_VPCE_BALEINE=$(aws --profile baleine ec2 create-security-group   --group-name baleine-s3-gateway-vpce-sg   --description "Security group for PrivateLink endpoint to gros chat S3 gateway"   --vpc-id "$VPC_BALEINE"   --query 'GroupId'   --output text)

# Autoriser les clients du VPC baleine à joindre l'endpoint sur le port 8080.
aws --profile baleine ec2 authorize-security-group-ingress   --group-id "$SG_VPCE_BALEINE"   --protocol tcp   --port 8080   --cidr "$CIDR_VPC_BALEINE"

export VPCE_BALEINE=$(aws --profile baleine ec2 create-vpc-endpoint   --vpc-id "$VPC_BALEINE"   --service-name "$VPCE_SERVICE_NAME"   --vpc-endpoint-type Interface   --subnet-ids "$SUBNET_BALEINE_A" "$SUBNET_BALEINE_B"   --security-group-ids "$SG_VPCE_BALEINE"   --no-private-dns-enabled   --tag-specifications 'ResourceType=vpc-endpoint,Tags=[{Key=Name,Value=baleine-to-groschat-s3-gateway}]'   --query 'VpcEndpoint.VpcEndpointId'   --output text)

aws --profile baleine ec2 wait vpc-endpoint-available   --vpc-endpoint-ids "$VPCE_BALEINE"

export DNS_PRIVATELINK_BALEINE=$(aws --profile baleine ec2 describe-vpc-endpoints   --vpc-endpoint-ids "$VPCE_BALEINE"   --query 'VpcEndpoints[0].DnsEntries[0].DnsName'   --output text)

cat >> ~/poc-s3-gateway/poc-vars.env <<EOF
export VPC_BALEINE=$VPC_BALEINE
export CIDR_VPC_BALEINE=$CIDR_VPC_BALEINE
export SERVICE_AZ_1=$SERVICE_AZ_1
export SERVICE_AZ_2=$SERVICE_AZ_2
export SUBNET_BALEINE_A=$SUBNET_BALEINE_A
export SUBNET_BALEINE_B=$SUBNET_BALEINE_B
export SG_VPCE_BALEINE=$SG_VPCE_BALEINE
export VPCE_BALEINE=$VPCE_BALEINE
export DNS_PRIVATELINK_BALEINE=$DNS_PRIVATELINK_BALEINE
EOF

echo "$DNS_PRIVATELINK_BALEINE"
Bloc 10 : instance de test SSM côté baleine
source ~/aws-poc-s3-gateway.env
source ~/poc-s3-gateway/poc-vars.env

# Rôle EC2 permettant à l'instance d'être pilotée par Systems Manager.
cat > /tmp/trust-ec2-ssm.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "ec2.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

aws --profile baleine iam create-role \
  --role-name poc-s3-gateway-ec2-ssm-role \
  --assume-role-policy-document file:///tmp/trust-ec2-ssm.json

aws --profile baleine iam attach-role-policy \
  --role-name poc-s3-gateway-ec2-ssm-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

aws --profile baleine iam create-instance-profile \
  --instance-profile-name poc-s3-gateway-ec2-ssm-profile

aws --profile baleine iam add-role-to-instance-profile \
  --instance-profile-name poc-s3-gateway-ec2-ssm-profile \
  --role-name poc-s3-gateway-ec2-ssm-role

# Petite attente de propagation IAM.
sleep 20

export AL2023_AMI=$(aws --profile baleine ssm get-parameter \
  --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
  --query 'Parameter.Value' \
  --output text)

export SG_TEST_EC2_BALEINE=$(aws --profile baleine ec2 create-security-group \
  --group-name baleine-s3-gateway-test-ec2-sg \
  --description "Security group for temporary S3 gateway test EC2" \
  --vpc-id "$VPC_BALEINE" \
  --query 'GroupId' \
  --output text)

# Pas de SSH ouvert : l'accès se fait via Session Manager.
export INSTANCE_TEST_BALEINE=$(aws --profile baleine ec2 run-instances \
  --image-id "$AL2023_AMI" \
  --instance-type t3.micro \
  --subnet-id "$SUBNET_BALEINE_A" \
  --security-group-ids "$SG_TEST_EC2_BALEINE" \
  --iam-instance-profile Name=poc-s3-gateway-ec2-ssm-profile \
  --associate-public-ip-address \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=baleine-s3-gateway-test-ec2}]' \
  --query 'Instances[0].InstanceId' \
  --output text)

aws --profile baleine ec2 wait instance-running \
  --instance-ids "$INSTANCE_TEST_BALEINE"

# Attendre que SSM voie l'instance Online.
sleep 60

aws --profile baleine ssm describe-instance-information \
  --filters "Key=InstanceIds,Values=$INSTANCE_TEST_BALEINE" \
  --query 'InstanceInformationList[*].{InstanceId:InstanceId,PingStatus:PingStatus,PlatformName:PlatformName}' \
  --output table

cat >> ~/poc-s3-gateway/poc-vars.env <<EOF
export AL2023_AMI=$AL2023_AMI
export SG_TEST_EC2_BALEINE=$SG_TEST_EC2_BALEINE
export INSTANCE_TEST_BALEINE=$INSTANCE_TEST_BALEINE
EOF
Bloc 11 : tests DNS, TCP, HTTP et S3 depuis baleine
source ~/aws-poc-s3-gateway.env
source ~/poc-s3-gateway/poc-vars.env

# Helper : envoyer une commande shell dans l'EC2 baleine via SSM.
run_baleine() {
  local comment="$1"
  local command="$2"
  local cid
  cid=$(aws --profile baleine ssm send-command \
    --instance-ids "$INSTANCE_TEST_BALEINE" \
    --document-name "AWS-RunShellScript" \
    --comment "$comment" \
    --parameters "commands=[\"$command\"]" \
    --query 'Command.CommandId' \
    --output text)

  aws --profile baleine ssm wait command-executed \
    --command-id "$cid" \
    --instance-id "$INSTANCE_TEST_BALEINE"

  aws --profile baleine ssm get-command-invocation \
    --command-id "$cid" \
    --instance-id "$INSTANCE_TEST_BALEINE" \
    --query '{Status:Status,Output:StandardOutputContent,Error:StandardErrorContent}' \
    --output json
}

# Test DNS : doit retourner une IP privée du VPC baleine.
run_baleine "Test PrivateLink DNS" \
  "getent hosts $DNS_PRIVATELINK_BALEINE"

# Test TCP : doit afficher OK.
run_baleine "Test PrivateLink TCP 8080" \
  "timeout 5 bash -c 'cat < /dev/null > /dev/tcp/$DNS_PRIVATELINK_BALEINE/8080' && echo OK || echo KO"

# Test HTTP brut : une erreur XML S3 est acceptable ; elle prouve que rclone répond.
run_baleine "Test HTTP S3-compatible" \
  "curl -i --max-time 10 http://$DNS_PRIVATELINK_BALEINE:8080/ | head -40"

# Test S3 final : listing du bucket logique.
run_baleine "Test aws s3 ls" \
  "AWS_ACCESS_KEY_ID=$LOCAL_ACCESS_KEY AWS_SECRET_ACCESS_KEY=$LOCAL_SECRET_KEY AWS_EC2_METADATA_DISABLED=true aws s3 ls s3://mab-documents --endpoint-url http://$DNS_PRIVATELINK_BALEINE:8080 --region eu-west-3"

# Test GetObject.
run_baleine "Test aws s3 cp fifi" \
  "AWS_ACCESS_KEY_ID=$LOCAL_ACCESS_KEY AWS_SECRET_ACCESS_KEY=$LOCAL_SECRET_KEY AWS_EC2_METADATA_DISABLED=true aws s3 cp s3://mab-documents/fifi.txt - --endpoint-url http://$DNS_PRIVATELINK_BALEINE:8080 --region eu-west-3"

# Copie sur le disque local de l'instance baleine.
run_baleine "Copy files to local disk" \
  "mkdir -p ~/mab-documents && AWS_ACCESS_KEY_ID=$LOCAL_ACCESS_KEY AWS_SECRET_ACCESS_KEY=$LOCAL_SECRET_KEY AWS_EC2_METADATA_DISABLED=true aws s3 cp s3://mab-documents ~/mab-documents --recursive --endpoint-url http://$DNS_PRIVATELINK_BALEINE:8080 --region eu-west-3 && ls -l ~/mab-documents"
Checkpoints attendus
# 1. DNS PrivateLink
# getent hosts <DNS_PRIVATELINK_BALEINE>
# -> retourne une IP privée du VPC baleine, par exemple 172.31.x.x

# 2. TCP
# -> OK

# 3. HTTP brut
# -> HTTP 400 / XML S3, acceptable avec curl non signé

# 4. Listing S3
# -> fifi.txt, loulou.txt, riri.txt

# 5. Lecture d'objet
# -> Fifi mange une figue

# 6. Copie locale
# -> les fichiers existent dans ~/mab-documents côté instance baleine
En cas de ressource déjà existante
# Si une commande create-role / create-repository / create-security-group échoue avec AlreadyExists,
# deux options propres :
# 1. réutiliser la ressource si elle appartient bien au POC ;
# 2. la supprimer puis relancer le bloc.

# Exemple : vérifier un rôle existant
aws --profile groschat iam get-role --role-name rclone-s3-gateway-task-role

# Exemple : vérifier un repository ECR existant
aws --profile groschat ecr describe-repositories --repository-names rclone-s3-gateway

# Exemple : retrouver une ressource par tag ou par nom
aws --profile groschat ec2 describe-vpcs \
  --filters "Name=tag:Name,Values=poc-s3-gateway-vpc" \
  --query 'Vpcs[*].VpcId' \
  --output text
Complément / fiabilisation

Corrections apportées au runbook de construction et de test

Ces corrections ne changent pas l'architecture du POC. Elles fiabilisent le déroulé CLI pour éviter les faux négatifs, les réexécutions fragiles et les incohérences avec le discours sur les secrets.

CORRIGÉ

SSM Run Command

Le runbook n'utilise plus un simple sleep 3 après send-command. Il attend explicitement la fin avec aws ssm wait command-executed avant de lire le résultat.

CORRIGÉ

Révision ECS

Le runbook ne dépend plus de rclone-s3-gateway:1. Il capture la task definition retournée par AWS dans TASK_DEF_ARN.

LIMITE POC

Credentials applicatifs dans SSM

Les tests injectent encore les credentials applicatifs dans les commandes SSM pour rester simples. C'est documenté comme limite POC, à remplacer par Secrets Manager, SecureString ou un autre mécanisme d'injection sécurisé en version durable.

22 / 25

Checkpoints de reproductibilité du runbook

Cette section remplace l'ancien “audit de conversation”. Pour un lecteur externe, l'objectif n'est pas de connaître nos 94 micro-étapes de terminal, mais de savoir si chaque grande phase du runbook laisse une trace vérifiable avant de passer à la suivante.

Règle de lecture. Après chaque bloc CLI autonome, le lecteur doit obtenir soit un identifiant AWS stocké dans une variable, soit un état attendu clairement vérifiable. Si un checkpoint échoue, il ne faut pas continuer : il faut corriger la brique concernée.
PhaseCommande ou contrôleRésultat attenduPourquoi c'est important
Profils isolésaws --profile groschat sts get-caller-identity
aws --profile baleine sts get-caller-identity
Deux comptes AWS différentsLe POC repose sur une vraie séparation inter-comptes.
Bucket sourceaws --profile groschat s3 ls s3://mab-documentsfifi.txt, loulou.txt, riri.txtLe bucket réel doit exister avant de construire la passerelle.
Image rclonedocker push .../rclone-s3-gateway:latest
ecr describe-images
Un digest sha256:...Fargate ne peut démarrer que si l'image existe dans ECR.
VPC endpoints AWS gros chatdescribe-vpc-endpointss3, ecr.api, ecr.dkr, logs en availableLa task Fargate en subnet privé doit pouvoir tirer l'image et écrire ses logs sans NAT.
ECS/Fargatedescribe-servicesDesired=1, Running=1, Pending=0La passerelle doit réellement tourner.
Logs rclonelogs get-log-eventsrclone serve s3 gateway:Confirme que le conteneur sert le remote combine, pas un mauvais mapping.
NLB / Target Groupdescribe-target-healthTarget healthy sur le port 8080PrivateLink ne servira à rien si le NLB ne voit pas la task.
Endpoint Service gros chatdescribe-vpc-endpoint-service-configurationsServiceState=AvailableLe service doit être consommable par baleine.
Interface Endpoint baleinedescribe-vpc-endpointsState=available + DNS PrivateLinkLe point d'entrée privé doit être créé dans le VPC baleine.
Client de test baleinessm describe-instance-informationPingStatus=OnlineLes tests doivent être lancés depuis le VPC baleine, pas depuis le poste local.
Validation S3aws s3 ls puis aws s3 cp avec --endpoint-urlListing des 3 fichiers puis contenu de fifi.txtC'est la preuve fonctionnelle du POC.

Les pièges explicitement couverts par la page

🧩

Mapping rclone

Le remote combine est indispensable pour exposer mab-documents comme bucket logique côté client.

🔐

Credentials locales

La paire transmise à baleine n'est pas une paire AWS IAM. Elle sert uniquement à entrer dans la passerelle.

🌐

DNS privé

Le DNS PrivateLink ne se teste pas depuis le poste local, mais depuis une ressource dans le VPC baleine.

23 / 25

Révocation et coupure rapide de l'accès

gros chat garde le contrôle opérationnel de la passerelle. Il peut couper globalement le service, couper spécifiquement baleine, ou révoquer les credentials applicatifs transmis au client.

Les commandes ci-dessous supposent que les variables du runbook autonome sont chargées, notamment VPCE_SERVICE_ID, BALEINE_ACCOUNT_ID et, si besoin, VPCE_BALEINE. Elles ne sont à lancer que si l'infrastructure existe encore.

Plus rapide

Mettre le service ECS à desired-count = 0. C'est la coupure globale la plus simple : plus aucune task Fargate ne sert la passerelle.

Plus ciblé

Retirer baleine des principals autorisés PrivateLink et/ou rejeter sa connexion existante, puis faire tourner les credentials applicatifs si nécessaire.

Niveau 1 — coupure globale immédiate côté gros chat

Arrêter la passerelle Fargate
source ~/aws-poc-s3-gateway.env
source ~/poc-s3-gateway/poc-vars.env
export AWS_PAGER=""

# Couper immédiatement la passerelle : plus aucune task Fargate ne tourne.
aws --profile groschat ecs update-service \
  --cluster rclone-s3-gateway-cluster \
  --service rclone-s3-gateway-service \
  --desired-count 0

aws --profile groschat ecs wait services-stable \
  --cluster rclone-s3-gateway-cluster \
  --services rclone-s3-gateway-service

aws --profile groschat ecs describe-services \
  --cluster rclone-s3-gateway-cluster \
  --services rclone-s3-gateway-service \
  --query 'services[0].{Desired:desiredCount,Running:runningCount,Pending:pendingCount}' \
  --output table

Niveau 2 — réouverture contrôlée

Relancer la passerelle si l'accès doit être rétabli
aws --profile groschat ecs update-service \
  --cluster rclone-s3-gateway-cluster \
  --service rclone-s3-gateway-service \
  --desired-count 1

aws --profile groschat ecs wait services-stable \
  --cluster rclone-s3-gateway-cluster \
  --services rclone-s3-gateway-service

Niveau 3 — empêcher baleine de créer ou recréer un endpoint

Retirer le principal autorisé côté Endpoint Service
# Empêche le compte baleine de créer de nouveaux endpoints vers le service.
aws --profile groschat ec2 modify-vpc-endpoint-service-permissions \
  --service-id "$VPCE_SERVICE_ID" \
  --remove-allowed-principals "arn:aws:iam::$BALEINE_ACCOUNT_ID:root"

aws --profile groschat ec2 describe-vpc-endpoint-service-permissions \
  --service-id "$VPCE_SERVICE_ID" \
  --query 'AllowedPrincipals[*].Principal' \
  --output text

Niveau 4 — rejeter une connexion PrivateLink existante

Rejeter l'endpoint consommateur si nécessaire
# À utiliser si l'endpoint consommateur baleine existe encore.
aws --profile groschat ec2 reject-vpc-endpoint-connections \
  --service-id "$VPCE_SERVICE_ID" \
  --vpc-endpoint-ids "$VPCE_BALEINE"

aws --profile groschat ec2 describe-vpc-endpoint-connections \
  --filters "Name=service-id,Values=$VPCE_SERVICE_ID" \
  --query 'VpcEndpointConnections[*].{Endpoint:VpcEndpointId,State:VpcEndpointState,Owner:VpcEndpointOwner}' \
  --output table

Niveau 5 — rotation des credentials applicatifs

Pour invalider l'ancienne paire <LOCAL_ACCESS_KEY> / <LOCAL_SECRET_KEY>, il faut modifier le secret ou la commande de démarrage rclone, reconstruire l'image si nécessaire, puis forcer un redéploiement ECS. En production, ces valeurs doivent venir d'un coffre-fort de secrets, pas du Dockerfile.

À retenir. Retirer le principal PrivateLink empêche surtout les nouveaux endpoints. Pour une coupure immédiate, le plus fiable reste desired-count = 0 ou la rotation de la paire applicative si l'on veut garder le service ouvert à d'autres consommateurs.
24 / 25

Durcissement à prévoir

SPOF Fargate et composant rclone : statut exact

Le POC a volontairement utilisé un déploiement minimal pour valider le flux. Il ne faut donc pas présenter cette version comme hautement disponible. Une seule task Fargate, un service minimal ou un paramétrage de test suffisent à démontrer la faisabilité, mais ne suffisent pas à qualifier une architecture de production.

Pour une version production, il faudrait au minimum : service ECS multi-AZ, plusieurs tasks, health checks TCP robustes, autoscaling, stratégie de déploiement, logs applicatifs, supervision, alerting, rotation des credentials applicatifs, TLS, tests de charge et décision claire sur le niveau de support acceptable de rclone comme composant de passerelle.

Important — ne pas confondre POC et production. Les points ci-dessous sont des améliorations à prévoir. Ils n'ont pas tous été réalisés pendant le POC.
PRODUCTION

HTTPS et endpoint privé propre

Le POC a utilisé HTTP sur :8080. Pour une version durable, prévoir TLS, un nom DNS privé stable et une gestion propre du certificat. Même sur PrivateLink, il vaut mieux éviter de faire circuler des credentials applicatifs en HTTP.

PRODUCTION

Secrets hors task definition

Ne pas laisser local-access-key,local-secret-key en clair dans le Dockerfile, l'image ou la task definition. Utiliser Secrets Manager ou SSM Parameter Store SecureString, avec KMS et une policy minimale.

Si Secrets Manager est utilisé dans des subnets privés, prévoir aussi l'endpoint privé com.amazonaws.eu-west-3.secretsmanager, et éventuellement KMS selon le mode de chiffrement retenu.

À PRÉVOIR

Path-style S3 explicite

Avec un endpoint S3 custom, forcer ou documenter le mode path-style évite que certains clients tentent un appel de type mab-documents.<endpoint>.

Configuration recommandée côté client
aws configure set profile.baleine.s3.addressing_style path
À PRÉVOIR

Health check NLB

Pour le POC, privilégier un health check TCP sur 8080. Un health check HTTP n'est pertinent qu'après avoir vérifié précisément le code retourné par rclone serve s3 sur le chemin choisi.

À PRÉVOIR

AZ et PrivateLink

Ne pas supposer aveuglément que eu-west-3a correspond physiquement à la même zone dans deux comptes. Le runbook doit s'appuyer sur les AZ exposées par le service PrivateLink, et en production raisonner en AZ IDs quand c'est possible.

À PRÉVOIR

Tests complémentaires

Valider les gros fichiers, les range requests, le streaming, les timeouts, les reprises, le comportement SDK applicatif et le niveau de logs CloudWatch. Le mode -vv est utile en POC, mais trop verbeux en production.

Le POC démontre le pattern. Pour le transformer en socle réutilisable, plusieurs points doivent être durcis.

🔐

Secrets

Remplacer les credentials locales codées dans l'image par AWS Secrets Manager ou un mécanisme équivalent.

🧯

IAM minimal

Supprimer les users admin temporaires et conserver uniquement les rôles nécessaires, avec droits strictement limités.

🔭

Observabilité

Exploiter CloudWatch Logs, métriques ECS, health checks NLB et alertes en cas d'échec de task.

🔏

TLS

Ajouter une terminaison TLS adaptée : NLB TLS avec ACM ou terminaison applicative selon les contraintes client.

🚦

Filtrage

Restreindre les Security Groups à la source exacte attendue et limiter les opérations aux besoins réels.

🔄

Rotation

Prévoir la rotation des credentials locales et un mécanisme de révocation côté passerelle.

🏗️

Terraform

Rejouer l'ensemble du POC en Infrastructure as Code quand le temps le permettra, pour éviter les écarts de configuration CLI.

🔍

Attribution

Avec un Task Role unique et une auth-key partagée, CloudTrail ne distingue pas quel appelant de baleine a lu quel objet : tous les accès se confondent en une seule identité. Prévoir une clé applicative par consommateur, voire un log applicatif côté passerelle, si la traçabilité par appelant est exigée.

🌐

Haute disponibilité

Le service tourne avec desired-count = 1 sur une seule task, donc une seule AZ effective : toute panne coupe la passerelle le temps qu'ECS relance. Passer à au moins deux tasks réparties sur plusieurs zones de disponibilité pour un usage continu.

🩺

Santé réelle

Le health check TCP ne valide que l'ouverture du port 8080, pas la santé applicative de rclone : un process figé qui garde le socket ouvert passe le check tout en servant des erreurs. Ajouter une sonde plus représentative du service S3 rendu.

Limite assumée du POC. Le service est validé en lecture avec ListObjectsV2 et GetObject. Les scénarios d'écriture, multipart upload, gros fichiers et range requests doivent être testés séparément avant production.
Composant frontière expérimental. rclone serve s3 est documenté comme expérimental en amont. Or c'est précisément lui qui porte la traduction d'identité (clé applicative → Task Role) et donc la frontière de sécurité du pattern. Avant industrialisation, valider sa stabilité sous charge réelle, ou évaluer une brique alternative (proxy S3-compatible dédié, MinIO) selon les exigences de l'équipe sécurité.
25 / 25

Nettoyage complet après démonstration

Le nettoyage doit supprimer les ressources coûteuses du POC sans supprimer par erreur les ressources métier conservées : le bucket mab-documents côté gros chat et le site statique www.mabillot.com côté baleine s'ils existaient déjà.

Section corrigée. Le nettoyage utilise bien les variables créées par le runbook : TG_ARN pour le Target Group et VPCE_BALEINE pour l'Interface Endpoint côté baleine. Les anciens noms TARGET_GROUP_ARN et BALEINE_VPCE_ID ne doivent pas être utilisés.
Point de vigilance. Les commandes de suppression AWS sont souvent asynchrones. En particulier, les Interface Endpoints peuvent laisser des ENI quelques minutes après leur suppression. Cette section ajoute donc des boucles d'attente réelles avant de supprimer les Security Groups et le VPC.
Préparer le terminal de nettoyage
source ~/aws-poc-s3-gateway.env
source ~/poc-s3-gateway/poc-vars.env
export AWS_PAGER=""

# Pour éviter les surprises, on vérifie les variables critiques.
echo "TG_ARN=$TG_ARN"
echo "TASK_DEF_ARN=$TASK_DEF_ARN"
echo "VPCE_BALEINE=$VPCE_BALEINE"
echo "VPC_GROSCHAT=$VPC_GROSCHAT"
echo "SG_VPCE_BALEINE=$SG_VPCE_BALEINE"

1. Couper puis supprimer ECS/Fargate côté gros chat

Cette étape arrête la facturation Fargate en ramenant le service à zéro task, puis supprime le service et le cluster.

Arrêter la passerelle et supprimer le service ECS
aws --profile groschat ecs update-service \
  --cluster rclone-s3-gateway-cluster \
  --service rclone-s3-gateway-service \
  --desired-count 0 || echo "Service ECS déjà absent ou déjà arrêté"

aws --profile groschat ecs wait services-stable \
  --cluster rclone-s3-gateway-cluster \
  --services rclone-s3-gateway-service || echo "Service ECS déjà absent"

aws --profile groschat ecs delete-service \
  --cluster rclone-s3-gateway-cluster \
  --service rclone-s3-gateway-service \
  --force || echo "Service ECS déjà supprimé"

aws --profile groschat ecs wait services-inactive \
  --cluster rclone-s3-gateway-cluster \
  --services rclone-s3-gateway-service || echo "Service ECS déjà inactif"

aws --profile groschat ecs delete-cluster \
  --cluster rclone-s3-gateway-cluster || echo "Cluster ECS déjà supprimé"

2. Supprimer PrivateLink, NLB et Target Group côté gros chat

Le Target Group doit être supprimé avec TG_ARN, la variable réellement exportée par le runbook de création.

Supprimer l'exposition réseau de la passerelle
# Supprime l'Endpoint Service PrivateLink exposé à baleine.
aws --profile groschat ec2 delete-vpc-endpoint-service-configurations \
  --service-ids "$VPCE_SERVICE_ID" || echo "Endpoint Service déjà supprimé"

# Supprime le listener TCP 8080.
aws --profile groschat elbv2 delete-listener \
  --listener-arn "$LISTENER_ARN" || echo "Listener déjà supprimé"

# Supprime le NLB interne.
aws --profile groschat elbv2 delete-load-balancer \
  --load-balancer-arn "$NLB_ARN" || echo "NLB déjà supprimé"

aws --profile groschat elbv2 wait load-balancers-deleted \
  --load-balancer-arns "$NLB_ARN" || echo "NLB déjà absent"

# Supprime le Target Group.
# Attention : la bonne variable est TG_ARN, pas TARGET_GROUP_ARN.
aws --profile groschat elbv2 delete-target-group \
  --target-group-arn "$TG_ARN" || echo "Target Group déjà supprimé"

3. Supprimer les endpoints AWS internes côté gros chat

Ces endpoints permettaient à Fargate de fonctionner sans NAT : S3 Gateway Endpoint, ECR API, ECR DKR et CloudWatch Logs.

S3 Gateway Endpoint, ECR API, ECR DKR, CloudWatch Logs
aws --profile groschat ec2 delete-vpc-endpoints \
  --vpc-endpoint-ids \
    "$VPCE_S3" \
    "$VPCE_ECR_API" \
    "$VPCE_ECR_DKR" \
    "$VPCE_LOGS" || echo "Endpoints gros chat déjà supprimés"
Attendre le drainage des ENI côté gros chat
# Les ENI des Interface Endpoints peuvent rester quelques minutes.
# On attend qu'il ne reste plus aucune ENI dans le VPC du POC avant de supprimer SG/VPC.
while aws --profile groschat ec2 describe-network-interfaces \
  --filters "Name=vpc-id,Values=$VPC_GROSCHAT" \
  --query 'NetworkInterfaces[*].NetworkInterfaceId' \
  --output text | grep -q .; do
  echo "ENI encore présentes côté gros chat, attente 15 secondes..."
  aws --profile groschat ec2 describe-network-interfaces \
    --filters "Name=vpc-id,Values=$VPC_GROSCHAT" \
    --query 'NetworkInterfaces[*].{Eni:NetworkInterfaceId,Status:Status,Description:Description}' \
    --output table
  sleep 15
done

4. Supprimer l'instance de test et l'Interface Endpoint côté baleine

C'est le point de nettoyage le plus important côté coût : l'Interface Endpoint PrivateLink côté baleine est facturé tant qu'il existe. Il faut utiliser VPCE_BALEINE.

Nettoyage du client de test baleine
# Termine l'EC2 temporaire utilisée pour tester depuis le VPC baleine.
aws --profile baleine ec2 terminate-instances \
  --instance-ids "$INSTANCE_TEST_BALEINE" || echo "Instance de test déjà absente"

aws --profile baleine ec2 wait instance-terminated \
  --instance-ids "$INSTANCE_TEST_BALEINE" || echo "Instance de test déjà terminée"

# Supprime l'Interface Endpoint consommateur côté baleine.
# Attention : la bonne variable est VPCE_BALEINE, pas BALEINE_VPCE_ID.
aws --profile baleine ec2 delete-vpc-endpoints \
  --vpc-endpoint-ids "$VPCE_BALEINE" || echo "Endpoint baleine déjà supprimé"

# Attend que l'endpoint ne soit plus visible.
while aws --profile baleine ec2 describe-vpc-endpoints \
  --vpc-endpoint-ids "$VPCE_BALEINE" >/dev/null 2>&1; do
  echo "Endpoint baleine encore visible, attente 15 secondes..."
  sleep 15
done

# Attend que les ENI associées au SG de l'endpoint disparaissent.
while aws --profile baleine ec2 describe-network-interfaces \
  --filters "Name=group-id,Values=$SG_VPCE_BALEINE" \
  --query 'NetworkInterfaces[*].NetworkInterfaceId' \
  --output text | grep -q .; do
  echo "ENI encore présentes côté baleine, attente 15 secondes..."
  aws --profile baleine ec2 describe-network-interfaces \
    --filters "Name=group-id,Values=$SG_VPCE_BALEINE" \
    --query 'NetworkInterfaces[*].{Eni:NetworkInterfaceId,Status:Status,Description:Description}' \
    --output table
  sleep 15
done

# Supprime les Security Groups du POC côté baleine.
aws --profile baleine ec2 delete-security-group \
  --group-id "$SG_TEST_EC2_BALEINE" || echo "SG EC2 baleine déjà supprimé"

aws --profile baleine ec2 delete-security-group \
  --group-id "$SG_VPCE_BALEINE" || echo "SG endpoint baleine déjà supprimé"

5. Supprimer Security Groups, route table, subnets et VPC du POC côté gros chat

Cette étape ne doit être lancée qu'après disparition des ENI. Sinon AWS renvoie souvent DependencyViolation.

Nettoyage réseau gros chat
# Supprime d'abord les Security Groups quand les ENI ont disparu.
aws --profile groschat ec2 delete-security-group \
  --group-id "$SG_VPCE_GROSCHAT" || echo "SG VPCE gros chat déjà supprimé"

aws --profile groschat ec2 delete-security-group \
  --group-id "$SG_ECS" || echo "SG ECS déjà supprimé"

# Désassocie les subnets de la route table dédiée.
aws --profile groschat ec2 disassociate-route-table \
  --association-id "$RTB_ASSOC_A" || echo "Association route table A déjà absente"

aws --profile groschat ec2 disassociate-route-table \
  --association-id "$RTB_ASSOC_B" || echo "Association route table B déjà absente"

# Supprime route table, subnets puis VPC du POC.
aws --profile groschat ec2 delete-route-table \
  --route-table-id "$RTB_GROSCHAT" || echo "Route table déjà supprimée"

aws --profile groschat ec2 delete-subnet \
  --subnet-id "$SUBNET_GROSCHAT_A" || echo "Subnet A déjà supprimé"

aws --profile groschat ec2 delete-subnet \
  --subnet-id "$SUBNET_GROSCHAT_B" || echo "Subnet B déjà supprimé"

aws --profile groschat ec2 delete-vpc \
  --vpc-id "$VPC_GROSCHAT" || echo "VPC gros chat déjà supprimé"

6. Supprimer ECR, CloudWatch Logs et task definitions

Ressources hors VPC côté gros chat
# Supprime les logs du conteneur rclone.
aws --profile groschat logs delete-log-group \
  --log-group-name /ecs/rclone-s3-gateway || echo "Log group déjà supprimé"

# Supprime l'image Docker et le repository ECR du POC.
aws --profile groschat ecr delete-repository \
  --repository-name rclone-s3-gateway \
  --force || echo "Repository ECR déjà supprimé"

# Les task definitions ne coûtent pas, mais on peut désenregistrer les révisions actives.
for TD in $(aws --profile groschat ecs list-task-definitions \
  --family-prefix rclone-s3-gateway \
  --status ACTIVE \
  --query 'taskDefinitionArns[]' \
  --output text); do
  aws --profile groschat ecs deregister-task-definition \
    --task-definition "$TD"
done

7. Supprimer les rôles techniques créés pour le POC

Rôles IAM techniques gros chat et baleine
# Côté gros chat : rôle applicatif Fargate.
aws --profile groschat iam delete-role-policy \
  --role-name rclone-s3-gateway-task-role \
  --policy-name ReadOnlyMabDocuments || echo "Policy inline déjà supprimée"

aws --profile groschat iam delete-role \
  --role-name rclone-s3-gateway-task-role || echo "Task Role déjà supprimé"

# Côté gros chat : execution role ECS.
aws --profile groschat iam detach-role-policy \
  --role-name ecsTaskExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy || echo "Policy execution role déjà détachée"

aws --profile groschat iam delete-role \
  --role-name ecsTaskExecutionRole || echo "Execution role déjà supprimé"

# Côté baleine : rôle EC2 SSM utilisé par l'instance temporaire.
aws --profile baleine iam remove-role-from-instance-profile \
  --instance-profile-name poc-s3-gateway-ec2-ssm-profile \
  --role-name poc-s3-gateway-ec2-ssm-role || echo "Rôle déjà retiré de l'instance profile"

aws --profile baleine iam delete-instance-profile \
  --instance-profile-name poc-s3-gateway-ec2-ssm-profile || echo "Instance profile déjà supprimé"

aws --profile baleine iam detach-role-policy \
  --role-name poc-s3-gateway-ec2-ssm-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore || echo "Policy SSM déjà détachée"

aws --profile baleine iam delete-role \
  --role-name poc-s3-gateway-ec2-ssm-role || echo "Rôle SSM baleine déjà supprimé"

8. Supprimer les users admin temporaires du POC

Risque sécurité supérieur au risque coût. Les users poc-s3-gateway-admin et poc-s3-gateway-client-admin ne coûtent rien, mais s'ils conservent une clé longue durée avec AdministratorAccess, ils deviennent le principal résidu dangereux du POC. À lancer depuis un autre accès administrateur, par exemple CloudShell ou un profil racine d'administration, car supprimer l'utilisateur qui porte le profil CLI courant peut couper l'accès en cours.
Suppression des users admin temporaires
# À lancer depuis un autre accès admin que les users à supprimer.

# gros chat
for KEY in $(aws --profile groschat iam list-access-keys \
  --user-name poc-s3-gateway-admin \
  --query 'AccessKeyMetadata[*].AccessKeyId' \
  --output text); do
  aws --profile groschat iam delete-access-key \
    --user-name poc-s3-gateway-admin \
    --access-key-id "$KEY"
done

aws --profile groschat iam detach-user-policy \
  --user-name poc-s3-gateway-admin \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess || echo "Policy admin gros chat déjà détachée"

aws --profile groschat iam delete-user \
  --user-name poc-s3-gateway-admin || echo "User admin gros chat déjà supprimé"

# baleine
for KEY in $(aws --profile baleine iam list-access-keys \
  --user-name poc-s3-gateway-client-admin \
  --query 'AccessKeyMetadata[*].AccessKeyId' \
  --output text); do
  aws --profile baleine iam delete-access-key \
    --user-name poc-s3-gateway-client-admin \
    --access-key-id "$KEY"
done

aws --profile baleine iam detach-user-policy \
  --user-name poc-s3-gateway-client-admin \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess || echo "Policy admin baleine déjà détachée"

aws --profile baleine iam delete-user \
  --user-name poc-s3-gateway-client-admin || echo "User admin baleine déjà supprimé"

9. Vérification finale exhaustive des coûts et résidus

Cette vérification compense le caractère best-effort du teardown. Les sorties doivent être vides ou indiquer explicitement des ressources déjà supprimées.

Contrôle synthétique final
echo "=============================="
echo "GROS CHAT — ressources coûteuses ou structurantes"
echo "=============================="

aws --profile groschat ecs list-clusters \
  --query 'clusterArns[]' \
  --output table

aws --profile groschat elbv2 describe-load-balancers \
  --query 'LoadBalancers[*].{Name:LoadBalancerName,VpcId:VpcId,State:State.Code}' \
  --output table

# Contrôle ajouté : les Target Groups orphelins ne sortent pas dans describe-load-balancers.
aws --profile groschat elbv2 describe-target-groups \
  --query 'TargetGroups[?contains(TargetGroupName,`rclone`)].{Name:TargetGroupName,Arn:TargetGroupArn,VpcId:VpcId}' \
  --output table

aws --profile groschat ec2 describe-vpc-endpoint-service-configurations \
  --query 'ServiceConfigurations[*].{ServiceId:ServiceId,State:ServiceState,ServiceName:ServiceName}' \
  --output table

aws --profile groschat ec2 describe-vpc-endpoints \
  --query 'VpcEndpoints[*].{Id:VpcEndpointId,State:State,Service:ServiceName,VpcId:VpcId}' \
  --output table

aws --profile groschat ec2 describe-network-interfaces \
  --query 'NetworkInterfaces[*].{Eni:NetworkInterfaceId,Status:Status,Description:Description,VpcId:VpcId}' \
  --output table

aws --profile groschat ec2 describe-nat-gateways \
  --query 'NatGateways[*].{Id:NatGatewayId,State:State,VpcId:VpcId}' \
  --output table

aws --profile groschat ec2 describe-instances \
  --query 'Reservations[*].Instances[*].{InstanceId:InstanceId,State:State.Name,Name:Tags[?Key==`Name`].Value|[0]}' \
  --output table

aws --profile groschat ec2 describe-addresses \
  --query 'Addresses[*].{PublicIp:PublicIp,AllocationId:AllocationId,AssociationId:AssociationId}' \
  --output table

aws --profile groschat ecr describe-repositories \
  --query 'repositories[*].repositoryName' \
  --output table || echo "Aucun repository ECR du POC"

aws --profile groschat logs describe-log-groups \
  --log-group-name-prefix /ecs/rclone-s3-gateway \
  --query 'logGroups[*].logGroupName' \
  --output table

echo "=============================="
echo "BALEINE — endpoint, EC2 et réseau de test"
echo "=============================="

# C'est le contrôle le plus important côté coût.
aws --profile baleine ec2 describe-vpc-endpoints \
  --query 'VpcEndpoints[*].{Id:VpcEndpointId,State:State,Service:ServiceName,VpcId:VpcId}' \
  --output table

aws --profile baleine ec2 describe-instances \
  --query 'Reservations[*].Instances[*].{InstanceId:InstanceId,State:State.Name,Name:Tags[?Key==`Name`].Value|[0]}' \
  --output table

aws --profile baleine elbv2 describe-load-balancers \
  --query 'LoadBalancers[*].{Name:LoadBalancerName,VpcId:VpcId,State:State.Code}' \
  --output table

aws --profile baleine ec2 describe-network-interfaces \
  --query 'NetworkInterfaces[*].{Eni:NetworkInterfaceId,Status:Status,Description:Description,VpcId:VpcId}' \
  --output table

aws --profile baleine ec2 describe-nat-gateways \
  --query 'NatGateways[*].{Id:NatGatewayId,State:State,VpcId:VpcId}' \
  --output table

aws --profile baleine ec2 describe-addresses \
  --query 'Addresses[*].{PublicIp:PublicIp,AllocationId:AllocationId,AssociationId:AssociationId}' \
  --output table

echo "=============================="
echo "IAM temporaires — sécurité"
echo "=============================="

aws --profile groschat iam list-access-keys \
  --user-name poc-s3-gateway-admin \
  --query 'AccessKeyMetadata[*].{User:UserName,AccessKeyId:AccessKeyId,Status:Status}' \
  --output table || echo "User admin gros chat absent"

aws --profile baleine iam list-access-keys \
  --user-name poc-s3-gateway-client-admin \
  --query 'AccessKeyMetadata[*].{User:UserName,AccessKeyId:AccessKeyId,Status:Status}' \
  --output table || echo "User admin baleine absent"
Limite POC à garder en tête. Les tests SSM du runbook injectent les credentials applicatifs dans les paramètres de send-command. Pour un POC jetable, c'est acceptable si l'environnement est nettoyé. Pour une version durable, ces credentials doivent être externalisés et l'historique SSM ne doit pas devenir le lieu de stockage involontaire des secrets.
Ce qui peut rester sans être un problème de coût : le bucket mab-documents avec ses trois fichiers, le VPC default vide de baleine, et une task definition ECS INACTIVE. En revanche, un Interface Endpoint côté baleine, un NLB, une EC2, une Elastic IP, une NAT Gateway ou des clés admin temporaires actives ne doivent pas rester après le POC.