Hey ! Bon, ça fait plusieurs projets pour lesquels je me re-coltine la procédure, donc je vais écrire ici, ça sera plus simple pour moi de m’y retrouver dans quelques mois, quand j’aurai de nouveau besoin de paramétrer un bouzin comme ça. Je parle donc ici de CD, ou “continuous deployment”. Alors plusieurs choses avant de commencer. D’abord, la bonne pratique professionnellement est de faire de la CI/CD, à savoir “intégration continue puis déploiement continu”. Ce qui en français signifie “dès que je push/tag (au choix), se lance automatiquement une suite de tests, puis, si elle passe, lance automatiquement un déploiement sur l’environnement de mon choix”. Ensuite, je vais parler ici de bricolage perso, je ne suis pas ops, et je considère, maintenant que mon setup fonctionne pour mes petits projets perso, que j’ai passé suffisamment de temps sur ces considérations. Ensuite, je parle d’utiliser l’outillage de Gitlab pour orchestrer tout ça. Et enfin, je vais ici utiliser Capistrano parce que j’ai envie.

Je me suis assez librement inspiré d’un tuto de la chouette agence Troopers, disponible ici : Déployer une application avec Gitlab CI et Capistrano

C’est parti

Donc pour commencer, je “capify” mon projet, c’est à dire que je crée les fichiers de configuration qui vont permettre à Capistrano de savoir quoi foutre pour déployer.

1
cap install

Ça me crée un fichier Capfile à la racine, et plus important, des fichiers config/deploy.rb, config/deploy/staging.rb et config/deploy/production.rb.

Alors le Capfile, on s’en fout, c’est le config/deploy.rb qui va contenir la configuration commune à tous nos environnements pour déployer. Les fichiers étant dans config/deploy/ contiendront des trucs plus spécifiques au staging et à la prod.

Voici le contenu de mon fichier config/deploy.rb pour une API sous Symfony :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
set :application, "mon_app_api"
set :repo_url, "git@gitlab.mon-gitlab.net:mon_app/api.git"
set :branch, 'master'
set :deploy_to, "/var/www/mon_app/api"
set :keep_releases, 5
append :linked_files, ".env.local"
append :linked_dirs, "var/logs", "public/images", "public/media/cache", "config/jwt"
server ENV['DEPLOY_TO'], user: "deployer-agent"
task :setup_composer do
on roles(:all) do |h|
execute "cd #{current_path} && php -r \"copy('https://getcomposer.org/installer', 'composer-setup.php');\""
execute "cd #{current_path} && php composer-setup.php"
execute "cd #{current_path} && php -r \"unlink('composer-setup.php');\""
end
end
task :install do
on roles(:all) do |h|
execute "cd #{current_path} && APP_ENV=prod ./composer.phar install"
end
end
task :readable_dirs do
on roles(:all) do |h|
execute "cd #{current_path} && chmod -R 777 var/cache/ && sudo chown -R www-data public/images && sudo chown -R www-data public/media/cache"
end
end
after "deploy:publishing", :setup_composer
after :setup_composer, :install
after :install, :readable_dirs

Pas grand chose de foufou, je spécifie où retrouver ma branche, dans quel répertoire déployer, combien de releases je garde, quels sont les fichiers et dossiers à garder d’une release à l’autre (si on drop toutes les images uploadées après chaque release c’est un peu la merde). Et quelques commandes à jouer après avoir déployé (composer install, tout ça). Je vais pas m’attarder sur Capistrano, c’est pas complètement le sujet. La seule ligne qui doit faire tilt ici est le server ENV['DEPLOY_TO'], user: "deployer-agent". Je vais chercher l’IP du serveur sur lequel je déploie à partir d’une variable d’environnement appelée DEPLOY_TO et ainsi ne commiterai pas l’IP des serveurs sur lesquels je déploie.

Dans les fichiers spécifiques aux environnements, je vais prendre l’exemple de config/deploy/production.rb, j’ai simplement ça :

1
set :stage, :production

Pour la mise en place avant le premier déploiement, il faudra lancer un DEPLOY_TO=X.X.X.X cap production deploy:check (où X.X.X.X est la véritable IP du serveur où on veut mettre les trucs en place, évidemment). Ça va créer toute mon arborescence de fichiers sur le serveur final, ça fait en somme un peu partie de la configuration de Capistrano pour le projet.

Ok, j’ai préparé Capistrano pour mon projet. Maintenant, Gitlab ! Wouhou ! C’est là que ça devient un peu relou. Tout d’abord, sur le serveur de prod, je me crée un user “deployer-agent”, qui n’aura pour but que de déployer, et je lui donne des droits sur mon dossier de déploiement :

1
2
3
4
5
adduser deployer-agent
setfacl -R -m u:www-data:rwX -m u:deployer-agent:rwX /var/www/mon_app
setfacl -dR -m u:www-data:rwX -m u:deployer-agent:rwX /var/www/mon_app
su deployer-agent
ssh-keygen -f /home/deployer-agent/.ssh/id_rsa_deployer

Attention à ne pas mettre de passphrase pour la clé. Ensuite, je copie le contenu de la clé publique générée dans les authorized_keys de mon serveur. Oui ça fait un peu schyzo comme ça mais tout va bien.

1
cat /home/deployer-agent/.ssh/id_rsa_deployer.pub >> /home/deployer-agent/.ssh/authorized_keys

Cette clé publique, je l’ajoute également à mon compte sur Gitlab, pour qu’elle soit autorisée à cloner des repos en SSH (dans Settings -> SSH Keys).

Ensuite, toujours dans Gitlab, je vais dans mon projet, puis Settings -> CI/CD -> Variables et j’ajoute deux variables qui sont :

  • “PRODUCTION_DEPLOYMENT_PRIVATE_KEY” avec la valeur qui équivaut au contenu de /home/deployer-agent/.ssh/id_rsa_deployer (la clé privée)
  • “PRODUCTION_SERVER” qui est l’IP de notre serveur de déploiement

Ensuite, je supprime les deux clés du serveur, je n’en ai plus besoin.

1
rm /home/deployer-agent/.ssh/id_rsa_deployer.pub /home/deployer-agent/.ssh/id_rsa_deployer

Pour continuer, j’ajoute un runner. C’est un truc qui se lance au moment où on l’aura défini en configuration et dans lequel toutes nos tâches vont se jouer. C’est la commande gitlab-runner register. Je me laisse porter par les questions, et j’y réponds avec docker, et les infos contenues dans la page Settings -> CI/CD -> Runners dans Gitlab. Pour l’image par défaut, je mets ce qui est proposé, pour le nom un truc adéquat genre “my_app deployer”, truc du genre.

Bon, et comment qu’c’est qu’on dit qu’il faut appeler Capistrano après qu’on a commit sur master pour qu’il déploie au bon endroit ? Eh ben dans le fichier .gitlab-ci.yml que je fous à la racine de mon projet. Et qui contient ces lignes :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
stages:
- deploy
.deploy: &deploy
image: ruby:latest
stage: deploy
before_script:
- gem install capistrano
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
- eval $(ssh-agent -s)
- 'ssh-add <(echo "$pkey")'
- mkdir -p ~/.ssh
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
- 'git clone "git@${CI_SERVER_HOST}:${CI_PROJECT_PATH}.git" $CI_PROJECT_DIR'
- 'cd "${CI_PROJECT_DIR}"'
script:
- cap $env deploy
deploy:production:
<<: *deploy
variables:
env: production
DEPLOY_TO: $PRODUCTION_SERVER
pkey: $PRODUCTION_DEPLOYMENT_PRIVATE_KEY
GIT_STRATEGY: "none"
only:
- tags
deploy:staging:
<<: *deploy
variables:
env: staging
DEPLOY_TO: $STAGING_SERVER
pkey: $STAGING_DEPLOYMENT_PRIVATE_KEY
GIT_STRATEGY: "none"
only:
- master

Allez, je détaille.

La propriété “stages”, c’est pour spécifier quelles étapes seront faites lorsque la CI/CD sera déclenchée. Ici j’ai juste deploy mais normalement on a aussi test (c’est la partie CI) et build.

La ligne “.deploy: &deploy” veut dire qu’on crée une ancre appelée “deploy”, à laquelle on réfèrera en tant que “deploy”. Bon, c’est pas clair parce que j’ai mis “deploy” partout. Si ça avait été “.plop: &plip”, le nom aurait été “plop” et la référence “plip”. Bon, passons. Ce que ça veut dire, c’est que tout ce qui suit cette ancre servira de configuration pour les définitions qui en hériteront. C’est un template quoi.

L’image “ruby:latest” est pour dire qu’on utilisera l’image “ruby:latest” pour déployer. Pourquoi Ruby ? Parce que Capistrano c’est du Ruby. “stage:deploy” veut dire que ceci considère l’étape de déploiement, pas de test ou de build. Le “before_script” et “script” sont toutes les étapes qui vont être effectuées au sein de notre container docker dans le but de déployer. Je fais un peu trop de trucs dans le “before_script”, mais avec mon instance de Gitlab, je sais pas trop ce que j’ai foutu, mais je ne peux pas cloner des repos en https dans mon container. Alors je clone moi-même, en SSH. Donc dans l’ordre, j’installe Capistrano dans le container, un client SSH, je lui définis la clé privée passée en paramètre (celle qu’on a foutu en variable dans Gitlab plus haut. Pour ça que je l’ai mise dans les authorized_keys sur le serveur de déploiement, et pour ça qu’elle est autorisée dans mon Gitlab également). Je désactive les autres trucs qui nécessitent de l’interaction dans SSH, puisqu’il faut que tout soit automatique. Et donc je clone via SSH et je me rends dans le dossier. Ces deux dernières étapes sont normalement comprises dans le process standard de déploiement de Gitlab, mais… pas chez moi. Enfin, le “script” de déploiement, je lance un cap deploy, qui se chargera de faire le déploiement.

Pfiou ! Et maintenant, dernière section, le “deploy:production:”. Ça pourrait s’appeler n’importe comment, mais j’ai décidé de vraiment mettre “deploy” partout. Donc ça, c’est un job. Il va hériter de “deploy”, donc de tout ce qu’on a défini au-dessus (ce serait de “plip” si l’ancre avait été “.plop: &plip”). Et on lui passe les variables “env” à “production”, pour qu’il sache qu’il faut faire “cap production deploy” et qu’il faut utiliser deploy/production.rb, “DEPLOY_TO” à “$PRODUCTION_SERVER” pour que le serveur de déploiement défini dans config/deploy.rb, qui s’appuie sur une variable d’environnement, soit celui qui est défini dans la variable Gitlab “PRODUCTION_SERVER” (l’IP du serveur de prod), et enfin “pkey” à “PRODUCTION_DEPLOYMENT_PRIVATE_KEY” pour setter la bonne clé SSH privée à notre container. Puis, “GIT_STRATEGY” à “none”, c’est pour dire à Gitlab de ne pas cloner notre repo, on le fera nous-mêmes. Et enfin, “only: -tags”, c’est pour dire “tu lances ça que quand on crée un nouveau tag”.

Le bloc du bas définit un nouveau job de deploy, mais cette fois en staging, avec des variables de staging, et qui ne se déclenche qu’au push sur master.

Bon, c’était un peu fatounet, mais ça a le mérite de fonctionner. Au moins pour moi. Allez, la bise.

(PS. Merci à Matthieu pour ses retours !)