Structurer ses projets Terraform : encore une proposition
10 mars 2020
Terraform est un outil extrêmement puissant d’Infrastructure as Code.
L’une de ses qualités est d’être très flexible dans l’organisation des fichiers. Il suffit d’un répertoire contenant au moins un fichier *.tf
pour exécuter l’outil. Libre au développeur de nommer et organiser ses fichiers comme il l’entend.
Mais de cette flexibilité émerge rapidement une question : comment organiser son projet de manière efficace et maintenable ?
Nombre d’articles ont fleuri sur le sujet et peut-être êtes-vous arrivé.e ici à la suite de la lecture d’un d’entre eux.
Cet article s’adresse aux développeurs Terraform pour leur exposer encore une possibilité d’organisation.
En aucun, cas il ne prétend être original, ni devoir servir de modèle, mais nous espérons qu’il pourra être utile à certains.
Intégrer l’ensemble de la chaîne
Les projets sur lesquels nous travaillons ne se limitent quasiment jamais à un déploiement Terraform.
Typiquement, le déploiement d’une application va nécessiter de construire au préalable des images des systèmes qui l’hébergeront : images Docker, Google Cloud Platform ou AMI AWS.
Les outils utilisés à cet effet sont intégrés au projet.
Voici à quoi ressemble un projet qui utilise Packer et Ansible à sa création :
Version des outils
Chacun des outils utilisés est publié sous de multiples versions, parfois (souvent) incompatibles entre elles.
Une première approche consiste à documenter les versions nécessaires au projet et laisser le soin au développeur de les installer.
- Pour Ansible, on proposera l’utilisation d’un environnement virtuel Python ou pipenv.
- pkenv ou tfenv peuvent aider à faire cohabiter des versions différentes de Packer et Terraform sur la machine du développeur.
Cette approche a toutefois un inconvénient majeur : elle demande au développeur un effort.
Certes, les efforts en question sont faibles. Installer un binaire, configurer un environnement virtuel, et l’activer sont à la portée de tout développeur. Mais ce sont autant d’étapes qui nous éloignent de l’objectif qui reste de modéliser des infrastructures et les déployer.
Afin de simplifier au maximum le processus de construction, nous avons fini par arriver à utiliser un container Docker qui contient tous les outils avec les bonnes versions.
dockerfile
docker-compose.yml
En exécutant docker-compose run cli
, le développeur est alors prêt à construire et déployer le projet.
Terraform
Travail en équipe
- Nous adhérons au consensus qui recommande d’utiliser des backends distants.
Typiquement, on utilisera un backend S3 pour AWS ou un bucket GCS pour GCP. - Nous sommes en cours d’adoption des préconisations de Terraform sur les revues de code et notamment concernant le fait d’attacher systématiquement/automatiquement le résultat du Terraform plan aux pull requests.
NB : Les ressources nécessaires au backend peuvent être elles-mêmes déployées avec Terraform.
Environnements
Comment structurer son projet pour permettre le déploiement vers des environnements multiples (production, staging, développement) ?
Préconisation officielle
La documentation de Terraform recommande de définir des modules, puis de créer une arborescence par environnement qui fera appel à ces derniers.
[…] Organizations commonly want to create a strong separation between multiple deployments of the same infrastructure serving different development stages (e.g. staging vs. production)[…].
[…] Use one or more re-usable modules to represent the common elements, and then represent each instance as a separate configuration that instantiates those common elements in the context of a different backend. In that case, the root module of each configuration will consist only of a backend configuration and a small number of module blocks whose arguments describe any small differences between the deployments.[…]
Les points positifs :
- L’isolation entre les environnements est explicite.
- Le fait d’avoir un répertoire par environnement permet l’utilisation de variables auto-chargées (
terraform.vars
ou*.auto.tfvars
). - Pas d’outil ni de dépendance externe.
Les points négatifs :
- Ooblige à créer et utiliser des modules.
- Implique de, soit factoriser au maximum les modules pour avoir un module “macro”, soit de dupliquer des appels aux modules entre environnements.
Terragrunt
Terragrunt tente de limiter les duplications de code tout en réduisant le surcoût lié à l’utilisation de modules Terraform.
Les points positifs :
- Permet effectivement d’avoir du code DRY.
Les points négatifs :
- Implique toujours la création de modules, ne simplifie que leur appel.
- Implique l’utilisation de commandes spécifiques à terraform-grunt pour le développeur.
- Très orienté vers une décentralisation du code (un dépôt pour les modules, séparé du code de déploiement).
Si ce dernier point a du sens et une réelle utilité dans le cas où les modules peuvent être réutilisés entre projets, il ajoute une complexité qui nous semble inutile dans le cas contraire.
Bascule d’environnement par paramètres
Plus rarement, il est proposé comme dans cet article, de ne pas créer d’arborescence pour les environnements, mais de jongler entre les variables et les states en référençant des fichiers par environnement via, respectivement, l’argument -var-file
de terraform plan
, et l’option -backend-con
fig de terraform init
.
Les points positifs :
- Evite de créer une arborescence supplémentaire… ?
Les points négatifs :
- Le développeur a la responsabilité de faire correspondre le state utilisé avec les bonnes variables.
Cette configuration nous paraît un peu dangereuse.
Au moment de faire un terraform plan
ou apply
, le développeur n’a pas de rappel de l’environnement qui a été configuré pour le state en cours.
Si des variables d’un autre environnement sont passées (et que le plan n’est pas suffisamment vérifié), le déploiement aura des effets très probablement non désirés.
Ce que nous utilisons
A propos des modules
Les solutions proposées ci-dessus utilisent comme unique groupement des ressources le module.
Les modules sont d’excellents moyens de réutiliser des ressources, soit au sein d’un même projet pour créer plusieurs ensembles d’éléments similaires, soit pour être utilisés entre plusieurs projets.
Néanmoins, les modules nécessitent de redéfinir l’ensemble des variables les concernant. Cette contrainte est compréhensible ; si un module avait accès aux variables du contexte parent, cela entraînerait un risque d’effet de bord important.
Il n’en reste pas moins qu’à l’usage, cette multiplication de déclarations et passages de variables crée un surcoût important.
L’un de nos objectifs était de pouvoir avoir le choix entre la création d’un module et une solution plus simple dans le cas où elle ne serait pas nécessaire.
Mono-dépôt et dépendances
Nous souhaitions également pouvoir garder centralisé l’ensemble des ressources dans un seul dépôt, pour en faciliter la prise en main et éviter les gestions de versions de dépendances.
Enfin, nous cherchions une solution qui soit la plus simple possible à appliquer.
Stages et liens symboliques
Voici le type de structure à laquelle nous sommes arrivés :
Le stage
, dont nous reprenons le terme de terragrunt, est un sous-projet qui représente une “couche” du déploiement.
Dans l’exemple, le network
est la couche la plus basse, puis viennent la base de donnée postgresql
qui en dépend, et enfin l’application, qui à son tour dépend de la base de données.
Chaque stage
peut accéder aux variables définies par les couches inférieures en utilisant les remote state.
Pour chaque environnement (dev
et prod
), on crée alors un répertoire qui reprend les stages
nécessaires et en inclut les fichiers en utilisant de simples liens symboliques.
Chaque stage
localisé contient la définition du backend à utiliser pour conserver le state.
Les variables sont ici définies dans un fichier unique par environnement mais peuvent être déplacées dans les stages correspondant et/ou combinées à l’aide de fichiers *.auto.tfvars
.
En pratique, pour le développeur :
- Le code est centralisé dans les stages, sans duplication.
- La réutilisation des stages entre environnements n’implique pas forcément la création de modules (mais ne l’empêche pas non plus).
- La syntaxe des commandes reste la plus simple possible.
Pour déployer un stage sur un environnement donné :
Conclusion
Encore une fois, les pratiques exposées ici ne sont pas des recommandations. Terraform offre suffisamment de liberté pour que chacun puisse adopter le mode de fonctionnement qui lui convient.
En ce qui nous concerne, nous avons pour l’instant trouvé un mode de fonctionnement qui semble nous convenir… jusqu’à la prochaine itération !
Expert DevOps