Réécrire Capistrano en une demi-journée
Introduction
Capistrano est un outil de déploiement. Depuis maintenant 4 ans nous l’utilisons chez Decitre pour déployer 3 de nos applications / sites. Après ces 4 ans d’utilisation, nous avons rencontré différents problèmes qui nous ont amenés à reconsidérer l’utilisation de cet outil : nous utilisons maintenant Ansible pour effectuer nos déploiements.
Nous utilisons Capistrano sur 3 projets : deux projets Symfony utilisant capifony (qui utilise la version 2 de Capistrano), et un projet Magento utilisant la version 3 de Capistrano.
Limites de Capistrano
Capifony et Capistrano 2 ne sont plus maintenus
Depuis 2015, capifony n’est plus maintenu. Capifony surchargeait une stratégie de déploiement permettant de builder les assets en local : la stratégie “copy”. Celle-ci a pour avantage de préparer les sources localement avant de les envoyer en production : il n’y a donc par exemple pas besoin de node ou ruby sur les serveurs de production, seulement sur le serveur de déploiement. De plus, ces étapes de build n’ont besoin d’être effectuées qu’une seule fois : autant éviter de les lancer sur chaque serveur de production. Avec Capistrano 3 cette stratégie n’est pas intégrée en standard, et aucun plugin (tels que xuwupeng2000/capistrano-scm-gitcopy ou wercker/capistrano-scm-copy) ne nous a paru, au moment de nos tests, satisfaisant.
Les déploiements de Capistrano n’étaient pas atomiques par défaut
Dans ses versions antérieures, Capistrano n’avait pas de déploiements atomiques, c’est un sujet qui a été abordé dans de nombreuses issues.
Nous utilisions la version 3.2.1 de Capistrano, qui n’effectuait pas de déploiement atomique.
Nous avons dû modifier la tâche deploy:symlink:release
afin que celle-ci le soit.
Cela a été corrigé dans la version 3.3.3 de Capistrano via cette pull request.
A noter que sur Ansible, le lien symbolique est posé de façon atomique et que nous n’avons pas eu de hack à effectuer. Cette limite n’en était donc plus une, nous aurions pu mettre à jour Capistrano (mais celle-ci n’était pas une des raisons principales du changement).
Le code de retour du build_script n’est pas vérifié
Il est possibe d’indiquer un build_script
à Capistrano : une ou plusieurs commandes à exécuter lors de la préparation des sources.
Nous utilisions cette fonctionalité pour préparer les assets (récupérer les dépendances via npm, bundler et lancer une tâche grunt).
Le problème de ce build script, c’est que le code de retour n’est pas vérifié.
Ainsi, sur 6 mois, un de nos projets a eu 2 indisponbilités de 3 minutes à cause d’une erreur lors d’une étape de build.
Ceci n’est pas acceptable : nous devons avoir confiance dans notre processus de déploiement.
Lisibilité et maintenance du déploiement
L’ajout d’étapes au déploiement dans Capistrano se fait via un mécanisme de hooks. Nous avons ajouté plusieurs étapes à notre déploiement, notamment liées à la gestion du cache, de l’exécution de patchs ou l’ajout de liens symboliques spécifiques à notre environnement.
A moins de bien connaître le flow de Capistrano, il n’est pas simple de voir d’un coup d’œil le fonctionnement du déploiement.
De plus, même si le DSL de Capistrano a pour effet de ne pas nécessiter de connaître parfaitement ruby, c’est un langage que nous ne maîtrisions pas ; au contraire d’Ansible, qui est utilisé et connu par toute l’équipe, car déjà utilisé pour provisionner nos environnements de production et de préproduction. C’est cette connaissance de l’outil qui a permis à toute l’équipe d’effectuer des adaptations sur le déploiement dans les jours qui ont suivi sa mise en place : la changement d’outil nous a permis de gagner en maintenabilité sur notre outil de déploiement.
Des solutions possibles
deployer
deployer : nous l’avons testé sur un projet très peu critique. Il est possible de modifier l’outil pour faire en sorte d’avoir des déploiements effectués avec une étape de build en local. Le fait de devoir tordre cet outil pour arriver à nos fins, et surtout des questionnements à propos de la pérénité/maintenance de celui-ci (par rapport à Ansible), nous ont poussés à ne pas l’utiliser.
Ansible et Ansistrano
Ansistrano est un rôle pour Ansible pour déployer des applications dans le style de Capistrano. Il se défini comme un portage de Capistrano sous Ansible. C’est une solution que nous avons fortement envisagée, mais que nous n’avons pas choisie en raison de la solution intégrée d’Ansible que nous allons présenter ci-dessous : le deploy_helper. En effet si le helper nous permet d’effectuer notre déploiement sans devoir utiliser un rôle dont nous devrions suivre les éventuels correctifs (la maintenance est reportée sur Ansible et non pas sur Ansistrano).
Ansible et son deploy_helper
Ansible deploy_helper va permettre de faire un deploiement “à la Capistrano” en ayant une vision sur tout le process, et permettant de facilement le modifier. Le remplacement sera “drop in” vu que ce helper est pensé pour les utilisateurs migrants depuis Capistrano, en effet la structure des dossiers est exactement la même.
Solution choisie
Nous avons donc choisi d’utiliser le deploy_helper
d’Ansible pour les raisons suivantes :
- la migration sera simplifiée,
- nous utilisons déjà Ansible pour le provisionning et l’équipe connait bien l’outil,
- cela nous permettra d’avoir une vision globale rapide du fonctionnement du déploiement,
- nous aurons la main sur le process de déploiement et pourrons le modifier facilement.
Exemple
Nous lançons Ansible dans un container docker ayant installé les dépendances nécéssaires au build (node, npm, ruby, bundler, php).
Les étapes de déploiement sont les suivantes :
- récupération des sources
- récupération de Composer
- installation des vendors
- build des assets
- suppression de fichiers/dossiers inutiles/de dev
- création du dossier release
- création des dossiers dans shared
- copie des fichiers buildés localement dans le dossier release
- ajout des liens symboliques depuis shared
- warmup du cache
- gestion des droits sur les fichiers
- modification du lien symbolique sur le dossier current
- suppression du cache opcache
Voici le détail de la configuration Ansible (simplifiée pour les besoins de l’exemple) :
- Récupération des sources via Git : le
delegate_to
et lerun_once
sont ici présentes pour lancer le clone localement, sur la machine effectuant le déploiement. Les premières étapes présentant aussi ces options font partie de l’étape de build local.
- Ici nous récupérons la dernière version de Composer et l’installons localement. Nous pourrions améliorer les choses en vérifiant la signature.
- Nous installons ici les différentes dépendances (dans l’exemple qui suit, seulement les dépendances PHP via composer, mais les autres dépendances sont récupérées en utilisant une tâche similaire).
- Préparation des assets : nous lançons notre tâche grunt préparant nos fichiers JS et CSS
- Suppression de fichiers et/ou dossiers inutiles / seulement utiles lors du développement. Nous avons un fichier avec différentes variables dont la variable
deploy_exclude_paths
utilisée ici (qui est un tableau) contient entre autres des dossiers de test et des contrôleurs de développement.
- Création du dossier où se trouveront les déploiements / dossier release : s’il n’existe pas le dossier de base contenant les dossiers
releases
etshared
sera créé ainsi que les deux dossiers précédement mentionnés. Après cette étape, une variabledeploy_helper
sera disponible contenant notamment le chemin de la nouvelle version (dans la clefnew_release_path
).
- Tout comme pour la suppression des fichiers, nous avons une liste de dossiers / fichiers devant se trouver dans le dossier
shared
dans un fichier contenant différentes variables (icishared_dirs
). Nous parcourons ce tableau et créons les dossiers (par exemple un dossierapp/logs
).
- Nous allons ici copier les fichiers se trouvant sur le serveur de déploiement vers les serveurs de production (via rsync). Les fichiers copiés sont donc les fichiers récupérés via Git, les dépendances, fichiers d’assets buildés.
- Dans cette étape, nous créons les liens symboliques dans le dossier de la nouvelle release, pointant vers le dossier shared. Pour ce faire nous utilisons la même variable que précédemment.
- Nous lançons ici la commande
cache:warmup
.
-
Après cela, nous modifions les droits sur quelques fichiers/dossiers afin qu’ils soient accessibles en écriture par l’utilisateur lançant Apache (par exemple les dossiers d’upload ou de log).
-
Enfin, nous faisons pointer le lien symbolique
current
vers la nouvelle release que nous venons de créer. L’option keep_release permet de limiter le nombre de versions dans le dossierrelease
(ici nous n’en gardons que 5).
- Enfin, après modification du lien symbolique nous supprimons le cache opcache.
Si l’une des étapes échoue le déploiement s’arrête. Chaque étape est effectuée en parallèle sur chaque serveur. L’étape suivante attend que la précédente soit executée sur tous les serveurs avant de se lancer.
Au final, notre fichier YAML de description du déploiement fait donc moins de 150 lignes, et l’ensemble des étapes se trouvent dans fichier, ce qui rend le déploiement lisible et compréhensible rapidement par toute personne ayant déjà lu un playbook Ansible.
Conclusion
La migration vers le deploy_hepler
a été très simple : il a nécéssité moins d’une demi-journée pour l’utiliser en environnement de préproduction sur nos deux projets.
Depuis le passage en production, nous avons eu une erreur lors du build des assets : avant la migration l’application n’aurait plus été accessible. Quand cette erreur a eu lieu, le déploiement s’est bien arrêté : cela nous a évité une indisponbilité sur notre application.
Enfin, depuis la mise en production, des changements sur le déploiement ont été effectués par toute l’équipe, ce qui n’était pas le cas précédemment.
Si vous utilisez déjà Ansible pour le provisionning de vos machines, le deploy_helper
est donc une solution très adaptée pour effectuer vos déploiements applicatifs.
PS : Decitre recherche actuellement 2 développeur(euse)s. Si vous souhaitez venir déployer avec nous, n’hésitez pas à consulter et répondre à l’annonce.