Drupal 8

Mettre en place un processus d'intégration continue d'un site Drupal 8 peut se faire avec des moyens relativement limités. Voici un prototype de modèle n'utilisant qu'une machine de développement, un compte GitLab.com et un serveur de production.

Mise en garde

Quelques avertissements sur la portée de cet article :

  • L'intégration continue peut être très complexe à mettre en place, c'est une expertise à part entière. Ici, je vous montrerai un modèle extrêmement simplifié, que je qualifie de prototype. Je m'en sers sur ce site personnel que vous êtes en train de lire. Les environnements sont basiques, il y a l'environnement de développement (mon ordinateur) et l'environnement de production (mon serveur VPS). Dans tout l'article, je parlerai d'environnement staging à la place de celui de production, car en fait, mon staging = ma production vu que je développe pour moi-même et tout seul.
  • Il y a plusieurs façons de gérer le code et la configuration d'un site Drupal 8, il y a plusieurs façons de builder le code et le thème, de réaliser les tests, plusieurs méthodes de déploiement... Les solutions que je vous présente sont juste des choix que j'ai effectués afin d'obtenir le résultat global attendu : un processus d'intégration continue simple et puissant pour gérer ce site. Dans un cas réel pour un projet d'envergure, je ne ferais peut-être pas les mêmes et assurément, le processus n'occulterait pas des parties dont je n'ai dans ce cas précis vraiment pas besoin.
  • Un mot sur l'environnement de développement : j'utilise une machine sous Linux disposant d'une installation métal de PHP et de Node.js pour builder le site en local, ainsi que de containers Docker pour héberger le site en local. Je ne détaillerai pas cet environnement de développement car là n'est pas le but de l'article.
  • Je ne détaillerai pas tout, certaines étapes seront sautées soit pour essayer de garder un propos concis, soit car j'ai oublié. N'hésitez pas à me poser des questions précises dans les commentaires, j'essaierai d'y répondre.

Présentation générale du modèle

Le code du site sera généré par l'excellent template Composer pour projets Drupal. De l'installation initiale de Drupal à l'ajout de modules, thèmes, librairies JS et PHP, tout sera géré, téléchargé et installé par Composer. Une exception : le CSS et le Javascript du thème custom seront compilés par des outils Node.JS. Cette partie sera donc gérée par NPM.

La configuration de Drupal sera gérée par le très robuste module core Config, avec un ajout du module contrib Config split pour gérer les différences de configuration entre environnements (dev et staging dans ce cas précis).

Les tests consisteront simplement en une batterie de tests comportementaux Behat. Ils seront executés en local en environnement de dev, avant de déclencher le build et le déploiement du site. Cela correspond à mon usage pour ce site personnel, pour un projet destiné à un client et/ou faisant intervenir plusieurs développeurs, ces tests seraient naturellement à automatiser. Il n'y a pas de tests unitaires non plus, car ce site n'a pour l'instant pas de module custom et pour simplifier, je fais confiance aux mainteneurs de Drupal et des modules pour avoir fait leurs tests.

Le code sera versionné avec Git et pushé vers un projet privé sur GitLab.com. Les fichiers de Drupal Core, des modules, thèmes, librairies contrib et tout le répertoire /vendor ne seront PAS commités.

Les tâches d'automatisation se feront sur GitLab, plus précisément sur GitLab-CI. A chaque Merge sur le projet GitLab vers une branche nommée staging, GitLab-CI lancera un job. Celui-ci sera executé par un worker GitLab-CI de type Docker. Il sera composé de 2 tâches : le build du site et le déploiement du site.

La première tâche, le build du site, sera effectuée dans un conteneur Docker qui utilise une image Docker custom comportant PHP, Composer ainsi que Node.js avec Gulp et les dépendances requises.

La deuxième tâche, le déploiement du site, sera executée par un conteneur Docker utilisant une image d'Ansible. Celle-ci exectuera toutes les actions nécessaires en télécommandant le serveur de production. Des scripts Bash auraient pu suffire, j'expliquerai plus loin pourquoi j'ai choisi d'utiliser Ansible. Des alias de Drush sont configurés pour l'instance locale de dev, et l'instance distante de prod.

Au final, une nouvelle version du site sera automatiquement déployée à chaque merge dans la branche staging.

Le modèle est facilement extensible à des workflows réels avec au moins une réelle branche production, à l'inclusion de nouvelles étapes entre le build et le déploiement, comme une étape de tests automatisés.

Partie 1 : initialisation du projet en environnement de développement local

Initialisation du projet

Il n'y a pas grand chose à dire sur l'initialisation en elle-même du site Drupal avec le template Composer, et pas mal d'aide est disponible. Je crée un projet nommé gd8 :

composer create-project drupal-composer/drupal-project:8.x-dev gd8 --stability dev --no-interaction

Vous devriez avoir la structure suivante dans le dossier du projet :

/
/config/
/drush/
/scripts/
/vendor/
/web/
/composer.json
/composer.lock
/phpunit.xml.dist
/README.md
  • /config/ va stocker dans un sous-répertoire sync la configuration de Drupal gérée par le module core Config. on y créera d'autres répertoires pour des sous-ensemble de configuration spécifiques à des environnements, avec Config split.
  • /drush/ contient des scripts qui empêchent d'installer, de mettre à jour ou de supprimer du code avec Drush. En effet, gérer tout le code avec Composer permet de profiter de sa gestion des dépendances. Si vous commencez à installer du code en dehors de Composer, vous allez vous retrouver avec des conflits qui peuvent être très pénibles à gérer. Le dossier /drush/ n'est pas nécessaire en production sauf si vous voulez bloquer l'installation par Drush de code.
  • /scripts/ contient des scripts Composer pour gérer l'installation et les mises à jour du projet. Il n'est pas nécessaire en production non plus, on le nettoiera plus tard.
  • /vendor/ contient toutes les dépendances de Drupal 8 et du template Composer pour Drupal 8, ainsi que les dépendances Composer de certains modules contrib. Il est nécessaire pour la production. Par contre, le build pour la production sera effectué spécifiquement, afin de n'y mettre que le strict nécessaire, en se débarrassant de beaucoup de code uniquement utile en développement / test.
  • /web/ contient Drupal 8, il contiendra aussi les modules et thèmes contrib et custom. Il est bien entendu nécessaire en production.
  • Les 4 fichiers ne sont pas nécessaires en production, on les supprimera dans la tâche de build.

A ce stade, vous devez pouvoir innstaller Drupal en ouvrant votre navigateur à l'URL locale que vous avez configurée et ne utilisant une BDD locale prévue pour. Je préfère toujours partir d'une installation minimale de Drupal et directement installer le français en plus de l'anglais de base. Attention, si vous avez xDebug et une configuration costaud de celui-ci, des étapes de l'installation peut parfois poser problème, personnellement je l'ai désactivé juste pour l'installation.

Mise en place du repository Git

A ce stade, j'aime initialiser le repository Git pour garder la trace de ce qui sera fait ensuite. Je passe tout de suite sur une branche de développement dev :

cd gd8
git init
git checkout -b dev
git add .
git commit -m 'init'

Vous pouvez regarder en local ce que Git a commité, ou pusher vers GitLab et regarder là-bas. On constate que le code récupéré par Composer est bien exclus du repository :

  • /vendor/ et /web/core/ : Rien, et ça restera comme ça.
  • /web/libraries/, /web/modules/ et /web/themes/ : Rien et ça restera comme ça même quand vous installerez des modules, librairies et thèmes contrib. Par contre, il y a de fortes chances pour qu'à terme, on crée un répertoire /web/modules/custom/ et un /web/themes/custom/ pour y mettre nos modules et thèmes custom. Là, ils seront commités (à moins que vous n'ayez un repository privé pour ceci, mais c'est une autre histoire...).
  • Je ne rentre pas dans le détail des autres fichiers, mon point était de montrer que la très grande majorité du code n'est pas dans le repository.

Gestion de la configuration de Drupal 8 selon les environnements

Pré-requis : à ce stade vous devez avoir une installation fonctionnelle de Drupal 8 et aussi de Drush (dans le répertoire /web/ lancer "drush status" doit montrer que tout va bien dont la connection à la base de données).

A ce stade, on a envie de construire notre site, mais je ne le conseille pas. Il y a encore beaucoup de boulot avant de passer à cette étape, la première tâche extrêmement importante consistant à s'assurer d'un gestion robuste de la configuration de Drupal 8.

La configuration de Drupal 8 est par défaut stockée en base de données, et on peut l'exporter ainsi que l'importer dans des fichiers. On va tout de suite l'exporter :

drush cex

On confirme et voilà, dans /config/sync/ on a maintenant tout un tas de fichiers YAML de configuration de Drupal 8. Je vous conseille de commiter à ce stade :

cd .. (project root)
git add .
git commit -m 'config export'

Mise en place de Configuration Split

Maintenant on va installer notre premier module, Configuration Split, qui va nous permettre de séparer la configuration spécifique à notre environnement de développement, de celle de la production. Je préconise au début de créer une nouvelle branche pour pouvoir revenir à une situation propre en cas de problème. Et on installe bien-sûr avec Composer :

git checkout -b 'config-split'
composer require drupal/config_split

On active avec le GUI Drupal 8 à /admin/modules ou alors avec Drush, et on vide les caches :

drush en config_split
drush cr

Ensuite on va créer une configuration splitée sur admin/config/development/configuration/config-split.

  • Label : Dev
  • Folder : ../config/dev (la Drupal root est sur /web/, on doit donc remonter d'un répertoire)
  • Active : coché

C'est tout, on ajoutera des modules tout de suite après, pour l'instant on enregistre. On crée aussi le répertoire :

mkdir config/dev

Enfin, on spécifie dans le fichier de settings local qu'on veut que ce ssous-ensemble de configuration soit actif sur notre environnement de dev. Dans /web/sites/default/settings.php, on ajoute :

$config['config_split.config_split.dev']['status'] = TRUE;

L'avantage, c'est qu'on pourra synchroniser la BDD de la prod sur la BDD locale de dev sans devoir réactiver le split en dev. On vide les caches :

drush cr

Ajout d'un premier module uniquement en dev

Ajoutons un module dont on aura besoin seulement en dev : le module Devel. On le récupère avec Composer :

composer require --dev drupal/devel

Notez le require --dev à la place de require : pour le build du code destiné à la production, on lancera dans notre job de CI :

composer install --no-dev

Ceci n'installera pas tous les packages inclus par composer require --dev, donc on aura pas le code dans notre archive de destinée à être installée en prod (aussi nommée <strong>artifact</strong>).

Encore faut-il que Drupal n'aie pas envie d'installer ce module. On va donc ajouter le module Devel au sous-ensemble de configuration Dev. Mais d'abord on va l'activer :

drush en devel
drush cr

On l'ajoute au config split Dev en retournant sur /admin/config/development/configuration/config-split/dev/edit. Dans la blacklist, on sélectionne le module Devel, on enregistre et on exporte la configuration :

drush cex

Maintenant, dans /config/sync/ on ne doit avoir aucun fichier YAML de Devel, il doit y avoir un fichier dans /config/dev/ par contre (devel.settings.yml). Si tout est OK, on peut commiter et passer à la suite.

Mise en place de tests comportementaux avec Behat

Au moment de l'écriture de cet article, j'ai mis en place Behat depuis un moment et je ne me rappelle plus exactement des étapes. Vu que l'article est déjà long et qu'il y a encore beaucoup à dire, je vais faire court et probablement incomplet.

composer require --dev drupal/drupal-extension
vendor/bin behat init

On doit se retrouver avec à la racine du projet un dossier /features/ qui contient un répertoire /features/bootstrap/ qui va permettre à Behat de charger les extensions nécessaire à son fonctionnement de Drupal.

On a aussi un fichier /behat.yml qui dans une version très simple ressemble à ceci :

default:
      suites
:
        default
:
          contexts
:
           - FeatureContext
            - Drupal\DrupalExtension\Context\ConfigContext
            - Drupal\DrupalExtension\Context\DrupalContext
            - Drupal\DrupalExtension\Context\MinkContext
            - Drupal\DrupalExtension\Context\MarkupContext
            - Drupal\DrupalExtension\Context\MessageContext
      extensions
:
        Behat\MinkExtension
:
          goutte
: ~
          selenium2
: ~
          base_url
: http://gd8:8080
        Drupal\DrupalExtension
:
          blackbox
: ~
          api_driver
: "drupal"
          drupal
:
            drupal_root
: "/sites/gd8/web"
          region_map
:
            left sidebar
: "#sidebar-first"
            content
: "#content"
          selectors
:
            error_message_selector
: '.messages--error'
  • base_url est l'URL d'accès local à votre site
  • drupal_root est le chemin système de la Drupal root de votre site

Ensuite, il n'y a plus qu'à ajouter des tests comportementaux dans /features/. Créons un test extrêmement simple dans /features/home.feature :

@api
    Feature: Homepage

    @api
    Scenario: assertHomepage
      Given I am an anonymous user
        When I am on the homepage
        Then I should see the text "Mon premier post avec Drupal 8"

En lançant vendor/bin/behat, le test doit se lancer et si vous avez "Mon premier post avec Drupal 8" alors le résultat sera OK, sinon le test échouera.

L'intérêt des tests comportementaux

Comme plusieurs points abordés dans ce long article, Behat mérite un article à part entière que je ferai peut-être un jour. Si vous ne l'utilisez pas, je vous conseille fortement de vous pencher dessus. Pourquoi ?

Un de mes clients réguliers est une start-up dans le domaine de l'aéronautique, qui construit une plateforme disposant de fonctionnalités custom avancées telles qu'un système de crédits, de monétisation, de différents niveaux de confidentialité etc. Ce sont eux qui m'ont fait découvrir Behat. A chaque fois que nous développons une nouvelle fonctionnalité, ou modifions une existante, nous le faisons dans une branche dédiée. Avant d'être mergée dans la branche principale, en plus des relectures mutuelles des Merge Request, nous lançons une batterie de tests Behat qui vérifie chacune des fonctionnalités spécifiques du site.

Je peux vous dire que presque tout le temps, on introduit des régressions dans le code. Même quand on est persuadé de travailler sur du code bien isolé du reste, je vous assure qu'on casse des choses quand même, et le pire est qu'on ne le sait pas. Or il est impossible, en intégration continue, de tester manuellement chaque fonctionnalité à chaque Merge Request.

Les tests comportementaux protègent des regressions, c'est le rempart principal. Je ne dis pas d'écrire des tests comportementaux pour tout, mais d'en avoir pour les choses les plus importantes. Un simple site vitrine ou de publication n'en a peut-être pas besoin, mais si le workflow est compliqué, si vous vous commencez à avoir une boutique, des interactions évoluées avec l'internaute, alors ça me semble incontournable.

Partie 2 : Build du site par GitLab-CI

Pré-requis

Dans la 1ère partie, vous devez avoir :

  • Initialisé un projet Drupal 8 avec le template Composer ;
  • Un site fonctionnel en local ;
  • Drush fonctionnel en local ;
  • La configuration globale de Drupal exportée dans /config/sync/ et une configuration splitée nommée dev dans /config/dev/ et active sur le site local ;
  • Des tests comportemenaux Behat tournant en local ; si vous ne l'avez pas fait, cette partie n'est pas indispensable pour la suite car dans ce modèle simple, on ne fera les tests qu'en dev local.

Pour être sûr que tout est OK, vous devriez pouvoir cloner votre repository dans un nouveau répertoire, builder le code avec composer install --no-dev, modifier le fichier settings.php crée par Composer pour utiliser une copie de votre BDD déjà prête. Après avoir configuré votre serveur pour ce nouveau site, tout doit marcher. On peut donc passer à la partie 2, qui consiste à faire builder le site par GitLab-CI (GitLab Continuous Integration).

Build automatisé du site avec GitLab-CI

L'offre ne manque pas en intégration continue, entre Travis, Circle-CI, Jenkys, GitLab-CI... Ma préférence va clairement à ces deux derniers car ce sont des logiciels libres et très performants. Jenkys est spécialisé dans l'intégration continue, tandis que GitLab-CI est une partie de GitLab, qui fait aussi gestionnaire de code source, d'issues, propose des boards, wiki...

J'ai choisi de capitaliser sur GitLab, pour ce projet en tous cas, et de ne pas m'embêter à gérer ma propre instance. J'utilise donc GitLab.com où je mets ce site en projet privé. Les workers pour les jobs d'intégration continue sont gratuits jusqu'à 2000 minutes par mois. Pour mon site personnel, c'est plus qu'il n'en faut. Je suis rassuré car je sais qu'à chaque instant, je peux exporter mon code et une bonne partie de mon espace GitLab.com vers une instance personnelle.

Dans d'autres circonstances, par exemple pour une usine à sites d'un grand groupe, je ne sais pas si je choisirais GitLab, ce que je sais c'est que si je le faisais, cela serait probablement une instance dédiée et probablement dans les locaux du client, car la protection des données est cruciale en industrie. A voir avec la DSI. Mais là on est dans un cas simple. Alors voici comment je vous propose de commencer à utiliser l'intégration continue de votre projet Drupal 8 avec un simple compte GitLab.com.

Avertissements :

  • L'utilisation de GitLab-CI décrite ici est extrêmement simple et passe à côté de plein de choses assez incroyables comme les environnements et les <strong>Review Apps</strong>. Ces dernières permettent de créer un environnement de test live sur le web par branche de fonctionnalité. Pour les très gros projets, c'est le must !

.gitlab-ci.yml

Toute la "magie" de GitLab-CI se concentre en 1 fichier : .gitlab-ci.yml, fichier à créer à la racine du projet. Ce fichier, dès qu'il est présent dans votre repository GitLab, active GitLab-CI. Il va suivre un certain nombre d'évenements comme des commits dans certaines branches, des merge request dans d'autres, et déclencher une série automatisée d'actions.

Organisons-nous comme ceci : normalement, après la partie 1 vous avez toujours une branche master vide, une branche dev et une branche config-split. Si tout fonctionne, mergeons config-split dans dev et pushons-là.

git checkout dev
git merge config-split
git push -u origin dev

Maintenant, sur GitLab.com on va créer une branche staging. Pour rappel, dans cet exemple simple, on utilise staging comme la branche de production car c'est un petit projet que je développe seul. Cette branche staging, nous allons la protéger, de manière à ce qu'on ne puisse que la modifier par Merge Request et non pas commiter dedans. Faisons pareil pour master, tant qu'on y est. Personnellement, dès que j'imagine travailler en équipe, je protégerais également la branche dev, mais on va la laisser ouverte aux commits. Pour la suite de l'exercice, je n'utiliserai que dev et staging.

On est donc dans la branche dev et on ajoute un .gitlab-ci.yml simple pour commencer :

variables:
  DOCKER_IMAGE_DRUPAL_BUILD
: guillaumeduveau/build-drupal8
build
:
  only
:
   - staging
  image
: $DOCKER_IMAGE_DRUPAL_BUILD
  script
:
   - composer install --no-dev
  artifacts
:
    paths
:
   - ./

Explications :

variables:
  DOCKER_IMAGE_DRUPAL_BUILD
: guillaumeduveau/build-drupal8

La partie variables permet... de définir des variables utilisées dans la suite du job. Ca permet d'être un peu plus propre, mais aussi d'utiliser des variables secrètes GitLab. Celles-ci sont configurées au niveau du projet, globalement ou par branche / environnement. Pour l'instant pas besoin, la variable DOCKER_IMAGE_DRUPAL_BUILD est tout simplement l'image Docker à utiliser pour builder le code du site Drupal 8.

Comme vous pouvez le voir, j'ai crée ma propre image Docker. Vous pouvez l'utiliser et le code source est ici. Bien sûr rien ne garantit que mon image sur Docker Hub correspond à ce code source, mais bientôt je linkerai mon compte GitHub avec Docker afin de fournir un build "certifié". En attendant vous pouvez soit faire confiance, soit cloner le code source, builder l'image et la pusher vers votre repository Docker.

Dans cette image, il y a quoi ? Le README.md n'est pas à jour, pour savoir son contenu il faut regarder https://gitlab.com/guillaumeduveau/docker-build-drupal8/blob/master/Doc…. En gros, il y a PHP avec ce qu'il faut pour lancer composer install --no-dev et il y a aussi un peu de Node.JS pour compiler le thème custom (on verra ça plus tard).

Tâche de build
build:

Passons à la suite : build est la définition une étape, ou stage, du process d'intégration continue. On aura une deuxième étape par la suite, pour le déploiement.

build:
  only
:
   - staging

Cela dit à GitLab-CI de n'executer ce job que quand la branche staging est modifiée. Et comme on l'a protégée des commits, le job ne s'executera que quand on acceptera une merge request d'une branche quelconque dans staging.

build:
 ...
  image
: $DOCKER_IMAGE_DRUPAL_BUILD

Ceci indique quelle image Docker utiliser pour cette étape build du job. On peut la définir de manière globale pour tout le job mais je préfère utiliser une image par tâche, d'autant que pour le déploiement l'image sera très spécifique !

build:
 ...
  script
:
   - composer install --no-dev

C'est tout simplement la commande qui doit être lancée dans le container Docker pour faire ce qu'on veut. Il peut y avoir plusieurs commandes et il y en aura ! Cette commande fait la même chose que si vous aviez cloné le projet dans un répertoire et lancé la commande : elle va construire tout le code de votre projet Drupal, en récupérant une à une chacune des dépendances ! C'est pourquoi je vous disais de vérifier au début de cette 2ème partie que le build avec Composer fonctionne ;)

build:
 ...
  artifacts
:
    paths
:
   - ./

Va demander à GitLab de prendre tous les fichiers du répertoire de travail actuel, c'est-à-dire le repository de départ qu'il a automatiquement cloné, plus tout le code récupéré par composer install --no-dev. GitLab-CI va passer l'ensemble du code résultant de cette tâche, ou artifact, à la tâche suivante s'il y en a une, et générer une archive .zip de l'ensemble, téléchargeable manuellement ou par les API de GitLab.

C'est parti, on va commiter ce fichier :

git add .gitlab-ci.yml
git commit -m 'first CI build job!'
git push

Ensuite, on va sur le GUI de GitLab et on doit voir une possiblité de créer une merge request. On crée une MR de dev dans staging. On l'accepte, et quand on va dans la branche staging, on va voir qu'un job a été lancé. Cliquez dessus, et vous verrez le GitLab-CI worker récupérer l'image Docker puis builder votre site. Si le job réussit, à la fin on peut télécharger le .zip de l'artifact de la tâche build, qui contient tout le code nécessaire à votre site, prêt à être déployé.

Amélioration du job de build

Je ne sais pas si vous avez remarqué, mais il y a un gros problème de permissions de fichiers complètement farfelues. Ca vient d'un bug de GitLab-CI. Comme je ne suis pas du genre à faire un chmod XXX sur quoi que ce soit, on va corriger ça. Aussi, on en profitera pour nettoyer l'artifact des fichiers inutiles et pour builder un thème custom.

Voici .gitlab.ci.yml dans sa version finale pour cette 2ème partie :

variables:
 # Build stage vars.
  # With the automatic git pull, we get file permission problems.
  GIT_STRATEGY
: none
  # Docker image used to build the project theme and code.
  # @see <a href="https://gitlab.com/guillaumeduveau/docker-build-drupal8">https://gitlab.com/guillaumeduveau/docker-build-drupal8</a>
  DOCKER_IMAGE_DRUPAL_BUILD
: guillaumeduveau/build-drupal8
  # Drupal theme build.
  DRUPAL_THEME_DIR
: web/themes/custom/gd8
  # Drupal theme cleanup.
  DRUPAL_THEME_DEL_DIRS
: 'node_modules bower_components scss css/maps'
  DRUPAL_THEME_DEL_EXTS
: '*.md *.json'
  DRUPAL_THEME_DEL_FILES
: 'gulpfile.js'
  # Drupal code cleanup.
  DRUPAL_DEL_DIRS
: '.git config/dev drush features scripts web/sites/default/files'
  DRUPAL_DEL_FILES
: '.gitignore .gitlab-ci.yml behat.yml composer.json composer.lock phpunit.xml.dist README.md web/sites/default/settings.php'

stages
:
 - build

build
:
  stage
: build
  only
:
   # Only for commits or MR in the 'staging' branch (staging server).
    - staging
  image
: $DOCKER_IMAGE_DRUPAL_BUILD
  script
:
  # Clone repository.
   - git clone -b staging $CI_REPOSITORY_URL
   # Build and clean theme.
   - cd $CI_PROJECT_NAME/$DRUPAL_THEME_DIR
   # Theme build.
   - npm install
   - bower install
   - gulp
   # Theme cleanup.
   - rm -rf $DRUPAL_THEME_DEL_DIRS
   - rm -r $DRUPAL_THEME_DEL_EXTS
   - rm $DRUPAL_THEME_DEL_FILES
   # Code build.
   - cd $CI_PROJECT_DIR/$CI_PROJECT_NAME
   # Don't install things that were composer require --dev.
   - composer install --no-dev
   # Code cleanup.
   - rm -rf $DRUPAL_DEL_DIRS
   - rm $DRUPAL_DEL_FILES
   # Store the job ID in a file to get the artifact later.
   - cd $CI_PROJECT_DIR
   - echo -e "$CI_JOB_ID" > job_id
  # Now, let's make a .zip of the whole build.
  artifacts
:
   # @see <a href="https://docs.gitlab.com/ee/ci/yaml/#artifacts">https://docs.gitlab.com/ee/ci/yaml/#artifacts</a>
    name
: "${CI_PROJECT_NAME}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}"
    paths
:
   - ./
    expire_in
: 1 week

Explications :

  • GIT_STRATEGY: none : pour contourner le bug de permissions de fichiers de GitLab-CI, on lui demande de ne pas cloner automatiquement le repository et on le fait manuellement. C'est pour ça que plus loin, il y a git clone -b staging $CI_REPOSITORY_URL et qu'on est obligés de gérer le fait de ne pas être à la racine pour le work directory du worker Docker, d'om les cd $CI_PROJECT_NAME et compagnie.
  • DRUPAL_THEME_DIR : C'est le répertoire de mon thème custom, on le build plus loin avec npm install ; bower install ; gulp, bien sûr ceci dépend de comment vous construisez et buildez votre thème.
  • DRUPAL_THEME_DEL_DIRS, DRUPAL_THEME_DEL_EXTS, DRUPAL_THEME_DEL_FILES, DRUPAL_DEL_DIRS, DRUPAL_DEL_FILES : Tout ceci définit des répertoires, fichiers et extensions à supprimer avant finalisation de l'artifact. Ce n'est pas franchement nécessaire mais j'aime bien les choses propres. D'ailleurs pour cela, il serait sûrement mieux de faire ça avec Ant, mais bon.
  • echo -e "$CI_JOB_ID" > job_id: Au début, je récupérais l'artifact depuis le serveur de prod avec une autre méthode. Elle est cassée depuis fin août. On récupère donc maintenant l'artifact via le job ID, sauf qu'il y a 2 jobs et que dans le job suivant de déploiement, l'ID n'est plus la même forcément. Je passe le job ID du build au deploy en passant par un fichier... C'est moche ! Plein de choses on changé d'ailleurs avec GitLab.com... ;)
  • name: "${CI_PROJECT_NAME}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}" permet de donner un joli nom versionné au .zip plutôt qu'un inémuable "artifact.zip".
  • expire_in: 1 week : Au bout d'une semaine, l'artifact sera effacé de GitLab, c'est plus qu'il n'en faut.

Voilà... C'est tout pour la partie 2 !

Partie 3 : Déploiement automatisé du site

Pré-requis : Tout comme à la fin de la partie 1, vous devez pouvoir générer une instance de site à partir d'un build de votre repository, sauf que cette fois il ne doit plus être buildé localement, mais par GItLab-CI. Vous devez donc pouvoir télécharger l'archive .zip de votre dernier artifact, la décompresser dans un répertoire et régler un virtualhost de votre serveur local dessus en y branchant un clone de votre BDD, et tout doit fonctionner.

Maintenant que le site est buildé avec le tout dernier code introduit par la dernière merge request, nous allons le déployer sur le serveur de staging. Pour rappel, sur ce projet, le serveur de staging = le serveur de production. Dans la réalité professionnelle il faut des environnements séparés avec au minimum un vrai staging et une vraie production.

Pour le déploiement, il y a foison de solutions. J'ai longtemps hésité à adopter une approche 100% Docker, en deployant simplement une image contenant à la fois l'infrastructure, le code, la base de données et les fichiers utilisateurs. Pour ce projet relativement simple, j'ai choisi une autre route, celle de conserver un serveur "classique" disposant de services installés en métal (= directement installés). J'ai désiré n'avoir à sûbir aucun souçi de micro-management du serveur, car dans mon esprit je souhaite appliquer ces méthodes à plus grande échelle dans un avenir proche. C'est pourquoi j'ai décidé de gérer et la configuration de mon serveur VPS, et le déploiement du site Drupal 8, avec l'outil fantastique qu'est Ansible.

Installation et configuration du serveur de production avec Ansible

Je ne vais pas trop entrer dans les détails car ce n'est pas l'objet de l'article. Sachez que pour appliquer mon prototype de modèle d'intégration continue de Drupal 8, vous n'avez pas besoin de gérer votre serveur avec Ansible, et vous n'avez même pas besoin d'avoir installé Ansible dessus. D'ailleurs je ne l'ai pas installé sur mon VPS, pas besoin. Ansible agit comme une télécommande de votre serveur : si on lui donne les clés d'entrée, typiquement, un user pouvant se connecter en SSH avec une clé publique et ayant tous les droits nécessaires pour ce qu'il a à faire, ça lui suffit. Je recommande de se pencher sur Ansible plutôt que de rester dans les scripts bash... que vous pouvez de toute façon évoquer depuis Ansible. De plus, ce qui est vraiment intéressant, c'est que certes je délègue à GitLab-CI les scripts Ansible de déploiement Drupal 8, mais que je peux entièrement les utiliser en local si je le souhaite. A tout moment, je peux mettre en pause le déploiement automatisé de GitLab-CI et le faire manuellement depuis mon ordinateur en local. Les scripts, les rôles Ansible sont capitalisés et peuvent ainsi me servir à synchroniser ma BDD ou mes fichiers depuis la prod. Bref, si vous voulez gérer l'installation de vos serveurs avec Ansible comme moi, je vous conseille les excellents rôles Ansible de GeerlingGuy. Ils sont géniaux, le type est sympa et en plus il bosse chez Acquia !

Tâche de déploiement avec GitLab-CI et Ansible

Le principe sera le suivant : après la tâche de build, si elle réussit, alors GitLab-CI va executer la tâche de déploiement, que j'ai appellé deploy_staging. Celle-ci va instancier un container Docker à partir d'une image comportant Ansible qui va executer le playbook qu'on lui aura donné, en utilisant les variables secrètes pour se connecter au serveur.

Cela suppose un peu de préparation côté serveur : il faut avoir un user qui peut se connecter de l'extérieur, avoir les droits sur les fichiers de déploiement, et en ce qui me concerne devenir root pour ré-attribuer les fichiers à mon utisateur www-data. Je ne vais pas tout détailler et me contenter de donner les principes généraux.

Revenons à notre .gitlab.ci.yml :

Pour la tâche de déploiement, on y ajoute une image Docker :

variables:
  DOCKER_IMAGE_DEPLOY
: williamyeh/ansible:alpine3

On y ajoute une étape deploy_staging :

stages:
 - build
  - deploy_staging

Et voici le contenu de la tâche :

deploy_staging:
  stage
: deploy_staging
  image
: $DOCKER_IMAGE_DEPLOY
  only
:
   # For commits or MR in the 'staging' branch.
    - staging
  before_script
:
   # Run ssh-agent (inside the build environment)
    - eval $(ssh-agent -s)
    # Add the SSH key to the agent store.
    # Define STAGING_SSH_PRIVATE_KEY as a Gitlab-CI secret variable in your repository.
    # @see <a href="https://gitlab.com/jamietanna/jvt.me/commit/733627fdf9ef1aad9bb338364672bde4c5e237da">https://gitlab.com/jamietanna/jvt.me/commit/733627fdf9ef1aad9bb33836467…</a>
    - echo -e "$STAGING_SSH_PRIVATE_KEY" > key
    - chmod 600 key
    - ssh-add key
    # Check the server's host key.
    # Define STAGING_SSH_SERVER_HOSTKEYS as a Gitlab-CI secret variable in your repository.
    # To get them:
    # ssh-keyscan -H your-staging-server-IP
    - mkdir -p ~/.ssh
    - '[[ -f /.dockerenv ]] && echo "$STAGING_SSH_SERVER_HOSTKEYS" > ~/.ssh/known_hosts'
  script
:
   # Define ANSIBLE_HOSTS as a Gitlab-CI secret variable in your repository.
    # e.g.
    # [staging]
    # my.host.com ansible_user=my_ansible_user_on_host
    - rm /etc/ansible/hosts
    - touch /etc/ansible/hosts
    - echo -e "$ANSIBLE_HOSTS" > /etc/ansible/hosts
    - # Get the job ID from build stage.
    - job_id=$(cat job_id)
    # Play the playbook.
    - ansible-playbook -i /etc/ansible/hosts $CI_PROJECT_DIR/$CI_PROJECT_NAME/deploy/ansible/staging-update.yml --extra-vars "gitlab_job_id=$job_id"

Dans toute la première partie before_script, je permets la connexion SSH du container Docker à mon serveur, en injectant des variables secrètes que j'ai définies dans mon projet GitLab.

Dans la deuxième partie script, je définis l'inventaire de machines Ansible en injectant des variables secrètes. Je pourrais d'ailleurs déplacer ça dans la 1ère partie, mais bon.

ansible-playbook -i /etc/ansible/hosts $CI_PROJECT_DIR/$CI_PROJECT_NAME/deploy/ansible/staging-update.yml --extra-vars "gitlab_job_id=$job_id"

La commande la plus importante est cette dernière. Elle dit d'executer le playbook, ou site d'instructions, staging-update.yml sur l'inventaire de serveurs dont elle dispose, c'est-à-dire mon serveur VPS. Juste avant dans le job de build, on a mis l'ID du job dans un fichier qu'on récupère pour le passer au container qui run Ansible.

Et puis "c'est tout", en tous cas côté GitLab-CI, car maintenant il reste la dernière partie de cet article : le déploiement en lui-même avec Ansible.

Déploiement et gestion d'un site Drupal 8 avec Ansible

Dans mon projet, je crée un répertoire /deploy/ansible/ où j'y mets mon staging-update.yml que voici :

---
- hosts
: staging
  roles
:
   - offline
    - backup-db
    - update-code
    - update-drupal-db
    - online

C'est tout simple :

  • Télécommande le serveur staging, STP
  • Fais ce que je t'ai appris à faire pour le mettre hors-ligne (offline)
  • Fais un back-up de la base de données (backup-db)
  • Mets à jour le code du site en récupérant le dernier artifact (update-code)
  • N'oublie pas de lancer les mises à jour Drupal concernant la base de données (update-drupal-db)
  • Et enfin, remets le site en ligne !

Minute, il faut définir ces actions et peut-être indiquer à Ansible où est le site, quelle est la base de données, etc. Non ?

Voici la structure de mon répertoire /deploy/ansible/ :

/deploy/ansible/group_vars/
/deploy/ansible/groups_vars/dev
/deploy/ansible/groups_vars/staging
/deploy/ansible/roles/offline/
/deploy/ansible/roles/backup-db/
/deploy/ansible/roles/update-code/
/deploy/ansible/roles/update-drupal-db/
/deploy/ansible/roles/online/
/deploy/ansible/staging-update.yml

Dans /deploy/ansible/group_vars/ j'ai 2 fichiers : dev et staging. Je ne les commite pas, ils contiennent des infos confidentielles. J'ai mis ces mêmes infos concernant staging dans la variable secrète de GitLab-CI qui est ANSIBLE_HOSTS, soit :

[staging]
IP.DE.MON.SERVEUR ansible_user=user_pour_le_deploiement

[staging:vars]
code_directory="/répertoire/du/projet"
web_directory="/répertoire/du/projet/web"
gitlab_personal_access_token="mon-token-personnel-d-acces-a-GitLab"
gitlab_instance="<a href="https://gitlab.com">https://gitlab.com</a>"
gitlab_namespace="mon-namespace-gitlab"
gitlab_project_name="nom_de_mon_projet"
ansible_become_pass="mot_de_passe_du_user_de_deploiement_ansible_qui_peut_devenir_root"

Et voici les rôles Ansible :

offline

/deploy/ansible/roles/offline/tasks/main.yml

---
- name
: Put the site in maintainance mode.
  command
: chdir={{ web_directory }} drush sset system.maintenance_mode 1

backup-db

/deploy/ansible/roles/backup-db/tasks/main.yml

---
- name
: Delete the previous DB dump.
  file
:
    path
: "{{ code_directory }}/deploy/backup/dump.sql"
    state
: absent
- name
: Backup the DB.
  shell
: drush sql-dump > {{ code_directory }}/deploy/backup/dump.sql
  args
:
    chdir
: "{{ web_directory }}"

update-code

Le gros morceau...

/deploy/ansible/roles/update-code/tasks/main.yml

---
- name
: Set the deploy directory.
  set_fact
:
    deploy_directory
: "{{ code_directory }}/deploy"
- name
: Set the build directory.
  set_fact
:
    build_directory
: "{{ deploy_directory }}/build"
- name
: Creates the build directory.
  file
:
    path
: "{{ build_directory }}"
    state
: directory
- name
: Set the artifact path.
  set_fact
:
    artifact_path
: "{{ build_directory }}/artifact.zip"
- name
: Get GitlabCI's artifact.
  uri
:
    url
: "<a href="https://gitlab.com/api/v4/projects/">https://gitlab.com/api/v4/projects/</a>{{ gitlab_project_id }}/jobs/artifacts/staging/download?job=build"
    method
: GET
    headers
:
      "PRIVATE-TOKEN"
: "{{ gitlab_personal_access_token }}"
    dest
: "{{ artifact_path }}"
- name
: Extract the artifact.
  unarchive
:
    src
: "{{ artifact_path }}"
    dest
: "{{ build_directory }}"
    remote_src
: True
- name
: Remove the artifact archive.
  file
:
    path
: "{{ artifact_path }}"
    state
: absent
- name
: Set the built directory.
  set_fact
:
    built_directory
: "{{ build_directory }}/{{ gitlab_project_name }}"
- name
: Copy settings.php.
  copy
:
    src
: "{{ deploy_directory }}/settings.php"
    dest
: "{{ built_directory }}/web/sites/default"
    remote_src
: yes
- name
: Set the backup directory.
  set_fact
:
    backup_directory
: "{{ code_directory }}/deploy/backup"
- name
: Delete old backup of the codebase.
  file
:
    path
: "{{ backup_directory }}/{{ item }}"
    state
: absent
  with_items
:
    - config
     - vendor
     - web
  become
: true
- name
: Backup current codebase.
  command
: "mv {{ code_directory }}/{{ item }} {{ backup_directory }}/{{ item }}"
  args
:
    creates
: "{{ backup_directory }}/{{ item }}"
    removes
: "{{ code_directory }}/{{ item }}"
  with_items
:
    - config
     - vendor
     - web
  become
: true
- name
: Install new codebase.
  command
: "mv {{ built_directory }}/{{ item }} {{ code_directory }}/{{ item }}"
  args
:
    creates
: "{{ code_directory }}/{{ item }}"
    removes
: "{{ built_directory }}/{{ item }}"
  with_items
:
    - config
     - vendor
     - web
  become
: true
- name
: Own the codebase by www-data:www-data.
  command
: "chown -R www-data:www-data {{ code_directory }}/{{ item }}"
  with_items
:
    - config
     - vendor
     - web
  become
: true
- name
: Put settings.php in read-only.
  command
: "chmod 444 {{ web_directory }}/sites/default/settings.php"
  become
: true
- name
: Symlink files directory.
  command
: "ln -s {{ code_directory }}/files {{ web_directory }}/sites/default/files"
  become
: true
- name
: Remove build directory.
  file
:
    path
: "{{built_directory}}"
    state
: absent

Désolé, je suis sur le point de partir en vacances, je n'ai pas le temps de détailler mais je suis sûr que c'est assez explicite. Merci pour votre compréhension !

Update 25 août : je viens de faire un test et depuis mon retour de vacances, l'API de Gitlab semble avoir changé car je récolte un 404 sur https://gitlab.com/api/v4/projects/{{ gitlab_project_id }}/jobs/artifacts/staging/download?job=build... Raison de plus pour moi de revoir ça prochainement... :-S

update-drupal-db

/deploy/ansible/roles/update-code/tasks/main.yml

---
- name
: Clear rebuild
  shell
: "drush cr all"
  args
:
    chdir
: "{{ web_directory }}"
- name
: Config sync import.
  shell
: "drush cim -y"
  args
:
    chdir
: "{{ web_directory }}"
- name
: Update database
  shell
: "drush updb -y"
  args
:
    chdir
: "{{ web_directory }}"
- name
: Clear rebuild
  shell
: "drush cr all"
  args
:
    chdir
: "{{ web_directory }}"

online

/deploy/ansible/roles/online/tasks/main.yml

---
- name
: Put the site out of maintainance mode.
  command
: chdir={{ web_directory }} drush sset system.maintenance_mode 0

"C'est tout" ! Maintenant, si on commite tout ceci, on devrait avoir un déploiement automatisé du site Drupal 8 après le build, à chaque merge request acceptée sur la branche staging.

Conclusion

Cet article est très long, j'ai hésité à le couper en plusieurs parties en en plus j'ai rushé la fin. Je vais partir en vacances et le publier ainsi, je verrai bien plus tard...

Ce process fonctionne à merveille sur mon site personnel, GuillaumeDuveau.com. J'ai investi pas mal de temps à le mettre au point, bien plus qu'à développer mon site. Mais j'ai maintenant une base reposant sur des méthodes industrielles, à partir de laquelle je vais pouvoir construire les sites de mes clients les plus exigeants, ou donner des conseils pointus en matière d'intégration et déploiement continu en Drupal 8.

Je suis convaincu que l'industrialisation des sites est incontournable dès aujourd'hui pour les projets relativement conséquents. Et que demain, il n'y aura plus de place au process manuels tels qu'ils existent encore aujourd'hui, et parfois dans des organisations où l'on attendrait du bien plus solide. Rien que dans le téléchargement de base de l'archive .zip de Drupal 8 (méthode déconseillée !!!) comprend plus de 15 000 fichiers, là où dans Drupal 7 core, il y en avait 10 fois moins. Et c'est sans compter les régressions de code et l'erreur humaine qui augmente potentiellement de manière exponentielle avec la complexité du code.

Demain ou dans quelques années, j'ai une vision du web avec des plateformes du type Wix et compagnie qui seront extrêmement abouties, qui vont absorber les petits marchés jusqu'à faire disparaître les petits projets web de quelques semaines à quelques mois. Je peux me tromper, je l'espère quelque part, mais je pense aujourd'hui qu'il n'y aura plus que de la place pour des méthodes professionnelles industrialisées au maximum. Comme je compte en faire partie puisque cela me passionne, je vais continuer à m'investir à fond dans cette direction.

Par cet article, j'espère vous aider à franchir le pas ou à vous convaincre de vous essayer à ces méthodes. Et peut-être aussi à renvoyer un peu l'ascenseur à la communauté opensource, pour les innombrables articles instructifs que j'ai pu lire et pour les projets incroyables que tous ces gens ont développé, dont je me sers souvent et qui me permettent de gagner ma vie.

Merci à toutes et à tous, pour vos contributions et pour votre lecture !

Update septembre 2017 : j'ai corrigé le problème de téléchargement d'artifact. Cependant, je déconseille fortement l'utilisation de GitLab.com dorénavant, en faveur d'une installation custom. D'une part cette mise à jour de GitLab.com de la fin de l'été a cassé "sans prévenir" mon déploiement continu. En situation pro, on ne peut pas se permettre de ne pas décider de quand on veut upgrader. D'autre part, le plan gratuit de GitLab.com ne propose depuis la rentrée de temps gratuit d'execution des travaux d'intégration continue. Avant, il y avait 2000 minutes gratuites par mois, de quoi faire. Suffisant pour un end-user ou freelance. Mais c'est fini, et bien que j'apprécie que pendant 1 an en tant qu'"ancien" je bénéficie encore de ce temps, je vais me tourner vers autre chose, probablement un GitLab-CE hébergé sur un serveur perso. Ce qui rejoint ce deuxième point : si j'ai voulu démontrer qu'il est possible de faire de l'intégration continue en mode minimal avec un ordinateur de dev, un serveur de prod et un compte GistLab.com, c'est au prix de pirouettes que je n'aime pas trop. Mon prochain projet, c'est l'évolution de celui-ci vers : l'ordinateur de dev, un VPS "à tout faire" pour GitLab-CE avec une installation packagée par Debian ou Ubuntu pour contrôler les MAJ + Docker pour des containers de GitLab runners en local + serveur de staging et si j'ai le courage en mode ReviewApps mais à mon avis ça sera dans un second temps, et serveur de prod. Pour le déploiement en prod, je changerai la méthode en exploitant un peu les web hooks.

Commentaires

Merci pour cet article très intéressant et instructif. Bonnes vacances.

En réponse à par Flocon de toile (non vérifié)

Merci pour ton retour et également pour tes articles très complets :)
Très bon article et instructif. Le déploiement via ansible ajoute, à mon sens, de la complexité car toute les commandes de déploiement peuvent être directement écrite dans le job lancé par le runner.
Merci beaucoup Franck ;)

Oui, Ansible est de la complexité ajoutée par rapport à des commandes du job du runner. Mais si j'ai choisi de procéder ainsi, c'est d'une part pour factoriser tout le déploiement. Ca me permet par exemple de disposer des mêmes rôles Ansible à partir de mon environnement de dev. Par exemple de faire un backup BDD avec le même rôle Ansible + dans le playbook un rôle de récupération du dump, etc. D'autre part, même si c'est complètement inutile dans le cadre d'un site perso et pour pas mal de sites, passer par Ansible permet potentiellement de déployer sur une flotte de serveurs. Et ça, en bash, ça va être plus pénible à gérer. Avec Docker ça aurait pu être pas mal aussi. En fait j'ai longtemps hésité à adopter l'approche 100% Docker en déployant carrément des images complètes infrastructure + contenu. Mais j'ai trouvé finalement que je serais trop restreint à cette approche 100% containers, comparé à la réalité du terrain où les gros clients préfèrent encore souvent le déploiement d'une tarball + 1 script d'update.

En conclusion c'était plus pour l'exercice, et parce que j'aime beaucoup Ansible :)
Super article, une approche très intéressante... Merci pour le partage! ?

Ajouter un commentaire