Aujourd’hui si vous voulez mettre en place une CI/CD sur GitHub il vous faut “linker” vos dépôts avec Travis-ci, Circle-ci, Codeship… Mais savez-vous que GitLab intègre une solution de CI/CD ? C’est l’objet de l’article d’aujourd’hui.

Dans cet article je vais juste vous présenter les possibilités que vous offre GitLab CI/CD. Mais pour aller plus loin je vous propose aussi deux tutos sur le codelabs d’Eleven Labs :

CI/CD c’est quoi ?

Je ne vais pas vous refaire une définition mais voici ce que nous dit Wikipédia pour CI et CD :

CI : Continuous Integration

“L’intégration continue est un ensemble de pratiques utilisées en génie logiciel consistant à vérifier à chaque modification de code source que le résultat des modifications ne produit pas de régression dans l’application développée. […] Le principal but de cette pratique est de détecter les problèmes d’intégration au plus tôt lors du développement. De plus, elle permet d’automatiser l’exécution des suites de tests et de voir l’évolution du développement du logiciel.”

CD : Continuous Delivery

“La livraison continue est une approche d’ingénierie logicielle dans laquelle les équipes produisent des logiciels dans des cycles courts, ce qui permet de le mettre à disposition à n’importe quel moment. Le but est de construire, tester et diffuser un logiciel plus rapidement. L’approche aide à réduire le coût, le temps et les risques associés à la livraison de changement en adoptant une approche plus incrémentale des modifications en production. Un processus simple et répétable de déploiement est un élément clé.”

GitLab en quelques mots

Alors Gitlab c’est :

  • Gitlab inc : la compagnie qui gère les développements des produits GitLab
  • Gitlab : c’est une version que vous pouvez installer sur votre machine, serveur ou dans le cloud facilement avec le Market place d’AWS
  • GitLab.com : c’est une version web comme GitHub ou BitBucket.

GitLab et GitLab.com sont des gestionnaires de repositories git basés sur le web avec des fonctionnalités comme :

  • un wiki,
  • un suivi d’issue,
  • un registry docker,
  • un suivi de code,
  • une review de code
  • une CI/CD,

GitLab est beaucoup plus fourni en fonctionnalités que GitHub dans sa version gratuite. Il est aussi possible d’avoir des dépôts privés sans avoir d’abonnement.

Avant de commencer

GitLab CI/CD va vous permettre d’automatiser les builds, les tests, les déploiements, etc de vos applications. L’ensemble de vos tâches peut-être divisé en étapes et l’ensemble des vos tâches et étapes constituent une pipeline.

Chaque tâche est exécutée grâce à des runners, qui fonctionnent grâce à un projet open source nommé GitLab Runner écrit en GO.

Vous pouvez avoir vos propres runners directement sur votre machine ou serveur. Pour plus d’information je vous laisse lire la documentation officielle :

GitLab propose aussi des runners publics, qui vous épargnent une installation, mais attention, il y a des quotas suivant le type de compte dont vous diposez. En compte gratuit, vous avez le droit à 2000 minutes de temps de pipeline par mois. Les runners publics de gitlab.com sont exécutés sur AWS.

Présentation de GitLab CI/CD

Comme je vous l’ai dit je ne vais pas vous montrer comment mettre en place une CI/CD de A à Z dans cet article mais je vais vous présenter les possibilités de la solution de GitLab CI/CD.

Le manifeste

Pour que la CI/CD sur GitLab fonctionne il vous faut un manifeste .gitlab-ci.yml à la racine de votre projet. Dans ce manifeste vous allez pouvoir définir des stages, des jobs, des variables, des anchors, etc.

Vous pouvez lui donner un autre nom mais il faudra changer le nom du manifeste dans les paramètres de l’interface web : Settings > CI/CD > General pipelines > Custom CI config path

Les Jobs

Dans le manifeste de GitLab CI/CD vous pouvez définir un nombre illimité de jobs, avec des contraintes indiquant quand ils doivent être exécutés ou non.

Voici comment déclarer un job le plus simplement possible :

job:
  script: echo 'my first job'

Et si vous voulez déclarer plusieurs jobs :

job:1:
  script: echo 'my first job'

job:2:
  script: echo 'my second job'

les noms des jobs doivent être uniques et ne doivent pas faire parti des mots réservés :

  • image
  • services
  • stages
  • types
  • before_script
  • after_script
  • variables
  • cache

Dans la définition d’un job seule la déclaration script est obligatoire.

Script

La déclaration script est donc la seule obligatoire dans un job. Cette déclaration est le coeur du job car c’est ici que vous indiquerez les actions à effectuer.

Il peut appeler un ou plusieurs script(s) de votre projet, voire exécuter une ou plusieurs ligne(s) de commande.

job:script:
  script: ./bin/script/my-script.sh ## Appel d'un script de votre projet

job:scripts:
  script: ## Appel de deux scripts de votre projet
    - ./bin/script/my-script-1.sh
    - ./bin/script/my-script-2.sh

job:command:
  script: printenv # Exécution d'une commande

job:commands:
  script: # Exécution de deux commandes
    - printenv
    - echo $USER

before_script et after_script

Ces déclarations permettront d’exécuter des actions avant et après votre script principal. Ceci peut être intéressant pour bien diviser les actions à faire lors des jobs, ou bien appeler ou exécuter une action avant et après chaque job

before_script: # Exécution d'une commande avant chaque `job`
  - echo 'start jobs'

after_script: # Exécution d'une commande après chaque `job`
  - echo 'end jobs'

job:no_overwrite: # Ici le job exécutera les action du `before_script` et `after_script` par défaut
  script:
    - echo 'script'

job:overwrite:before_script:
  before_script:
    - echo 'overwrite' # N'exécutera pas l’action définie dans le `before_script` par défaut
  script:
    - echo 'script'

job:overwrite:after_script:
  script:
    - echo 'script'
  after_script:
    - echo 'overwrite' # N'exécutera pas l’action définie dans le `after_script` par défaut

job:overwrite:all:
  before_script:
    - echo 'overwrite' # N'exécutera pas l’action définie dans le`before_script` par défaut
  script:
    - echo 'script'
  after_script:
    - echo 'overwrite' # N'exécutera pas l’action définie dans le `after_script` par défaut

Image

Cette déclaration est simplement l’image docker qui sera utilisée lors d’un job ou lors de tous les jobs

image: alpine # Image utilisée par tous les `jobs`, ce sera l'image par défaut

job:node: # Job utilisant l'image node
  image: node
  script: yarn install

job:alpine: # Job utilisant l'image par défaut
  script: echo $USER

Stages

Cette déclaration permet de grouper des jobs en étapes. Par exemple on peut faire une étape de build, de codestyling, de test, de code coverage, de deployment, ….

stages: # Ici on déclare toutes nos étapes
  - build
  - test
  - deploy

job:build:
  stage: build # On déclare que ce `job` fait partie de l'étape build
  script: make build

job:test:unit:
  stage: test # On déclare que ce `job` fait partie de l'étape test
  script: make test-unit

job:test:functional:
  stage: test # On déclare que ce `job` fait partie de l'étape test
  script: make test-functional

job:deploy:
  stage: deploy # On déclare que ce `job` fait partie de l'étape deploy
  script: make deploy

CI Stages

Only et except

Ces deux directives permettent de mettre en place des contraintes sur l’exécution d’une tâche. Vous pouvez dire qu’une tâche s’exécutera uniquement sur l’événement d’un push sur master ou s’exécutera sur chaque push d’une branche sauf master.

Voici les possibilités :

  • branches déclenche le job quand un un push est effectué sur la branche spécifiée.
  • tags déclenche le job quand un tag est créé.
  • api déclenche le job quand une deuxième pipeline le demande grâce à API pipeline.
  • external déclenche le job grâce à un service de CI/CD autre que GitLab.
  • pipelines déclenche le job grâce à une autre pipeline, utile pour les multiprojets grâce à l’API et le token CI_JOB_TOKEN.
  • pushes déclenche le jobquand un push est effectué par un utilisateur.
  • schedules déclenche le job par rapport à une planification à paramétrer dans l’interface web.
  • triggers déclenche le job par rapport à un jeton de déclenchement.
  • web déclenche le job par rapport au bouton Run pipeline dans l’interface utilisateur.

Je vais vous montrer trois exemples d’utilisation :

only et except simple

Dans son utilisation la plus simple, le only et le except se déclarent comme ceci :

job:only:master:
  script: make deploy
  only:
    - master # Le job sera effectué uniquement lors d’un événement sur la branche master

job:except:master:
  script: make test
  except:master:
    - master # Le job sera effectué sur toutes les branches lors d’un événement sauf sur la branche master

only et except complex

Dans son utilisation la plus complexe, le only et le except s’utilisent comme ceci :

job:only:master:
  script: make deploy
  only:
    refs:
      - master # Ne se fera uniquement sur master
    kubernetes: active # Kubernetes sera disponible
    variables:
      - $RELEASE == "staging" # On teste si $RELEASE vaut "staging"
      - $STAGING # On teste si $STAGING est défini

only avec schedules

Pour l’utilisation de schedules il faut dans un premier temps définir des règles dans l’interface web. On peut les configurer dans l’interface web de Gitlab : CI/CD -> Schedules et remplir le formulaire.

CI Schedules

Si vous souhaitez, vous pouvez définir un intervalle de temps personnalisé. C’est ce que j’ai fait dans mon exemple. La définition se fait comme un cron

when

Comme pour les directives only et except, la directive when est une contrainte sur l’exécution de la tâche. Il y a quatre modes possibles :

  • on_success : le job sera exécuté uniquement si tous les jobs du stage précédent sont passés
  • on_failure : le job sera exécuté uniquement si un job est en échec
  • always : le job s’exécutera quoi qu’il se passe (même en cas d’échec)
  • manual : le job s’exécutera uniquement par une action manuelle
stages:
  - build
  - test
  - report
  - clean

job:build:
  stage: build
  script:
    - make build

job:test:
  stage: test
  script:
    - make test
  when: on_success # s'exécutera uniquement si le job `job:build` passe

job:report:
  stage: report
  script:
    - make report
  when: on_failure # s'exécutera si le job `job:build` ou `job:test` ne passe pas

job:clean:
  stage: clean
  script:
    - make clean # s'exécutera quoi qu'il se passe
  when: always

allow_failure

Cette directive permet d’accepter qu’un job échoue sans faire échouer la pipeline.

stages:
  - build
  - test
  - report
  - clean

...

stage: clean
  script:
    - make clean
    when: always
    allow_failure: true # Ne fera pas échouer la pipeline
...

tags

Comme je vous l’ai dit en début d’article, avec GitLab Runner vous pouvez héberger vos propres runners sur un serveur ce qui peut être utile dans le cas de configuration spécifique.

Chaque runner que vous définissez sur votre serveur à un nom, si vous mettez le nom du runner en tags, alors ce runner sera exécuté.

job:tag:
  script: yarn install
  tags:
    - shell # Le runner ayant le nom `shell` sera lancé

services

Cette déclaration permet d’ajouter des services (container docker) de base pour vous aider dans vos jobs. Par exemple si vous voulez utiliser une base de données pour tester votre application c’est dans services que vous le demanderez.

test:functional:
  image: registry.gitlab.com/username/project/php:test
  services:
    - postgres # On appel le service `postgres` comme base de données
 before_script:
   - composer install -n
 script:
   - codecept run functional

environment

Cette déclaration permet de définir un environnement spécifique au déploiement. Vous pouvez créer un environnement dans l’interface web de GitLab ou tout simplement laisser GitLab CI/CD le créer automatiquement.

Il est possible de spécifier :

  • un name,
  • une url,
  • une condition on_stop,
  • une action en réponse de la condition précédente.
...

deploy:demo:
  stage: deploy
  environment: demo # Déclaration simple de l'environnement
  script:
    - make deploy

deploy:production:
  environment: # Déclaration étendue de l'environnement
    name: production
    url: 'https://blog.eleven-labs/fr/gitlab-ci/' # Url de l'application
  script:
    - make deploy

En déclarant des environments vous pouvez, depuis l’interface web de GitLab, déployer / redéployer votre application ou directement accéder à votre site si vous avez déclaré une url. Ceci se fait dans Operations > Environment.

CI Environment

Le bouton undo permet de redéployer, le bouton external link permet d’aller sur l’application et le bouton remove permet de supprimer l’environnement.

on_stop et action seront utilisés pour ajouter une action à la fin du déploiement, si vous souhaitez arrêter votre application sur commande. Utile pour les environnements de démonstration.

...

deploy:demo:
  script: make deploy
  environment:
    name: demo
    on_stop: stop:demo

stop:demo: # Ce job pourra être visible et exécuté uniquement après le job `deploy:demo`
  script: make stop
  environment:
    name: demo
    action: stop

Voici le lien officiel de la documentation sur les environments si vous souhaitez aller plus loin.

variables

Cette déclaration permet de définir des variables pour tous les jobs ou pour un job précis. Ceci revient à déclarer des variables d’environnement.

...
variables: # Déclaration de variables pour tous les `job`
  SYMFONY_ENV: prod

build:
  script: echo ${SYMFONY_ENV} # Affichera "prod"

test:
  variables: # Déclaration et réécriture de variables globales pour ce `job`
    SYMFONY_ENV: dev
    DB_URL: '127.0.0.1'
  script: echo ${SYMFONY_ENV} ${DB_URL} # Affichera "dev 127.0.0.1"

Comme pour environment je vous laisse regarder la documentation officielle sur les variables si vous souhaitez aller plus loin.

Il est aussi possible de déclarer des variables depuis l’interface web de GitLab Settings > CI/CD > Variables et de leur spécifier un environnement.

CI Variables

cache

Cette directive permet de jouer avec du cache. Le cache est intéressant pour spécifier une liste de fichiers et de répertoires à mettre en cache tout le long de votre pipeline. Une fois la pipeline terminée le cache sera détruit.

Plusieurs sous-directives sont possibles :

  • paths : obligatoire, elle permet de spécifier la liste de fichiers et/ou répertoires à mettre en cache
  • key : facultative, elle permet de définir une clé pour la liste de fichiers et/ou de répertoires. Personnellement je n’en ai toujours pas vu l’utilité.
  • untracked : facultative, elle permet de spécifier que les fichiers ne doivent pas être suivis par votre dépôt git en cas d’un push lors de votre pipeline.
  • policy : facultative, elle permet de dire que le cache doit être récupéré ou sauvegardé lors d’un job (push ou pull).
stages:
  - build
  - deploy

job:build:
  stage: build
  image: node:8-alpine
  script: yarn install && yarn build
  cache:
    paths:
      - build # répertoire mis en cache
    policy: push # le cache sera juste sauvegardé, pas de récupération d'un cache existant

job:deploy:
  stage: deploy
  script: make deploy
  cache:
    paths:
      - build
    policy: pull # récupération du cache

artifacts

Les artefacts sont un peu comme du cache mais ils peuvent être récupérés depuis une autre pipeline. Comme pour le cache il faut définir une liste de fichiers ou/et répertoires qui seront sauvegardés par GitLab. Les fichiers sont sauvegardés uniquement si le job réussit.

Nous y retrouvons cinq sous-directives possibles :

  • paths : obligatoire, elle permet de spécifier la liste des fichiers et/ou dossiers à mettre en artifact
  • name: facultative, elle permet de donner un nom à l’artifact. Par défaut elle sera nommée artifacts.zip
  • untracked : facultative, elle permet d’ignorer les fichiers définis dans le fichier .gitignore
  • when : facultative, elle permet de définir quand l’artifact doit être créé. Trois choix possibles on_success, on_failure, always. La valeur on_success est la valeur par défaut.
  • expire_in : facultative, elle permet de définir un temps d’expiration
job:
  script: make build
  artifacts:
    paths:
      - dist
    name: artifact:build
    when: on_success
    expire_in: 1 weeks

dependencies

Cette déclaration fonctionne avec les artifacts, il rend un job dépendant d’un artifact. Si l’artifact a expiré ou a été supprimé / n’existe pas, alors la pipeline échouera.


build:artifact:
  stage: build
  script: echo hello > artifact.txt
  artifacts: # On ajoute un `artifact`
    paths:
      - artifact.txt

deploy:ko:
  stage: deploy
  script: cat artifact.txt
  dependencies: # On lie le job avec 'build:artifact:fail' qui n'existe pas donc la pipeline échouera
    - build:artifact:fail

deploy:ok:
  stage: deploy
  script: cat artifact.txt
  dependencies: # On lie le job avec 'build:artifact' qui existe donc la pipeline n'échouera pas
    - build:artifact

coverage

Cette déclaration permet de spécifier une expression régulière pour récupérer le code coverage pour un job.

...

test:unit:
  script: echo 'Code coverage 13.13'
  coverage: '/Code coverage \d+\.\d+/'

Le code coverage sera visible dans les informations du job dans l’interface web de GitLab :

CI Coverage

Si vous le souhaitez voici un autre article de notre blog écrit par l’astronaute Pouzor sur le code coverage : Ajouter le code coverage sur les MR avec avec GitLab-CI

retry

Cette déclaration permet de ré-exécuter le job en cas d’échec. Il faut indiquer le nombre de fois où vous voulez ré-exécuter le job

job:retry:
  script: echo 'retry'
  retry: 5

include

Pour cette fonctionnalité il vous faudra un compte premium. Cette fonctionnalité permet d’inclure des “templates”. les “templates” peuvent être en local dans votre projet ou à distance.

Les fichiers sont toujours évalués en premier et fusionnés récursivement. Vous pouvez surcharger ou remplacer des déclarations des “templates”.

  • template en local
# template-ci/.lint-template.yml

job:lint:
  stage: lint
  script:
    - yarn lint
  • template à distance
# https://gitlab.com/awesome-project/raw/master/template-ci/.test-template.yml

job:test:
  stage: test
  script:
    - yarn test
  • manifeste principal
# .gitlab-ci.yml

include:
  - '/template-ci/.lint-template.yml'
  - 'https://gitlab.com/awesome-project/raw/master/template-ci/.test-template.yml'

stages:
  - lint
  - test

image: node:9-alpine

job:lint:
  before_script:
    - yarn install

job:test:
  script:
    - yarn install
    - yarn unit

Voici ce que gitlab CI/CD va interpréter :

stages:
  - lint
  - test

image: node:9-alpine

job:lint:
  stage: lint
  before_script: # on surcharge `job:lint` avec `before_script`
    - yarn install
  script:
    - yarn lint

job:test:
  stage: test
  script: # on remplace la déclaration `script` du "template" https://gitlab.com/awesome-project/raw/master/template-ci/.test-template.yml
    - yarn install
    - yarn unit

Ceci peut être intéressant dans le cas où votre manifeste est gros, et donc plus difficile à maintenir.

Anchors

Cette fonctionnalité permet de faire des templates réutilisables plusieurs fois.

.test_template: &test_template
  stage: test
  image: registry.gitlab.com/username/project/php:test
  before_script:
    - composer install -n
  when: on_success

.db_template:
  services:
    - postgres
    - mongo

test:unit:
  <<: *test_template
  script:
    - bin/phpunit --coverage-text --colors=never tests/

test:functional:
  <<: *test_template
  services: *db_template
  script:
    - codecept run functional

Voici ce que gitlab CI/CD va interpréter :

test:unit:
  stage: test
  image: registry.gitlab.com/username/project/php:test
  before_script:
    - composer install -n
  script:
    - bin/phpunit --coverage-text --colors=never tests/
  when: on_success

test:functional:
  stage: test
  image: registry.gitlab.com/username/project/php:test
  services:
    - postgres
    - mongo
  before_script:
    - composer install -n
  script:
    - codecept run functional
  when: on_success

Ressources