Ansible pour déployer des applications
Ansible est un excellent outil de provisioning. L’outil n’est a priori pas prévu pour déployer des applications bien que l’on soit fortement tenté de l’utiliser dans ce but. Ce post traite des problèmes que cela pose et d’une manière de les résoudre.
L’exercice a porté sur le déploiement d’une webapp Java - et donc d’un WAR - dans un Tomcat.
Version initiale et ses défauts
La première version d’un playbook Ansible que j’ai écrite était la reproduction de scripts de déploiement “maison”. Cette version était impérative, assez brutale et pas vraiment idiomatique :
- on vérifie si une instance Tomcat existe
- si l’instance existe, on l’arrête et on la supprime
- on télécharge Tomcat et on l’installe
- on télécharge le WAR et on le déploie dans Tomcat
- on démarre Tomcat.
Voici un extrait du playbook Ansible résultant (sans variables pour simplifier la lecture) :
- name: Test si une instance existe
stat: path=/PROD/mywebapp/bin/catalina.sh
register: p
- name: Arrêt de l'instance
command: /PROD/mywebapp/bin/catalina.sh stop
when: p.stat.isreg is defined and p.stat.isreg == true
- name: Suppression de l'instance
command: rm -rf /PROD/mywebapp removes=/PROD/mywebapp
- name: Téléchargement de Tomcat
get_url:
url=http://archive.apache.org/dist/tomcat/tomcat-7/v7.0.56/bin/apache-tomcat-7.0.56.tar.gz
dest=/tmp/apache-tomcat-7.0.56.tar.gz
- name: Extraction de Tomcat
command: chdir=/tmp tar xvf /tmp/apache-tomcat-7.0.56.tar.gz creates=/tmp/apache-tomcat-7.0.56
- name: Déploiement de Tomcat
command: cp -r /tmp/apache-tomcat-7.0.56 /PROD/mywebapp creates=/PROD/mywebapp
- name: Téléchargement du WAR
get_url:
url=http://.../mywebapp-1.0.war
dest=/tmp/mywebapp-1.0.war
- name: Copie de la webapp dans l'instance
command: mv /tmp/mywebapp-1.0.war /PROD/mywebapp/webapps/mywebapp.war
- name: Démarrage de l'instance
command: nohup /PROD/mywebapp/bin/catalina.sh start
environment:
CATALINA_HOME: /PROD/mywebapp
CATALINA_PID: /PROD/mywebapp/logs/tomcat.pid
- name: Attente du démarrage de l'instance
wait_for: port=8080
Défaut : le playbook n’est pas idempotent
Le défaut principal de la version ci-dessus est qu’elle n’est pas idempotente. Pour rappel, l’idempotence est définie comme suit sur Wikipedia :
En mathématiques et en informatique, le concept d’idempotence signifie essentiellement qu’une opération a le même effet qu’on l’applique une ou plusieurs fois, ou encore qu’en la réappliquant on ne modifiera pas le résultat.
En clair, si notre webapp est déjà déployée et que nous lançons notre playbook, Ansible ne devrait rien modifier et Tomcat ne devrait pas redémarrer. Ce n’est pas le cas puisque, à chaque exécution, l’instance Tomcat est détruite et re-créée.
Nous allons donc chercher à déployer Tomcat ainsi que le WAR uniquement en cas de besoin.
Problématique des mises à jour de l’application
Dans notre recherche d’idempotence, il faudra toutefois prendre en compte la possibilité de mettre à jour l’application :
- si la version déployée est identique à la version décrite dans le playbook, aucune modification ne doit être effectuée ;
- si la version déployée est différente de la version décrite dans le playbook, la nouvelle webapp doit être déployée et Tomcat doit être redémarré.
Multiples installations de Tomcat
Enfin, notre playbook crée une nouvelle installation de Tomcat pour chaque application déployée.
Il est possible de :
- déployer une unique installation de Tomcat :
- un répertoire partagé contenant les répertoires
bin
etlib
- variable
CATALINA_HOME
pointant vers ce répertoire
- un répertoire partagé contenant les répertoires
- créer une instance Tomcat par webapp :
- un répertoire dédié contenant les répertoires
conf
,logs
,webapps
etwork
- variable
CATALINA_BASE
pointant vers ce répertoire
- un répertoire dédié contenant les répertoires
Version modifiée
Installation de Tomcat
Nous commençons donc par créer une liste de tâches repsonsables de l’installation partagée de Tomcat.
La première étape vérifie si Tomcat est installé (module stat). Puis, s’il n’est pas installé (directive when: not st.stat.exists
), Tomcat est téléchargé et installé.
- name: check tomcat
stat:
path=/usr/share/tomcat7/bin
register: st
- name: download tomcat
get_url:
url=http://archive.apache.org/dist/tomcat/tomcat-7/v7.0.56/bin/apache-tomcat-7.0.56.tar.gz
dest=/tmp/apache-tomcat-7.0.56.tar.gz
when: not st.stat.exists
- name: extract tomcat
unarchive:
src=/tmp/apache-tomcat-7.0.56.tar.gz
copy=no
dest=/tmp
when: not st.stat.exists
- name: copy tomcat
shell: cp -r /tmp/apache-tomcat-7.0.56/* /usr/share/tomcat7 creates=/usr/share/tomcat7/bin
when: not st.stat.exists
Création d’une instance Tomcat
Tomcat étant installé system-wide, nous pouvons créer une instance dédié à notre application.
Nous créons l’arborescence nécessaire et nous déployons les fichiers de configuration à partir de templates (module template). Nous déployons également un script d’init et enregistrons un service qui pourra être manipulé avec le module service.
Ici, nous n’utilisons que les modules file et template qui n’effectuent des modifications que lorsque celles-ci sont nécessaires, garantissant ainsi l’idempotence.
- name: create tomcat instance
file:
name={{item}}
state=directory
owner=tomcat7
group=tomcat7
with_items:
- "/PROD/mywebapp"
- "/PROD/mywebapp/conf"
- "/PROD/mywebapp/logs"
- "/PROD/mywebapp/webapps"
- "/PROD/mywebapp/work"
- name: setup conf for tomcat instance
template:
src=tomcat-conf/{{item}}
dest=/PROD/mywebapp/conf/{{item}}
owner=tomcat7
group=tomcat7
with_items:
- catalina.policy
- catalina.properties
- context.xml
- logging.properties
- server.xml
- tomcat-users.xml
- web.xml
- name: install tomcat init script
template:
src=tomcat.sh
dest=/etc/init.d/tomcat-mywebapp
mode=0755
notify:
- register tomcat init script (add)
- register tomcat init script (level)
Notez que les variables CATALINA_HOME
et CATALINA_BASE
sont définies dans le script d’init :
#CATALINA_HOME is the location of the bin files of Tomcat
export CATALINA_HOME=/usr/share/tomcat7
#CATALINA_BASE is the location of the configuration files of this instance of Tomcat
export CATALINA_BASE=/PROD/mywebapp
Déploiement de l’application
Enfin, nous pouvons déployer notre application. Pour rappel, nous souhaitons installer le WAR uniquement si celui-ci a été modifié. Nous allons donc comparer la somme MD5 du WAR installé (s’il existe) avec celle du WAR à déployer. Nous utilisons pour cela le module stat avec l’option get_md5=True
.
Nous utilisons un handler pour redémarrer l’instance Tomcat dans le cas où le WAR est mis à jour (directive notify: restart tomcat
).
- name: get the WAR
get_url:
url=http://.../mywebapp-1.0.war
dest=/tmp/mywebapp-1.0.war
- name: compute the MD5 of the new WAR
stat:
path=/tmp/mywebapp-1.0.war
get_md5=True
register: tmp_war_stat
- name: compute the MD5 of the existing WAR
stat:
path=/PROD/mywebapp/webapps/mywebapp.war
get_md5=True
register: app_war_stat
#- debug: var=tmp_war_stat.md5
#- debug: var=app_war_stat.md5
- name: copy the WAR
shell: cp /tmp/mywebapp-1.0.war /PROD/mywebapp/webapps/mywebapp.war
when: not app_war_stat.stat.md5 is defined or not tmp_war_stat.stat.md5 == app_war_stat.stat.md5
notify: restart tomcat
Le handler de (re)-démarrage de Tomcat :
- name: restart tomcat
service:
name=tomcat-mywebapp
state=restarted
Notez que, pour éviter tout redémarrage de Tomcat lors de la modification du WAR, l’autodeploy
est désactivé dans le server.xml
:
<Host ... autoDeploy="false">
Exécution du playbook
Au premier run, Tomcat est installé, une instance Tomcat est créée, et la webapp est déployée :
Si on relance le playbook sans avoir effectué de modification, aucun changement n’est effectué :
Enfin, si on met à jour le WAR à déployer, le minimum d’actions est effectuée (copie du WAR et redémarrage de Tomcat) :
Conclusion
Le but premier d’Ansible n’est pas déployer des applications mais de provisionner des machines. Pourtant, pour peu qu’on prenne la peine de respecter les conventions (idempotence) et qu’on utilise pleinement les fonctionnalités de l’outil (modules, handlers), on peut déployer très simplement une application. Objectif atteint.
Le playbook complet est disponible sur Github.