Este es el primer post sobre desarrollo Mobile del blog y para estrenarnos no podíamos haber elegido un tema más jugoso que el modelo de Integración Continua (IC) y Despliegue Continuo (DC) orientado al desarrollo de aplicaciones para dispositivos móviles.
Ventajas y virtudes de la Integración y Despliegue Continuos
Seguramente no hará falta que os hable sobre las bondades de los ciclos de IC y DC ni de las aportaciones que este modelo ofrece al desarrollo de software en general pero sí que me gustaría resaltar alguna de las ventajas que ofrece para el desarrollo de apps en las principales plataformas móviles del mercado como son Android e iOS.
Una de las peculiaridades del desarrollo de apps es la variedad de plataformas disponibles en el mercado y las diferentes estrategias que se siguen para el desarrollo y despliegue de apps en dichas plataformas. De esta forma, es habitual encontrarnos hoy en día con proyectos de desarrollo de apps móviles multiplataforma de dos tipos:
- Desarrollo nativo, de forma que cada plataforma constan de su propio repositorio y código fuente independiente.
- Desarrollo híbrido, con un único repositorio y código fuente común. Esto implica que, a menudo, para distribuir una sola app necesitamos tener conocimientos sobre todas las plataformas sobre las que se distribuye además de disponer del Hardware, entorno de desarrollo y certificados necesarios para su firma y construcción.
Con este escenario, la automatización y centralización de los procesos de IC y DC, nos proporcionan herramientas automatizadas con un alto valor añadido que nos ayudan a minimizar el error humano durante estos procesos.
Dispondremos de un equipo que tendrá instalados:
- Una instancia de Jenkins.
- Los frameworks necesarios para construir las apps.
- Los certificados necesarios para realizar la firma de las apps.
- Las cuentas necesarias para acceder a Stores oficiales y otras plataformas de distribución.
Todo esto nos permite unificar el mantenimiento del entorno de desarrollo necesario para realizar todos los procesos de IC y DC, minimizando agujeros de seguridad al centralizar el acceso a los Markets oficiales y almacenaje de certificados, además de permitirnos el despliegues de apps en cualquiera de las plataformas sin necesidad de disponer de conocimientos técnicos al respecto.
La selección de Jenkins como orquestador de los procesos frente a otras alternativas se debe a que Jenkins es una herramienta Open Source con un gran ecosistema de plugins disponibles y una importante comunidad de respaldo. En este caso montaremos nuestra instancia de Jenkins en una máquina Apple, lo que nos permitirá utilizar esta misma instancia para construir y firmar en el futuro Apps para iOS.
Con todo esto contemplamos los siguientes procesos de Integración y Despliegue Contínuo:
- Construcción: Se compilará el código de la app con independencia de la plataforma de destino.
- Empaquetado: Se generará un fichero .apk para Android y/o un .ipa para iOS.
- Test unitarios: Se lanzarán los test desarrollados para la app tanto para Android como para iOS.
- Análisis de Código estático: Se procesará el código de la app y lo subirá a una herramienta de análisis estático.
- Test funcionales: Ejecutados sobre dispositivos físicos o emuladores a través de Appium y Selenium WebDriver.
- Firma: Distinguiendo la firma a aplicar para cada uno de los entornos y plataformas.
- Distribuciones: Tanto para App Store y Google Play como para distribuciones OTA privadas (Beta by Fabric, TestFairy...).
En este post sobre IC y DC vamos a ver cómo implementar los pipelines que permitan automatizar los procesos de construcción, empaquetado, firma y distribución mediante la herramienta Fabric, para apps nativas Android.
Vamos al meollo. Preparemos nuestros pipelines
Ya tenemos nuestro Jenkins instalado además del framework de desarrollo utilizado para generar las apps y hemos generado la estructura de carpetas adecuada en Jenkins. Estamos listos para generar nuestro primer pipeline.
Para esto creamos un "Nuevo item" de tipo "Pipeline" y le damos un nombre adecuado.
Configuración del pipeline
Nodo de ejecución
Para seleccionar en qué nodo de Jenkins se va a ejecutar una tarea de tipo Pipeline, definimos en la primera instrucción del mismo el nodo en el que se va a ejecutar:
node(<Nodo>) {
...
Pasos que se ejecutarán en este nodo
...
}
Directorio de ejecución
También podemos seleccionar en qué directorio se ejecutará el pipeline. Para ello lo definimos de una forma semejante a como lo hemos hecho con el nodo:
ws(<Directorio>) {
...
Pasos que se ejecutarán en este directorio
...
}
Nuevo paso de ejecución
Para establecer la diferencia entre las diferentes fases de ejecución del pipeline utilizamos el comando stage. Al utilizarlo al comienzo de un bloque de instrucciones, agrupamos estas en un mismo paso de ejecución, hasta que aparece un nuevo stage o se finaliza la ejecución del pipeline. Su sintaxis sería:
stage 'Nombre del paso 1'
...
Sentencias a ejecutar
...
stage 'Nombre del paso 2'
...
Sentencias a ejecutar
...
.
.
.
stage 'Nombre del paso n'
...
Sentencias a ejecutar
...
.
.
.
stage 'Nombre del paso final'
...
Sentencias a ejecutar
...
Repositorio de código fuente
Para la creación del paso de descarga del código desde Git, hacen falta 3 datos, la rama desde la que se quiere desplegar, la URL del repositorio, y un identificador de unos credenciales válidos cargados en Jenkins:
stage 'Git'
git branch: <Rama>,
git credentialsId: <Identificador de credenciales>,
url: <url del repositorio>'
Compilación
Todas las tareas de construcción se pueden lanzar mediante Gradle. Para ello, lanzamos la tarea base de construcción de Gradle, que llevará a cabo la compilación de los binarios en los distintos flavors que puedan existir, y en versiones tanto para debug como para release. Para ello utilizamos el gradle (gradlew) incrustado en el proyecto, para utilizar la misma versión para la que esté compilado:
stage 'Android build'
sh './gradlew build'
En este punto nuestro pipeline quedaría así:
Firma
En primer lugar debemos eliminar el binario que puede haber sido generado en anteriores ejecuciones, puesto que si aún sigue en la carpeta, el comando de firma fallará, y no podrá ser llevado a cabo. Para ello, utilizaremos el comando de UNIX "rm", añadiendo el parámetro "-f" para evitar que pueda fallar la ejecución por no encontrar el binario:
sh 'rm -rf <Ruta del apk a eliminar>'
Una vez tenemos generado el apk de release, que habrá sido generado con el comando construcción anteriormente especificado, es necesario alinearlo, lo que añade un grado de optimización, y deja preparado el binario para su firma. Para ello, usamos la herramienta del SDK Android Zipalign, a la que se le proveen como parámetros de entrada la ruta del apk a alinear, y el nombre del binario de salida. Además, se le indica el tipo de alineación a 4 bytes (-p 4) y que informe de forma detallada de las tareas que ejecuta (-v).
El uso de zipalign sería:
sh 'zipalign -v -p 4 <Ruta del apk a alinear> <Ruta del apk alineado resultante>'
Para más detalles del uso de zipalign: https://developer.android.com/studio/command-line/zipalign.html
Tras haber alineado el apk, es necesario firmarlo. Para ello, necesitaremos contar en la máquina con un certificado de Android válido (keystore). Si no se dispone del mismo, este certificado se puede generar, directamente con Android Studio o mediante la herramienta keytool del JDK. Este certificado es el que deberá ser utilizado a partir de ese momento para firmar la aplicación, puesto que si se sube a Google Play, se exigirá que la aplicación este siempre firmada con este certificado, no pudiendo subir una nueva versión de la misma si se utiliza un certificado diferente para firmarla.
Para realizar la firma como tal, hay que usar la herramienta apksigner, pasándole los siguientes parámetros:
- sign: Se indica a apksigner que tiene que iniciar el proceso de firmado de un apk.
- --ks: Ruta al keystore para firmar la aplicación.
- --ks-pass: Password del keystore, indicando previamente que el tipo de clave que se va a usar es un password.
- --ks-key-alias: Alias de la aplicación a firmar en el keystore.
- --key-pass: Password del alias de la aplicación en el keystore, indicando previamente que el tipo de clave que se va a usar es un password.
- Ruta del apk a firmar.
Para más detalles de la firma en Android: https://developer.android.com/studio/publish/app-signing.
Es decir, que el comando sería:
sh 'apksigner sign --ks <Ruta al keystore> --ks-pass pass:<password keystore> --ks-key-alias <alias> --key-pass pass:<password alias> <Ruta del apk a firmar>'
Se puede modificar este comando, para que en lugar de indicar directamente el password en el comando de jenkins, estos passwords estén incluidos en variables globales, u obtenerlos desde un fichero, pero esencialmente la sintaxis del mismo y el objetivo es muy similar.
Por último se verifica que la firma haya ido correctamente mediante el apksigner, pero esta vez indicando que lo que se quiere hacer es verificar la firma mediante el parámetro verify. Es decir, la definición de parámetros sería:
- verify: Se indica a apksigner que la acción que ha de realizar es comprobar que el proceso de firma se haya llevado a cabo de forma satisfactoria.
- --verbose: Con este parámetro, la ejecución de apksigner indicará el máximo de información posible.
- Ruta del apk a verificar.
Con lo que el comando para verificar la firma quedaría:
sh 'apksigner verify --verbose <Ruta al apk firmado>'
Para más detalles del uso de apksigner: https://developer.android.com/studio/command-line/apksigner.html
Distribución OTA
Como herramienta para distribuciones OTA seleccionamos Beta (by Fabric) debido principalmente a la compatibilidad que nos ofrece no sólo con Android y gradle sino con otras plataformas como son iOS y xcode o cordova.
Para llevar a cabo la distribución OTA de una aplicación Android mediante Fabric, únicamente es necesario utilizar el comando de Gradle que lleva a cabo la subida. Para ello, previamente deben estar configurado el incluidos en el gradle.build de la aplicación el API KEY y el BUILD SECRET de Fabric dentro del código de la aplicación, así como los parámetros de la distribución que se se deseen, como la lista de correos y los grupos. Además, es necesario pasarle los siguientes parámetros:
- Flavor: Si existen flavors en la aplicación a distribuir, se debe indicar cuál de los flavors se quiere distribuir. En caso de que no existan, basta con no ponerlos.
- Modo de compilación: Release o Debug.
sh './gradlew assemble<Flavor><Modo de compilación> crashlyticsUploadDistribution<Flavor><Modo de compilación>'
Con esto completaríamos las instrucciones del pipeline:
Resultado
Con esto ya tenemos nuestro pipeline funcionando y podemos gestionar directamente desde Jenkins todo el ciclo de vida de nuestra App Android. Tan sólo lanzando la construcción desde Jenkins del pipeline generado se compilará y generará la distribución correspondiente a los correos fijados a través de Fabric.
Podemos ver también el resultado de cada uno de los stages que componen nuestro pipeline, lo que nos permite tener mayor control sobre tiempos de ejecución o detección de errores en caso de que alguna ejecución de nuestro pipeline falle.
Con todo lo que hemos visto en este artículo somos capaces de construir una plataforma basada en Integración y Despliegue Continuo para Apps Android que nos permite controlar todo el ciclo de vida de nuestras Apps, desde su implementación hasta su distribución OTA.
Espero que os haya gustado el post. Si es así síguenos en Twitter para estar al día de próximos post de esta serie y también de otros temas.