Forces et faiblesses d'une Usine de Développement Android dans le cloud
L'utilisation des Usine De Développement (UDD) ne fait plus débat. C'est même parfois un sujet sur lequel la DSI a repris la main, gérant et rationnalisant son parc. Cependant, comme sur beaucoup d'autres sujets, le mobile fait bande à part à cause de ses particularités. Utilisation de technos spécifiques ou considérées comme non-standards dans l'entreprise, écosystème changeant et fréquemment mis à jour, voir même nécessité de tourner sur un OS spécifique pour iOS : les développeurs mobiles finissent souvent par installer un mac mini, caché sous un bureau de l'open-space, pour y faire tourner leurs builds.
Alors que les problématiques liées à ce type d'installations sont globalement similaires sur Android et iOS, les solutions sont différentes. Cet article s'intéresse aux spécificités d'Android.
Limites d'une UDD mobile locale
Le développeur mobile n'est pas un DevOps ! Pourtant, en installant son UDD, il va devoir répondre à des problématiques qu'il ne connait pas forcément :
- De quelle manière installer Jenkins sur la machine ? De quelle façon provoquer le lancement du service après un redémarrage ?
- Comment permettre ou interdire l'accès à la machine depuis l'extérieur ou le réseau interne de l'entreprise ?
- Sur quel type de session (root ou non) lancer Jenkins ? Depuis quelle session les outils "vitaux" et SDK sont-ils installés ? Les droits en lecture/écriture sont-ils correctement gérés ?
- Comment gérer les mises à jour de l'OS du serveur ? Et beaucoup plus fréquemment, celles du SDK ?
Les douleurs résultantes de cette installation plus ou moins artisanale ne sont généralement pas invivables, et ne se feront ressentir qu'occasionnellement. S'il est un peu chanceux, le développeur mobile pourra d'ailleurs profiter d'un Jenkins "master" déjà existant sur son plateau de développement, et n'y ajouter qu'un "slave" qui servira uniquement aux projets mobiles.
Mais quelle que soit la configuration dont on parle, une limitation plus importante se présentera : la nécessité de disposer d'un émulateur, d'un simulateur, ou même d'un téléphone branché sur l'UDD afin de pouvoir exécuter les tests automatisés.
Pourquoi ce besoin d'émulateur ? Android tourne sur Dalvik, et non sur la JVM. Avant une mise à jour relativement récente (et toujours incomplète à ce jour) des outils de développements en février 2015, les tests écrits par les développeurs devaient forcément être exécutés sur une machine virtuelle Dalvik, autrement dit, sur un émulateur ou un téléphone. (Note 1)
L'utilisation d'un device physique est difficilement industrialisable : il faut le laisser allumé et branché en permanence. Les temps de communication par USB sont relativement longs, mais surtout, il perdra régulièrement sa "connexion" à l'ordinateur, et il faudra alors de débrancher/rebrancher. Inimaginable sur une UDD. L'émulateur est donc la seule solution viable. On constate généralement que les développeurs Android économisent les plusieurs minutes de temps de boot de l'émulateur en le laissant tourner en permanence sur l'UDD. Cette pratique, si elle fait gagner du temps à chaque build, implique plusieurs limites :
- On impose que tous les projets qui sont gérés par l'UDD fonctionnent sur la version d'Android précise de l'émulateur.
- L'UDD ne pourra exécuter qu'un seul job à la fois, car dans le cas contraire les projets pourraient rentrer en conflit pendant l'exécution des tests sur l'émulateur.
- L'UDD ne saura vérifier que les tests fonctionnent que sur une seule version d'Android.
- Tous les builds seront cassés lorsque l'émulateur aura décidé de planter... ce qui arrive forcément trop souvent !
Alors que nous avons pris le réflexe depuis plusieurs années, de faire héberger sur le cloud tous les services vitaux mais pénibles à maintenir, qu'a-t-il à nous offrir d'un point de vue "intégration continue" ?
Ce que propose le cloud
On trouve quelques outils spécialisés dans l'intégration continue pour mobile, ainsi que d'autres plateformes de CI supportant Android. Le tableau suivant est obtenu en testant les possibilités de chaque plateforme en exploitant un projet exemple (disponible sur github). Ce projet contient un test unitaire non-instrumenté, ainsi qu'un test instrumenté espresso, vérifiant que le TextView de l'activité affiche bien "Hello world!".
Sur le tableau suivant, la ligne "Git" indique si la plateforme sait consommer n'importe quel dépôt git accessible par HTTPS ou SSH (avec clef publique), ou si elle est limitée à Github. "Nombre de jobs" indique le nombre de jobs différents qu'il est possible de définir puis de lancer séparément sur la plateforme. "Différenciation CI / Livraison" indique s'il est possible, ou non, de différencier les jobs d'intégration continue de jobs permettant d'aller jusqu'à la livraison d'une nouvelle version. "Paramétrage de l'émulateur" : versions de l'émulateur disponible, à la main signifie que l'émulateur doit être configuré avec du code plutôt qu'une interface web. "Déploiement" indique les plugins disponibles depuis une interface web pour déployer l'application, "A la main" signifie qu'il est quand même possible de déployer l'apk, mais qu'il faudra le faire par une tâche gradle ou un script shell.
Note : La configuration de l'émulateur sur SnapCI doit être réalisée à la main par ligne de commande. Je n'ai pas réussi à démarrer un émulateur dans un délai d'une heure. Le test d'interface n'a donc pas pu être lancé sur cette plateforme.
On peut remarquer le manque de maturité des différents acteurs pour lesquels le résultat est un KO, alors que le support d'Android est publiquement annoncé dans leurs fonctionnalités.
GreenHouseCI et Travis pourront suffire sur des projets "simples". La limitation principale venant du fait qu'il est impossible d'y différencier un job d'intégration continue d'un job de livraison. Il est aussi impossible d'y suivre les métriques de qualité du code.
Seul CloudBees, qui ne propose au final qu'un Jenkins "as a service", semble aujourd'hui assez mature pour un projet professionnel.
Pour comparer ces différents acteurs sur leur coût, le projet d'exemple n'est plus pertinent. Afin d'obtenir le tableau suivant, je me suis basé sur un projet existant, d'environ 140k lignes de code, sur lequel un build met environ 15min, et est exécuté 10 fois par jour ouvré. Les abonnement retenus sont les plus bas permettant le support d'au moins 5 utilisateurs et l'exécution de deux builds en parallèle.
L'ensemble des acteurs excepté CloudBees propose un abonnement fixe par mois, dépendant du nombre d'utilisateurs et du nombre de builds exécutables en parallèle.
Sur Cloudbees, il faut faut rajouter 1,32$ par heure de build à l'abonnement mensuel qui commence à 60$. Le prix fluctue donc en fonction de la charge. Avec les même paramètres qu'annoncés précédemment, le total mensuel tomberait à 104$ pour des builds moyens de 10min et monterait à 148$ avec des builds de 20min.
L'UDD Android sur CloudBees
Il semble logique que l'outil de référence de l'intégration continue, Jenkins, reste la solution la plus puissante une foix hébergée dans le cloud. CloudBees, dans son offre Dev@Cloud, met à disposition une instance hébergée de Jenkins, qui exécute chaque job dans une VM configurable ("taille", SDK préinstallé...) instanciée pour l'occasion. L'outil restant le même, il est facile de migrer des jobs existants et de réinstaller les plugins.
Notons qu'afin de s'assurer que chaque instance exécutant le build dispose d'un SDK Android à jour, il devient indispensable d'utiliser le plugin sdk-manager de Jake Wharton. Une fois utilisé dans le build du projet, ce plugin s'assure que le sdk, les build tools, le dépôt support et les google libraries sont à jour avant chaque compilation.
Sur CloudBees, l'ensemble des problématiques "OPS" énumérées en première partie de l'article disparaît. Le master ne plante plus jamais (sauf bien sur en cas d'indisponibilité de la plate-forme). Tout est compartimenté : chaque build provoque le démarrage d'une instance de slave. Cette instance est aussi rapide qu'on le souhaite. Enfin, on peut choisir, pour chaque job, sur quelle version d'Android l'émulateur doit se baser pour jouer les tests.
Ce dernier point est à la fois une force et une faiblesse.
Chaque projet peut désormais exécuter ses tests instrumentés et d'interface sur la plus petite version d'Android supportée, celle sur laquelle les bugs sont le plus souvent présents, par faute d'étourderie. Ces tests peuvent aussi être exécutés sur une matrice de configuration mêlant par exemple version d'Android, langue du device et taille d'écran.
On peut alors toucher la principale limitation de CloudBees : la difficulté à booter des émulateurs sur les versions les plus récentes d'Android. Les émulateurs officiels d'Android sont réputés pour être "lourds", longs à démarrer et lents à l'usage, effets qui augmentent avec la version d'Android. De plus, sur CloudBees, impossible de profiter de l'accélération matérielle (GPU ou HAXM) ni des versions compilées pour architectures x86/64. Du coup, il devient difficile de lancer de manière fiable un émulateur à partir de la version 4.4.4 (19) d'Android : le taux de réussite est inférieur à 25%. (Note 2)
Cette limitation, si elle est franchement gênante, est mitigée par plusieurs facteurs :
- Il est recommandé d'exécuter ses tests automatisés sur la plus ancienne version d'Android supportée par l'application (car il est "facile" sur Android de faire appel à des méthodes étant apparues dans des versions plus récentes de l'OS). L'état actuel du marché oblige à supporter au moins Jelly Bean (Android 16), qui représente encore un tiers du parc. Sur nos projets, l'émulateur Jelly Bean boot de façon fiable en moins de 90 secondes.
- Des offres sont en train d'apparaître pour permette l'exécution de tests instrumentés "as-a-service" sur émulateurs ou sur de vrais devices physiques, comme par exemple sur le Device Farm d'Amazon, ou prochainement le Cloud Test Lab de Google. Ces outils seront probablement assez murs lorsque le parc d'appareils Android poussera les applications existantes à fort harnais de tests instrumentés à supporter au minimum Kit Kat et Lollipop.
- Pour les nouveaux projets lancés actuellement ou très récemment, les nouvelles architectures d'applications Android (MVP) ainsi que les avancées sur les outils de développements font qu'une majorité de tests peuvent désormais être non-instrumentés, réservant uniquement les cas les plus complexes et les tests d'interface aux émulateurs
Conclusion
Sur mon projet Android précédent (140k lignes de codes, 3 ans de développement), la migration d'une UDD locale vers CloudBees a permis de réduire les temps de compilation par deux pour l'intégration continue, par 4 pour la livraison. Malgré quelques écueils lors de la migration, notamment autour des limitations abordées sur les émulateurs, cette migration a été bénéfique au projet.
Les limitations abordées sur les émulateurs ne doivent freiner que des projets actuellement en production, ne supportant au minimum qu'une version "très récente" d'Android (comme KitKat), et disposant d'importants harnais de tests instrumentés. L'apparition de solutions plus matures pour disposer d'un émulateur ou d'un device "as a service" en quelques secondes permettra, je l'espère, de réviser prochainement ma position sur ce point.
Notes :
- Il existe un outil non-officiel, Robolectric, permettant depuis plusieurs années d'exécuter des tests sur la JVM. Cet outil a du mal à suivre le rythme de mises à jour des outils de développement imposé par Google, et il peut arriver que le jeu de tests entier soit "cassé" suite à une mise à jour des outils de builds. C'est donc une solution qui fait débat, et que j'ai décidé d'ignorer dans l'article.
- Sur ce sujet, le support de CloudBees s'avoue impuissant et recommande l'utilisation de slaves personnalisés, hébergés en interne ou sur un cloud supportant l'accélération matérielle. Or c'est précisément pour éviter d'avoir à maintenir des slaves nous-même que nous avons entamé cette démarche de migration vers le cloud.