Rails + Docker + Gitlab = ❤

Publié il y a presque 3 ans~4 min

Le cycle de vie d'un projet web ne s'arrête pas à son développement ! Reste alors à le publier : c'est la phase de déploiement. Cette étape nécessaire implique pourtant très souvent nombre de complications... Heureusement, nous disposons désormais d'un outillage solide : la CI/CD en est le plus fort exemple, comme ici celle de Gitlab.

Sur la base d'un projet Ruby On Rails dockerisé, nous allons mettre en place les différents éléments nécessaires à notre CI/CD : des fichiers Compose spécifiques à chaque environnement, et bien sûr un fichier .gitlab-ci.yml, pour orchestrer le tout !

Pré-requis

  1. Docker et Docker Compose
  2. Un projet Ruby On Rails dockerisé (cf. cet article)
  3. Un compte Gitlab 😲
  4. Un serveur (DigitalOcean, ...) avec Docker installé

Un projet de démonstration a été créé dans le cadre de cet article, n'hésitez donc pas à vous y référer pour plus de détails (scripts, ...) !

Docker Compose(s)

Sur la base d'un simple fichier docker-compose.yml, nous allons décliner pour l'ensemble des environnements (développement, test et production). Voici un exemple, extrait du projet de démonstration :

1version: '3.0'
2services:
3 db:
4 image: postgres:11-alpine
5 ports:
6 - 5433:5432
7 environment:
8 POSTGRES_PASSWORD: postgres
9
10 webpacker:
11 image: railsondocker_development
12 command: ["./docker/start_webpack_dev.sh"]
13 environment:
14 - NODE_ENV=development
15 - RAILS_ENV=development
16 - WEBPACKER_DEV_SERVER_HOST=0.0.0.0
17 volumes:
18 - .:/railsondocker:cached
19 ports:
20 - 3035:3035
21
22 app:
23 image: railsondocker_development
24 build:
25 context: .
26 args:
27 - PRECOMPILEASSETS=NO
28 environment:
29 - RAILS_ENV=development
30 links:
31 - db
32 - webpacker
33 ports:
34 - 3000:3000
35 volumes:
36 - .:/railsondocker:cached

Il s'agira ici globalement d'ajuster les variables (RAILS_ENV, ...), rien de bien compliqué donc.

Configuration de Gitlab

Le point de départ de la CI/CD de Gitlab est un fichier .gitlab-ci.yml (placé en racine de projet), qui orchestrera les différents scripts via un Runner (un exécuteur de tâche mis à disposition gratuitement par Gitlab, ou personnalisé suivant le besoin).

Ici, nous allons définir trois étapes : test, release et deploy. Chaque étape pourra être spécifique à un environnement, une branche, et surtout pourra conditionner l'exécution de la suivante. Plus (ou presque !) de production "cassée" grâce à cela ! 👍

Paramétrage du dépôt

Avant toute chose, en vue de déployer notre projet sur un serveur externe, nous devons permettre à la CI/CD de Gitlab de travailler dessus. Voici la marche à suivre, laborieuse mais finalement assez simple :

  1. Depuis le serveur, générer une clé SSH (via la commande ssh-keygen -t rsa). S'assurer que la clé publique est bien autorisée pour ce serveur, et enregistrée dans le dossier athorized_keys (ou équivalent).
  2. Dans le dépôt Gitlab, déclarer une variable d'environnement PRODUCTION_SERVER_PRIVATE_KEY correspondant à la clé privée (dans Paramètres > Intégration et livraison continue)
  3. Toujours dans la section des variables d'environnement du dépôt Gitlab, déclarer une variable PRODUCTION_SERVER_IP, avec l'adresse IP du serveur concerné.

Et voilà, le serveur et votre dépôt Gitlab sont reliés ! 🥂 A noter que la manoeuvre sera à répéter pour chaque environnement supplémentaire (staging, ...).

Le fichier Gitlab

Les bases d'un fichier .gitlab-ci.yml sont les étapes (stages) définissant les séquences de scripts, les variables (qui peuvent être des variables d'environnement du dépôt) et bien entendu, les scripts à proprement parler. Là encore, la structure du fichier est assez claire :

1image: docker
2services:
3 - docker:dind
4
5cache:
6 paths:
7 - node_modules
8
9variables:
10 DOCKER_HOST: tcp://docker:2375/
11 DOCKER_DRIVER: overlay2
12 CONTAINER_STABLE_IMAGE: $CI_REGISTRY_IMAGE:stable
13
14stages:
15 - test
16 - release
17 - deploy
18
19before_script:
20 - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
21 - apk add --no-cache py-pip python-dev libffi-dev openssl-dev gcc libc-dev make
22 - pip install docker-compose
23 - docker-compose --version
24
25# ...

Notre fichier commence ici par la déclaration de l’image de référence (ici Docker). S'ajoutent quelques paramétrages (cache, variables, ajout de dépendences, ...) directement issus de la documentation officielle. Nous sommes maintenant prêts à démarrer la configuration de notre CI/CD !

Test

Pour l'étape de test nous déclarons un simple script, s'appuyant sur un fichier Compose spécifique. Concrètement, il s'agit ici simplement d'exécuter les tests (Rspec, ...) puis de purger les images, conteneurs et volumes Docker issus de cette étape :

1test:
2 stage: test
3 script:
4 - docker-compose -f docker-compose.test.yml build --pull
5 - docker-compose -f docker-compose.test.yml run --rm app sh -c "./docker/wait_for_services.sh && bundle exec rake db:create spec"
6 after_script:
7 - docker-compose -f docker-compose.test.yml run --rm app rm -rf tmp/
8 - docker-compose -f docker-compose.test.yml down
9 - docker volume rm `docker volume ls -qf dangling=true`

Relase

C'est ici que se déroule le plus important : nous construisons le conteneur Docker de notre application, avant de le pousser sur le serveur (si tout se passe bien).

1release_stable:
2 stage: release
3 only:
4 - production
5 script:
6 - docker-compose -f docker-compose.production.yml build --pull
7 - docker tag railsondocker_production $CONTAINER_STABLE_IMAGE
8 - docker push $CONTAINER_STABLE_IMAGE

Une ligne ici est particulièrement importante : docker push $CONTAINER_STABLE_IMAGE. Après construction du conteneur, nous sauvegardons en effet celui-ci dans un registre de conteneurs lié à notre dépôt Gitlab. Nous n'aurons ainsi plus qu'à utiliser le conteneur de ce registre pour déployer notre projet sur le serveur.

A cette étape seul le conteneur de l'application nous intéresse. Pour cette raison pas de conteneur de base de données dans le fichier docker-compose.production.yml. Ceci afin d'éviter d'écraser la base de données à chaque déploiement ! 😉

Deploy

Le conteneur Docker de notre application est donc disponible dans le registre du dépôt Gitlab. Pour le déployer, nous allons donc simplement copier un fichier Compose spécifique au déploiement sur le serveur (en nous authentifiant grâce à la clé SSH créée précédemment). Celui-ci orchestrera l'assemblage des différents conteneurs (application issue du registre, base de données), et sera exécuté via le script suivant :

1deploy_stable:
2 stage: deploy
3 only:
4 - production
5 environment: production
6 before_script:
7 - mkdir -p ~/.ssh
8 - echo "$PRODUCTION_SERVER_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
9 - chmod 600 ~/.ssh/id_rsa
10 - which ssh-agent || (apk add openssh-client)
11 - eval $(ssh-agent -s)
12 - ssh-add ~/.ssh/id_rsa
13 - ssh-keyscan -H $PRODUCTION_SERVER_IP >> ~/.ssh/known_hosts
14 script:
15 - scp -rp ./docker-deploy.production.yml root@${PRODUCTION_SERVER_IP}:~/
16 - ssh root@$PRODUCTION_SERVER_IP "docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY};
17 docker pull $CONTAINER_STABLE_IMAGE;
18 docker-compose -f docker-deploy.production.yml stop;
19 docker-compose -f docker-deploy.production.yml rm app --force;
20 docker-compose -f docker-deploy.production.yml up -d"

🎉

Et voilà ! Comme souvent la configuration est la partie la plus délicate pour ce type de projet, mais le jeu en vaut la chandelle : une fois cela fait nous obtenons un projet équipé d'une CI/CD parfaitement fonctionnelle, que l'on pourra faire évoluer en fonction du besoin (ajout d'un environnement de staging, ...). 🙂