Faire du déploiement continu avec Deployer

Clément Michelet 7 minutes de lecture
Bâteaux avec conteneurs en cours de déchargements au port
Crédits: Andy Li @ Unsplash

Le déploiement continu vise à automatiser l’envoi de tout changement dans la base de code vers l’environnement de destination. Cela peut être un environnement bac-à-sable, de validation ou de production. La volonté est de ne plus dépendre du facteur humain lors de la livraison de ces mises à jour et donc de potentielles erreurs. Elle permet également de pouvoir reproduire la livraison des mises à jour à un état donné et, lorsque cela est mis en place, de revenir à des versions antérieures rapidement.

Dans l’écosystème DevOps, on retrouve des outils comme Puppet ou Ansible avec Ansistrano dédié au déploiement continu. Les plateformes comme GitHub, GitLab ou Jenkins permettent également de mettre en œuvre le déploiement continu avec la définition de pipeline.

Cependant, ces outils ont le désavantage d’être complexes à prendre en main. De plus, lorsqu'il est nécessaire de mettre en place des actions nécessaires au déploiement qui ne sont pas supportées de base par l’outil, il peut être complexe d’étendre les fonctionnalités lorsque le langage derrière n’est pas un langage que l’on connaît. Par exemple, pour Ansible, il faut connaître le langage Python pour développer des extensions à l’outil.

Aujourd’hui, je vais vous présenter Deployer, un outil simple et conçu avec PHP qui permet de faire du déploiement continu. Je vous donnerais notamment mon retour d’expérience dans le cadre d’une mission pour un client.

Deploy PHP everywhere

Les bases avec Deployer

La documentation de Deployer l’illustre correctement, il n’y a que peu de concepts à assimiler pour mettre en place l’outil.

Le premier est la notion concernant la destination où déployer le code source. C’est la notion appelée Host et qui va permettre de déclarer nominativement le/les serveurs ainsi que comment se connecter à la machine.

On peut les déclarer en PHP ou avec des fichiers de configuration YAML. Par exemple, pour 1 environnement de développement et 1 de production, voici comment on peut déclarer les serveurs :

<?php 

namespace Deployer;

host('dev.website.local')
  // Equivalent à indiquer de faire ssh clement@remote.website.local -p 1234
  ->set('hostname', 'remote.website.local')
  ->set('port', '1234')
  ->set('remote_user', 'clement')
  // Pour spécifier l'environnement de déploiement
  ->set('labels', ['env' => 'dev', 'app' => 'backend'])
  // Pour indiquer le dossier où est déployé l'application
  ->set('deploy_path', '/var/www/html/dev.website.local')
  // Pour forcer la branche git à déployer spécifiquement
  ->set('branch', 'develop');

host('website.local')
  ->set('hostname', 'remote.website.local')
  ->set('port', '1234')
  ->set('remote_user', 'clement')
  ->set('labels', ['env' => 'prod', 'app' => 'backend'])
  ->set('deploy_path', '/var/www/html/website.local')
  ->set('branch', 'main');
Depuis la version 7.x, la notion de stage qui permettait de définir l’environnement (dev, prod, etc.) a été remplacée par les labels offrant plus de flexibilité d’utilisation.

Le deuxième concept à assimiler concerne les actions à réaliser pour effectuer le déploiement et qui sont appelées Task. Une tâche peut être une ou plusieurs lignes de commandes à exécuter. Elle peut être également un regroupement de plusieurs autres tâches.

Deployer fournit un ensemble de tâches déjà existantes avec les Recipes. On a toutes les tâches nécessaires pour déployer des applications conçues avec les frameworks PHP, les CMS et les solutions e-commerce les plus courantes du marché. On a même des tâches pour exécuter des commandes NPM.

Prenons un exemple simple de tâche nécessaire au déploiement. On a besoin d’effectuer la migration des données avec Doctrine Migrations. Il s’agit d’exécuter un script PHP pour lancer la migration.

<?php

namespace Deployer;

// Cette description apparaîtra pendant l'exécution du déploiement
desc('Run database migrations script');
task(
  // C'est l'identifiant de la tâche qui sera utilisé pour l'exécuter
  'db:migrate',

  // Ici on mets tout le code relatif à l'exécution de la commande
  function () {
    // On se positionne dans le dossier où est déployé le nouveau code
    cd('{ { release_path }}');

    // On exécute la commande Symfony pour lancer les migrations
    run('bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration');
  }
});

Et voilà, on a désormais une tâche nous permettant de déclencher des migrations Doctrine. Dans une tâche, on peut exécuter n’importe quelle commande autorisée par le serveur destinataire, copier ou télécharger des fichiers, demander une information à l’utilisateur et bien d’autres choses grâce à l’API de Deployer.

La recipe Symfony fournit déjà une Task pour permettre l’exécution des migrations Doctrine.

Avec ces 2 concepts, il ne reste plus qu’à définir le plan d’action à dérouler afin de déployer l’application. C’est le rôle du fichier deploy.php / deploy.yaml à créer dans son projet. Celui-ci joue le rôle de Recipe du projet pour permettre de configurer et réaliser le déploiement.

Il pourra prendre la forme suivante dans le cas d’une application Symfony :

<?php

namespace Deployer;

// On importe les recipes fournies par Deployer
require 'recipe/symfony.php';
require 'recipe/npm.php';

// On importe notre Task "db:migrate"
require 'tasks.php'

// On importe le listing de nos Hosts
require 'hosts.php'

// On définit le repository Git à utiliser pou récuperer le code source
set('repository', 'git@git.local:clement/website.git');

// On définit les dossiers à conserver entre chaque déploiement
set('shared_dirs', ['var/log', 'var/export']);

// On définit les dossiers qui doivent autoriser l'écriture
set('writable_dirs', ['var/cache', 'var/log', 'var/export']);

// On indique qu'on ne veut garder que les 3 dernières versions sur le serveur
set('keep_releases', 3);

// On va programmer notre tâche de migrations juste avant la fin du déploiement
before('deploy:symlink', 'db:migrate');

Et voilà, on a notre script pour déployer notre application Symfony sur nos 2 serveurs. Pour déclencher le déploiement, on a juste à exécuter l’une de ces commandes :

# Deploiement sur tous les Hosts
vendor/bin/dep deploy

# Deploiement sur l'environnement de dev par son alias
vendor/bin/dep deploy dev.website.local

# Deploiement sur l'environnement de production par le label env
vendor/bin/dep deploy env=prod

# Deploiement sur tous les environnements par le label app
vendor/bin/dep deploy app=backend

Le déploiement se lancera, exécutera toutes les tâches pour finaliser le déploiement ou exécutera les tâches relatives à la gestion de l’échec du déploiement le cas échéant.

Retour d’expérience avec Deployer 6.x

Dans le cadre d’une mission pour un client, j’ai mis en place Deployer en version 6.x sur un backoffice conçu avec un framework PHP peu commun. Il n’y avait à ce moment-là pas vraiment de déploiement continu en place. L’équipe copiait le contenu du projet par transfert SFTP sur les serveurs ou par commande rsync sur la plateforme de CI/CD.

Une fois les étapes préliminaires de configuration des permissions systèmes de l’utilisateur pour pouvoir effectuer le déploiement, la configuration de Deployer a pu se faire en moins d’une journée et sans difficulté.

Bien que n’ayant pas de Recipe prête à l’emploi pour le framework, les Recipe fournies par Deployer ont largement suffit pour réaliser la majorité des tâches de déploiement.

Dans la version initiale de la Recipe du projet pour le déploiement, je n’ai eu qu’à créer les tâches pour :

  • Générer les assets CSS/JS du projet en me basant sur la recipe communautaire
  • Migrer la base de données avec Doctrine Migrations
  • Générer le cache Doctrine

Par la suite, quand le projet a évolué, j’ai pu ajouter sans obstacles de nouvelles tâches pour permettre notamment :

  • d’installer de nouvelles dépendances systèmes nécessaires au projet
  • intégrer la récupération de données présentes dans un dépôt SVN pour la gestion des assets

Deployer a été un vrai bénéfice dans la fiabilisation du processus de déploiement de l’application. Il a également offert une grande liberté d’exécution du déploiement sans dépendre de la plateforme de CI/CD. En effet, les développeurs étaient en capacité d’exécuter le déploiement depuis leurs machines. C’était par exemple le cas quand le quota de temps de CI/CD avaient été intégralement consommé bloquant de facto les déploiements automatiques.