Histoire d'un fail : s3Fs sur ECS

C’est une histoire qui semblait bien partie : déployer sous forme de conteneurs un Content Management System qui utilise s3Fs pour synchroniser des images et des fichiers json.

Mis à part s3Fs qui est un composant open-source, on est dans le monde connu et stable d'Amazon Web Services avec s3 et ECS. Tout devait donc bien passer… eh bien non.

Voyons ensemble pourquoi.

Use case

Fonctionnellement, il s’agit donc de déployer un CMS qui gère le contenu éditorial d’un site e-Commerce. Par contenu éditorial, nous entendons les différentes images (header / footer / bannière) qui sont à afficher par le navigateur de l’utilisateur. Les images à afficher sont fonctions du contexte du navigateur, telles que marques, catégories de produits et date courante. Ces images doivent être servies à tous les utilisateurs grand public du site, elles seront donc servies par un CDN.

Pour que le front sache quelles images il doit afficher, il requête un index dont nous ne parlerons pas ici.

Avant d’aller plus loin, répondons à deux questions qui doivent vous tarauder : pourquoi ECS et pourquoi s3Fs ?

Pourquoi ECS ?

Lorsque l’on parle d’ECS, une des premières remarques qui suit est : pourquoi partir sur ECS qui est propriétaire AWS, et pas Kubernetes ?

La réponse est simple, ECS est prêt à l’emploi :

  • Pas de cluster scheduler à déployer, maintenir et configurer
  • La remontée des indicateurs et logs est native (CloudWatch et CloudWatch Logs)
  • Le load-balancing repose sur les mécanismes éprouvés d’EC2

La gestion des données est peut-être plus limitée comparée à d’autres solutions mais ce n’était pas un problème pour nous puisque tous les micro-services ECS étaient stateless, les données étant dans des bases RDS+Elasticache.

L’argument de la facilité de mise en oeuvre et de maintien a été déterminant pour notre client qui nous laissait deux mois pour monter une infrastructure micro-services from-scratch.

Au final, nous ne regrettons pas notre choix puisque nous avons réussi dans le temps imparti à déployer 30 services ECS en auto-scaling 1 à 20, le tout sur un cluster lui-même en auto-scaling 1 à 20.

Pourquoi s3Fs ?

La bonne solution qui fonctionne et qui scale pour du stockage persistent sur ECS, est de mettre en oeuvre des montages EFS.

Dans notre cas, le sujet n'est pas tant de stocker des fichiers images de manière persistente que de les servir "massivement" à des utilisateurs. Un pré-requis est donc le CDN CloudFront. Se pose alors la question du stockage des images source, et S3 y répond bien.

Le point dur est que l'application d'administration repose sur un système de fichiers local pour le téléchargement des images. La bonne approche aurait été de s'affranchir de stockage local, en faisant un envoi direct vers S3, mais cela implique des changements au niveau de l'application que nous n'avons pas réussi à faire valider faute de temps...

Plutôt que de modifier l’application, il a été préféré de continuer en l’état et travailler au niveau de l'infrastructure.

Nous avons donc subi s3Fs plus que choisi.

Est-ce grave ?

s3Fs reposant sur s3, il y a des limites à prendre en compte :

  • Stockage objet et pas block, toute modification sur un fichier suppose un renvoi complet de tout le fichier
  • API HTTP Rest, donc une latence forcément plus élevée qu’un protocole moins verbeux (NAS).

Cela étant dit, pas de problème bloquant compte tenu des volumétries cibles :

  • 10 utilisateurs max en parallèle
  • Upload de 20 fichiers images par heure

Le plan initial via Docker

Dans une pure approche conteneur (un processus par conteneur), nous déployons donc : un conteneur s3Fs et une Webapp (ici une image Ubuntu).

Comme indiqué sur le readme s3Fs, la réplication des points de montage entre namespaces doit être autorisée (plus de détails ici sur le mode de partage des systèmes de fichiers entre namespaces). D’où la suppression de MountFlags=slave dans le lancement du daemon Docker et l’option ‘shared’ fournie lors du montage du volume s3Fs sur les conteneurs.

Lançons le conteneur s3Fs :

[root@localhost mnt]# mkdir /mnt/tests3fs-bucket
[root@localhost mnt]# docker run --name tests3fs --detach -v /root/.s3fs:/root/.s3fs --privileged  -v /mnt/tests3fs-bucket:/mnt/local:shared xueshanf/s3fs /usr/bin/s3fs -f -o allow_other -o use_cache=/tmp -o passwd_file=/root/.s3fs tests3fs-bucket /mnt/local
e8b0a9ecb48dd142c6ab8e5c740be822ff148e7eabdd5103f54ffc7725cd8d10  

A ce stade, nous pouvons voir que le volume doit propager les montages (tout montage s3Fs (via FUSE) au sein du conteneur sur /mnt/local sera propagé au namespace host sur /mnt/tests3fs-bucket et vice-versa) :

[root@localhost mnt]# docker inspect  tests3fs|jq '.[].Mounts[]|select(.Destination == "/mnt/local")'
{
  "Source": "/mnt/tests3fs-bucket",
  "Destination": "/mnt/local",
  "Mode": "shared",
  "RW": true,
  "Propagation": "shared"
}
[root@localhost mnt]# mount|grep s3
s3fs on /mnt/tests3fs-bucket type fuse.s3fs (rw,nosuid,nodev,relatime,user_id=0,group_id=0,allow_other)  

Vérifions que cela fonctionne sur le host :

[root@localhost mnt]# aws s3 ls s3://tests3fs-bucket
[root@localhost mnt]# ls -lh /mnt/tests3fs-bucket/
total 0  
[root@localhost mnt]# touch /mnt/tests3fs-bucket/test-from-host
[root@localhost mnt]# aws s3 ls s3://tests3fs-bucket
2017-02-09 17:11:45          0 test-from-host  
[root@localhost mnt]# aws s3 rm s3://tests3fs-bucket/test-from-host
delete: s3://tests3fs-bucket/test-from-host  
[root@localhost mnt]# ls -lh /mnt/tests3fs-bucket
total 0  

Vérifions que cela fonctionne depuis le conteneur webapp :

[root@localhost vagrant]# docker run --name webapp -ti --privileged -v /mnt/tests3fs-bucket:/mnt/local ubuntu
root@8b610f2467c7:/# ls -l /mnt/local  
total 0  
root@8b610f2467c7:/# touch /mnt/local/test-from-webapp  
root@8b610f2467c7:/# exit  
exit  
[root@localhost vagrant]# aws s3 ls s3://tests3fs-bucket
2017-02-09 17:22:53          0 test-from-webapp  

Le plan initial sur ECS

Avant de passer sur ECS, une phase d’amélioration est nécessaire :

  • Côté s3Fs : activation de l’authentification par rôle ECS IAM, nettoyage du cache au démarrage et arrêt, activation du contrôle d’intégrité par hashing md5.
  • Côté webapp et s3Fs :

Nous sommes maintenant prêts à définir notre tâche ECS… sauf que la définition de volumes au sein des containers ECS ne supporte que partiellement les options disponibles dans Docker !

Ainsi, là où Docker permet de gérer les options [[rw|ro], [z|Z],[[r]shared|[r]slave|[r]private], and [nocopy]](https://docs.docker.com/engine/reference/run/#/volume-shared-filesystems), ECS ne supporte qu'un sous-ensemble de ces options: read-only|read-write.

Impossible de fournir l’option ‘shared’ aux volumes Docker.

A ce stade, les alternatives ont été de :

  • Lancer un seul conteneur Docker avec plusieurs processus lancés via supervisord par exemple. Cette solution n’a pas été retenue car c’est la porte ouverte à du multi processus dans des conteneurs. Autoriser cela c’est autoriser des conteneurs qui tendront à ressembler à des VMs.
  • OU
  • Insister sur les modification applicatives : faire un upload direct vers s3 et gérer le bucket dans les liens d’images
  • OU
  • Modifications infrastructures : déployer webapp et s3Fs au sein d’une même VM EC2, au lieu de conteneurs.

Je vous laisse deviner ce que le client a retenu …

Conclusion

Dans cet article, nous avons vu :

  • Les précautions à prendre avec s3Fs.
  • EFS est la bonne solution pour stockage persistent avec ECS.
  • La notion de ‘shared subtree’ sur les systèmes de fichiers Linux, qui permet d’avoir des points de montages dans un conteneur Docker, visibles sur l’hôte Docker et donc d’éventuels conteneurs sur le même hôte.
  • Cette gestion récente des 'shared subtree' dans Docker n'est pas encore (une feature request est ouverte dans ce sens) intégrée dans ECS.