Cloud & DevOps

Structurer ses projets Terraform : encore une proposition

by Benoît Vidis 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 :

.
├── ansible/
├── packer/
├── README.md
└── terraform/

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.

.
├── ansible/
├── docker-compose.yml
├── Dockerfile
├── packer/
├── README.md
└── terraform/

 

dockerfile

FROM alpine:3.11

ARG ANSIBLE_VERSION=2.9.5
ARG AWSCLI_VERSION=1.18.0
ARG MOLECULE_VERSION=2.22
ARG PACKER_VERSION=1.5.4
ARG TERRAFORM_VERSION=0.12.20
ARG TESTINFRA_VERSION=4.0.0

RUN  set -x \
  \
  && echo "install tools and dependencies" \
  [..]

 

docker-compose.yml

version: "3"

services:
  cli:
    build: .
    environment:
      SSH_AUTH_SOCK: /var/run/ssh
    volumes:
      # credentials aws en lecture seule
      - $HOME/.aws:/home/cli/.aws:ro
      # accès au démon docker depuis le container
      - /var/run/docker.sock:/var/run/docker.sock
      # ssh-agent du host
      - $SSH_AUTH_SOCK:/var/run/ssh
      # les sources du projet
      - .:/app

En exécutant docker-compose run cli, le développeur est alors prêt à construire et déployer le projet.

Terraform

Travail en équipe

  1. 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.
  2. 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-config 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 :

.
├── dev
│   ├── 00.network
│   │   ├── backend.tf
│   │   ├── main.tf -> ../../stages/00.network/main.tf
│   │   └── terraform.tfvars -> ../terraform.tfvars
│   ├── 10.postgresql
│   │   ├── backend.tf
│   │   ├── main.tf -> ../../stages/10.postgresql/main.tf
│   │   └── terraform.tfvars -> ../terraform.tfvars
│   ├── 20.app
│   │   ├── backend.tf
│   │   ├── main.tf -> ../../stages/20.app/main.tf
│   │   └── terraform.tfvars -> ../terraform.tfvars
│   └── terraform.tfvars
├── modules
│   └── my-module
│       └── main.tf
├── prod
│   ├── 00.network
│   │   ├── backend.tf
│   │   ├── main.tf -> ../../stages/00.network/main.tf
│   │   └── terraform.tfvars -> ../terraform.tfvars
│   ├── 10.postgresql
│   │   ├── backend.tf
│   │   ├── main.tf -> ../../stages/10.postgresql/main.tf
│   │   └── terraform.tfvars -> ../terraform.tfvars
│   ├── 20.app
│   │   ├── backend.tf
│   │   ├── main.tf -> ../../stages/20.app/main.tf
│   │   └── terraform.tfvars -> ../terraform.tfvars
│   └── terraform.tfvars
└── stages
    ├── 00.network
    │   └── main.tf
    ├── 10.postgresql
    │   └── main.tf
    └── 20.app
        └── main.tf

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é :

cd dev/network

terraform init
terraform plan
terraform apply

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 !

Benoît Vidis

Benoît Vidis

Expert DevOps

Spécialisé dans le développement backend et le DevOps, j'adore construire des architectures logicielles et expérimenter de nouvelles technologies.

Commentaires

Ajouter un commentaire

Votre commentaire sera modéré par nos administrateurs

Vous avez un projet ? Nos équipes répondent à vos questions

Contactez-nous