Growing Pains: Desacople de Apps Móviles para Agilizar el Desarrollo

Leonel Corso
PeYa Tech
Published in
8 min readSep 23, 2021

A raíz de los desafíos que comentamos en este post, debimos repensar la arquitectura de nuestras apps móviles para lograr un proceso que nos permita escalar, darle libertad a los equipo de desarrollo y a la vez, eliminar todas las tareas operativas de lanzar una nueva versión.

El primer paso era partir el monolito en módulos, es decir, bloques de construcción que en conjunto formen la aplicación como un todo pero que cada equipo pueda trabajar de manera independiente.

Aquí aparecen dos tipos de módulos:

  • Módulos de funcionalidades (cupones, checkout, etc)
  • Módulos base (capa de servicios, manejo de notificaciones, componentes UI, etc)

La idea de los módulos base es tener un lugar donde tener toda la lógica compartida por los módulos de funcionalidades -por ejemplo llamar a un endpoint o responder a una notificación push- evitando así repetir código.

Una de las cuestiones más importantes que buscábamos con esta arquitectura era que cada equipo fuera realmente responsable de su código, de la calidad del mismo y de monitorear cualquier crash que pudiera ocurrir.

Debido a esta responsabilidad, para ayudar a los equipos a arrancar, pero también para estandarizar, cada módulo se creó en base a un template, una plantilla básica siguiendo las mejores prácticas para que los desarrolladores encaren en la dirección correcta cuando estén frente a nuevos desafíos.

Cada módulo reside en su propio repositorio y es autosuficiente, en el sentido que es responsable de resolver sus múltiples dependencias y construir el contexto que necesite para su correcto funcionamiento.

Esto es muy importante dado que la forma en la que se navega de un módulo a otro es a través de Deeplinks, es decir navegar de un módulo a otro llamando a links de similar forma a como sería una Web.

Si un parámetro es necesario, lo enviamos en la URL como llamaríamos a una página, evitando así compartir objetos complejos entre ellos, dado que generaría acoplamiento.

Como se puede ver en la imagen de ejemplo, si necesitamos navegar desde la home a un partner, sólo debemos llamar a la URL del módulo responsable pasando el ID del mismo y el módulo hablará con el backend para traer toda la información necesaria.

Es importante mencionar que cada vez que se agrega un Deeplink a un módulo, el mismo será suscrito automáticamente a la app. Sin necesidad de Pull Requests adicionales, podrá ser llamado por otros módulos de manera inmediata.

Continuous Integration

Cuando pensamos sobre integración continua, teníamos en mente los tres pasos típicos (Build, Deploy, Release) y queríamos asegurarnos que todos los chequeos posibles se hayan realizado antes de que cualquier equipo suba algún cambio a producción.

El pipeline que construimos, ejecuta los tests unitarios y de UI durante la etapa de Build, registrando la cobertura y otras métricas.

Durante la etapa de deploy, se ejecutan pruebas de dependencia, compilando cada módulo en el árbol de dependencias, ejecutando los tests de integración y finalmente suscribiendo automáticamente los Deeplinks que el módulo tenga declarados.

Finalmente si todos los tests pasan con éxito, el módulo es parte del release train.

Release Trains

Como venimos mencionando en la sección anterior, otro concepto que adoptamos fue el de Release Train, el cual es una técnica para coordinar a múltiples equipos para el lanzamiento de una nueva versión.

Cada lanzamiento ocurre en una fecha fija, sin esperar a que todos los equipos hayan terminado sus funcionalidades. El tren no espera a nadie, en caso que un equipo no pueda dejar una funcionalidad lista para producción, deberá esperar al próximo tren.

Quien actúa de Release Manager coordina dichos Release Trains creando, cerrando, abriendo y lanzándolos al entorno que desee, como Staging, Night, Beta o Producción.

Cuando crea un tren, los equipos deben deployar allí sus módulos, sólo si están listos para producción.

Este enfoque provee mucha visibilidad sobre los cambios, dado que el alcance de los mismos son los límites de cada módulo, contrario a tener todo en un monolito.

Gestión Mobile en Jarvis

Para asegurar que todos los equipos trabajen de la misma manera creamos Jarvis, una herramienta para automatizar el proceso completo de desarrollo. Puedes ver más detalle sobre Jarvis visitando este post

Dentro de esta herramienta creamos los comandos que se observan más abajo, agrupado por aquellos que usan desarrolladores y aquellos que usa quien actúa de Release Manager.

Trabajando con Módulos

Miremos un poco en detalle cómo es trabajar en un esquema modularizado:

En el diagrama de arriba, vemos un proceso típico de compilación de un módulo por parte de un equipo. Elegimos no sumar al diagrama a Jarvis, AWS o Jenkins para poder hacer foco en los procesos principales y mostrar algunas partes de la infraestructura que le dan sustento.

En los pasos 1 y 2, cuando se llama al comando “add module”, el/la desarrollador/a obtiene un repositorio con todo el código base de un template dejándolo listo para clonar y trabajar.

Cuando compila su código, pasos 3 y 4, Jarvis se conecta con uno de nuestros nodos en Mac Stadium y como parte del pipeline, ejecuta todos los tests unitarios informando la cobertura del mismo a nuestra instancia de Sonarqube. A su vez, se ejecutan los UI tests que contenga, usando Espresso para Android y KIF para iOS.

Finalizada la compilación del módulo, el mismo es almacenado en jFrog en el caso de Android y en un repositorio local de pods para iOS, como se puede observar en el paso 5. En este punto, el módulo está listo para ser deployado, pero primero hablemos de cómo están modelados los Release Trains en la vida real.

Los Release Trains son ramas en github, así que cada vez que quien actúa de Release Manager crea uno, una nueva rama es creada del tren anterior y el nuevo tren es listado como abierto para recibir deploy en la consola de Jarvis.

Cuando un deploy es iniciado — paso 6 — el pipeline obtiene todas las dependencias para ese módulo y compila cada una contra la nueva versión que estamos deployando, asegurando que no se rompa ningún contrato/interfaz.

Una vez que ese chequeo es completado, se genera un build de toda la app controlando que la integración fue exitosa y que ningún otro squad deba sufrir de un build roto.

Finalmente en el paso 7, una vez que el proceso de deploy finaliza, se actualiza la metadata de Jarvis y la branch de ese tren en particular con una referencia a la nueva versión del módulo.

Delivery Continuo

Nuestro proceso de delivery continuo tiene estos cuatro pasos o hitos principales que son posible gracias al esquema de desacople comentado en las secciones anteriores junto con la implementación de Release Trains.

Diariamente, a las 11 de la noche, un job es ejecutado automáticamente para generar dos builds de nuestra app. Uno apuntando a nuestro entorno de pruebas que se sube a Appcenter y el otro, con el mismo código, apuntado a producción y subida a Testflight en el caso de iOS y a un track beta en el caso de Android.

Estos dos builds son distribuidos a todo nuestro equipo Tech para obtener un feedback rápido sobre lo que está funcionando y lo que no.

Una vez que el tren se cierra, una versión Beta es distribuida a lo que llamamos “Amigos y Familia” para obtener mayor feedback aumentando la audiencia.

Finalmente, promovemos esa versión Beta y comenzamos a hacer un rollout progresivo monitoreando Crashes, CVR y velocidad de la aplicación para mayor control. Si todas las métricas se mantienen en niveles aceptables, el rollout se completa al 100%.

Releases en detalle

El siguiente diagrama muestra todas las partes involucradas en el proceso automatizado de release:

Quien actúa de Release Manager indica el comando “release” a Jarvis, quien se comunica con la correspondiente Step Function en AWS, quien llama a una función Lambda que termina invocado a un Job en Jenkins.

A partir de este punto, nos apoyamos en Fastlane para la completa automatización del release; completar las “Release Notes”, imágenes, esperar a que el build sea aprobado por Apple en el caso de iOS y otras cuestiones.

Una vez completado, el pipeline nos envía una notificación vía slack para saber de manera rápida si todo salió bien.

Y lo que es interesante aquí, es que no hubo necesidad de validar o mergear branches, actualizar el número de versión u otras tareas operativas; todo fue realizado desde Jarvis. Quien actúa de Release Manager tiene el control completo del proceso y una vez que cierra un tren, no puede ingresar ningún commit más.

Impacto de adoptar este enfoque

Haber adoptado este enfoque nos ha permitido:

  • Disminuir enormemente la cantidad de bugs que aparecían en producción. Esto en parte a nuestros nightly builds que proveen feedback rápido a nuestro equipo de Tech pero también gracias a la reducción del alcance de los cambios debido a que estamos trabajando en módulos en vez de un gran repositorio.
  • Aumentar significativamente el ownership de cada equipo, permitiéndoles a la vez libertad para innovar. Esto también permitió entender dónde estamos parados en términos de cobertura de código y tests UI para promover estas prácticas
  • Liberar versiones a producción muchísimo más rápido gracias a la eliminación del 100% de las tareas operativas relacionadas al release y la creación de un pipeline fácil de entender.

En nuestro próximo post, estaremos abordando los desafíos que encontramos en el mundo Web y cómo cambiamos la arquitectura para permitir mayor independencia entre los equipos.

--

--