Blog

Integración continua con Jenkins y Amazon CodeDeploy

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:

estructura_ficheros_codedeploy

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:

  1. ApplicationStop: tomcat-stop.sh para Tomcat
    #!/bin/bash
    /etc/init.d/tomcat8 stop
    
  2. 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}
    
  3. Install: no puede tener scripts asociados. Se copia el contenido del directorio source en destination (sección files del fichero appspec.yml).
  4. 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
    
  5. 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, es nodejs, 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.
  • Ruby y compass.

Job

La configuración del «job» es ésta:

configuracion_job_jenkins

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 carpeta amazon/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:

codedeploy_revisions_mod

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.