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
- Docker et Docker Compose
- Un projet Ruby On Rails dockerisé (cf. cet article)
- Un compte Gitlab 😲
- 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-alpine5 ports:6 - 5433:54327 environment:8 POSTGRES_PASSWORD: postgres910 webpacker:11 image: railsondocker_development12 command: ["./docker/start_webpack_dev.sh"]13 environment:14 - NODE_ENV=development15 - RAILS_ENV=development16 - WEBPACKER_DEV_SERVER_HOST=0.0.0.017 volumes:18 - .:/railsondocker:cached19 ports:20 - 3035:30352122 app:23 image: railsondocker_development24 build:25 context: .26 args:27 - PRECOMPILEASSETS=NO28 environment:29 - RAILS_ENV=development30 links:31 - db32 - webpacker33 ports:34 - 3000:300035 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 :
- 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 dossierathorized_keys
(ou équivalent). - 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) - 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: docker2services:3 - docker:dind45cache:6 paths:7 - node_modules89variables:10 DOCKER_HOST: tcp://docker:2375/11 DOCKER_DRIVER: overlay212 CONTAINER_STABLE_IMAGE: $CI_REGISTRY_IMAGE:stable1314stages:15 - test16 - release17 - deploy1819before_script:20 - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY21 - apk add --no-cache py-pip python-dev libffi-dev openssl-dev gcc libc-dev make22 - pip install docker-compose23 - docker-compose --version2425# ...
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: test3 script:4 - docker-compose -f docker-compose.test.yml build --pull5 - 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 down9 - 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: release3 only:4 - production5 script:6 - docker-compose -f docker-compose.production.yml build --pull7 - docker tag railsondocker_production $CONTAINER_STABLE_IMAGE8 - 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: deploy3 only:4 - production5 environment: production6 before_script:7 - mkdir -p ~/.ssh8 - echo "$PRODUCTION_SERVER_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa9 - chmod 600 ~/.ssh/id_rsa10 - which ssh-agent || (apk add openssh-client)11 - eval $(ssh-agent -s)12 - ssh-add ~/.ssh/id_rsa13 - ssh-keyscan -H $PRODUCTION_SERVER_IP >> ~/.ssh/known_hosts14 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, ...). 🙂