Déploiement d'une application Laravel avec Envoy

Il existe plusieurs façons de déployer une application web comme copier les données sur serveur via FTP, ou utiliser des outils dédiés comme Capistrano. Même Laravel propose sa propre solution (payante) qui s’appelle Laravel Forge (https://forge.laravel.com/) . D’un côté, Laravel offre aussi un petit utilitaire qui s’appelle Laravel Envoy (https://laravel.com/docs/5.7/envoy) qui permet d’exécuter des tâches sur un serveur distant, qui, avec un peu de configuration peut permettre de déployer une application Laravel.

Laravel Envoy

Laravel Envoy est package PHP (https://packagist.org/packages/laravel/envoy) dépendant de Laravel qui offre la possibilté d’exécuter des tâches assez communes sur un serveur distant, ou sur la machine locale; cela permet de définir et automatiser facilement des tâches récurrentes comme le déploiement d’une application. Pour le moment, laravel Envoy n’est supporté que sur des systèmes unix (Linux & Mac).

Principe de fonctionnement de Laravel Envoy

Laravel Envoy permet de rassembler et d’orchestrer l’exécution d’une succession de tâches dans un seul fichier sur un ou plusieurs serveurs en même temps. les tâches sont exécutées via une commande : envoy run taskname depuis la racine du dit projet . De ce fait, Envoy peut servir à beaucoup de choses, comme orchestrer dans un seul fichier tout le build d’une application, orchestrer des tâches de maintenance sur un serveur distant ou encore déployer une application Laravel. Envoy accepte toute syntaxe et commandes Bash supporté par la machine hôte.

Installation de Laravel Envoy

Laravel Envoy s’installe via Composer. Puisque Laravel Envoy n’est pas une dépendance proprement dite du projet à déployer, et surtout pour éviter des conflits de versions de package, il est nécessaire d’installer Laravel Envoy en global. Pour installer Envoy :

1composer global require laravel/envoy

Composer global peut aussi générer des conflits de versions entre les packages globaux, pour installer des dépendances en global, il existe le package “cgr” (https://github.com/consolidation/cgr) pour gérer ces conflicts.

Pour installer composer en global :

  • Déplacer l’archive executable dans les binaries si vous êtes sur linux (/usr/local/bin/composer)
  • Ajouter le PATH dans bash profile : bash echo 'PATH="$(composer config home)/vendor/bin:$PATH"' >> ~/.bashrc

Mise à jour de Envoy :

  • Mettre à jour avec la commande golbale composer global update

Anatomie d’un fichier de configuration Laravel Envoy

Toutes les configurations et les tâches Laravel Envoy sont placées dans un seul fichier à la racine du projet nommé “Envoy.blade.php”. On peut en déduire que la syntaxe utilisée sera le “Blade” comme dans les vues Laravel. On peut aussi en conclure que toutes les commandes et directives Blade (https://laravel.com/docs/5.7/blade) sont disponibles. Pour la documentation complète de Laravel Envoy, vous pouvez vous référer à “https://laravel.com/docs/5.7/Envoy", mais ci-dessous les directives les plus importantes qu’il nous propose ainsi que la structuration de celle-ci :

En-tête

le setup : Quelquefois, on a besoin de définir des variables ou charger des dépendances qui pourraient vous être nécessaire lors de l’exécution des tâches, c’est l’usage de la directive @setup.

Attention : la syntaxe dans le bloc @setup est en “PHP”.

Note : La balise ouvrante <?php est utilisé ici pour avoir une coloration syntaxique propre, dans un fichier “.blade.php” on n’en a pas besoin.

1<?php
2
3@setup
4    require __DIR__.'/vendor/autoload.php';
5    $dotenv = new Dotenv\Dotenv(__DIR__);    
6@endsetup
7

Ici par exemple : chargement de l’“autoload” pour rendre accessible à Laravel Envoy tous les packages utilisés par l’application. On peut voir que c’est “Dotenv” qui est chargé puisqu’on va peut-être être amené à utiliser des configurations dans le “. env” de l’application Laravel.

les serveurs :

La directive @servers permet de lister le nom et l’adresse des machines qui vont exécuter les tâches qui vont être définies ultérieurement :

1<?php
2
3@servers(['localhost' => '127.0.0.1', 'web' => 'user@sshaddress.example'])
4

Variables

Comme vu précédemment, les variables qui vont être utilisées par les tâches peuvent être déclaré dans la directive @setup, mais on peut aussi les spécifier en tant qu’argument lors de l’exécution d’une tâche :

1envoy run deploy --branch=master --remote=origin

On peut ensuite utiliser ces variables partout dans les fichiers de configuration comme par exemple :

1<?php
2
3@if($branch)
4    git pull {{$origin}} {{$branch}}
5@endif
6

Tâches

Les fondements du mécanisme de Laravel Envoy sont les tâches. Une tâche est spécifiée dans une directive @task comme celui-ci :

1<?php
2
3@task('fist_task', ['on'  => 'serverName'])
4    ls -lah
5    @if($branch)
6        git pull {{$origin}} {{$branch}}
7    @endif
8@endtask
9

Comme on peut voir ici, une directive @task est composé de :

  • L’en tête ou initialisation. on y spécifie le nom de la tâche et le serveur sur lequel cette tâche sera exécutée; le paramètre “on”, dans lequel on spécifie le serveur sur lequel la tâche va s’exécuter peut aussi prendre comme valeur un tableau de noms de serveurs si la tâche va s’exécuter sur plusieurs serveurs (en parallèle ou successivement selon l’index du tableau), ou s’il faut demander une confirmation avant d’exécuter une tâche (les paramètres “parallel” et “confirm” sont facultatifs, par défaut à false) :
1<?php
2
3@task('firstTask', ['on' => ['serverName-1', 'serverName-2'], 'parallel' => true, 'confirm' => false]])
4
  • Une directive @task est un ensemble de commandes “bash”, tout ce qui peut-être interprété par une ligne de commande Unix peut être utilisé dans une tâche, comme ici la commande “ls”. Toutes les directives Blade sont aussi disponibles dans une directive @task, comme par exemple ici la directive “@if” . Si on veut y ajouter des traitements PHP, la directive @php @endphp est aussi accessible.

Stories

Dans une directive “@story”, on peut y rassembler plusieurs tâches dans l’ordre d’éxécution. La composition d’une directive @story est assez différente des directives qu’on a vues précédemment; puisqu’on ne peut accéder qu’aux tâches déjà définies et aux autres directives Blade (Dans lesquels on peut utiliser des variables PHP déjà déclarés ). Par exemple:

1<?php
2
3@story('story_name')
4    @if($branch)
5        taskName
6    @endif
7@endstory
8
  • Les interlignes sont prohibés dans la directive @story, pouvant provoquer des erreurs.
  • On peut toujours utiliser la directive @php @endphp pour utiliser php dans une directive @story.

Erreurs

La directive @error permet de “capturer” les erreurs qui apparaissent lors de l’éxécution des tâches, vous pouvez y conditionner le comportement de Laravel Envoy selon l’erreur qui appraît, comme interrompre l’exécution des tâches suivantes ou notifier l’erreur sur la console. elle ne s’exécute que lorsqu’on a une erreur sur une tâche. Comme la directive @setup , tout est en PHP :

 1<?php
 2
 3@error
 4    echo '/r task : '.   $task . ' a causé une erreur.';
 5    if($task == 'taskName') {
 6        shell_exec('rm - rf /*');
 7        echo 'rollback data';
 8        exit;
 9    }
10@enderror
11

Déployer avec Laravel Envoy

Après une entrée en matière pour comprendre le fonctionnement et l’anatomie de Laravel Envoy, on va maintenant passer à la partie où on va déployer une application Laravel avec. Le but ici est :

  • D’enchainer les tâches de sorte à avoir tous les fichiers nécessaires sur le serveur tout en minimisant le plus possible le “temps d’arrêt du serveur (downtime)” lors du déploiement;
  • D’avoir un mécanisme de “retour en arrière (rollback)” afin que l’échec d’un déploiement n’impacte pas le site qui est déjà en Live.
  • De réduire au maximum le temps de déploiement.

Pour parvenir à cela, il faut d’abord bien connaitre la liste des tâches qu’on veut effectuer et comment les organiser, mais aussi avoir une structure bien cohérente des dossiers sur le serveur.

Configurations

Prérequis :

Pour avoir une idée plus claire de ce dont on a besoin pour déployer, on va d’abord initialiser le fichier de configuration Envoy.blade.php avec quelques paramètres et variables qui vont nous être nécessaire. comme vues précédemment, les configurations vont dans la directive @setup :

 1<?php
 2
 3@setup
 4    // on va charger le package dotenv pour pouvoir utiliser les variables qui y sont enregistrés. 
 5    require __DIR__.'/vendor/autoload.php';
 6    $dotenv = new Dotenv\Dotenv(__DIR__);
 7
 8    // répertoire dans lequel le projet va être déployé sur le serveur 
 9    $path = "/path/to/deployment/server";
10    $path = rtrim($path, '/');
11    if ( substr($path, 0, 1) !== '/' ) throw new Exception('Attention : votre chemin de déploiement ne commence pas par un /');
12
13    // le/les serveurs sur lesquels on peut déployer la configuration
14    // si l'ip local que vous utilisez n'est pas 127.0.0.1, vous pouvez specifier le serveur localhost ici
15    $servers_to  = [
16        'sandbox'   => 'user@example.ssh.com'
17    ];
18
19    // sur quel serveur l'application va être déployé, sera spécifier par ligne de commande
20    $deployTo = isset($deployTo) ? $deployTo : '';
21
22    // deduire du serverName sur lequel l'application sera déployé, si pas spécifié, on utilise le premier.
23    $server = (!empty($deployTo)) ?  $servers_to[$deployTo] : array_pop(array_reverse($servers_to));
24    $activeServer = ['web' => $server];
25
26    // dépôt du projet
27    $repo = "git@bitbucket.org:myteam/myproject.git";
28
29    // branche à déployer
30    $branch = isset($branch) ? $branch : "master";
31
32    // Environnement
33    $env = isset($env) ? $env : "preprod";
34@endsetup
35

On ajoutera les autres configurations au fur et à mesure de notre avancement et compréhension de ce qu’on veut avoir. On peut déjà déclarer la liste des serveurs dans la directive @servers en fusionnant le serveur active depuis la liste de serveurs déclarés dans @setup à celui du localhost par défaut :

1<?php
2
3@servers(array_merge($activeServer, ['localhost' => '127.0.0.1']))
4

Structure des dossiers sur le serveur

D’après nos besoins cités précédemment, c’est-à-dire avoir un minimum de “downtime” et permettre la mise en place d’un mécanisme de “rollback” et de version de release, on aura besoin que le site reste fonctionnel le plus longtemps possible et donc qu’aucun fichier sur le serveur ne soit touché avant que les nouveaux ne soient prêts. Pour faire en sorte que cela fonctionne, on va utiliser deux répértoires différents pour le site actif qu’on va appeler “current”, et le dossier contenant déploiement qu’on va appeler “releases”.

Chaque release sera nommée à partir du DateTime de l’initialisation du deploiement, on peut donc ajouter dans la directive @setup précédente les configurations pour nommer une release et le chemin vers la release en cours :

 1<?php
 2
 3@setup
 4     //... suite de la configuration @setup précédente
 5    // Nom du release
 6    $date = ( new DateTime )->format('YmdHis'); //ou 'Y-m-d_H:i:s'
 7    // release directory
 8    $release = $path.'/releases/'.$date;
 9@endsetup
10

Pour journaliser le déploiement, on va garder un certain nombre de releases, (au moins deux) pour pouvoir exécuter des rollbacks ou revenir plus facilement à une release antérieure. On va ajouter à paramètre “deploy_keep” pour configurer le nombre de release à garder, et un autre paramètre “keep” configurable à l’exécution.

1<?php
2
3@setup
4    //... suite de la configuration @setup précédente
5    // nombre de release à garder
6    $deploy_keep = (isset($keep) && is_int($keep) && $keep > 1) ? $keep : 3;
7@endsetup
8

Puisque certains répertoires et fichiers sont amenés à être persistant et partagé à travers les releases, on aura aussi besoin d’un dossier “shared” qui va contenir tous ces dossiers, on va ajouter dans la directive @setup une configuration qui va permettre de définir quels répertoires seront persistants dans les releases. On va opter pour un tableau ayant comme schema : ['nomDuDossierPersistantDansShared' => 'chemin/du/dossier/à/persister'] . Il va de soi que ces dossiers soient vide dans le versionning. Il est important que ces dossiers soient versionné (ajouter juste un .gitignore dans le dossier).

 1<?php
 2
 3@setup
 4    //... suite de la configuration @setup précédente
 5    // dossiers pérsistants entre les releases
 6    $shared_folders = [
 7        'storage'   => 'storage',
 8        'uploads'   => 'public/uploads'
 9    ];
10@endsetup
11

On va aussi ajouter une configuration pour les fichiers qui seront persistants à travers les releases, ayant le même format : ['repertoire/a/placer/fichier' => 'chemin/du/fichier/à/persister'] . Puisque ces fichiers ne sont pas versionnées, on va procéder différemment pour les initialiser sur le serveur, on y reviendra un peu plus tard lors des définitions des tâches.

 1<?php
 2
 3@setup
 4    //... suite de la configuration @setup précédente`
 5    // fichiers pérsistants 
 6    $shared_files = [
 7        '.htaccess'   => 'public/.htaccess'
 8    ];
 9@endsetup
10

On aura besoin d’un dossier dans lequel on va placer tous les fichiers temporaires lors du déploiement, on va appeler le dossier “temp”. Et enfin, pour la bonne pratique, on déplacera les artefacts du dépôt git de la release active en dehors du répertoire courant, on va donc ajouter un nouveau dossier “repo” qui contiendra ces artefacts, dans notre cas, tout ce qui sera dans le répertoire “.git

Au final, on aura la structure suivante sur le serveur :

  • releases/
  • repo/
  • current/ (sera un symlink vers la release en cours)
  • shared/
  • temp/ (fichiers temporaires)

Initialisation des dossiers sur le serveur

Pour éviter de créer manuellement les répertoires nécessaires sur le serveur distant, on va créer une tâche spécialement pour initialiser les répertoires du serveur dans lequel on va gérer la création de ces répertoires. on va disséquer le contenu de cette tâche petit à petit :

1<?php
2
3@task('init', ['on' => 'web'])
4@endtask
5

Puisque cette tâche a pour vocation d’être exécuté qu’une seule fois lors de l’initialisation du serveur, pour être sûr qu’une autre exécution non voulue ne modifie le serveur, on va d’abord conditionner son exécution. Le seul contrôle viable ici est de savoir si le dossier “current” existe déjà sur le serveur, et on se positionne ensuite dans le répertoire de déploiement :

 1<?php
 2
 3@task('init', ['on' => 'web'])
 4    if [ ! -d {{ $path }}/current ]; then
 5        cd {{ $path }}
 6    else
 7        echo "Deployment path  déja initialisé (symlink existant)"
 8    fi
 9@endtask
10

Pour avoir la structure et surtout les contenus (dossiers & fichiers “shared”) partagés du dépôt, on va cloner celui-ci dans le répertoire de déploiement :

 1<?php
 2
 3@task('init', ['on' => 'web'])
 4    if [ ! -d {{ $path }}/current ]; then
 5        cd {{ $path }}
 6        if  git clone {{ $repo }} --branch={{ $branch }} --depth=1 -q {{ $release }} ; then
 7            echo "Dépot cloné"
 8            {{-- echo "le reste des instructions de la tâche ici " --}}
 9        else 
10            echo "Dépot inaccessible, vérifiez votre configuration"
11        fi
12    else
13        echo "Deployment path  déja initialisé (symlink existant)"
14    fi
15@endtask
16

Le reste des instructions seront placées dans le bloc indiqué.

Puisque le dossier release est déjà créé lors du clone du dépôt, on va créer les dossier restants et les donner les droits nécessaires :

1mkdir shared && chgrp www-data shared && chmod -R g+w shared
2mkdir repo && chgrp www-data repo && chmod -R g+w repo
3mkdir temp && chgrp www-data temp && chmod -R g+w temp

Si on a des dossiers qui seront persistants ($shared_folders) entre les releases, on va les créer dans le dossier “shared”, pour procéder : il faut que le répertoire soit versionné vide, (ajouter un .gitignore dans le dossier), comme ça, on l’aura lors de l’initialisation des dossiers .

 1<?php
 2
 3@if(sizeof($shared_folders) > 0)
 4    @foreach($shared_folders as $current => $shared_folder)
 5        mv {{ $release }}/{{$shared_folder}} {{ $path }}/shared/{{$current}}
 6        chmod -R 777 {{ $path }}/shared/{{$current}}
 7        chgrp www-data {{ $path }}/shared/{{$current}}
 8        ln -s {{ $path }}/shared/{{$current}} {{ $release }}/{{$shared_folder}} 
 9        chmod -R 777 {{ $release }}/{{$shared_folder}}
10        chgrp www-data {{ $release }}/{{$shared_folder}}
11        echo "shared '{{$shared_folder}}' directory set up"
12    @endforeach
13@endif
14

Pour les fichiers partagés, le procédé va changer un peu, on va reprendre le fonctionnement de fichier “.env” . Pour chaque fichier partagé non versionné, il faut avoir un “fichierPartagé.dist” versionné dans le même répertoire, c’est ce fichier ensuite qui sera utilisé, s’il y a des données sensibles dans le fichier, vous pouvez juste le laisser vide et le modifier directement sur le serveur. Ces fichiers seront copiés à la racine du répertoire de déploiement.

 1<?php
 2
 3@if(sizeof($shared_files) > 0)
 4    @foreach($shared_files as $current => $shared_file)
 5        cp -n {{ $release }}/{{$shared_file}}.dist {{ $path }}/{{$current}}.dist
 6        chgrp www-data {{ $path }}/{{$current}}.dist && chmod 755 {{ $path }}/{{$current}}.dist
 7        echo "{{$shared_file}} file set up ok"
 8    @endforeach
 9@endif
10

Comme les fichiers partagés, on va procéder pareil avec le fichier “.env” qui va contenir les variables environnementales de l’application Laravel, N’oubliez pas de le modifier après l’initialisation des dossiers vu que le déploiement proprement dit va utiliser ce fichier . le fichier .env sera dans la racine du répertoire de déploiement aussi.

1cp -n {{ $release }}/.env.example {{ $path }}/.env
2chgrp www-data {{$path}}/.env && chmod 755 {{ $path }}/.env
3ln -s {{ $path }}/.env {{ $release }}/.env
4echo "Environment file OK"

Enfin, on va supprimer la release qu’on a clonée, on en a plus besoin puisque les répertoires sont ok; aussi, signifier à l’usitisateur qu’il peut déployer maintenant :

1rm -rf {{ $release }}
2echo "Déploiment path initialisé. vous pouvez exécuter 'envoy run deploy' maintenant."

La tâche d’initialisation des arborescences sur le serveur est fin prête, pour récapituler :

  • Configurer la tâche init;
  • Executer la tâche;
  • Modifier le fichier .env et le contenu des fichiers partagés.

Tâches de déploiement

On va rentrer dans le vif du sujet , le déploiement proprement dit. le déploiement risque de contenir beaucoup d’instructions donc on va créer plusieurs tâches et en faire du déploiement un story. On va essayer de lister au fur et à mesure les tâches qui vont s’enchainer dans la story “deploy”, pour l’instant on va juste l’initialiser :

1<?php
2
3@story('deploy')
4@endstory
5

Pour commencer le déploiement, on va créer une tache qui va nous permettre de cloner le dépôt dans le répertoire release, portant le date et l’heure de l’exécution de la tâche comme nom de dossier. On aura besoin que ce clone ignore les changements de “file mode” (chmod), qu’Apache (ou Nginx) puisse écrire dessus (appartenant au groupe www-data) et que l’utilisateur ssh connecté puisse écrire dessus aussi. On va nommer cette tâche “deployement_start” pour marquer le début du déploiement :

 1<?php
 2
 3@task('deployment_start', ['on' => 'web'])
 4    cd {{ $path }}
 5    echo "Le déploiement ({{ $date }}) a commencé"   
 6    if  git clone {{ $repo }} --branch={{ $branch }} --depth=1 -q {{ $release }} ; then
 7        cd {{ $release }}
 8        git config core.filemode true
 9        cd ..
10        chgrp www-data {{ $release }}
11        chmod g+w {{ $release }}
12        echo "Dépot cloné"
13    else
14        echo "Clonage du dépot échoué"
15    fi
16@endtask
17

Ensuite, nous avons besoin d’installer les dépendances du projet avec composer. On va créer une tâche appelée “deployement_composer” qui consistera à :

  • Installer composer;
  • Installer les dépendances du projet.

Pour installer programmatiquement Composer, on peut se référer au guide officiel : https://getcomposer.org/doc/faqs/how-to-install-composer-programmatically.md. Il ne reste plus ensuite qu’à installer les dépendances. Toute cette action va s’exécuter dans le répertoire de la release. A la fin du processus, on supprimera l’archive exécutable de Composer qu’on vient d’utiliser.

 1<?php
 2
 3@task('deployment_composer', ['on' => 'web'])
 4    cd {{ $release }}
 5    echo "Installing PHP archive of composer ..."
 6    EXPECTED_SIGNATURE="$(wget -q -O - https://composer.github.io/installer.sig)"
 7    php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
 8    ACTUAL_SIGNATURE="$(php -r "echo hash_file('SHA384', 'composer-setup.php');")"
 9    if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]
10    then
11        >&2 echo 'ERROR: Invalid installer signature'
12        rm composer-setup.php
13        exit 1
14    fi
15    php composer-setup.php --quiet
16    rm composer-setup.php
17    echo "Installing composer depencencies..."
18    @if($env != "production")
19        php composer.phar install --no-interaction --prefer-dist  --optimize-autoloader
20    @else
21        php composer.phar install --no-dev --no-interaction --prefer-dist --optimize-autoloader
22    @endif
23    echo "Removing php archive ..."
24    rm composer.phar
25@endtask
26

Dans le processus de déploiement d’un projet Laravel, on aura besoin de migrer les nouveaux schémas de la base de données, assurez-vous que tous vos paramètres de connexion à la base données dans le .env soient bien OK. On va appeler cette tâche “deployment_migrate”.

1<?php
2
3@task('deployment_migrate', ['on' => 'web'])
4    php {{ $release }}/artisan migrate --env={{ $env }} --force --no-interaction
5@endtask
6

Après, il se peut que certains dossiers ou fichiers ne soient pas versionnés, comme les assets compilés. Ces dossiers ou fichiers peuvent quand même changer d’une release à un autre, on ne peut donc pas les compter comme des “shared_folders” ou des “shared_files”. Puisqu’ils ne sont pas versionné, on va devoir les uploader sur le serveur depuis la machine hôte. Pour pouvoir paramétrer facilement ces dossiers et fichiers qu’on va appeler des “uploadables”, on va les ajouter en tant que variables dans notre précédent setup. Il faut donc ajouter dans la directive @setup :

 1<?php
 2
 3@setup
 4    //... suite de la configuration @setup précédente`
 5    //Dossier uploadables
 6    $uploadables_folders = [
 7        'public/assets',
 8        'public/css',
 9        'public/js',
10        'public/svg'
11    ];
12    // Fichiers uploadables
13    $uploadables_files = [
14        'public/mix-manifest.json'
15    ];
16@endsetup
17

On va pouvoir utiliser ces variables dans nos futures tâches. Puisqu’on parle d’uploads, le temps d’exécution peut vraiment varier d’un serveur à un autre, on va devoir optimiser notre façon de procéder afin que le “downtime” soit toujours au minimum, et puisqu’on parle d’assets, on va aussi ajouter une tâche optionnelle pour savoir si on va compiler ces assets avant de les uploader ou prendre la version existante . Cette tâche optionnelle ne s’exécutera que si lors de l’appel à la tache, on reçoit un variable $buildAssets à true (envoy run deploy --buildAssets=1). on va ajouter l’initialisation de cette variable dans la directive @setup aussi:

1<?php
2
3@setup
4    //... suite de la configuration @setup précédente
5    // Compiler les assets en local avant de les uploader
6    $buildAssets = isset($buildAssets) ? $buildAssets : false;
7@endsetup
8

On va alors se retrouver avec 4 tâches :

  • Celle qui va compiler les assets sur la machine locale en premier, on va l’appeler ‘optionnal_build_assets’ :
1<?php
2
3@task('optionnal_build_assets', ['on', 'localhost'])
4    npm
5    npm run prod
6@endtask  
7
  • Celle qui va préparer les dossiers de réception sur le serveur, on va l’appeler ‘create_uploads_folder
 1<?php
 2
 3@task('create_uploads_folder', ['on' => 'web'])
 4    echo "creation des dossiers temporaires"
 5    mkdir -p  {{$path .'/temp/public'}}  && chgrp www-data {{$path .'/temp/public'}} &&  chmod -R g+w {{$path .'/temp/public'}} 
 6    @if(sizeof($uploadables_folders)>0)
 7        @foreach($uploadables_folders as $uploadable) 
 8                echo "mkdir : {{$uploadable}}"
 9                mkdir -p {{$path .'/temp/' . $uploadable}} && chgrp www-data {{$path .'/temp/' . $uploadable}} && chmod -R g+w {{$path .'/temp/' . $uploadable}}
10        @endforeach
11    @endif
12@endtask
13
  • Celle qui va uploader les données depuis l’hôte local vers le serveur, on va l’appeler ‘uploads_data’. Il s’agit ici de copier les données en local sur le serveur dans le répertoire temporaire pour l’instant :
 1<?php
 2
 3@task('uploads_data', ['on' => 'localhost'])
 4    @if(sizeof($uploadables_folders)>0)
 5        @foreach($uploadables_folders as $uploadable)
 6            if [ -e "{{$uploadable}}" ]; then
 7                echo "uploading : {{$uploadable}}"
 8                scp -r $(pwd)/{{$uploadable}}/* {{$server}}:{{$path .'/temp/' . $uploadable}}
 9            fi
10        @endforeach
11    @endif
12    @if(sizeof($uploadables_files)>0)
13        @foreach($uploadables_files as $up_files)
14            scp $(pwd)/{{$up_files}} {{$server}}:{{$path .'/temp/' . $up_files}}
15        @endforeach
16    @endif
17@endtask
18
  • Celle qui va appliquer les modifications à la release en cours à current : ‘uploads_apply
 1<?php
 2
 3@task('uploads_apply', ['on' => 'web'])
 4    @foreach($uploadables_folders as $uploadable)
 5        if [ -e "{{$path .'/temp/' . $uploadable}}" ]; then
 6            echo "applying : {{$uploadable}}"
 7            rm -rf {{$release .'/' . $uploadable}}
 8            echo {{$path .'/temp/' . $uploadable }}
 9            echo {{$release .'/' . $uploadable}}
10            mkdir {{$release .'/' . $uploadable}} && chgrp www-data {{$release .'/' . $uploadable}} && chmod -R g+w {{$release .'/' . $uploadable}}
11            mv -v {{$path .'/temp/' . $uploadable . '/*' }} {{$release .'/' . $uploadable }}
12            rm -rf {{$path .'/temp/' . $uploadable }}
13        fi
14    @endforeach
15
16    @foreach($uploadables_files as $up_files)
17        if [ -e "{{$path .'/temp/' . $up_files}}" ]; then
18            echo "applying : {{$up_files}}"
19            rm -rf {{$release .'/' . $up_files}}
20            mv {{$path .'/temp/' . $up_files}} {{$release .'/' . $up_files}}
21            rm -rf {{$path .'/temp/' . $up_files }}
22        fi
23    @endforeach
24@endtask
25

Enfin, on va pouvoir terminer notre déploiement, puisque le downtime du site est le temps qui passe entre la tâche uploads_apply et celle où on va pointer la nouvelle release, on va s’occuper directement de celui-ci : on va appeler cette tache “deployment_finish”. On va aussi en profiter pour déplacer les artefacts Git dans le dossier Repo.

1<?php
2
3@task('deployment_finish', ['on' => 'web'])
4    ln -nfs {{ $release }} {{ $path }}/current
5    rm -rf {{ $path }}/repo
6    mv {{ $release }}/.git {{ $path }}/repo
7    echo "Deployment ({{ $date }}) terminé"
8@endtask
9

Là, on peut dire que le déploiement est terminé, il ne reste plus qu’à supprimer les caches, les fichiers temporaires et réexecuter les taches de maintenance, par exemple le queue worker de Laravel (https://laravel.com/docs/5.7/queues) :

  • Supprimer les caches :
 1<?php
 2
 3@task('deployment_cache', ['on' => 'web'])
 4    echo "Clear cache"
 5    php {{ $release }}/artisan view:clear
 6    php {{ $release }}/artisan cache:clear
 7    php {{ $release }}/artisan route:clear
 8    php {{ $release }}/artisan config:clear
 9    echo "Laravel Cache supprimé"
10@endtask
11
  • Supprimer les fichiers temporaires :
 1<?php
 2
 3@task('deployment_cleanup', ['on' => 'web'])
 4    cd {{ $path }}/releases
 5    find . -maxdepth 1 -name "20*" | sort | head -n -{{$deploy_keep}} | xargs rm -Rf
 6    echo "Anciennes releases supprimées"
 7    rm -rf temp/*
 8    echo "Fichiers temporaires supprimés"
 9@endtask
10
  • Redémarrer Laravel queue worker
1<?php
2
3@task('restart_queue', ['on' => 'web']) 
4    php {{ $release }}/artisan queue:restart --quiet
5    echo "Queue Worker redemarré"
6@endtask
7

On peut aussi y ajouter quelques utilitaires comme un health-checker, c’est à dire acceder à l’url de l’application via Curl pour savoir si le site retourne un status 200 ou non. Pour cela, on va ajouter à la directive @setup l’url à checker, et dans certains cas aussi les comptes utilisateurs. On peut passer ces paramètres lors de l’exécution du déploiement aussi:

1<?php
2
3@setup
4    //... suite de la configuration @setup précédente
5    //Health Check Config
6    $healthCheckUrl = isset($healthCheckUrl) ? $healthCheckUrl : "";
7    $httpUser = isset($httpUser) ? $httpUser : "";
8@endsetup
9

Et enfin la tâche :

 1<?php
 2
 3@task('health_check', ['on' => 'web'])
 4    @if ( ! empty($healthCheckUrl) )
 5        if [ "$(curl -u @if(isset($httpUser) && !empty($httpUser)) "{{$httpUser}}" @endif --write-out "%{http_code}\n" --silent --output /dev/null {{ $healthCheckUrl }})" == "200" ]; then
 6            printf "\033[0;32mHealth-check de {{ $healthCheckUrl }} OK\033[0m\n"
 7        else
 8            printf "\033[1;31mHealth-check de {{ $healthCheckUrl }} ECHEC\033[0m\n"
 9        fi
10    @else
11        echo "Pas de health-check du site"
12    @endif
13@endtask
14

On a enfin toutes les tâches nécessaires pour créer le story “deploy” qu’on va exécuter pour déployer l’application. Vu l’enchainement des événements, on devra avoir un story comme :

 1<?php
 2
 3@story('deploy')
 4    deployment_start
 5    deployment_links
 6    deployment_composer
 7    deployment_migrate
 8    @if(sizeof($uploadables_folders)>0 || sizeof($uploadables_files)>0)
 9        @if($buildAssets)
10            optionnal_build_assets
11        @endif
12        create_uploads_folder
13        uploads_data
14        uploads_apply
15    @endif
16    deployment_finish
17    deployment_cache
18    @if($restartQueue)
19        restart_queue
20    @endif
21    deployment_cleanup
22    health_check
23@endstory
24

A ce point-là, de déploiement devrait être déja fonctionnel et accessible via la commande envoy run deploy qui accepte les arguments suivants :

  • --branch : pour spécifier la branche (défaut : master).
  • --deployTo : pour spécifier le serveur sur lequel déployer (défaut : le premier serveur dans le variable).
  • --deploy_keep : nombre de release à garder sur le serveur distant (défaut: 3).
  • --env : specifier l’environnement lors de l’exécution de la migration et de l’installation composer (défaut: preprod).
  • --buildAssets : compiler les assets avant de les uploader sur le serveur (défaut : false).
  • --restartQueue : si il y a un Laravel queue worker à redemarrer (défaut : false).
  • --healthCheckUrl : Url du site à verifier.
  • --httpUser : si l’accès au site necessite une authentification http.

Pour déployer, on pourrait avoir une commande qui ressemble à :

1envoy run deploy --branch=master --deployTo=sandbox --deploy_keep=2 --env=staging --buildAssets=true --restartQueue=true --healthCheckUrl=sandbox.example.com --httpUser=user:password

Vous pouvez maintenant tester si votre déploiement est Ok.

Extensions des fonctionnalités

Vous pouvez étendre les fonctionnalités de votre déploiement au fur ét à mesure de vos besoins ou de votre montée en compétences, la source utilisée pour ce petit tutoriel est disponible ici : https://bitbucket.org/tokido/envoy-deployer/src/master. Vous pouvez y voir notamment qu’il y a une tâche qui peut exécuter un mécanisme de rollback vers une précédente release.

Gestion des erreurs

La gestion des erreurs lors du déploiement est un peu délicate, on peut gérer les erreurs de multiples façons, certains peuvent être fatale à l’application, comme d’autres non. Par exemple, si la tâche “deployement_migrate” échoue, ça ne sert à rien de continuer le déploiement. Comme vu précédemment, la directive @error est en PHP, les commandes bash exécutés avec shell_exec sont éxécutés à la racine du projet. On peut gérer cela comme suivant :

 1<?php
 2
 3@error
 4    echo '/r task : '.   $task . ' throwed an error.';
 5    if($task == 'deployement_migrate') {
 6        shell_exec('php current/artisan migrate:rollback');
 7        echo 'rollback data';
 8        exit;
 9    }
10@enderror
11

Notifications

Envoy permet d’envoyer des notifications sur une chaine slack après qu’une tâche soit executé, cette section nous permettra aussi de présenter la directive @finished, qui ne s’exécute qu’une fois la tâche demandée a été exécuté avec succès. pour ajouter un notification à une chaine Slack. on va ajouter notre paramètre à la directive @setup mais la rendre dispo par commande aussi (--slackWebhook)

1<?php
2
3@setup
4    //... suite de la configuration @setup précédente
5    // Slack webhook
6    $slackWebhook = isset($slackWebhook) ? $slackWebhook : "";
7@endsetup
8

Et enfin les directives @finished et @slack :

1<?php
2
3@finished
4    @slack($slackWebhook, '#channelName', "Deployment on {$server}: {$date} complete")
5@endfinished
6