Alors que les entreprises technologiques s'empressent de sortir leurs prochaines fonctionnalités, tout retard de productivité peut être extrêmement préjudiciable, d'où l'importance d'un processus de développement efficace.
Les entreprises qui utilisent Kubernetes et Docker dans des environnements de production utilisent très probablement Docker pour le développement local. Docker-compose, un outil permettant de définir et d'exécuter des applications Docker multiconteneurs, garantit des processus de développement local cohérents, ce qui facilite la gestion des dépendances des applications. Docker-compose permet aux ingénieurs de choisir les versions spécifiques des dépendances à exécuter avec le conteneur d'application, au lieu de demander à chaque ingénieur d'installer manuellement les dépendances sur sa machine. Il est essentiel d'avoir des versions cohérentes des dépendances pour un cycle de développement rapide, car il y a peu de chances d'avoir des problèmes de compatibilité dus à des versions incorrectes des dépendances.
DoorDash a été confronté à un problème similaire lorsque notre équipe a dû faire face à un temps de construction lent dans notre environnement local, ce qui a considérablement ralenti le processus de développement. Chaque fois qu'un ingénieur ajoutait un nouveau code à la base de code, le processus d'intégration prenait quatre à cinq minutes. Peu importe que nous modifiions 20 fichiers ou que nous ajoutions simplement une ligne de code pour créer un journal rapide, il fallait toujours environ cinq minutes pour terminer la compilation. Tous les ingénieurs savent qu'il est difficile de rester concentré et productif si une partie du cycle de développement interrompt leur processus de réflexion.
Le problème de notre processus de construction
Nous utilisons Gradle pour compiler notre application, puis Docker pour l'exécuter avec ses dépendances. Cependant, en raison de l'absence d'un processus de compilation uniforme pour le développement local, nous n'avons pas été en mesure de réaliser un développement efficace.
Un des problèmes était que le Dockerfile que notre équipe utilisait était la réplique exacte du Dockerfile de production. Notre fichier Docker de production est destiné à compiler l'application à partir de zéro, car nous n'utilisons pas le cache de construction Gradle en production. En utilisant localement le fichier Docker de production, nous ne tirons pas profit du cache Gradle et nous téléchargeons également des dépendances qui ne sont pas nécessaires dans notre environnement local. La figure 1, ci-dessous, est une représentation du fichier Docker qui causait des retards :
Un autre problème était que l'équipe utilisait la cible Makefile suivante pour exécuter notre processus de construction, dans lequel nous compilons en fait notre application en utilisant une instance locale de Gradle avant d'exécuter la construction Docker :
.PHONY: build
build:
git submodule update --init --recursive --remote
./gradlew clean installDist
docker-compose build
L'idée était d'utiliser le cache Gradle avant de construire l'image entière pour accélérer le temps de construction. Cependant, cette partie du processus est devenue un gaspillage d'efforts car nous exécutions la même commande à l'intérieur du Dockerfile, comme le montre la Figure 1 ci-dessus. En fait, notre fichier Docker contenait cette commande :
RUN ./gradlew clean installDist test ktlintCheck --no-daemon && \
mv build/install/* /home/ && \
rm -r build
Non seulement cette commande était similaire à celle que nous venons d'exécuter en dehors du fichier Docker, mais elle incluait également un test et une vérification du format en même temps que la construction. Le pire, c'est qu'elle n'a pas utilisé le cache de la machine locale et qu'elle a donc dû retélécharger toutes les dépendances de notre application.
Solution : Mettre à jour nos scripts de construction et notre fichier Docker.
Nous avons résolu le problème du temps de construction en examinant notre script de construction et notre fichier Docker et en réorganisant la façon dont nous construisons notre application et ses dépendances.
Comment nous avons mis à jour notre fichier Docker pour le développement local
- Nous avons supprimé toutes les applications tierces inutiles que nous n'utilisons pas localement. Par exemple, nous avons supprimé l'agent New Relic dont nous avons absolument besoin en production mais qui n'est pas nécessaire dans un environnement local.
- Nous avons supprimé l'installation de Gradle. Pour utiliser correctement le cache Gradle, nous avons utilisé l'installation de Gradle sur notre machine locale et construit l'application en dehors de notre build docker.
- Puisque nous ne compilons plus l'application à l'intérieur du fichier Docker, nous devons copier les fichiers du contexte de construction dans le répertoire approprié à l'intérieur de notre image.
Séparer les exécutions de la compilation et des tests unitaires
Dans notre Makefile, nous avons séparé nos exécutions de build et de test pour nous donner plus de flexibilité. L'équipe écrit et exécute constamment des tests unitaires via l'IDE pendant le développement, il n'est donc pas nécessaire de les réexécuter pour chaque build local. De plus, il n'est pas pratique d'exécuter tous les tests unitaires pour chaque version locale, surtout si les changements dans le code sont minimes. Bien sûr, chaque membre de l'équipe exécute toujours le build complet - qui exécute tous les tests, vérifie le format, et construit avant de livrer les changements à notre dépôt.
S'assurer que nous utilisons correctement le cache Gradle
Nous avons activé le cache Gradle en mettant org.gradle.caching=true
dans notre gradle.properties
. Comme nous n'avons pas de tâches complexes dans nos build.gradle
Pour les tâches complexes, c'est tout ce qu'il faut faire pour utiliser efficacement le cache de Gradle. Pour les tâches complexes dans build.gradle
L'optimisation de ces tâches peut permettre de tirer le meilleur parti du cache Gradle.
Résultats
Comme le montre la figure 2 ci-dessous, notre fichier Docker est plus léger que la version précédente (figure 1). En apportant ces modifications, nous avons réduit le temps de construction d'environ cinq minutes à une moyenne de 45 secondes, soit une diminution d'environ 85 %.
Si l'on considère que nous exécutons les commandes de construction plusieurs fois par jour, cette amélioration est une grande victoire pour l'équipe, car elle nous fait gagner beaucoup de temps et permet à tout le monde de se concentrer sur les tâches à accomplir.
Ce type de problème peut arriver à toute équipe qui débute dans la conteneurisation ou les microservices. Si vous gérez un Dockerfile, c'est toujours une bonne idée de le revisiter de temps en temps pour chercher des améliorations potentielles. Les meilleures pratiques de Dockerfile et le cache de construction de Gradle sont d'excellentes documentations pour mieux comprendre les problèmes de construction spécifiques.