Bonnes pratiques et considérations
Introduction
L'idée de cet article m'est venue, en début d'année, après ma publication sur les éléments à considérer quand on démarre un projet de développement et en discutant avec des collègues qui commençaient à apprendre un environnement cloud.
Les applications sont maintenant souvent déployées dans des environnements distribués, que ce soit via des fournisseurs cloud (AWS, Azure, GCP), des orchestrateurs comme Kubernetes, des services managés, des fonctions serverless, ou même des infrastructures on-premise distribuées.
Le développement d'applications destinées à ces environnements distribués nécessite de prendre en considérations plusieurs aspects spécifiques.
Voici une liste de pratiques et de considérations clés pour développer des applications distribuées résilientes et scalables.
Pour les fins de cet article, nous énoncerons principalement des considérations pour les services à durée de vie prolongée.
La plupart de ces considérations s'appliquent également aux fonctions serverless et aux applications événementielles.
Multiples instances jetables pour rendre un même service
Plusieurs clones, une seule mission
La considération la plus importante est que votre application doit être conçue pour fonctionner dans un environnement où plusieurs instances peuvent être exécutées simultanément. Cela signifie qu'une instance de votre application ne doit pas interférer avec une autre instance, et qu'il faut éviter la duplication de traitement. De plus, votre application ne doit pas dépendre d'un état local ou de ressources spécifiques à une instance, car ces éléments ne sont pas garantis d'être disponibles ou persistants entre les instances ou les redémarrages.
Que ce soit pour scaler horizontalement (ajouter plus d'instances) ou pour la résilience (redémarrer une instance défaillante), parce que le nombre d'instances change en fonction de la charge réelle ou anticipée, en fonction de l'heure, etc., votre application doit être capable de gérer plusieurs instances sans conflit.
Elle sera aussi probablement démarrée sans intervention humaine. Vous devez donc conserver ces aspects en tête lors de la conception et du développement de votre application.
Architecture derrière un load balancer/reverse proxy
Comme une répartitrice d'appels en centre de services : chaque requête est envoyée au bon département — facturation, support, ou comptes — selon ce qu'elle demande.
Dans la plupart des cas, les instances de votre application seront déployées derrière un load balancer (équilibreur de charge).
Le load balancer distribue le trafic entrant entre les différentes instances de votre application.
L'utilisation de sticky sessions permet de faire en sorte que le load balancer va envoyer le trafic qui revient à la même instance.
Mais, il se peut que l'instance qui a fait le traitement initial ne soit plus disponible (scale down, mise à jour, etc.) ou que le load balancer ne supporte pas les sessions sticky, cela signifie que votre application doit être capable de prendre le relais d'une requête sans avoir répondu à la requête précédente dans le workflow.
Dans les architectures modernes, le load balancer ou l'API Gateway peut aussi router vers des services différents selon la requête :
- Par chemin (path-based routing), ex. :
/api/users→ service Utilisateurs,/api/orders→ service Commandes - Par hôte (host-based routing), ex. :
api.example.com→ API,static.example.com→ ressources statiques - Par en-tête ou version d'API (header/version routing), ex. :
X-Client: mobileouAccept: application/vnd.company.v2+json - Par méthode ou port (rare), selon les contraintes techniques
Les conséquences du côté développement sont que vous devez éviter les hypothèses d'état de session local, car une requête peut arriver à un autre service ou une autre instance. Il devient très utile de propager les identifiants de corrélation pour tracer la requête à travers plusieurs services.
Externaliser la configuration
Externaliser la configuration, c'est comme insérer une carte SIM dans un téléphone : même appareil, réglages adaptés au réseau. En changeant de SIM, on peut aussi changer de fournisseur — Bell, Vidéotron, Orange — sans toucher au téléphone lui-même.
Un aspect important du développement pour le déploiement distribué (dont le cloud) est de séparer la configuration de l'application du code lui-même.
Cela permet notamment de modifier la configuration sans avoir à recompiler l'application.
Une pratique très courante est d'utiliser des variables d'environnement pour fournir la configuration à l'application. Voir https://12factor.net/fr/config
Les différents fournisseurs cloud offrent aussi un service d'externalisation de la configuration, comme AWS Systems Manager Parameter Store, Azure App Configuration ou GCP Secret Manager.
Ces services permettent de stocker et de gérer la configuration de manière sécurisée et centralisée. Il est aussi possible de déployer votre propre service de configuration, comme Consul, etcd ou Spring Cloud Config. Par contre, vous devrez adapter votre application pour qu'elle puisse récupérer la configuration à partir de ces services.
Le choix final que vous ferez dépendra de si vous prévoyez déployer votre application dans un seul fournisseur cloud ou dans plusieurs ou s'il est possible que vous changiez éventuellement de fournisseur. Il est probablement plus prudent d'utiliser une solution agnostique au fournisseur cloud pour éviter le vendor locking, c'est-à-dire de ne pouvoir déployer que sur un seul fournisseur.
Feature flags et configuration dynamique
Les feature flags, c'est comme les scènes post-crédits Marvel : on peut les activer pour certains, les cacher pour d'autres, et les couper net si ça tourne mal. Zéro recompilation, juste un interrupteur.
Les feature flags (drapeaux de fonctionnalités) permettent d'activer ou désactiver des fonctionnalités sans recompiler l'application, ou même la redéployer si vous mettez en place une façon d'en prendre compte pendant que l'application roule. Cette stratégie peut être très utile, notamment pour les cas d'utilisation suivants.
Déploiements progressifs : Activer une nouvelle fonctionnalité pour un pourcentage d'utilisateurs ou certains groupes d'utilisateurs
A/B testing : Tester différentes versions d'une fonctionnalité
Kill switch : Désactiver rapidement une fonctionnalité problématique en production sans redéployer
Environnements spécifiques : Activer des fonctionnalités seulement dans certains environnements, comme dev ou qa mais pas en production pour valider avant le déploiement complet sans devoir gérer plusieurs branches de code.
Des outils comme LaunchDarkly, Unleash, Split.io, ou AWS AppConfig peuvent être utilisés pour gérer les feature flags.
Gestion des secrets
Mettre des secrets dans le code source, c'est comme cacher la clé de la maison sous le paillasson : tout le monde sait où chercher. Utilisez un vrai coffre-fort.
Ne jamais stocker des secrets (mots de passe, clés API, certificats) directement dans le code ou dans les fichiers de configuration versionnés.
Utilisez plutôt des services de gestion des secrets qui offrent des fonctionnalités de sécurité avancées, comme des services clouds dédiés (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager), le système d'encryption de votre orchestrateur (Kubernetes Secrets avec encryption at rest activée), ou des outils tiers comme HashiCorp Vault ou SOPS.
Ces solutions offrent plusieurs avantages, comme l'encryption des secrets au repos et en transit, un contrôle d'accès granulaire, la rotation automatique des secrets, et l'audit des accès.
Sauvegarde de fichiers de travail ou téléversés
Écrire sur le disque local en cloud, c'est laisser vos notes sur la table du café : ça marche… jusqu'au ménage.
Imaginons une application qui génère un rapport (PDF, Excel, etc.) à partir de données fournies par l'utilisateur.
Dans un scénario "classique", l'application pourrait générer le rapport sur le disque dur local de l'instance, puis le rendre disponible pour téléchargement.
Par contre, dans un environnement à multiple instance, il faut mettre ce fichier soit dans un stockage partagé, un volume réseau, ou, au pire, la base de données.
Les fournisseurs offrent des services de stockage, comme AWS S3, AWS EFS, Azure Blob Storage, Azure Files, GCP Cloud Storage ou GCP Filestore, qui peuvent être utilisés pour stocker ces fichiers intermédiaires.
Tâches périodiques / Cron Job
Les crons en multi-instances, c'est comme nettoyer la maison en colocation : si 5 colocataires font le ménage au même moment pour la même pièce, c'est du travail gaspillé. Il faut qu'un seul le fasse à la fois. Sinon, même pièce nettoyée 5 fois = 5 fois le travail, 5 fois le coût.
Les tâches périodiques, comme l'envoi de courriel de rappel, la création de rapports ou la purge de données sont fréquentes dans les applications.
Dans un environnement distribué, il est important de s'assurer que ces tâches ne sont pas exécutées simultanément par plusieurs instances de l'application.
Pour y arriver, il faut réfléchir à une façon de synchroniser l'exécution de ces tâches. Plusieurs approches sont possibles :
Utiliser un service de planification externe, comme AWS CloudWatch Events, Azure Logic Apps ou GCP Cloud Scheduler, pour déclencher les tâches périodiques.
Utiliser un mécanisme de verrouillage distribué, comme une entrée dans une base de données ou un service de cache distribué (Redis, Memcached) pour s'assurer qu'une seule instance exécute la tâche à un moment donné.
Utiliser une bibliothèque de planification comme Quartz Scheduler ou autre similaire.
Il peut être intéressant de développer une mécanique qui permet de lancer des tâches spécifiques, qui peuvent être appelées par le service de planification externe ou par un job interne avec un verrouillage distribué. Par exemple de créer des endpoints HTTP sécurisés qui déclenchent les tâches périodiques. Dans ce cas, des endpoints complémentaires pour obtenir le statut des tâches peuvent aussi être utiles pour le monitoring.
L'important, c'est de faire en sorte que les tâches périodiques soient exécutées de manière fiable et sans conflit entre les différentes instances de l'application.
Gestion de l'état et des sessions
Les sessions en déploiement distribué, c'est un vestiaire : on présente son ticket et on retrouve son manteau, peu importe la porte d'entrée ou le préposé.
Dans un environnement distribué avec plusieurs instances, la gestion de l'état utilisateur (sessions) devient critique. Si un utilisateur se connecte à une instance et que sa prochaine requête est routée vers une autre instance, l'application doit pouvoir récupérer les informations de la session. Plusieurs approches existent pour gérer les sessions dans un environnement distribué.
Sessions stateless : Utiliser des tokens JWT (JSON Web Tokens) qui contiennent toutes les informations nécessaires. Le client transmet le token à chaque requête. Il peut le transmettre par header, par cookie, ou dans le corps de la requête.
Cache distribué : Stocker les sessions dans un cache distribué comme Redis ou Memcached, accessible par toutes les instances.
Sticky sessions : Configurer le load balancer pour router toujours le même utilisateur vers la même instance (moins recommandé car crée des dépendances et ne fonctionnera pas si l'instance est arrêtée).
Base de données : Stocker les sessions dans la base de données (moins performant mais plus simple).
L'approche à privilégier dépendra de vos besoins spécifiques, mais les sessions stateless (JWT) ou le cache distribué (Redis, Memcached) souvent généralement préférés. Les sessions stateless permettent de réduire la dépendance à un stockage centralisé et facilitent le scaling horizontal. Par contre, la révocation immédiate des sessions peut être plus complexe avec JWT. De l'autre côté, l'utilisation d'un cache distribué offre une gestion centralisée des sessions, mais introduit une dépendance supplémentaire et peut nécessiter une infrastructure plus complexe.
Persistance et transactions de base de données
La base de données, c'est comme le seul guichet de la banque : si 10 clients ouvrent chacun leur porte (connexion), c'est du gaspillage. Il faut une file d'attente partagée. Et si 10 clients payent la même facture en même temps, il faut éviter les doublons. Une seule transaction réussie, pas 10 écritures identiques.
Dans un environnement distribué, la gestion des transactions de base de données nécessite une attention particulière.
Connexions à la base de données : Utilisez un pool de connexions pour optimiser l'utilisation des ressources. Configurez le nombre maximum de connexions en fonction du nombre d'instances.
Transactions distribuées : Évitez les transactions distribuées (2PC - Two-Phase Commit) autant que possible, car elles sont complexes et réduisent la performance. Privilégiez le pattern Saga pour gérer les transactions sur plusieurs services.
Idempotence : Concevez vos opérations de base de données pour être idempotentes, c'est-à-dire qu'elles produisent le même résultat même si elles sont exécutées plusieurs fois.
Retry et resilience : Implémentez des mécanismes de retry pour gérer les échecs temporaires de connexion à la base de données.
Migrations de schéma de base de données
Migrer le schéma, c'est rénover sans fermer le magasin : on déplace les rayons, on garde les clients.
Les migrations de schéma dans un environnement distribué nécessitent une planification minutieuse.
Migrations forward-compatible : Les changements doivent être compatibles avec l'ancienne version pendant le déploiement, on peut ainsi déployer les nouvelles instances avant de retirer les anciennes ou même faire un retour en arrière applicatif si nécessaire.
Outils de migration : Utilisez Liquibase, Flyway, ou des outils natifs (Alembic pour Python, migrate pour Go)
Déploiements testés : Pour les changements, particulièrement les changements majeurs, il est important de tester la migration de la base de données avant d'appliquer à la production
Migrations à l'arrêt vs au démarrage : Décidez si les migrations s'exécutent avant ou pendant le déploiement
Rollback : Préparez toujours un plan de rollback pour les migrations complexes
Patterns pour les changements de schéma
Il existe certains patterns courants pour gérer les changements de schéma de base de données dans un environnement distribué. Ils permettent de minimiser les interruptions de service et de garantir la compatibilité entre les versions.
-
Expand-Contract Pattern :
- Expand : Ajoutez la nouvelle colonne/table
- Dual-write : Écrivez dans l'ancienne et la nouvelle structure
- Migrate : Migrez les anciennes données
- Contract : Supprimez l'ancienne structure
Versioning des changements : Quand les changements de schéma affectent l'API, utilisez le versioning (v1, v2)
Mise en cache et performance
Le cache, c'est comme garder les documents importants sur votre bureau plutôt qu'au sous-sol : on accède vite à ce qu'on utilise souvent. Mais attention, il faut parfois faire le ménage pour éviter de travailler avec des versions périmées.
Le cache est crucial pour la performance dans un environnement distribué. Obtenir une information en mémoire est beaucoup plus rapide que d'aller la chercher dans une base de données ou un service externe.
Types de cache
Il existe plusieurs types de cache que vous pouvez utiliser.
Cache en mémoire local : Rapide mais non partagé entre instances (Caffeine, Guava Cache)
Cache distribué : Partagé entre toutes les instances (Redis, Memcached, Hazelcast)
CDN : Pour les ressources statiques (CloudFront, Azure CDN, Cloud CDN, autres)
HTTP caching : Utilisez les headers HTTP (Cache-Control, ETag) pour le cache côté client
Reverse proxy cache : Cache comme varnish
Stratégies de cache
La stratégie de cache dépendra de vos besoins spécifiques.
Cache-aside : L'application vérifie le cache, puis la base de données si nécessaire
Write-through : Les données sont écrites dans le cache et la base de données simultanément
Write-behind : Les données sont écrites dans le cache d'abord, puis dans la base de données de façon asynchrone
Invalidation : Définissez des stratégies claires pour invalider le cache (TTL, événements)
Invalidation et nettoyage du cache
L'un des défis les plus importants avec le cache est de savoir quand le nettoyer ou l'invalider. Un cache obsolète peut causer des bugs difficiles à diagnostiquer.
TTL (Time-To-Live) : Définissez une durée de vie maximale pour chaque entrée du cache. Après ce délai, l'entrée est automatiquement supprimée ou marquée comme expirée.
Invalidation par événement : Lorsque des données sont modifiées dans la base de données, invalidez ou mettez à jour les entrées de cache correspondantes. Utilisez des event listeners ou des patterns pub/sub.
Cache tagging : Associez des tags aux entrées de cache pour pouvoir invalider plusieurs entrées liées d'un coup (par exemple, toutes les entrées liées à un utilisateur spécifique).
Versioning : Incluez une version dans la clé de cache. Quand vous déployez un changement de format, incrémentez la version pour invalider automatiquement les anciennes entrées.
Flush sélectif vs global : Évitez de vider tout le cache (flush) sauf en cas d'urgence. Préférez l'invalidation ciblée pour maintenir les performances.
Cache warming : Après un vidage ou au démarrage, préremplissez le cache avec les données les plus utilisées pour éviter une charge soudaine sur la base de données.
Certaines considérations sont importantes pour l'invalidation et le nettoyage de la cache pour les environnements distribués. Si vous utilisez un cache distribué (Redis, Memcached), l'invalidation sera visible par toutes les instances. Par contre, si vous utilisez un cache local en mémoire, chaque instance aura son propre cache. Vous devrez propager les événements d'invalidation à toutes les instances (via pub/sub ou message queue). Documentez clairement votre stratégie d'invalidation pour chaque type de données cachées.
Autres considérations importantes
- Attention au thundering herd : quand plusieurs instances tentent de recharger le même cache expiré simultanément. On revient ici avec la notion selon laquelle plusieurs instances tentent de faire la même chose en même temps. On peut mettre en place une stratégie de grace mode pour mitiger le problème.
- Utilisez des clés de cache bien structurées pour faciliter l'invalidation ciblée
- Surveillez le taux de hit/miss du cache pour optimiser la configuration
- Loggez les invalidations de cache pour faciliter le debugging
Considérations pour l'équipe devops
Le devops, c'est comme piloter un avion qu'on n'a pas construit : on a besoin d'instruments de bord clairs, de voyants qui s'allument au bon moment, et d'un manuel qui dit ce qu'il faut faire si ça tourne mal.
L'équipe qui va déployer et gérer l'application a des besoins spécifiques qui doivent être pris en compte dès le début du développement.
Observabilité : Logs, métriques et traces
Et quand l'avion a des problèmes, on devient détective : les logs racontent ce qui s'est passé dans le cockpit, les métriques montrent les tendances du vol, et les traces suivent la route à travers le ciel. Sans ces instruments, on enquête à l'aveugle dans le brouillard.
L'observabilité est cruciale dans un environnement distribué où il peut être difficile de diagnostiquer les problèmes.
Logging structuré
Utilisez un format de log structuré tel JSON pour faciliter la recherche et l'analyse.
Incluez un identifiant de corrélation (correlation ID) dans chaque log pour tracer une requête à travers plusieurs services et instances.
Centralisez vos logs dans un système comme AWS CloudWatch, Azure Monitor, GCP Cloud Logging, ELK Stack (Elasticsearch, Logstash, Kibana), Datadog ou Loki.
Évitez de logger des informations sensibles (mots de passe, tokens, données personnelles).
Évitez aussi de logger trop d'informations pour ne pas noyer les logs importants.
Si possible, utilisez des bibliothèques de logging qui supportent la reconfiguration dynamique du niveau de log (DEBUG, INFO, WARN, ERROR) sans redémarrer l'application.
Métriques
Exposez des métriques sur la santé et la performance de votre application (temps de réponse, nombre de requêtes, taux d'erreur, utilisation CPU/mémoire).
Utilisez des outils comme Prometheus, Grafana, ou les outils natifs de votre plateforme de déploiement pour collecter et visualiser ces métriques.
Configurez des alertes basées sur ces métriques pour être notifié des problèmes.
Tracing distribué
Implémentez le tracing distribué avec des outils comme Jaeger, Zipkin ou AWS X-Ray pour suivre le parcours d'une requête à travers vos différents services.
Utilisez OpenTelemetry comme standard pour instrumenter votre application.
Health checks et readiness probes
Les probes, c'est le test du micro avant le concert : on vérifie qu'on est prêt avant de monter le son.
Les orchestrateurs comme Kubernetes et les load balancers ont besoin de savoir si une instance de votre application est en bonne santé et prête à recevoir du trafic.
Il existe généralement trois types de probes.
Le Liveness probe indique si l'application est vivante et fonctionne. Si elle échoue, l'orchestrateur redémarre l'instance.
Le Readiness probe indique si l'application est prête à recevoir du trafic. Par exemple, si les connexions à la base de données ne sont pas encore établies, l'instance n'est pas prête.
Le Startup probe, pour les applications qui prennent du temps à démarrer, évite que la liveness probe ne tue l'application prématurément.
Implémentez des endpoints HTTP dédiés (par exemple /health et /ready) qui retournent le statut approprié.
Graceful shutdown
Fermer en douceur, c'est dire au revoir avant de raccrocher : on termine la phrase, puis on coupe.
Lorsqu'une instance de votre application est arrêtée (mise à jour, scaling down, etc.), elle doit se terminer proprement :
- Arrêter d'accepter de nouvelles requêtes
- Terminer le traitement des requêtes en cours
- Fermer proprement les connexions aux bases de données et aux services externes
- Libérer les ressources
Implémentez un signal handler (SIGTERM) pour gérer l'arrêt gracieux de votre application. La plupart des orchestrateurs envoient un SIGTERM avant de forcer l'arrêt avec SIGKILL.
Gestion des dépendances et vulnérabilités
Faites votre inventaire comme chez Home Alone : vérifiez les pièges, ne laissez pas d'entrées faciles.
La gestion proactive des dépendances est cruciale pour la sécurité et la stabilité :
Scan régulier : Utilisez des outils comme Dependabot, Snyk, ou OWASP Dependency-Check pour identifier les vulnérabilités dans vos dépendances
Mises à jour automatisées : Configurez des pull requests automatiques pour les mises à jour de sécurité
Versioning sémantique : Respectez le versioning sémantique pour vos propres bibliothèques
Lockfiles : Utilisez des fichiers de verrouillage (package-lock.json, uv.lock, Pipfile.lock, go.sum, pom.xml) pour garantir la reproductibilité
Audit régulier : Faites des audits de sécurité réguliers de vos dépendances
CI/CD et pipelines de déploiement
Le pipeline, c'est la chaîne de montage : on sort des versions fiables, pas des prototypes du lundi matin.
L'automatisation du déploiement est essentielle pour le cloud, elle est aussi très utile quand on déploie sur des serveurs traditionnels. Elle permet de réduire les oublis, les erreurs humaines et d'accélérer les mises à jour.
Intégration continue : Exécutez les tests automatiquement à chaque commit (GitHub Actions, GitLab CI, Jenkins, CircleCI).
Tests : Incluez des tests unitaires, d'intégration, et end-to-end dans votre pipeline.
Build d'images : Automatisez la création des images de conteneur.
Scan de sécurité : Intégrez le scan de sécurité des images et des dépendances dans le pipeline.
Déploiement progressif : Utilisez des stratégies comme blue/green deployment, canary deployment, ou rolling updates pour minimiser les risques.
Rollback automatique : Configurez un rollback automatique si les health checks échouent après un déploiement.
Infrastructure as Code : Gérez votre infrastructure avec Terraform, CloudFormation, Pulumi, ou ARM templates.
Faites en sorte que votre pipeline injecte la version de l'image dans les variables d'environnement de l'application.
Conteneurisation et images
Un bon conteneur, c'est une valise bien rangée : légère, sécurisée, et sans le tag 'latest' perdu à l'aéroport.
La conteneurisation est quasi-universelle dans le déploiement distribué :
Images légères : Utilisez des images de base minimales (Alpine, distroless) pour réduire la taille et la surface d'attaque.
Multi-stage builds : Utilisez des builds multi-étapes pour séparer la compilation de l'exécution et réduire la taille finale.
Scan de sécurité : Scannez vos images pour détecter les vulnérabilités (Trivy, Snyk, Clair).
Versioning : Versionnez vos images et évitez d'utiliser le tag latest en pré-production ou en production.
Non-root user : Exécutez vos conteneurs avec un utilisateur non-root pour la sécurité.
Pipelines : Automatisez la construction, le test et le déploiement de vos images via des pipelines CI/CD.
Assurez-vous d'exposer la version de l'image dans les variables d'environnement de l'application.
Optimisation des coûts (cloud)
Le cloud facture comme un taxi : laissez le compteur tourner, et la note grimpe. Mettez des limites.
Le cloud peut devenir coûteux si on ne fait pas attention :
Auto-scaling intelligent : Configurez l'auto-scaling basé sur des métriques réelles (CPU, mémoire, nombre de requêtes).
Right-sizing : Choisissez la taille d'instance appropriée pour votre charge de travail. Ne sur-provisionnez pas.
Utilisation de spot instances : Pour les charges de travail non-critiques ou les environnements de test, utilisez des instances spot/préemptibles.
Shutdown automatique : Arrêtez les environnements de développement et de test en dehors des heures de travail.
Monitoring des coûts : Utilisez les outils de monitoring des coûts fournis par votre cloud provider.
Gestion des ressources et limites
Les ressources, c'est le budget calorique : trop peu, on cale; trop, on somnole. Trouvons le bon dosage.
Définir correctement les ressources allouées à votre application est essentiel :
Requests et Limits : Dans Kubernetes, définissez des requests (ressources garanties) et des limits (ressources maximales) pour CPU et mémoire
Memory leaks : Surveillez activement les fuites mémoire qui peuvent causer des redémarrages fréquents
JVM tuning : Pour les applications Java, configurez correctement le heap size et le garbage collector en fonction des ressources allouées
Connection pools : Dimensionnez correctement vos pools de connexions (DB, HTTP clients) en fonction du nombre d'instances et de la charge
Thread pools : Configurez les thread pools pour éviter l'épuisement des threads et les blocages
Développement local et environnement de dev
Développer localement, c'est répéter en studio : on règle les fausses notes avant le concert en prod.
Développer pour un environnement distribué ne signifie pas que tout doit se faire dans cet environnement.
Émulation locale des services distribués
LocalStack : Émule les services AWS localement (S3, DynamoDB, SQS, etc.)
Azurite : Émulateur pour Azure Storage
Docker Compose : Orchestrez localement vos services (base de données, cache, message queues)
Testcontainers : Lancez des conteneurs pour vos tests d'intégration
Minikube/Kind : Exécutez Kubernetes localement pour tester vos déploiements
Configuration locale vs déploiement distribué
Utilisez des profils de configuration différents pour le développement local vs déploiement distribué. Évitez de dépendre de services cloud spécifiques pendant le développement local quand possible. Documentez clairement les étapes pour configurer l'environnement de développement local, idéalement avec des scripts d'automatisation. Utilisez des variables d'environnement avec des valeurs par défaut pour simplifier la configuration locale. Si vous devez utiliser des secrets, utilisez des fichiers .env locaux ou des services de gestion des secrets adaptés au développement comme SOPS. Voir l'article que j'ai rédigé à ce sujet: SOPS - Secrets encryptés dans un dépôt GIT
Hot reload et développement rapide
Utilisez des outils de hot reload (Spring Boot DevTools, Nodemon, Air pour Go) pour accélérer le cycle de développement.
Configurez des volumes montés dans Docker pour un rechargement rapide du code.
Utilisez des outils comme Skaffold ou Tilt pour le développement continu sur Kubernetes.
Tests pour applications distribuées
Les tests, c'est comme les crashs tests de voitures : on fracasse des prototypes dans un labo pour éviter d'avoir de vrais accidents sur l'autoroute. Et quand quelqu'un modifie le volant ou les freins, on refracasse tout pour vérifier que ça tient toujours la route.
Tester des applications distribuées nécessite une approche différente.
Tests unitaires
- Tests isolés de la logique métier
- Mockez les dépendances externes (base de données, services tiers)
- Utilisez des frameworks de test adaptés à votre langage
Tests d'intégration
- Testcontainers : Lancez des conteneurs Docker pour tester avec de vraies dépendances
- Tests de base de données : Testez les requêtes SQL et les migrations avec une vraie base de données
- Tests d'API : Testez vos endpoints REST/gRPC avec des outils comme RestAssured, Supertest
Tests end-to-end
- Testez le système complet dans un environnement similaire à la production
- Utilisez des outils comme Selenium, Cypress, Playwright pour les applications web
- Automatisez ces tests dans votre pipeline CI/CD
Tests de charge et de performance
- Load testing : Testez le comportement sous charge normale (Apache JMeter, Gatling, k6)
- Stress testing : Testez les limites du système
- Spike testing : Testez la réponse à des pics soudains de trafic
- Soak testing : Testez la stabilité sur une longue période pour détecter les fuites mémoire
Tests de chaos engineering
Si nécessaire, intégrez des tests de chaos engineering pour valider la résilience de votre application :
- Testez la résilience en introduisant des défaillances volontaires
- Utilisez des outils comme Chaos Monkey, Gremlin, ou LitmusChaos
- Simulez des pannes de serveurs, des latences réseau, des bases de données indisponibles
Tests de régression visuelle
- Pour les applications frontend, testez que l'interface n'a pas changé de façon non intentionnelle
- Utilisez des outils comme Percy, Chromatic, ou Applitools
Communication entre services
La communication entre services, c'est comme un orchestre de jazz : chaque musicien doit savoir quand jouer, dans quelle tonalité, et à quel tempo. Sans protocole commun (REST, gRPC, ou messages asynchrones), c'est la cacophonie. Tout le monde improvise, mais pendant le même jam-session.
Si votre application est composée de plusieurs microservices ou composants qui doivent communiquer entre eux, il est crucial de choisir les bons protocoles et patterns de communication.
API REST : Standard et simple, utilisez HTTP/HTTPS avec des formats JSON.
gRPC : Plus performant que REST, utilise Protocol Buffers et HTTP/2.
Message queues : Pour la communication asynchrone, utilisez RabbitMQ, Apache Kafka, AWS SQS, Azure Service Bus, Postgres Notify, ou GCP Pub/Sub.
Service mesh : Pour gérer la communication entre services (Istio, Linkerd) avec features comme le load balancing, le circuit breaking, et le mTLS.
Certaines considérations importantes sont à ne pas négliger. Utilisez des timeouts pour toutes les communications inter-services. Implémentez des retry avec backoff exponentiel. Utilisez des circuit-breakers pour éviter les cascades de défaillances. Propagez les correlation IDs pour le tracing distribué.
Résilience et gestion des erreurs
La résilience, c'est l'amortisseur : la route est cahoteuse, mais on garde le cap sans casser le châssis.
Dans un environnement distribué, les erreurs sont inévitables. Votre application doit être conçue pour être résiliente.
Circuit breaker : Évite de surcharger un service défaillant en "ouvrant le circuit" après un certain nombre d'échecs.
Retry avec backoff exponentiel : Réessaie les opérations échouées avec un délai croissant entre chaque tentative.
Timeout : Définissez des timeouts pour toutes les opérations réseau pour éviter d'attendre indéfiniment.
Bulkhead : Isolez les ressources pour qu'une défaillance dans une partie du système n'affecte pas les autres.
Fallback : Prévoyez un comportement de secours quand une opération échoue (cache, valeur par défaut, mode dégradé).
Des bibliothèques comme Resilience4j (Java), Polly (.NET) ou Hystrix facilitent l'implémentation de ces patterns.
Patterns de conception à considérer
Les bons patterns, c'est une carte routière : on évite les chemins de terre et on arrive à l'heure.
Plusieurs patterns architecturaux sont particulièrement adaptés aux déploiements distribués.
Strangler Fig Pattern : Migrez progressivement une application monolithique vers des microservices en "étranglant" l'ancien système
Backend for Frontend (BFF) : Créez des API spécifiques pour chaque type de client (web, mobile, etc.)
API Gateway : Point d'entrée unique pour tous vos services, gérant l'authentification, le routage, le rate limiting
Event Sourcing : Stockez tous les changements d'état comme une séquence d'événements
CQRS (Command Query Responsibility Segregation) : Séparez les opérations de lecture et d'écriture pour optimiser chacune indépendamment
Sidecar Pattern : Déployez des fonctionnalités auxiliaires (logging, monitoring) dans un conteneur séparé mais adjacent
Il est important de bien comprendre ces patterns, dans quel contexte les utiliser et de les adapter à vos besoins spécifiques.
Anti-patterns à éviter
Les anti-patterns, ce sont les panneaux 'route barrée' : si on insiste, on finit dans le fossé.
Distributed Monolith : Microservices trop couplés qui doivent être déployés ensemble. L'indépendance relative entre les services est cruciale.
Chatty Services : Trop de communication entre services, créant de la latence, pensez à regrouper les appels, à la caching, ou à utiliser des messages asynchrones.
Tight Coupling : Services dépendant fortement les uns des autres, rendant les changements et le déploiement difficiles. Pensez à utiliser des interfaces claires et des contrats stables.
Data Ownership Violations : Services accédant directement à la base de données d'autres services. Si vous choisissez de considérer la base de données comme un service, chaque service doit gérer son propre schéma de données. De plus, s'il est plus efficace pour un service d'accéder directement à la base de données d'un autre service, pensez à exposer l'information sous la forme d'une vue qui devient le contrat entre les deux services.
Ignorer la loi de fallacies of distributed computing : Supposer que le réseau est fiable, la latence nulle, la bande passante infinie, etc.
Sécurité
La sécurité, c'est la serrure ET l'alarme : le cloud a des voisins curieux, mieux vaut être prudent.
Tant dans un déploiement distribué traditionnel que dans le cloud, la sécurité est une responsabilité partagée par les développeurs et l'équipe devops doit être intégrée dès le début.
Principe du moindre privilège : Donnez uniquement les permissions nécessaires aux applications et aux utilisateurs.
Encryption : Chiffrez les données au repos et en transit (TLS/HTTPS).
Authentification et autorisation : Utilisez des standards comme OAuth2/OIDC pour l'authentification, JWT pour les tokens.
Scan de vulnérabilités : Scannez régulièrement vos dépendances et vos images.
WAF : Utilisez un Web Application Firewall pour protéger contre les attaques courantes.
DDoS protection : Activez la protection DDoS offerte par votre cloud provider. Sinon, regardez ce que vous pouvez mettre en place vous-même.
Audit logs : Conservez des logs d'audit pour toutes les opérations sensibles.
Environnements multiples
Multi-environnements, ce sont des costumes adaptés : même comédien, répétitions en studio, première en gala.
Maintenez plusieurs environnements pour différents stades du cycle de vie. Ils permettent une liberté pour tester et valider les changements avant de les déployer aux testeurs, puis en production.
Local : Pour le développement individuel
Développement : Pour s'assurer que les nouvelles fonctionnalités fonctionnent ensemble, que ça déploie correctement, pour permettre aux développeurs de tester des intégrations.
Test/QA : Pour les tests d'intégration et d'acceptation
Staging/Pre-prod : Une copie de la production pour les tests finaux par un groupe restreint d'utilisateurs, souvent du côté du client, avant le déploiement en production
Production : L'environnement de production
Utilisez l'Infrastructure as Code est une excellente pratique pour garantir que tous les environnements sont configurés de manière cohérente. Ça évite aussi qu'une configuration manuelle soit oubliée dans un environnement ou qu'une configuration essentielle soit omise ou différente entre les environnements.
Conclusion
Adopter ces pratiques, c'est comme apprendre le piano : on ne joue pas du Beethoven dès le premier jour. Commencez par les bases, adaptez à votre rythme, et avant longtemps vous ferez de la musique — sans péter les plombs ni le budget.
Développer pour des environnements distribués n'est pas simplement une question de déploiement dans un fournisseur cloud ou dans des serveurs de l'organisation.
Il faut penser à certains aspects spécifiques du développement pour s'assurer que l'application fonctionne correctement dans une architecture distribuée.
Ce petit guide couvre plusieurs des considérations les plus importantes, mais il en existe d'autres en fonction des besoins spécifiques de chaque application.
Il n'est certes pas exhaustif, mais j'espère qu'il servira de point de départ pour ceux qui débutent dans le développement pour le déploiement distribué.
L'adoption de ces pratiques peut sembler lourde au début, mais elle apporte de nombreux avantages : meilleure scalabilité, résilience accrue, facilité de maintenance, et réduction des coûts à long terme. L'important est de commencer progressivement et d'adapter ces principes aux besoins réels de votre application.
Si vous voulez aller plus loin, les principes des 12-Factor App, une méthodologie bien établie pour construire des applications SaaS (Software as a Service) modernes est un bon point de départ.
Cet article fait partie du Advent of Tech 2025 @ Onepoint, une série d'articles tech publiés par Onepoint pour patienter jusqu'à Noël.
Voir tous les articles du Advent of Tech 2025
Top comments (0)