De despliegue continuo y de responsabilidad personal

Despliegue continuo con Jenkins, Maven, Selenium y Junit

Están surgiendo nuevos conceptos como el despliegue continuo y la responsabilidad personal que están cambiando la forma de desplegar en un entorno de producción.

Digamos que la “manera tradicional” es decidir que funcionalidades van a ir en cada versión codificarlas, probar esa versión y por fin el despliegue en producción. Quién se haya enfrentado a esto sabe el estrés que produce el despliegue de una nueva versión sobre todo si conlleva bastantes cambios, …si ya sabemos que todo está probado, que no va a pasar nada, pero el mal trago del despliegue no te lo quita nadie … “¿irá todo bien? ¿no irá?

Despliegue continuo

¿Cómo están haciendo las cosas empresas como Facebook? … pues simplemente no hay versiones. Básicamente todo lo que se sube al repositorio de código es desplegado en producción. Claro está, que es un poco más sofisticado que esto. La idea es que cada ‘push‘ o ‘merge‘ que se haga en una determinada branch del repositorio se despliegue en “producción”. Pero ese commit está ‘controlado’ por las pruebas unitarias y de integración y además el que lo realiza es consciente de que ese cambio se va a reflejar en producción.

El flujo de lo que quiero expresar es el siguiente:

[Flujo de despliegue continuo]

[Flujo de despliegue continuo]

Ese despliegue se puede realizar en una primera instancia a un número limitado de usuarios de la aplicación. En Facebook esta puesta en producción con la nueva funcionalidad se realiza en un número limitado de servidores (y por tanto de usuarios) y si todo va bien se propaga al resto de servidores.

El despliegue continuo no es integración continua. Es quizás llevar este concepto un paso más allá. Obliga a trabajar de manera diferente. Se basa en que se puedan hacer varios despliegues por semana e incluso varios por día. Es más sencillo trabajar con historias de usuario lo más pequeñas posibles y por tanto desarrollos pequeños, para que puedan adaptarse a este concepto. Además obliga a generar los test de la nueva funcionalidad que va en cada despliegue.

Todo esto no significa que vaya en contra de un entorno de integración y los procesos de integración continua. Como intentaré explicar más adelante en el ejemplo, con la misma configuración y jugando con los perfiles de Maven y las tareas de Jenkins, en un proyecto  podrían coexistir un entorno de integración apuntando a una rama de código y otro de producción apuntando a su propia rama. En integración subirían las funcionalidades realizadas por los diferentes desarrolladores y que se probarían en su conjunto y desde esta se haría ‘push‘ a la de producción cuando se den por validos.

Ventajas

Las ventajas son varias:

  • Se pierde el miedo a los despliegues.  Hay que tener en cuenta que en sistemas replicados y distribuidos el despliegue se realiza sin que el usuario perciba que el servicio está caído por lo que se pueden hacer en caliente.
  • Está todo más controlado, hay menos desorden. Los cambios están muchos más localizados, si algo falla se acota donde se ha producido el error.
  • Se simplifica todo. No hay versiones y por tanto no hay equívocos. Lo que está en la rama de producción es lo que está desplegado en producción.
  • Finalmente las funcionalidades que ya están completas se suben a inmediatamente a producción y el usuario puede utilizarlas antes.

Responsabilidad personal

En el despliegue continuo, y sobre todo en grupos pequeños de trabajo, cada desarrollador es responsable de su commit y de su pequeño despliegue. Ahí es donde entra el concepto de la responsabilidad personal. Eres directamente responsable de esa parte que se ha subido a producción. Por eso tienes que poner cuidado en codificar el código y las pruebas para que no genere bugs.

Esto en Facebook lo llevan al extremo otorgando una especie de ‘karma‘. Si el ‘karma‘ del desarrollador es alto (vamos si es confiable y su código no ha producido errores) se confía en su commit. Si es ‘karma‘ es bajo, su código es revisado y probado por otros técnicos antes de que se despliegue. El desarrollador no conoce el valor de su karma… ¿excesivo?… A mi modo ver si que lo es un poco.

Test si o si

Son la red de seguridad en este tipo de despliegue por lo que son esenciales. Uncle Bob Martin remarca la importancia de hacer primero el test antes que el código. No es una correspondencia biunívoca, del test se puede obtener el código pero no del código se puede obtener el test. El mero hecho de hacer el test primero condiciona a que tu código sea testable, cosa que es difícil al revés.

Se puede ser más pragmático en lo referente a los test, pero en este tipo de despliegue no hay duda de que son necesarios. Lo que si es cierto es que montar infraestructura de test no debería ser un desarrollo en si mismo. Lo más sensato es no inventar la rueda y basarse en estándares y herramientas que ya nos dan esta funcionalidad .

El ejemplo

El ejemplo está basado en un proyecto Java tiene la siguientes características:

  • Es un proyecto web ‘Hola Mundo’ hecho en Java Server Faces
  • Tiene la estructura de proyecto Maven y se utilizará esta herramienta para la construcción, pruebas y despliegue.
  • Se utiliza Junit para las pruebas unitarias.
  • Jetty (embebido en el plugin de Maven) para desplegar el entorno de pruebas para poder lazar las pruebas de integración.
  • Se utiliza Selenium para las pruebas de integración
  • Por último se ha supuesto que el despliegue en producción se haría en  un Tomcat 7
  • Se utiliza Jenkins como herramienta de despliegue continuo, se configurará para que se lance Maven siempre que haya un cambio en el repositorio.

Lo que quiero es mostrar un ejemplo práctico, que contenga una prueba unitaria, otra de integración, y que en su proceso de construcción estén todos los pasos desde la compilación, hasta el despliegue final, mostrar como se refleja todo esto en la configuración del proyecto Maven (pom.xml) y como se configuraría Jenkins.

El código está disponible en github:

EjemDespliegueCont

Proyecto y clases

Como ejemplo de proyecto he utilizado un HolaMundo echo en Java Server Faces. Tiene un controlador HolaMundo.java  y después se ha creado una prueba unitaria en Junit HolaMundoTest.java  y una prueba de integración en Selenium HolaMundoIT.java . Las tres clases son muy sencillas pero sirven para explicar el concepto.

Pom (Maven)

Es el pom.xml donde se define los plugin que se van a usar y como se va a montar el entorno que nos permita realizar el flujo indicado más arriba.  Lo explico un poco por encima, de todas maneras he insertado comentarios que explican cada paso.

He configurado el plugin ‘jetty-maven-plugin‘ para que se arranque un servidor jetty con la aplicación desplegada en la fase ‘pre-integration-test’ y se pare en la fase de ‘post-integration-test‘. Esto nos permitirá lanzar los test de integración contra un entorno ya desplegado.

01
...
02
 <plugin>
03
  <groupId>org.mortbay.jetty</groupId>
04
  <artifactId>jetty-maven-plugin</artifactId>
05
  <version>8.1.13.v20130916</version>
06
  <configuration>
07
   <scanIntervalSeconds>10</scanIntervalSeconds>
08
   <stopKey>foo</stopKey>
09
   <stopPort>9999</stopPort>
10
  </configuration>
11
  <executions>
12
   <execution>
13
    <id>start-jetty</id>
14
    <phase>pre-integration-test</phase>
15
    <goals>
16
     <goal>run</goal>
17
    </goals>
18
    <configuration>
19
     <scanIntervalSeconds>0</scanIntervalSeconds>
20
     <daemon>true</daemon>
21
    </configuration>
22
   </execution>
23
   <execution>
24
    <id>stop-jetty</id>
25
    <phase>post-integration-test</phase>
26
    <goals>
27
     <goal>stop</goal>
28
    </goals>
29
   </execution>
30
  </executions>
31
 </plugin>
32
...

Código 01. (pom.xml) Configuración del plugin jetty embebido

Por otro lado he configurado el  plugin ‘maven-failsafe-plugin‘ que lanza los test de integración para que se lance en la fase de ‘integration-test‘.

01
...
02
<plugin>
03
 <artifactId>maven-failsafe-plugin</artifactId>
04
 <version>2.16</version>
05
 <executions>
06
  <execution>
07
   <goals>
08
    <goal>integration-test</goal>
09
    <goal>verify</goal>
10
   </goals>
11
  </execution>
12
 </executions>
13
</plugin>
14
...

Código 02. (pom.xml) Configuración de plugin del test de integración

Este plugin lanzará todos los test que terminen el *IT.java. Por eso no es casual el nombre del test de Selenium  HolaMundoIT.java

Por último he configurado el plugin ‘maven-antrun-plugin‘ para que lance un ‘ant‘ en la fase ‘install‘ que despliegue la aplicación en un entorno final en un Tomcat o en varios.

01
...
02
<plugin>
03
 <groupId>org.apache.maven.plugins</groupId>
04
 <artifactId>maven-antrun-plugin</artifactId>
05
 <version>1.7</version>
06
 <configuration>
07
 <target>
08
  <ant antfile="src/main/ant/deployTomcat.xml" inheritrefs="true" inheritAll="true">
09
   <property name="project.build.finalName" value="${project.build.finalName}"/>
10
  </ant>
11
 </target>                     
12
 </configuration>
13
 <!-- Dependencias para las task de despliegue de tomcat -->
14
 <dependencies>
15
  <dependency>
16
   <groupId>org.apache.tomcat</groupId>
17
   <artifactId>tomcat-catalina-ant</artifactId>
18
   <version>7.0.42</version>
19
  </dependency>
20
 </dependencies>
21
 <executions>
22
  <execution>
23
  <id>install-tomcat</id>
24
  <phase>install</phase>
25
   <goals>
26
    <goal>run</goal>
27
   </goals>
28
  </execution>
29
 </executions>
30
</plugin>
31
...

Código 03. (Pom.xml) Configuración del ant de despliegue del tomcat en la fase install

El ant que ejecuta es ‘deployTomcat.xml‘. Como se puede ver si están definidas las propiedades ‘tomcat.servidor1‘ y ‘tomcat.servidor2‘, se ejecutarían los targets ‘deployServer1‘ y ‘deployServer2‘ respectivamente.

01
...
02
<target name="deployServer" depends="deployServer1, deployServer2"/>    
03
<target name="deployServer1" if="tomcat.servidor1">
04
 <deploy url="http://${tomcat.servidor1}/manager/text" 
05
  username="${tomcat.usuario}" 
06
  password="${tomcat.contrasena}"
07
  path="/${project.build.finalName}" 
08
  war="file:${project.build.directory}/${project.build.finalName}.${project.packaging}" update="true"/>
09
</target>
10
<target name="deployServer2" if="tomcat.servidor2">
11
<deploy url="http://${tomcat.servidor1}/manager/text" 
12
  username="${tomcat.usuario}" 
13
  password="${tomcat.contrasena}"
14
    path="/${project.build.finalName}" 
15
    war="file:${project.build.directory}/${project.build.finalName}.${project.packaging}" update="true"/>                    
16
</target>
17
...

Código 04. (deployTomcat.xml) Definición de los target de despliegue

Perfiles de maven

Definiendo estas variables en diferentes perfiles de maven se puede jugar si en la fase ‘install‘ no se despliega en el Tomcat (si no se definen ninguna de las dos propiedades)  o en uno sólo (definiendo sólo una) o en los dos (definiendo las dos propiedades) y cuales son sus direcciones. Con el mismo pom.xml y ant se puede desplegar en diferentes entornos (Por ejemplo ‘desarrollo‘ ‘preproduccion‘,’produccion,…)

Pongo un ejemplo de los perfiles que he definido para este ejemplo. Se añaden en el archivo ‘settings.xml‘ del directorio ‘.m2‘ del home del usuario que lanza el maven.

01
...
02
<profiles>
03
...
04
 <profile>
05
 <id>desarrollo</id>
06
 <properties>
07
  <tomcat.usuario>tomcat</tomcat.usuario>
08
  <tomcat.contrasena>tomcat</tomcat.contrasena>
09
  <tomcat.servidor1>127.0.0.1:8080</tomcat.servidor1>
10
 </properties>  
11
 </profile>
12
 <profile>
13
 <id>produccion</id>
14
  <properties>
15
  <tomcat.usuario>tomcat</tomcat.usuario>
16
  <tomcat.contrasena>tomcat</tomcat.contrasena>
17
  <tomcat.servidor1>127.0.0.1:8080</tomcat.servidor1>
18
  <tomcat.servidor2>10.2.0.15:8080</tomcat.servidor2>
19
  </properties>  
20
 </profile>
21
...
22
</profiles>
23
....

Código 05.(settings.xml de maven) Configuración de perfiles

En los perfiles también se definen el usuario del Tomcat que nos permite desplegar aplicaciones a través de la red.

Configuración del tomcat

Hay que dar de alta un usuario en el Tomcat del despliegue de  ‘producción’ para que permita desplegar una aplicación por la red. Esto se configura en el archivo ‘conf/tomcat-users.xml

1
<tomcat-users>
2
...
3
 <role rolename="manager"/>
4
 <role rolename="manager-gui"/>
5
 <role rolename="manager-script"/>
6
 <role rolename="admin"/>
7
 <user username="tomcat" password="tomcat" roles="manager,manager-gui,admin,manager-script"/>
8
...  
9
</tomcat-users>

Código 06. (tomcat-users.xml) Configuración del tomcat.

Se añade el usuario ‘tomcat‘ con el perfil ‘manager−script

Job de JenkinsPantallazo. Job de jenkins

Ya sólo queda configurar el job del jenkins para que lance el proyecto maven cada vez que haya un cambio en la rama del repositorio. He elegido un proyecto maven que mire en el repositorio cada cierto tiempo. Esta es la configuración del repositorio de código:

Pantallazo. Configuración del repositorio de versiones del job de jenkins

Pantallazo. Configuración del repositorio de versiones del job de jenkins

Donde para el ejemplo lo he apuntado directamente a github. Se indican también los goal de maven indicando el puerto donde se arrancará el jetty de pruebas y el perfil que se va a usar:

Pantallazo. Definición de los goal y de los perfiles de maven

Pantallazo. Definición de los goal y de los perfiles de maven

Conclusiones

Como se puede ver fácilmente con herramientas estándar como maven, ant, junit, selenium  y jenkins se puede montar un entorno que nos permita realizar un despliegue continuo. Queda sólo cambiar la mentalidad y atreverse a dar el paso en un entorno de producción.

Espero que os sirva.

M.E.

PD: La inspiración de este articulo me la dio la charla de Alex Fernandez en MadridJS sobre despliegue continuo