Bien externaliser la configuration de votre application est la clé qui ouvre les portes de l’automatisation des déploiements, du déploiement continu, des astreintes du dimanche sans appels des équipes de production, des transferts de connaissance de fin de mission sans lendemains, etc. À chaque fois qu’une application Web est déployée sur un environnement alors que sa configuration n’est pas externalisée, un chaton meurt ; externaliser vos conf est donc également une bonne action envers tous les chatons du monde.
Dans cet article, je vais commencer par rappeler rapidement les stratégies de reconfiguration que l’on peut croiser dans ce monde sauvage avec leurs avantages et leurs inconvénients. Je présenterai ensuite en détail une de ces stratégies, appliquée à une application web basée sur Spring. Les principes de cette stratégie peuvent être résumés par cette citation :
One WAR to package them all, One WAR to serve them all,
One WAR to deploy them all and in JNDI bind them.
Les stratégies de configuration
Il existe plusieurs stratégies de configuration des applications en fonction de leur environnement de déploiement. Elles peuvent toutes être décrites par l’une des approches suivantes :
- Configuration manuelle,
- Duplication du livrable,
- Duplication de la configuration dans le livrable,
- Externalisation totale.
Configuration manuelle
La stratégie de configuration manuelle consiste à ouvrir le livrable et à changer manuellement les valeurs de configuration qui doivent être adaptées à un nouvel environnement de déploiement.
Cette stratégie est extrêmement risquée :
- Elle implique l’intervention de l’être humain pour effectuer sans erreurs des tâches très répétitives. Même s’il ne s’agit que de copier-coller un fichier dans le bon dossier puis de refaire lepackage, le risque d’erreur est présent.
- L’information de la manipulation à réaliser peut être facilement perdue lors d’un départ et sa rétro-analyse sera sans doute très complexe.
- La personne qui effectue la manipulation pour les environnements sécurisés doit avoir les habilitations correspondantes au risque d’exposer le SI.
- Le livrable testé sur chaque environnement est différent. Lepackage validé en recette ne sera pas celui monté en production. Difficile de mettre en place des contrôles de signature et de promotion depackages dans ces conditions.
- Toute modification d’environnement nécessite de réouvrir le livrable, de faire le changement à la main et de lerepackager.
Cette approche doit être évitée autant que possible. Elle peut éventuellement être justifiée en tout début de projet à condition d’être très vite remplacée par une stratégie industrialisée.
Duplication du livrable
Étant donné les outils debuild modernes (maven, sbt, gradle, etc.), il est relativement simple de mettre en place un processus debuild répétable. Il suffit alors de paramétrer le processus pour qu’il embarque la configuration de l’environnement cible.
Cette stratégie est déjà plus sûre que la précédente mais elle présente quand même des défauts importants :
- Le livrable déployé dans chaque environnement sera différent. Il ne sera pas possible d’utiliser la signature du livrable de test pour identifier le livrable de production.
- Toute modification du livrable implique le repackaging et la relivraison.
- Les processus debuilds sont généralement mis en œuvre par les développeurs qui ont ainsi accès aux paramétrages de production (logins, mots de passe, etc.).
Duplication de la configuration dans le livrable
Si le livrable contient toutes les configurations possibles, il ne reste qu’à lui faire passer une propriété pour qu’il puisse sélectionner la configuration adaptée. Faire passer une unique propriété est assez simple, que ce soit par une variable d’environnement ou par une propriété Java passée en argument de la JVM.
Il reste deux défauts :
- Toute modification d’environnement nécessite un repackaging et une relivraison.
- Puisque toutes les configurations sont dans lepackage, les développeurs ont nécessairement accès à la configuration de production (logins, mots de passe, etc.).
Externalisation totale
Il existe deux variantes pour cette stratégie. La première stratégie se base sur une variable d’environnement ou sur une propriété Java pour faire passer le chemin vers la configuration à l’application. Cette approche est à privilégier pour les applications de type client lourd ou les batchs du fait de sa simplicité. Elle est moins adaptée à des webapps qui vont potentiellement partager leur JVM, leur serveur et donc le namespace
de l’environnement avec d’autres applications.
La seconde variante utilise des binding JNDI pour découpler l’application de sa configuration. Cette seconde approche est à privilégier pour les webapps. Elle ne nécessite pas de modification des scripts de démarrage du serveur, les configurations de différentes applications peuvent facilement être mutualisées ou isolées en fonction des besoins. C’est cette approche que je vous propose de mettre en œuvre maintenant.
Étude de cas : Une webapp basée sur Spring
Pour l’exercice, nous allons reprendre une webapp triviale créée pour l’occasion. Le code source de son état initial est disponible sur github. Dans cette version initiale, les dépendances de l’application sur l’environnement sont paramétrées dans des fichiers de configuration embarqués dans le livrable final. Plus précisément, nous y trouverons le nom de l’environnement actuel et les paramètres de connexion à la base de données.
État initial
La déclaration à la base de données est faite avec la forme classique que l’on peut retrouver dans les applications exemples de spring :
- src/main/webapp/WEB-INF/applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="..." > <!-- DATASOURCE DEFINITION --> <context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close" p:driverClass="${jdbc.driverClassName}" p:jdbcUrl="${jdbc.url}" p:user="${jdbc.username}" p:password="${jdbc.password}"/> </beans>
Le fichier de properties correspondant :
- src/main/resources/jdbc.properties:
jdbc.driverClassName=org.hsqldb.jdbc.JDBCDriver jdbc.url=jdbc:hsqldb:file:/tmp/xweb-dev.db jdbc.username=sa jdbc.password=
Pour la servlet Dispatcher de Spring, nous n’allons déclarer qu’une propriété permettant d’afficher l’environnement dans lequel on se trouve. On pourrait imaginer que notre application utilise des webservices pour lesquels il faut spécifier des serveurs différents en fonction de l’environnement de déploiement. La dépendance au fichier de properties se trouve ligne 3 :
- src/main/webapp/WEB-INF/SpringDispatcher-servlet.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="..." > <context:property-placeholder location="classpath:env.properties"/> <context:annotation-config/> <context:component-scan base-package="fr.xebia.xweb"/> <mvc:annotation-driven/> <bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> <property name="suffix" value=".jsp"/> </bean> </beans>
Le fichier de properties:
- src/main/resources/env.properties:
env=dev
Externaliser la connexion à la base de données
L’externalisation de la connexion à la base de données est l’étape la plus simple à mettre en œuvre car elle est pleinement supportée dans Spring depuis longtemps (présent en standard depuis Spring 2.0). Le code source de cette étape est disponible sur github sous forme d’un commit séparé. Voici comment nous allons procéder :
- Déclarer la connexion à la base comme une ressource JNDI,
- Configurer Spring pour utiliser la ressource JNDI.
Déclarer la connexion comme une ressource JNDI
Notre application web en mode développement s’exécute dans Jetty. Déclarons une source de données de niveau conteneur. Pour cela, créons un fichier de configuration pour le conteneur :
- src/dev/jetty-config.xml:
<?xml version="1.0"?> <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd"> <Configure id="Server" class="org.eclipse.jetty.server.Server"> <Array id="plusConfig" type="java.lang.String"> <Item>org.eclipse.jetty.webapp.WebInfConfiguration</Item> <Item>org.eclipse.jetty.webapp.WebXmlConfiguration</Item> <Item>org.eclipse.jetty.webapp.MetaInfConfiguration</Item> <Item>org.eclipse.jetty.webapp.FragmentConfiguration</Item> <Item>org.eclipse.jetty.plus.webapp.EnvConfiguration</Item><!-- add for jndi --> <Item>org.eclipse.jetty.plus.webapp.PlusConfiguration</Item><!-- add for jndi --> <Item>org.eclipse.jetty.webapp.JettyWebXmlConfiguration</Item> <!-- not needed for jetty-8 --> <!-- <Item>org.eclipse.jetty.webapp.TagLibConfiguration</Item> --> </Array> <New id="dataSource" class="org.eclipse.jetty.plus.jndi.EnvEntry"> <Arg>jdbc/dataSource</Arg> <Arg> <New class="com.mchange.v2.c3p0.ComboPooledDataSource"> <Set name="driverClass">org.hsqldb.jdbc.JDBCDriver</Set> <Set name="jdbcUrl">jdbc:hsqldb:file:/tmp/xweb-dev.db</Set> <Set name="user">sa</Set> <Set name="password"></Set> </New> </Arg> <Arg type="boolean">true</Arg> </New> </Configure>
La section Array
permet de configurer les modules du serveur qui seront activés. Il faut ajouter EnvConfiguration
et PlusConfiguration
pour activer le registre JNDI. Ensuite, nous créons unedatasource enregistrée sous le nom jdbc/dataSource
. C’est exactement la mêmedatasource que celle qui était configurée à travers le fichier jdbc.properties précédement. Elle sera disponible avec l’URL complète java:comp/env/jdbc/dataSource
.
Nous pouvons vérifier que notredatasource est bien enregistrée dans le contexte en démarrant l’application et en inspectant le contenu du contexte :
\__/ \__/jdbc/ \__/jdbc/dataSource => org.eclipse.jetty.plus.jndi.EnvEntry jdbc/ jdbc/dataSource => javax.naming.Reference
Il ne reste plus qu’à configurer Spring pour utiliser notre nouvelledatasource.
Configurer Spring pour utiliser unedatasource JNDI
Spring support nativement ce type de configuration. Il suffit de changer notre fichier applicationContext.xml comme suit :
- src/main/webapp/WEB-INF/applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="..." > <jee:jndi-lookup id="dataSource" jndi-name="jdbc/dataSource"/> </beans>
Votre application utilise désormais unedatasource hébergée dans JNDI. Le même WAR peut être déployé sur n’importe quel serveur déclarant unedatasource sous le nom jdbc/dataSource
et trouver sa connexion à la base de données.
Externaliser les fichiers properties de configuration
Il existe trois possibilités pour l’externalisation des properties :
- Déclarer un nom JNDI pour chaque paire clé-valeur.
- Déclarer un objet de type
Properties
qui contient la liste des paires. - Déclarer un nom JNDI pour le chemin d’accès à un fichier de properties.
La première approche peut être efficace s’il n’y a pas trop de paires à déclarer, dans le cas contraire cette technique va rapidement aboutir à une pollution du contexte JNDI. La seconde consiste à déclarer un objet de type Properties
dans le namespace. À moins de disposer d’une factory permettant de créer cet objet à partir d’un fichier de properties existant, il faudra redéclarer toutes les paires dans la déclaration de cet objet. Nous allons donc explorer la troisième solution qui nous permet de simplement copier notre fichier existant à un emplacement sur le serveur et d’en référencer le chemin dans JNDI.
Une fois cette étape réalisée, il restera à le consommer dans Spring. La version 3.1 de ce dernier offre de nouveaux mécanismes de résolution des properties qui rendent cette approche particulièrement concise, nous verrons ensuite une variante pour les autres versions de Spring. Le code est, là encore, disponible sous la forme d’un commit spécifique sur Github.
Déclarer le chemin du fichier de properties dans JNDI
Nous allons ici déclarer la valeur dans un contexte JNDI spécifique à la webapp. Pour cela, nous allons devoir déclarer un fichier de contexte de niveau webapp auprès de Jetty :
- src/dev/jetty-xweb.xml :
<?xml version="1.0"?> <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd"> <Configure id="xweb" class="org.eclipse.jetty.webapp.WebAppContext"> <New id="envPropertiePath" class="org.eclipse.jetty.plus.jndi.EnvEntry"> <Arg>envPropertiePath</Arg> <Arg type="java.lang.String">file:///Users/jean/dev/jdev/src/work/art_externalize_conf/xweb/src/main/resources/env.properties</Arg> <Arg type="boolean">true</Arg> </New> </Configure>
L’inspection de notre contexte JNDI avec la page dumpjndi.jsp montre notre nouvelle ressource de type String ainsi que sa valeur, prête à être consommée :
\__/ \__/jdbc/ \__/jdbc/dataSource => org.eclipse.jetty.plus.jndi.EnvEntry \__/envPropertiePath => org.eclipse.jetty.plus.jndi.EnvEntry jdbc/ jdbc/dataSource => javax.naming.Reference envPropertiePath => java.lang.String[file:///path/to/xweb/src/main/resources/env.properties]
Configurer Spring 3.1 pour utiliser un chemin fourni par JNDI
Spring 3.1 offre un nouveau mécanisme de gestion des variations de contexte applicatif en fonction de l’environnement. Le mécanisme est assez complexe avec entre autre un système de profils, mais il offre une fonctionnalité qui nous interesse : celle d’être capable d’utiliser une source JNDI pour faire de la substitution de placeholder dans le reste de la configuration. Il est donc possible d’écrire <bean attribute=${java:comp/env/name}.../>
dans un fichier XML. Nous n’allons pas explorer les profils de configuration car ils s’approchent plus d’une implémentation de type duplication de configuration dans le livrable.
Dans le cas d’une webapp, le contexte créé par Spring contient automatiquement un DefaultWebEnvironment
. Ce type d’environement déclare par défaut une JNDIPropertySource
. Nous pouvons donc simplement configurer notre PropertyPlaceHolder comme suit :
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="..." > <util:properties id="myprops" location="${java:comp/env/envPropertiePath}"/> <context:property-placeholder properties-ref="myprops"/> <context:annotation-config/> <!-- ... --> </beans>
Notre objectif est atteint : l’application web utilise désormais les ressources qui lui sont données par le serveur d’application pour se configurer.
Variante pour les versions de Spring avant 3.1
Pour configurer une application dans une version de Spring précédant la version 3.1, il est nécessaire de réaliser quelques ajustements. Mais le principe reste le même. Le code de cette variante est disponible sur github.
Le mécanisme de transformation en objet properties est un peu différent. Il faut changer le chemin du fichier de configuration (ligne 7) :
- src/dev/jetty-xweb.xml:
<?xml version="1.0"?> <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd"> <Configure id="xweb" class="org.eclipse.jetty.webapp.WebAppContext"> <New id="envPropertiePath" class="org.eclipse.jetty.plus.jndi.EnvEntry"> <Arg>envPropertiePath</Arg> <Arg type="java.lang.String">/path/to/xweb/src/main/resources/env.properties</Arg> <Arg type="boolean">true</Arg> </New> </Configure>
Il faut ensuite faire en sorte que Spring puisse l’utiliser :
- src/main/webapp/WEB-INF/SpringDispatcher-servlet.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="..." > <bean id="placeholderProperties" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <bean class="org.springframework.core.io.FileSystemResource"> <constructor-arg> <jee:jndi-lookup id="dataSource" jndi-name="jdbc/dataSource"/> </constructor-arg> </bean> </list> </property> </bean> <context:annotation-config/> <!-- ... --> </bean> </beans>
La propriété locations
de la classe PropertyPlaceholderConfigurer
a la signature suivante :
private Resource[] locations;
Malheureusement, le résultat de notre résolution JNDI est une String
. Il faut donc la convertir en Resource
. C’est le résultat obtenu en passant la chaîne en argument au constructeur de la classe utilitaire FileSystemResource
de Spring.
Notre objectif est là encore atteint : l’application web utilise désormais les ressources qui lui sont données par le serveur d’application pour se configurer.
Conclusion
Nous avons démontré qu’il est relativement facile de construire un livrable, ici un WAR, complètement découplé de sa configuration par le biais du contexte JNDI. Les avantages de ce type de livrables sont nombreux, aussi bien pour les équipes de développement que pour les équipes d’exploitation.
Pour les développeurs, déjà, cela signifie que le package exécuté en dev est le même que celui exécuté en intégration, en recette et finalement en production. Pas besoin pour eux de gérer d’innombrables profiles maven pour tous les environnements de déploiement, pas besoin non plus de jouer avec les répertoires de sources pour inclure les bons fichiers de configuration dans le classpath. Il y a juste à configurer une fois le serveur de développement pour qu’il expose les fichiers de configuration dans son contexte. Il devient alors possible d’automatiser totalement lesbuilds et les livraisons au niveau d’un serveur d’intégration continue.
Les équipes d’exploitation gagnent en liberté pour la gestion de leur parc de serveurs. Si une base de données ou un serveur de webservices doit changer de machine, il suffit de changer les fichiers de configuration des applications dépendantes et de redémarrer les instances. Pas besoin de demander aux équipes de développement de préparer une version et de la faire recetter par les équipes de test. Tout le monde gagne du temps. Elles gagnent aussi en sécurité : pas besoin de donner aux équipes de développement les logins et mots de passe de la base de données de production par exemple.
Certaines actions à la croisée des périmètres de ces deux équipes s’en trouvent simplifiées. Un bug survient en production ? Il suffit de mettre à jour le fichier de configuration du framework de log. Dans le pire des cas, il faudra aussi redémarrer l’instance, dans le meilleur, le framework détectera la modification et activera les niveaux de logs demandés. La documentation de livraison de votre application devient triviale : il suffit de déposer le WAR, aucune manipulation manuelle. Les équipes de production peuvent facilement le scripter. En cas d’ajout de variable de configuration, elles peuvent être déclarées par les études et positionnées en avance de la livraison. Si une variable n’a pas la bonne valeur, pas besoin d’annuler la livraison, il est simple de la corriger et de relancer l’application.
Au delà de ces problèmes courants, vous pouvez imaginer gagner en vitesse et en réactivité dans vos livraisons, jusqu’à passer en déploiement continu avec des mécanismes de promotion de package d’un environnement à l’autre. Le package étant binairement identique vous pouvez l’identifier grâce à une signature.
N’attendez plus pour sauver des chatons, externalisez vos configurations !
Liens utiles
- http://blog.springsource.org/2011/06/10/spring-3-1-m2-configuration-enhancements/
- http://stackoverflow.com/questions/8648349/jeejndi-lookup-default-value-and-the-use-of-classpath
- http://forum.springsource.org/showthread.php?14361-PropertyPlaceholderConfigurer-search-in-JNDI
- http://static.springsource.org/spring/docs/2.5.x/reference/xsd-config.html
- http://wiki.eclipse.org/Jetty/Feature/JNDI
- http://wiki.eclipse.org/Jetty/Feature/Jetty_Maven_Plugin
- http://tomcat.apache.org/tomcat-7.0-doc/jndi-datasource-examples-howto.html
- http://tomcat.apache.org/tomcat-7.0-doc/jndi-resources-howto.html