Integración continua con Jenkins y Amazon CodeDeploy
Recientemente he tenido que implantar integración continua para un proyecto elaborado en JHipster (Spring Boot + AngularJS). La principal novedad de esta tarea era el despliegue en Amazon CodeDeploy.
La compilación se realiza en Jenkins, que sube el fichero WAR resultante a CodeDeploy. Empezaré por la configuración de éste último, puesto que la necesitaremos posteriormente en Jenkins. He utilizado dos instancias de EC2, una para Jenkins y otra para CodeDeploy (servidor de producción).
Amazon CodeDeploy
Tres tareas:
1º – Consola de Amazon.
He seguido esta guía. Ésta otra es igualmente buena.
Nota: la guía emplea codedeploy.us-west-2.amazonaws.com
y codedeploy.us-east-1.amazonaws.com
en el Trust Relationship del rol CodeDeploy. Yo estaba en AWS Irlanda, por lo que lo cambié por codedeploy.eu-west-1.amazonaws.com
. Otra diferencia es que en el servicio S3 irlandés no admite mayúsculas para el nombre del bucket.
2º – Configuración de la Instancia EC2
Esta instancia de EC2 será nuestro servidor de producción. Requiere el Agente de CodeDeploy (ver guía, sección A punto 3). Aparte, para nuestra aplicación, necesitaba también Java 8, Tomcat 8 y MySQL.
3º – Ficheros de configuración
Básicamente, un fichero appspec.yml
y una carpeta content
, incluídos en nuestro repositorio Git. He metido todo esto en un directorio amazon
en la raíz del proyecto:

Appspec.yml
controla el despliegue. Veamos:
version: 0.0
os: linux
files:
- source: content
destination: /var/lib/tomcat8/webapps
hooks:
ApplicationStop:
- location: deploy_hooks/tomcat-stop.sh
timeout: 1800
runas: root
BeforeInstall:
- location: deploy_hooks/clean.sh
timeout: 1800
runas: root
ApplicationStart:
- location: deploy_hooks/tomcat-start.sh
timeout: 1800
runas: root
ValidateService:
- location: deploy_hooks/validate.sh
timeout: 3600
runas: root
La sección files
copia el contenido de source
, nuestro WAR, en destination
, el directorio webapps de Tomcat en el servidor de despliegue.Hooks
hace referencia a unos scripts que podemos ejecutar en cada una de las fases de despliegue (lifecycle event hooks) definidas por CodeDeploy. No es necesario definir todas, sólo las que necesitemos. Por ejemplo, yo no he utilizado AfterInstall
. Pueden ser del tipo que queramos: shell, python, ruby, chef… y pueden hacer lo que estimemos oportuno para el despliegue de nuestra aplicación. En nuestro caso, la secuencia es la siguiente:
ApplicationStop
:tomcat-stop.sh
para Tomcat#!/bin/bash /etc/init.d/tomcat8 stop
BeforeInstall
:clean.sh
borra el antiguo WAR y su directorio expandido de webapps. Se llama ROOT porque en este caso el despliegue es en la URL raíz / del servidor.#!/bin/bash rm -rf /var/lib/tomcat8/webapps/{ROOT.war,ROOT}
Install
: no puede tener scripts asociados. Se copia el contenido del directorio source en destination (sección files del fichero appspec.yml).ApplicationStart
:tomcat-start.sh
inicia Tomcat#!/bin/bash cd /var/lib/tomcat8/webapps && { mv proyecto*.war.original ROOT.war; rm -rf README.txt; } && /etc/init.d/tomcat8 start
ValidateService
:validate.sh
consulta a Tomcat si la aplicación se está ejecutando. Si nuestra aplicación tiene un API o algún otro tipo de mecanismo propio para comprobar su correcto funcionamiento, éste sería un buen lugar para realizar una prueba.#!/bin/bash running_root_app_line_count=$(curl --silent -X GET http://user:password@localhost/manager/text/list | grep -E "/:running:.:ROOT" | wc -l) if [ $running_root_app_line_count -eq 1 ] then echo "PROYECTO: Amazon CodeDeploy successful. ROOT application deployed and running in Tomcat" >> /var/log/tomcat8/catalina.out else echo "PROYECTO: Amazon CodeDeploy fail. ROOT application not running in Tomcat" >> /var/log/tomcat8/catalina.out fi
Jenkins
Instancia
Para la instancia EC2 de Jenkins, necesitamos:
Instalados a través de Jenkins:
- Maven
- Git plugin
- AWS CodeDeploy plugin
Instalados manualmente:
- NodeJS: hay un plugin, por lo que en principio no haría falta instalarlo manualmente, pero no me funcionó con una versión reciente de Jenkins, por este bug, así que lo compilé e instalé siguiendo esta guía de JHipster. NOTAS:
- Es mejor compilar frente a una instalación por paquetes porque el nombre del binario resultante es
node
. En algunas distribuciones como Debian y Ubuntu, esnodejs
, lo que provoca error al compilar. - Para que el
export PATH
se conserve después de reinicio, podemos incluirlo en/etc/profile.d/node.sh
. Borrar/etc/profile.d/nodejs.sh
si estuviese.
- Es mejor compilar frente a una instalación por paquetes porque el nombre del binario resultante es
- Ruby y compass.
Job
La configuración del «job» es ésta:

Comentarios:
- «Source Code Management» -> Git no aparece la rama master o develop, que sería lo normal, sino una
/feature/test_codedeploy
, porque estaba haciendo pruebas. - Build Triggers, se hace polling al repositorio cada 15 minutos, aunque sería mejor un hook de Git.
- Build: JHipster ya viene preparado para Maven (o Gradle, según se elija al configurarlo). En los «goals and options» empaquetamos un war (
package
) de produccción (-Pprod
). Ver documentación de JHipster. - Post steps: El comando shell borra el antiguo
.war.original
de la carpetaamazon/content
, y copia allí el nuevo, desde la carpeta target. JHipster genera dos WAR, uno con extensión.war
y otro con.war.original
. El primero tiene Tomcat embebido, mientras que el segundo no. Como nuestro servidor de producción ya tiene instalado Tomcat, utilizamos.war.original
. - Post-build Actions: Deploy an application to AWS CodeDeploy: aquí especificamos la configuración de Amazon CodeDeploy, la que creamos siguiendo la guía. El campo
Subdirectory
es importante. Es la carpeta a subir a CodeDeploy. Sólo seráamazon
. Todo lo demás es redundante, puesto que ya está incluído en el.war.original
La salida de consola de Jenkins es ésta:
Building in workspace /var/lib/jenkins/workspace/PROYECTO
> git rev-parse --is-inside-work-tree # timeout=10
Fetching changes from the remote Git repository
[...]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 55.876 s
[INFO] Finished at: 2015-07-02T10:52:47+00:00
[INFO] Final Memory: 47M/351M
[INFO] ------------------------------------------------------------------------
[JENKINS] Archiving /var/lib/jenkins/workspace/PROYECTO/pom.xml to com.cycleit.proyecto/proyecto/0.0.1-SNAPSHOT/proyecto-0.0.1-SNAPSHOT.pom
[JENKINS] Archiving /var/lib/jenkins/workspace/PROYECTO/target/proyecto-0.0.1-SNAPSHOT.war to com.cycleit.proyecto/proyecto/0.0.1-SNAPSHOT/proyecto-0.0.1-SNAPSHOT.war
channel stopped
[PROYECTO] $ /bin/sh -xe /tmp/hudson1602361537331254853.sh
+ rm -rf amazon/content/*.war
+ cp target/proyecto-0.0.1-SNAPSHOT.war.original amazon/content
Zipping files into /tmp/PROYECTO-7910803975804927112.zip
Uploading zip to s3://proyecto/deploy/PROYECTO-7910803975804927112.zip
Registering revision for application 'CodeDeployCICD'
Creating deployment with revision at {RevisionType: S3,S3Location: {Bucket: proyecto,Key: deploy/PROYECTO-7910803975804927112.zip,BundleType: zip,ETag: 7c2efb5a6588c45490bce8910fd4fee7},}
Finished: SUCCESS
Ahora podemos ver el resultado del despliegue en la consola de Amazon:

Jenkins: ¿local o en Amazon?
Instalar Jenkins en Amazon supone una ventaja frente a tenerlo en un pequeño servidor en la empresa: no tener que subir zips continuamente a través de Internet. Parece una tontería, pero nuestros WARs ocupan 50 MB. Entre máquinas de Amazon esto supone 5-10 segundos nada más. No existe el mismo problema para el servidor Git, puesto que sólo es necesario subir cambios de código, que no son de gran tamaño.
Ventajas de CodeDeploy
¿Qué ventajas proporciona CodeDeploy frente a una simple subida por SSH?
- Varios servidores: si desplegamos nuestra aplicación en varios servidores simultáneamente (Autoscaling), podemos reducir riesgos actualizando servidor por servidor (OneAtATime), o mitad y mitad (HalfAtATime), evitando que una nueva revisión nos deje sin servicio. Todo esto es automático.
- Rollback: CodeDeploy no soporta rollbacks por sí mismo, pero podríamos redesplegar la penúltima revisión por comandos de aws cli. Un buen lugar para hacer esto sería en ValidateService de los lifecycle event hooks.
- Separar entornos de test y producción: tal y como se explica aquí, podemos usar las variable
$DEPLOYMENT_GROUP_NAME
en los scripts de lifecycle event hooks para diferenciar entre distintos grupos de servidores, para test y producción, por ejemplo.