REX : j'ai fait du terraform pour 278 projets
Hello les amigos,
Pour mon client actuel, j’ai eu le plaisir de réaliser un projet à la fois sympathique et challengeant : piloter l’activation et la désactivation des API, créer des comptes de service, et gérer les permissions pour les groupes et les comptes de service, tant au niveau des projets que des dossiers GCP. Et tout ça, pour pas moins de 278 projets. Pas mal, non ? J’ai votre attention ?
Alors bien sûr, vous le savez, je ne vais pas divulguer la moindre ligne de code pour des raisons évidentes de propriété intellectuelle, mais je vais quand même sortir ma plus belle plume numérique pour vous conter l’histoire d’un de mes projets, le plus ambitieux en termes d’échelle et le plus improbable que j’ai dû réaliser en Terraform.
Persévérance, comme le rover sur Mars, parce qu’il ne va pas falloir en manquer !
Mais c’est quoi le besoin déjà ?
Au départ, mon client ne se souciait pas de Terraform. Il activait les APIs à la demande, créait des comptes de service et gérait les permissions manuellement.
Ensuite, pour des raisons de rapidité et de délégation de la gestion, ils ont mis en place des Cloud Functions avec des permissions transitives sur tous les projets, pour cloner des comptes de service et des APIs.
J’ai aidé à la reprise de ces Cloud Functions et à la mise en place d’une sorte de GUI très simple pour que ces Cloud Functions puissent être utilisées de manière plus ergonomique et unifiée.
Ça a fonctionné pendant un temps, puisque la grande majorité des projets étaient en fait des applications serverless en App Engine ou en Cloud Functions, avec au maximum des buckets GCS et des bases de données managées.
Tout au plus, on activait quelques APIs en début de projet, un ou plusieurs comptes de service, et parfois des permissions sur des groupes.
Cela explique l’explosion du nombre de projets GCP : on a un besoin, on développe le projet, et hop, il part en production. Fire and forget.
Puis vient le moment où l’on décide de vraiment créer une organisation avec des VPC, des computes, des clusters Kubernetes, et une interconnexion entre le on-prem et GCP.
Les demandes de permissions et de comptes de service explosent, nécessitant beaucoup plus de suivi et de traçabilité des permissions, des APIs et des comptes de service.
Comme la nouvelle norme est de faire de l’infrastructure as code et que les contraintes des nouveaux projets imposent l’utilisation de Terraform pour la reconstruction, l’automatisation et le versionning des actions, il n’y a pas d’autre choix que d’enrôler l’ensemble des projets, anciens et nouveaux, dans Terraform.
La montagne à gravir
La première chose à comprendre, c’est que Terraform repose sur un système de différentiel d’état entre le code qui décrit l’infrastructure et la dernière exécution de ce code.
Si Terraform n’a jamais été lancé une première fois, son état est vide, même si les ressources décrites dans notre code existent à l’identique dans le cloud.
Donc, on a plusieurs problèmes massifs qui nous tombent dessus avant même de commencer la moindre ligne de Terraform :
- Je n’ai pas le code qui décrit mon infrastructure.
- J’ai plusieurs milliers de ressources existantes cumulées dans mon cloud sur 278 projets, impensable d’essayer de créer les ressources à la main.
- Je n’ai pas l’état Terraform de mon code qui décrit mon infrastructure (il faudra donc une mécanique automatique d’importation des ressources).
- Mon écosystème est vivant et continue d’évoluer, et je ne peux pas figer tous les projets le temps de finir mon projet Terraform.
- Je touche aux parties les plus sensibles de mon infrastructure.
- Il faudra prendre des précautions pour éviter de casser mes projets en détruisant des droits ou en désactivant des APIs accidentellement.
- Je suis seul sur le coup, mais je dois enrôler et convaincre mes collègues de ne plus faire les actions manuellement, mais de tout faire en Terraform par la suite.
Et le dernier, mais pas des moindres, je ne dois en aucun cas me louper sur l’organisation de mon code Terraform, sinon ce sera inutilisable et jeté à la poubelle.
Et si tu passes par là, dans ton camion benne, Ne me ramasse pas, c'est plus la peine (Gérald de Palmas)
Problème colossal, idée créative
Donc, en dressant ma petite liste de courses, j’ai eu l’idée de prendre le problème dans l’autre sens. J’ai des droits de lecture dans tous les projets, donc en théorie, j’ai l’information brute des APIs, des comptes de service et des permissions sur l’ensemble des projets. Donc, on va basiquement lire les ressources de GCP pour recréer le code Terraform associé aux ressources déjà créées.
En somme, on va faire l’exact inverse de ce pour quoi Terraform est fait, dans le but de pouvoir l’utiliser ensuite pour faire exactement ce pour quoi il est fait. Tu me suis ?
Et donc, pour faire des scripts de débiles à base de gcloud et de création de fichiers et dossiers, quoi de plus naturel que de faire du bon gros Bash qui tâche ? Donc la première étape, avant même de faire quoi que ce soit en multi-projet, est de poser les bases d’une organisation qui scale et de faire fonctionner au moins un projet cible.
Je sais, on dirait le début d’un article complotiste, mais je ne vais pas mettre de 5G dans mon Terraform, c’est promis.
Stratégie de gestions des ressources
Par expérience, voici les choses auxquelles j’ai tout de suite pensé en commençant ce projet :
- Il me faut un moyen déterministe et répétable de créer mes fichiers et dossiers sur l’ensemble de mes projets.
- Je dois utiliser des modules pour réduire au maximum la déclaration de mes ressources.
- Les modules prennent en paramètres des dictionnaires de ressources, pour que les modules ne soient déclarés qu’une seule fois.
- Les ressources générées ne doivent jamais être des listes numérotées de ressources mais uniquement des ressources nommées avec des noms uniques.
- Cela évite les suppressions et créations intempestives si on change l’ordre des ressources dans les variables (ce qui arrive quand on utilise des listes).
Il me faut un découplage fort entre mes projets et les types de ressources :
- Un dossier et un remote backend par projet pour les APIs.
- Un dossier et un remote backend par projet pour les comptes de service et les permissions des projets.
- Un dossier et un remote backend par dossier.
Cette organisation a les avantages suivants :
- Nombre réduit de ressources par tfstate.
- Pas d’effets de bord sur des ressources qui n’ont rien à voir avec le scope qu’on est en train de modifier (je ne peux pas casser des APIs si je travaille avec des comptes de service).
- Cloisonnement des projets.
- Possibilité de faire des recherches sur l’ensemble des projets.
- Onboarding simple.
Désavantages :
- Énormément de dossiers.
- Organisation rigide.
- Modules complexes.
On commence par l’apéro, c’est l’API hour
Donc, une fois qu’on a dit ça, mon idée est de commencer par les APIs qui semblent simples à générer, une liste basique d’APIs, presque pas de transformation, avec un module qui va faire la conversion de liste vers dictionnaire.
Donc, basiquement, mon script itère sur une liste de projets :
Supprime tous les dossiers de la liste (pour partir d’un contexte d’exécution propre). Crée un dossier pour chaque projet. Va dans le dossier. Crée un fichier backend.tf. Crée un fichier main.tf qui contient le module. Récupère la liste des APIs et génère une liste Terraform d’APIs, ligne par ligne.
Ça se termine par un terraform fmt (pour formater les fichiers Terraform) et un terraform init pour que le dossier soit prêt à travailler.
Du coup, on a 50 % du travail de fait. Il faut maintenant importer les ressources Terraform une à une pour que l’état soit synchronisé avec l’état des projets. Sinon, on ne peut pas rajouter de nouvelles APIs, car les anciennes seront en conflit. On ne pourra pas non plus désactiver d’APIs, car les APIs ne sont pas dans l’état de Terraform.
Tu prend l’Apéro ?
Import des ressources
Pour l’import des ressources, on utilise une technique un peu brute mais fonctionnelle.
On itère dans les dossiers Terraform et on lance un terraform plan. On analyse le retour du plan pour récupérer les noms des ressources Terraform marquées comme “to be created”. Ensuite, on forge la commande d’import à partir du nom de la ressource que l’on souhaite importer.
En pratique, ça donne quelque chose comme :
Copier le code Je suis Terraform, je vais créer la ressource API compute On prend le nom de l’API compute et on dit à Terraform : la ressource compute que tu veux créer s’appelle compute.googleapi.com sur GCP.
On répète ce processus pour les ~60 000 APIs (ce qui peut prendre plus de 6 heures).
Ça semble facile mais en fait, pas du tout
Dans la liste des trucs rigolos que j’ai dû gérer :
- Le cache Terraform : 10 Ko local par dossier, ce qui totalise environ 50 Go de cache pour l’ensemble du projet…
- Les APIs activées peuvent activer d’autres APIs parfois.
- Certaines APIs dépréciées sont encore activées, mais ne peuvent plus être activées ou désactivées avec Terraform (seulement via l’interface web pour les désactiver).
- Il faut continuer à gérer les APIs en attendant que mon équipe soit formée et que le projet soit utilisable. C’est chiant.
- Il faut que toute mon équipe puisse travailler ensemble sans se gêner et sans rien casser.
Bon, et les comptes de services, les permissions ?
L’apéro est fini, maintenant on gère les comptes de services. On part sur le même principe, sauf que le niveau de complexité augmente drastiquement.
Les modules en premier :
Un module pour la création des comptes de services et leurs bindings. Un module pour ce que j’appelle les bindings externes (des comptes de services et des groupes qui ne sont pas dans le même projet). Cela implique de boucler dans des boucles et d’écrire les fichiers de variables séquentiellement en même temps que les données sont récupérées, filtrées et formatées.
Il faut également écrire les fichiers de contexte Terraform et déclarer les modules.
Dans les défis intéressants :
Exclure les comptes techniques du projet GCP. Exclure les suffixes des comptes de service du projet. Parser les descriptions des comptes de service (parfois sur plusieurs lignes). La difficulté vient dans la création d’un dictionnaire de comptes de service qui contient une liste de permissions associées à chaque compte.
Mon astuce est de créer des ressources uniques avec une concaténation du nom et de la permission pour la gestion des bindings. Tout ça en Bash, ce qui a bien failli avoir raison de ma santé mentale. Mais ce n’est pas fini, il faut aussi gérer les imports ensuite.
Script d’import de la mort
Donc, on reprend la même idée mais il faut trouver la bonne syntaxe pour générer le bon format de commande d’import et cibler des ressources Terraform complexes. C’était long, c’était dur (that’s what she said), mais après le script terriblement épuisant de création de comptes de services et de bindings, c’était une promenade de santé.
La difficulté de ce script, c’est de trouver la syntaxe d’import des ressources et d’adapter cette syntaxe au fur et à mesure qu’on lit les ressources à importer. Donc, il faut parser les ressources de la sortie de terraform plan, boucler sur ces ressources, trouver la bonne syntaxe d’import de Terraform et boucler sur tous les projets.
Encore un dernier truc : je vous ai parlé des folders aussi.
Dernière mission à finir : gérer les bindings sur les dossiers également.
Petite particularité : dans cette organisation, on a plein de folders qui ont le même nom, ce qui complique un peu la tâche pour les identifier. Pas grave, j’ai décidé de concaténer le nom du projet et son ID pour les identifier.
Au final, c’est bien plus simple de s’y retrouver. Le script de création de ressources n’est qu’un remix plus simple du script des comptes de services. En somme, on fait la même chose, mais un peu différemment.
Script de suppression de projets
Pour garder le projet propre et éviter que le chaos s’installe, j’ai mis à disposition un script de suppression de projets. Il supprime le projet sur GCP et nettoie les fichiers sur le projet Terraform ainsi que les states distants. Propre, net et sans erreurs.
Conclusion
Après avoir fait des efforts substantiels pour documenter tout ça et configurer une machine partagée, je trouve que mon équipe a très bien géré la transition de “on fait les trucs à la main” à “on fait du Terraform et c’est tout”.
L’utilisation de modules a permis de réduire drastiquement le copier-coller de ressources, et le format des variables, bien que compliqué, permet de gérer les ressources très rapidement et efficacement.
On fait des commits unitaires avec des IDs de tickets, ce qui nous permet de tracer très facilement les changements et les demandes associées. Je dis que le niveau de galère que j’ai eu compense largement les avantages apportés par Terraform.
Le système est résilient, car rattrapable à l’échelle en cas de défaillance. Le scope de perturbation est réduit au projet sur lequel Terraform est cassé.
Ça marche, mon job ici est fini, je me casse à Las Vegas !