Déployer un projet PHP depuis un monorepo

Je parlais dans un billet précédent de comment publier des composants PHP sur Packagist depuis un dépôt de code monolithique. Une autre question récurrente venant des équipes projet qui souhaitent mettre en place ce type de structure est: comment déployer un sous-projet du dépôt de manière indépendante ?

En effet, il y a quelques années un formidable outil, Capistrano, a modifié la manière dont nous déployons nos applications. Capistrano est un outil Open Source développé en Ruby qui permet d'automatiser le processus de création et de publication d'une application sur un ou plusieurs serveurs Web (source Wikipédia). L'outil a eu un tel succès que différentes alternatives basées sur le même principe sont apparues. On peut par exemple citer Deployer en PHP ou Ansistrano une solution équivalente fonctionnant avec Ansible.

Nous avons donc avec ces solutions pris l'habitude de déployer nos applications en clonant directement le dépôt de sources (que se soit Git ou Subversion auparavant) afin de déployer le tag ou la branche de production de nos projets. Or lorsque l'on souhaite opter pour une approche monorepo, cela signifie que notre déploiement va cloner tous les projets composant notre dépôt de code. Bien que cela puisse être envisagé dans certains cas, c'est loin d'être une solution optimale (notamment quand vous allez vouloir mettre vos applications sur différents serveurs).

Cloner un dépôt de source n'est qu'une solution parmi tant d'autres pour déployer nos projets. C'est également omettre que les outils qui font ce travail pour nous sont un peu plus intelligents que cela et offrent de nombreuses autres possibilités. En plus de pouvoir cloner un dépôt Git, les solutions d'automatisations sont bien souvent capables de copier des fichiers localement sur un système de fichiers, par FTP ou encore via SSH. C'est donc une stratégie de copie qui peut ne pas remettre en cause votre processus de déploiement ni même la configuration (ou très peu) de ce dernier.

Prenons par exemple une configuration de base générée par Deployer (le principe sera le même pour l'ensemble des outils, mais la syntaxe de configuration variera):

<?php

namespace Deployer;

require 'recipe/common.php';

set('application', 'my_project');
set('repository', 'https://github.com/foo/bar.git');

set('git_tty', true);

set('shared_files', ['shared-files.php']);
set('shared_dirs', ['var/log']);
set('writable_dirs', ['data/uploads']);

host('project.com')
    ->set('deploy_path', '~/');

desc('Deploy your project');
task('deploy', [
    'deploy:info',
    'deploy:prepare',
    'deploy:lock',
    'deploy:release',
    'deploy:update_code',
    'deploy:shared',
    'deploy:writable',
    'deploy:vendors',
    'deploy:clear_paths',
    'deploy:symlink',
    'deploy:unlock',
    'cleanup',
    'success'
]);

after('deploy:failed', 'deploy:unlock');

Comme je l'ai évoqué juste avant, par défaut la configuration se base sur un dépôt Git pour déployer (ici https://github.com/foo/bar.git). Lors du processus de déploiement l'ensemble des tâches deploy seront exécutées. C'est au niveau de la tâche deploy:update_code que tout se joue. C'est cette dernière qui va mettre à jour le code sur notre serveur project.com en clonant le dépôt spécifié dans la configuration repository.

Nous devons donc modifier cette tâche afin de déployer non plus via Git, mais en effectuant une copie de fichier en SSH (via rsync dans notre cas). Pour cela, nous allons installer les recettes additionnelles de Deployer via Composer : composer require --dev deployer/recipes. Nous pourront ensuite ajouter la configuration nécessaire au fonctionnement de cette dernière:

<?php

namespace Deployer;

require 'recipe/common.php';
require 'recipe/rsync.php'; // inclusion de la recette

set('application', 'my_project');
set('repository', 'https://github.com/foo/bar.git');

set('git_tty', true);

set('shared_files', ['shared-files.php']);
set('shared_dirs', ['var/log']);
set('writable_dirs', ['data/uploads']);

// configuration de rsync
set('rsync',[
    'exclude'      => [
        '.git',
        'var',
        'vendor',
        'deploy.php',
    ],
    'exclude-file' => false,
    'include'      => [],
    'include-file' => false,
    'filter'       => [],
    'filter-file'  => false,
    'filter-perdir'=> false,
    'flags'        => 'rzcE',
    'options'      => ['delete'],
    'timeout'      => 60,
]);

// configuration du répertoire devant être copié et sa destination
set('rsync_src', __DIR__);
set('rsync_dest', '');

host('project.com')
    ->set('deploy_path', '~/');

desc('Deploy your project');
task('deploy', [
    'deploy:info',
    'deploy:prepare',
    'deploy:lock',
    'deploy:release',
    'rsync', // pour finir nous remplaçons la tâche de déploiement Git
    'deploy:shared',
    'deploy:writable',
    'deploy:vendors',
    'deploy:clear_paths',
    'deploy:symlink',
    'deploy:unlock',
    'cleanup',
    'success'
]);

after('deploy:failed', 'deploy:unlock');

Et voilà ! Nous avons maintenant notre projet dont le fonctionnement est identique à la première version mis à part que la copie des fichiers ne se fait plus via un clone du dépôt Git, mais via une copie de fichier en SSH.

Il se peut que vous ne souhaitiez pas modifier ce qui est fait par la tâche deploy. C'est le cas, si par exemple vous déployez un projet Symfony en utilisant la recette fournie par Deployer. Dans ce cas, remplacer la tâche update_code par rsync vous obligerez à redéfinir l'ensemble des opérations de déploiement, ce qui peut-être gênant.

Il est alors préférable de redéfinir uniquement le fonctionnement de la tâche update_code.

<?php

namespace Deployer;

require 'recipe/symfony4.php';
require 'recipe/rsync.php';

set('application', 'my_symfony_project');

add('shared_files', ['.env']);
add('shared_dirs', ['var/log']);
add('writable_dirs', ['var']);

set('rsync',[
    /* configuration rsync */
]);

set('rsync_src', __DIR__);
set('rsync_dest', '');

// surchage de la tâche `deploy:update_code`
task('deploy:update_code', function() {
    invoke('rsync'); // appel de la tâche `rsync`
});

host('symfony-project.com')
    ->set('deploy_path', '~/');

Avec ce genre de configuration, vous avez donc la possibilité de déployer les projets présents dans votre monorepo de manière complètement indépendante.

J'ai dans ce billet, principalement évoqué comment configurer Deployer, mais comme je l'ai précisé au début de ce billet, les autres outils peuvent fonctionner avec le même principe. Par exemple, si vous utilisez Ansistrano, vous devrez rajouter les configurations ci-dessous à votre playbook Ansible pour effectuer une copie de fichier rsync:

vars:
    # ...
    ansistrano_deploy_via: "rsync"
    ansistrano_rsync_extra_params: ""
    ansistrano_rsync_set_remote_user: yes
    ansistrano_rsync_path: ""
    ansistrano_rsync_use_ssh_args: no