Avant toute chose, je tiens à dire que j’adore Terraform. Cet outil vous permet de décrire l’infrastructure de votre application complètement avec du code. Et qui dit code dit travailler avec des outils géniaux comme Git et adopter leurs modes de fonctionnement. Les premiers projets où j'ai utilisé Terraform étaient des projets monolithiques où nous n’avions pas l’utilité des modules : un réseau privé, une poignée de machines virtuelles de même taille et quelques règles pare-feu.
Dans une précédente mission, j’ai eu l’opportunité de travailler sur un projet de plus grande ampleur basé sur des technologies cloud. Cela se traduit par un ensemble de micro-services hébergés sur Kubernetes consommant des bases de données managées sur le cloud (GCP, Aiven, …). Cela fut une très bonne occasion pour moi d’explorer la grandeur de Terraform et d’en rencontrer certaines limites.
Un peu de contexte
Avant de parler d’affaires de geek, il faut que je vous parle des quelques spécificités de l'application et de l’organisation de l'équipe.
L’application est une application web à destination d’un usage interne sur plusieurs environnements et est utilisée par plusieurs pays sans être multi-tenant, impliquant qu’il faut la déployer une fois par pays. Cela fait pas mal d’infrastructures à gérer au quotidien.
Une autre chose que vous devez savoir, nous sommes une équipe unique avec une vision DevOps, dédiée à l’application. Cela signifie plusieurs choses :
- Tout le monde dans l’équipe doit avoir les mêmes droits, dans le sens où il n’y a pas de raison de refuser des droits selon le profil, et tout le monde doit être en capacité d’achever n’importe quelle tâche sur le projet.
- Notre but est de pouvoir déployer nos releases en continu et par conséquent, nous n’avons pas de procédure complexe de revue de code et de test par plusieurs partis.
- Nous devons faire en sorte que la croissance de l’infrastructure n’impacte pas son évolution.
Conception du code de l’infrastructure
D’abord, parlons des principes que nous avons mis en place pour le développement de notre code Terraform.
Afin de réduire la charge de travail liée à la maintenance de l’infrastructure, nous voulions faire en sorte qu‘il y ait le moins de différences possibles entre chaque déploiement de l’application. Cela implique que le code doit produire des résultats consistants. Afin de réduire le risque de mal nommer un composant de l’infrastructure, chaque workspace Terraform utilisera seulement 2 variables : le pays qui l’utilisera, et le type d’environnement. Dans les faits, nous avons créé une troisième variable car nous avons plusieurs environnements de développement en France, mais tout est calculé à partir de ces 3 variables.
Toujours dans l’optique de réduire la charge de travail liée à la maintenance de l’infrastructure, nous nous assurons que tout objet créé par Terraform peut être facilement identifié et compris sans avoir à lire le code source associé.
Maintenant, afin de faciliter la croissance et l’évolution de l’infrastructure, nous voulions construire des blocs d’infrastructure facilement réutilisables. Quand on dit facilement réutilisables, c’est-à-dire comme avec des legos. Pour ce faire, nous avons fait en sorte que nos modules Terraform déclarent le moins de variables en entrée. Tout ce qui est spécifique à l’application - des conventions de nommage aux décisions architecturales et à la configuration en tant que code - sera entièrement englobé dans ces modules.
Décomposition des modules
Maintenant que nous avons établi nos principes, parlons de ce qui vous a amené ici : le code.
Si vous avez déjà travaillé avec Terraform, vous êtes certainement arrivé à un point où vous devez découper votre infrastructure monolithique en modules réutilisables. Mais la question est “jusqu’où faut-il découper ces modules ?”
Nous avons décidé dans l’équipe qu’il fallait les découper le plus finement possible tout en évitant d’avoir des modules utilisés systématiquement ensemble. Cela nous a amené à créer un modules pour gérer une instance Cloud SQL, un autre pour gérer une base de données dans Cloud SQL avec son utilisateur dédié, etc.
Nous appelons ces modules des modules techniques, puisqu'ils gèrent les aspects techniques de l’infrastructure. Ces modules embarquent les décisions architecturales de l’application et des détails de la configuration en tant que code. Par exemple, notre module de base de données Cloud SQL gère :
- la base de données (avec le nommage calculé à partir de la convention de nommage)
- son utilisateur dédié (ainsi que son mot de passe généré, bien sûr)
- les règles d’accès pour que l’utilisateur puisse utiliser la base de données
- le secret dans Vault contenant toutes les informations pour que le micro-service puisse utiliser la base de données.
Ce faisant, lorsqu’un développeur utilise ce module, il n’a pas besoin de se soucier des détails techniques, il doit juste savoir où trouver le secret pour se connecter à la base de données.
Maintenant que nous avons une montagne de modules, nous avons besoin d’un moyen intelligent de les assembler. Pour cela, nous avons créé un nouveau niveau de modules regroupant les ressources dédiées à un micro-service. Nous appelons ces modules des modules applicatifs. Ils vivent avec le code source de l’application, dans le même dépôt. En ce sens, les développeurs peuvent faire évoluer l’infrastructure au fil des besoins des nouvelles fonctionnalités. Une illustration est un micro-service qui avait besoin de lires des messages dans un topic Kafka. Le développeur en charge de l’implémentation, alors qu’il ne connaissait rien à Terraform, a juste eu besoin de copier une partie du code d’un autre micro-service utilisant déjà Kafka, adapter les règles d’accès et voilà !
Maintenant que nous avons une myriade de modules, nous avons tout agrégé dans un bon gros monolithe qui gérera toute l’infrastructure, de A à Z. Tout cela est orchestré avec Terraform Cloud et nous gérons les infrastructures de tout le monde en un claquement de doigts ! Cependant, vous allez voir en quoi ce bon gros monolithe posera problème sur le long cours.
Conclusion
Nous avons géré plusieurs instances de notre application de cette manière pendant plus d'un an, et tout s'est déroulé sans encombres. Cette organisation nous a permis d’embarquer un nouveau pays et de faciliter l’évolution de l’infrastructure pour les développeurs. Encore mieux, l'infrastructure a grandi, mais nous n’avions pas eu besoin d’agrandir l’équipe pour tout gérer. Il existe néanmoins une contre partie dont je vais à présent vous parler.
Tout d’abord, avoir un gros projet Terraform mène mécaniquement à des temps d’exécution énormes. Dans notre cas, nous gérions plus de 400 ressources par exécution, et le temps d’exécution montait jusqu’à 10 minutes. Pour éviter cela, vous pouvez gérer votre pipeline de déploiement de manière à ce que chaque module applicatif soit un projet Terraform autonome qui ne sera exécuté que lorsque le micro-service est publié.
Ces temps d’exécution nous ont menés à séparer les procédures de déploiement d’infrastructure et d’application : le déploiement de l’infrastructure et le déploiement de l’application sont deux actes distincts et décorrelés. Cela contrevenait à notre volonté de coupler fortement le code applicatif et d’infrastructure, mais demander aux développeurs d’attendre 10 minutes à chaque fois qu’un micro-service évolue n’était pas supportable. D’autant plus lorsque, les micro-services évoluent bien plus souvent que l’infrastructure. Une fois de plus, diviser votre code d’infrastructure de manière à ce que les modules applicatifs deviennent autonomes signifie que vous pouvez ne déployer que la partie d’infrastructure liée au micro-service modifié.
Un dernier point, et pas des moindres, les versions de modules ne peuvent pas être gérées dynamiquement dans Terraform. Ce qui signifie que vous devez figer dans le code la version du module à utiliser et le commiter dans le dépôt. Ce qui signifie de devoir faire un commit dans le projet de base d’infrastructure à chaque fois que vous publiez un micro-service. Cela représente une charge de travail non-négligeable mais, heureusement, Dependabot est là pour vous aider ! Et, je vais vous donner le même conseil : divisez votre projet Terraform de manière à ne pas avoir à référencer vos modules applicatifs dans votre projet d’infrastructure de base !
tl;pl : Nos modules applicatifs devraient être des projets Terraform autonomes, faiblement couplés à un projet Terraform de base regroupant les modules techniques commun à tous les micro-services.