Livraison sur environnements multiples avec Maven ... ou pas

Il s'agit d'un problème récurrent lorsque l'on développe une application JEE et pour lequel on n'est jamais vraiment au point : "comment gérer la configuration de mon application sur les différents environnements ?" Pourtant on dispose de tous les ingrédients pour y parvenir, et Maven n'est pas forcément le seul...

Depuis la nuit des temps

J'exagère un peu mais depuis qu'on développe des applications JEE, on doit gérer différents environnements :

  • Le poste de travail du développeur
  • Le serveur d'assemblage
  • Le serveur de pré-prod
  • Le serveur de prod
  • ... et parfois d'autres intermédiaires

La plupart du temps, on maintenait plusieurs fichiers properties et au moment de générer le package, c'était la galère : on prenait le bon fichier ("quel environnement je livre déjà ?"), on le copiait dans le bon répertoire ("euh, quel environnement j'ai dit ?"). Avec Ant, on s'en sortait plus ou moins automatiquement mais au prix de nombreuses lignes d'XML!

Maven est arrivé...

... sans s'presser ! Et surtout sans rien révolutionner. Du moins dans les premiers temps car on refaisait avec Maven 1 (à coup de scripts jelly) ce qu'on faisait avant avec Ant. Mais l'arrivée des profiles a changé la donne !

Profiles et ressources

Je ne vais pas refaire le chapitre 11 de l'excellent livre de sonatype ni reprendre la doc Maven à 0 mais pour résumer, il est possible d'avoir différent profils, qui comme leurs noms l'indiquent, permettent de définir plusieurs configurations et donc de gérer plusieurs environnements. Pour chaque profile, on peut définir des dépendances particulières, des plugins particuliers ... mais surtout un build particulier et notamment les ressources (voir ici pour le détail du contenu de la balise profiles).

Le plus simple pour comprendre est encore de prendre un exemple :

<project>
  [...]
  <profiles>
    <profile>
      <id>dev</id>
      <activation>
        <activebydefault>true</activebydefault>
      </activation>
      <build>
        <resources>
          <resource>
            <directory>${basedir}/src/main/resources_dev</directory>
          </resource>
        </resources>
        [...]
      </build>
      [...]
    </profile>
    <profile>
      <id>prod</id>
      <build>
        <resources>
          <resource>
            <directory>${basedir}/src/main/resources_prod</directory>
          </resource>
        </resources>
        [...]
      </build>
      [...]
    </profile>
  </profiles>
 </project>

Et on a donc une arborescence qui ressemble à ça :

MyProject | - src
| - main
| - resources
- common.properties | - resources_dev
- specific.properties | - resources_prod - specific.properties

Ainsi, sur votre poste de travail, vous n'aurez qu'à faire : mvn package pour que Maven sélectionne (par défaut : activeByDefault) le profile de dev et donc avoir en sortie les fichiers common.properties (qui provient par convention du répertoire src/main/resources) et specific.properties provenant du répertoire src/main/resources_dev.

Ensuite, lorsque vous devrez livrer votre application, vous devrez sélectionner le profile de prod et donc exécuter mvn package **-Pprod**. En sortie, vous aurez toujours votre common.properties, mais par contre vous aurez le specific provenant de src/main/resources_prod.

Avantage : On peut sélectionner différents fichiers de configuration en fonction de l'environnement sur lequel on veut déployer.

Inconvénients :

  • Lorsque vous aurez fait vos tests sur un environnement donné (préprod par exemple) avec votre fichier "specific" de préprod, vous devrez faire un nouveau package avec votre fichier "specific" de prod. Vous ne constatez pas un problème ? Effectivement, l'application testée et validée en préprod n'est pas la même que vous livrez en prod, et donc vous n'êtes pas sûr qu'elle fonctionnera !
  • Il faut maintenir plusieurs fichiers et ne pas oublier d'ajouter les variables dans chacun d'eux.
  • La configuration est faite en dur dans vos fichiers. Par conséquent vous devez connaître chacun des environnements. Hors, il est rare de connaître les paramètres de la base de donnée de production...

Profiles et filtres

Il s'agit là encore d'une nouvelle possibilité bien pratique de Maven, filtrer les différentes propriétés de vos ressources. En clair, vous avez :

  • Un fichier properties, un fichier xml, peu importe tant qu'il se situe dans un répertoire de ressources référencé dans le pom (src/main/resources par défaut).
  • Dans ce fichier de configuration quelconque, vous n'utilisez pas de valeurs en dur mais des variables du style ${mavariable}.
  • Et dans le pom, vous définissez les différentes valeurs possibles pour ces variables en fonction des environnements.

Exemple : je reprends l'exemple fourni par sonatype dans son livre, vous pouvez l'avoir en détail ici.

Vous avez un fichier Spring qui définit une datasource, et comme je le disais des variables ... variables :

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
    <bean id="someDao" class="com.example.SomeDao">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <bean id="dataSource" destroy-method="close"
             class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>

Pour faire simple, ce fichier se trouve dans le répertoire par défaut src/main/resources. Et donc associé à cela, vous avez évidemment le pom.xml magique :

<project>
  [...]
  <!--
     On définit ici les valeurs par défaut des propriétés
     qui apparaîtront dans le fichier Spring.
   --> 
 <properties>
    <jdbc .driverClassName>com.mysql.jdbc.Driver
    </jdbc><jdbc .url>jdbc:mysql://localhost:3306/development_db
    </jdbc><jdbc .username>dev_user
    </jdbc><jdbc .password>s3cr3tw0rd
 </jdbc></properties>
  [...]
  <!--
    On est obligé de redéfinir la ressource car on active le filtrage. 
    Cet élément est indispensable puisqu'il permet de dire à Maven
    que les ressources se trouvant dans ce répertoire doivent être
    filtrées, c'est à dire parsées pour remplacer les variables par
    des valeurs.
   -->
  <build>
    <resources>
      <resource>src/main/resources</resource>
      <filtering>true</filtering>
    </resources>
  </build>
  [...]  
<!--
    On peut ensuite définir des valeurs particulières en fonction
    d'un profile, et donc d'un environnement particulier.
   -->
  <profiles>
    <profile>
      <id>production</id>
      <properties>
        <jdbc .driverClassName>oracle.jdbc.driver.OracleDriver
        </jdbc><jdbc .url>jdbc:oracle:thin:@proddb01:1521:PROD
        </jdbc><jdbc .username>prod_user
        </jdbc><jdbc .password>s00p3rs3cr3t
      </jdbc></properties>
    </profile>
  </profiles>
 </project>

Avantage par rapport à la solution précédente : On ne maintient qu'un seul fichier, tout est dans le pom.xml.

Inconvénients : Par contre, on se retrouve avec les autres problèmes : un package testé avec un profile peut ne pas marcher avec un autre profile. De même, il vous faudra connaître l'environnement pour configurer correctement l'application.

Pas mal mais pas idéal !

Les profiles, c'est pratique, ça permet de simplement gérer différents environnements (simplement mais avec pas mal de XML supplémentaire dans le pom). Le problème, nous l'avons vu, c'est que cela nécessite de connaître les environnements sur lesquels sera déployée l'application, chose relativement rare dans les grandes sociétés, où la production gère la plupart des environnements.

Remarque 1 : Je ne comprends pas cette page de Maven où les profiles et le plugin antrun sont décrits comme étant le moyen de gérer divers environnements. Effectivement, c'en est un, je viens de le décrire mais il y a tout de même mieux... ne serait-ce qu'en n'utilisant pas le plugin antrun !

Remarque 2 : Les IDE ne gèrent pas (ou très mal) les profiles, donc si vous avez une configuration relativement complexe, vous devrez absolument avoir la ligne de commande à côté de l'éditeur pour vous en sortir.

Retour au JEE !

Et oui, depuis tout ce temps, on avait la solution sous les yeux sans vraiment s'en servir. JEE fournit une extension pour configurer correctement votre application : Java Naming and Directory Interface (JNDI). Cette extension permet de configurer votre application directement sur le serveur d'application. Je ne vais pas entrer dans les détails de JNDI, ce n'est pas l'objet de ce billet mais sachez simplement que vous pouvez configurer une url qui pointe vers un fichier et par exemple votre ficher de configuration.

Configuration JNDI de l'application

Dans le code

Soit en standard avec le lookup :

try {
   URL configURL = (URL)initialContext.lookup("java:comp/env/url/MyAppConfig");
   InputStream inputstream = configURL.openStream();
   Properties properties = new Properties();
   properties.load(inputstream);
 } catch (Exception e) {   // ... }

Soit si vous utilisez spring :

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
         <list>
              <ref bean="appConfig" />
              [...]
         </list>
    </property>
 </bean>
 <bean id="appConfig" class="org.springframework.core.io.UrlResource">
    <constructor -arg ref="appConfigJndiUrlResource"/>
 </bean>
 <bean id="appConfigJndiUrlResource"
      class="org.springframework.jndi.<strong>JndiObjectFactoryBean">
    <property name="jndiName" value="java:comp/env/url/MyAppConfig" />
 </bean>
Descripteur de déploiement

Bien entendu on référence la ressource dans le web.xml :

<resource -ref>
       <res -ref-name>myAppConfigResource</res>
       <res -type>java.net.URL</res>
       <jndi -name>java:comp/env/url/MyAppConfig</jndi>
 </resource>
Sur le serveur d'application

Tout dépend du serveur dont vous disposez mais vous n'avez qu'à configurer la resource URL url/MyAppConfig vers le fichier properties qui convient (ex : file:/usr/Websphere/6.0/config/specific.properties).

Évidemment, il faut déployer le fichier au bon endroit sur le serveur et donc le livrer avec l'application mais en dehors de l'ear ou du war et c'est là que Maven entre à nouveau en jeu !

Maven assembly

Le plugin assembly de Maven permet de générer un paquetage (pour ne pas dire package) zip, targz... regroupant un ou plusieurs packages et des fichiers divers (et notamment ceux qui nous intéressent, les properties). Encore une fois, je ne saurais vous conseiller le livre de sonatype pour les détails sur la construction d'assemblies. Ici, je vais simplement construire un zip contenant l'ear et les fichiers properties externalisés.

Dans un premier temps, il faut créer un projet supplémentaire :

<project>
  [...]
  <dependencies>
    <dependency>
      <groupid>com.octo</groupid>
      <artifactid>project-ear</artifactid>
      <version>1.2</version>
      <type>ear</type>
    </dependency>
  </dependencies>
 <build>
    <plugins>
      <plugin>
        <artifactid>maven-assembly-plugin</artifactid>
        <version>2.2-beta-2</version>
        <executions>
          <execution>
            <id>create-zip-package</id>
            <phase>package</phase>
            <goals>
              <goal>attached</goal>
            </goals>
            <configuration>
              <descriptor>
                  <strong>src/main/assembly/dep.xml</strong>
              </descriptor>
           </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
 </project>

Ce pom dépend de l'ear et précise qu'il a besoin du plugin assembly et d'un fichier dep.xml décrivant comment sera fait l'assemblage :

<assembly>
  <id>project-assembly</id>
  <formats>
    <format>zip</format>
  </formats>
  <filesets>
    <fileset>
      <directory>../project-war/src/main/resources</directory>
      <outputdirectory>configuration</outputdirectory>
      <includes>
        <include>**/*.properties</include>
      </includes>
     </fileset>
   </filesets>
   <dependencysets>
      <dependencyset>
        <scope>runtime</scope>
        <outputdirectory>/</outputdirectory>
        <unpack>false</unpack>
      </dependencyset>
    </dependencysets>
 </assembly>

Ce petit fichier dep.xml nous dit qu'il faudra prendre les fichiers properties se trouvant dans les resources du projet war pour les mettre dans le répertoire configuration : c'est le fileSets. Il nous dit également qu'il faudra prendre les dépendances (l'ear) et le mettre à la racine sans le décompresser (remarque : unpack false est la valeur par défaut) : c'est le dependencySets.

En sortie de ce projet, après avoir saisi la commande mvn assembly:assembly, vous aurez un zip contenant l'ear à la racine et les fichiers resources dans un répertoire configuration. L'ear est donc indépendant de l'environnement, et la production se chargera de configurer les spécificités de chaque plateforme (base de données, resources spécifiques... et bien sûr le jndi qui vous permettra d'aller chercher cette configuration). Bien entendu, il faudra penser à livrer un minimum de documentation pour que la production puisse faire ce travail efficacement.

Conclusion

Maven fournit de nombreux moyens de gérer plusieurs environnements (mes exemples étaient simples, on peut pousser la configuration bien plus loin). Il faudra donc choisir celui qui s'adapte le plus à votre contexte :

  • Est-ce que l'équipe de production accepte/impose les properties externalisées accessibles en JNDI ?
  • Est-ce que l'équipe de production ne déploie en prod que le package testé en préprod ?
  • Est-ce qu'on prend le temps d'investir dans un pom un peu plus complexe/complet mais qui nous permet de gérer efficacement les environnements ou bien on continue à la main au risque de faire des erreurs ?

A défaut d'employer Maven pour générer un package avec le plugin assembly, vous pouvez toujours utiliser JNDI et fabriquer vous même le zip avec l'ensemble des binaires et de la config.

Après, si on est dans un process d'intégration continue un peu poussé (cf. mon billet sur l'intéraction avec la production), on pourra songer à utiliser Maven, relativement agréable une fois mis en place.