Terraform sur Microsoft Azure | 4 – Organisation des projets et modules Terraform

Temps de lecture : 9 minutes

Julien CORIOLAND
Senior Software Engineer & Cloud Architect chez Microsoft

« Je suis ingénieur logiciel senior et architecte du Cloud, vivant à Paris et travaillant pour Microsoft depuis 2015. Avant de rejoindre Microsoft, j’ai été développeur et formateur pendant 7 ans, principalement sur les technologies Microsoft .NET, Web et cloud. »

Terraform sur Microsoft Azure | 4 – Organisation des projets et modules Terraform

Ce billet de blog fait partie d’une série d’articles sur l’utilisation de Terraform sur Microsoft Azure :

    • Dans cette partie, vous sera présenté les aspects fondamentaux de Terraform et comment démarrer facilement pour déployer une infrastructure sur Azure.

      Ce billet de blog fait partie d’une série d’articles sur l’utilisation d. Dans cette partie, nous allons voir comment organiser vos fichiers Terraform et comment maximiser la réutilisation du code, en utilisant notamment les modules Terraform.

      Remarque : cette série d’articles de blog est accompagnée d’une mise en œuvre de référence hébergée sur mon GitHub. 

      L’infrastructure en tant que code consiste à suivre les mêmes pratiques pour les modèles de déploiement d’infrastructure que pour du code applicatif. L’une des règles d’or, c’est d’essayer de mutualiser le code dès que cela est possible. Comme les développeurs, évitez de copier/coller des portions de code d’un fichier à un autre. Ce n’est pas toujours facile, mais pour vous aider, vous disposez d’outils tels que les modules Terraform.

       

      Webinar

      CSRD : comment se préparer à la nouvelle réglementation ?

      Le reporting d’entreprise évolue, intégrant des données extra-financières liées à l’impact social, environnemental et de gouvernance. En Europe, la réglementation CSRD guide ce changement.

      Je m’inscris

      Structure d’un projet Terraform

      Avant d’étudier les modules Terraform plus en détail, parlons de la structure/organisation de base d’un projet Terraform. Vous avez déjà lu qu’un projet Terraform est, en gros, un ensemble de fichiers *.tf dans un répertoire spécifique.

      Un répertoire de projet Terraform de base ressemble à ceci :

      /myproject
      -- main.tf
      -- feature1.tf
      -- feature2.tf
      -- outputs.tf
      -- variables.tf
      -- README.md

      Si votre code Terraform est dans le même dépôt de code source que celui de l’application (ce qui est très bien !), vous pouvez l’isoler dans un dossier dédié :

      /myproject
      -- /src
      ---- app stuff
      -- /tf
      ---- main.tf
      ---- feature1.tf
      ---- feature2.tf
      ---- outputs.tf
      ---- variables.tf
      ---- README.md

      Vous pouvez même aller plus loin et avoir un sous-répertoire par environnement, si vous voulez gérer la configuration depuis votre référentiel git ou si votre infrastructure varie selon l’environnement :

      /myproject
      -- /src
      ---- app stuff
      -- /tf
      ---- /dev
      ------ main.tf
      ------ feature1.tf
      ------ feature2.tf
      ------ outputs.tf
      ------ variables.tf
      ------ README.md
      ---- /prod
      ------ main.tf
      ------ feature1.tf
      ------ feature2.tf
      ------ feature3.tf
      ------ outputs.tf
      ------ variables.tf
      ------ README.md

      Examinons la configuration de base dans la mise en œuvre de référence que j’utilise comme support pour cette série d’articles de blog (oublions les modules pour l’instant, nous les verrons un peu plus loin).

    • Cette configuration se charge de :

      • Créer le groupe de ressources, pour un environnement donné (dev, test, production…)
      • Créer les réseaux et sous-réseaux virtuels, pour un environnement donné

Remarque : le diagramme de l’architecture de mise en œuvre de référence est disponible ici, si vous voulez vous rafraîchir la mémoire.*

  • Main.tf : techniquement, ce n’est pas le point d’entrée du projet (chaque fichier est chargé dans l’ordre alphabétique) mais ce fichier contient généralement la configuration du fournisseur, la configuration du backend, les imports liés aux modules à utiliser et, le cas échéant, quelques ressources communes qui n’ont pas été isolées dans un fichier spécifique. Pour les petits projets, on peut tout à fait définir toute la configuration d’infrastructure ici. Lorsque le projet devient plus complexe, découpez-le en plusieurs autres fichiers *.tf.
provider "azurerm" {
version = "~> 1.31"
}

terraform {
backend "azurerm" {}
}

resource "azurerm_resource_group" "rg" {
name     = "tf-ref-${var.environment}-rg"
location = "${var.location}"
}

resource "azurerm_virtual_network" "aks" {
name                = "aks-vnet"
address_space       = ["10.1.0.0/16"]
location            = "${azurerm_resource_group.rg.location}"
resource_group_name = "${azurerm_resource_group.rg.name}"
}

resource "azurerm_subnet" "aks" {
name                 = "aks-subnet"
resource_group_name  = "${azurerm_resource_group.rg.name}"
virtual_network_name = "${azurerm_virtual_network.aks.name}"
address_prefix       = "10.1.0.0/24"
}

resource "azurerm_virtual_network" "backend" {
name                = "backend-vnet"
address_space       = ["10.2.0.0/16"]
location            = "${azurerm_resource_group.rg.location}"
resource_group_name = "${azurerm_resource_group.rg.name}"
}

resource "azurerm_subnet" "backend" {
name                 = "backend-subnet"
resource_group_name  = "${azurerm_resource_group.rg.name}"
virtual_network_name = "${azurerm_virtual_network.backend.name}"
address_prefix       = "10.2.0.0/24"
}

resource "azurerm_virtual_network_peering" "peering1" {
name                      = "aks2backend"
resource_group_name       = "${azurerm_resource_group.rg.name}"
virtual_network_name      = "${azurerm_virtual_network.aks.name}"
remote_virtual_network_id = "${azurerm_virtual_network.backend.id}"
}

resource "azurerm_virtual_network_peering" "peering2" {
name                      = "backend2aks"
resource_group_name       = "${azurerm_resource_group.rg.name}"
virtual_network_name      = "${azurerm_virtual_network.backend.name}"
remote_virtual_network_id = "${azurerm_virtual_network.aks.id}"
}

 

  • outputs.tf : contient les définitions des variables de sortie de déploiement, c’est-à-dire toutes les informations que vous voulez récupérer à la fin du déploiement.
output "resource_group_name" {
value = "${azurerm_resource_group.rg.name}"
}

output "location" {
value = "${var.location}"
}

output "environment" {
value = "${var.environment}"
}
  • variables.tf : contient les définitions des variables utilisées dans ce projet de configuration.
variable "environment" {
description = "Name of the environment"
}

variable "location" {
description = "Azure location to use"
}
  • autres fichiers *.tf : contiennent la définition des ressources et sources de données liées que vous utilisez pour déployer votre infrastructure (par exemple network.tf et virtual_machine.tf…)
  • README.md : il est toujours utile de documenter ce que fait le projet Terraform. Un fichier README.md est parfait pour cela

Définitions de ressources ou sources des données

Lorsque vous travaillez avec Terraform, il y a deux façons de faire référence à une instance de service exécutée dans Azure. Vous pouvez utiliser une définition de ressource, avec le mot clé resource, comme pour les extraits ci-dessus, ou vous pouvez utiliser une source de données, avec le mot clé data :

resource "azurerm_resource_group" "rg" {
name     = "tf-ref-${var.environment}-rg"
location = "${var.location}"
}

data "azurerm_resource_group" "rg" {
name = "tf-ref-${var.environment}-rg"
}

Avec le mot clé ressource, on indique à Terraform que la configuration actuelle se charge de la gestion du cycle de vie de l’objet, notamment de sa création/mise à jour lors d’un appel à la commande terraform apply ou de sa suppression lors d’un appel à la commande terraform destroy.

Avec le mot clé data, on indique à Terraform qu’on ne veut obtenir qu’une référence de l’objet existant, mais qu’on ne veut pas le gérer dans le cadre de cette configuration (parce qu’il est géré par une autre équipe, un autre module, etc.). Si l’objet n’existe pas lorsque vous appliquez la configuration, la commande Terraform échouera. C’est un bon moyen de créer une dépendance à une ressource qui doit exister au moment du déploiement de votre module.

Lorsque vous avez une référence de ressource ou de source de données, vous pouvez l’utiliser dans une autre partie de votre modèle, en utilisant le nom de cette ressource ou source de données (dans ce cas rg – la chaîne qui vient après le type d’objet) comme suit :

# reference a resource
resource_group_name = "${azurerm_resource_group.rg.name}"

# reference a data source
resource_group_name = "${data.azurerm_resource_group.rg.name}"

Il est très important de comprendre la différence entre ces deux types d’objets pour pouvoir écrire un module et gérer les dépendances entre tous les modules qui composent votre solution !

Webinar

Améliorez la sécurité de vos PC dans le Cloud avec Windows

76% des décideurs IT en charge de PC et technologies prévoient d’augmenter les investissements au cours des deux prochaines années dont 36 % s’attendent à un double investissement.

Je m’inscris

Écriture d’un module Terraform

Maintenant que vous connaissez la structure de base d’un projet de configuration Terraform et que vous commencez à vous familiariser avec la syntaxe, nous pouvons aborder les modules Terraform. La bonne nouvelle, c’est qu’un module, c’est tout simplement un répertoire contenant des fichiers Terraform et structuré comme un projet. Plutôt cool, non ?

Les modules Terraform sont utilisés pour regrouper diverses ressources ayant le même cycle de vie. L’utilisation de modules n’est pas obligatoire, mais dans certains cas ils peuvent être très utiles.

Comme tous les mécanismes qui permettent de mutualiser/factoriser du code, les modules peuvent aussi être dangereux : vous ne voulez pas d’un gros module contenant tout ce dont vous avez besoin pour le déploiement ni que toutes les ressources soient fortement couplées. Vous risqueriez de vous retrouver avec un monolithe très difficile à gérer et à déployer.

Voici quelques questions que vous pouvez vous poser avant d’écrire un module :

    • Toutes les ressources engagées ont-elles le même cycle de vie ?

— Les ressources seront-elles toujours déployées ensemble ?
— Les ressources seront-elles toujours mises à jour ensemble ?
— Les ressources seront-elles toujours supprimées ensemble ?

  • Y a‑t-il plusieurs ressources impliquées ? S’il n’y en a qu’une, le module est probablement inutile.
  • D’un point de vue architectural/fonctionnel, est-il logique de regrouper toutes ces ressources ? (Réseau, calcul, stockage, etc.)
  • Est-ce que l’une des ressources impliquées dépend d’une ressource qui n’est pas dans ce module ?

Si vous répondez non à la plupart de ces questions, c’est que vous n’avez probablement pas besoin d’écrire de module.

Parfois, au lieu d’écrire un gros module, il vaut mieux en écrire plusieurs plus petits, puis les imbriquer, selon le scénario que vous voulez mettre en œuvre.

Vous pouvez écrire plusieurs modules dans un répertoire distinct de votre projet, ou bien dans différents référentiels. Vous pouvez aussi importer des modules existants depuis le Registre Terraform.

Dans la mise en œuvre de référence que j’utilise pour cette série d’articles de blog, le module de base est défini dans le référentiel principal d’autres modules comme Azure Kubernetes Service, défini dans son propre référentiel GitHub.

Si vous comparez la structure des deux modules, vous verrez que c’est exactement la même ! La seule chose qui change, c’est la manière dont vous allez l’importer et l’utiliser dans le projet de configuration principal. Nous aborderons ce point à la prochaine section.

Regardez le fichier main.tf du module AKS.

data "azurerm_resource_group" "rg" {
name = "tf-ref-${var.environment}-rg"
}

data "azurerm_subnet" "aks" {
name                 = "aks-subnet"
virtual_network_name = "aks-vnet"
resource_group_name  = "${data.azurerm_resource_group.rg.name}"
}

Vous pouvez voir que j’ai utilisé le mot clé data pour référencer le groupe de ressources et le sous-réseau où AKS doit être déployé. Dans ce cas, c’est parce que j’ai supposé que ces deux ressources qui font partie du module de base doivent être déployées avant AKS. Autrement dit, j’ai créé une dépendance entre les deux modules, ce qui me permet de gérer leur cycle de vie différemment. Ils pourraient même être gérés par deux équipes différentes de l’entreprise, si nécessaire.

Utilisation d’un module Terraform

Comme tout projet de configuration Terraform, un module Terraform prend des paramètres d’entrée (variables), crée des éléments d’infrastructure et renvoie des valeurs de sortie.

Pour importer un module Terraform dans un autre projet, utilisez la directive module :

module "tf-ref-aks-module" {
source                           = "../../"
environment                      = "Development"
location                         = "francecentral"
kubernetes_version               = "1.14.6"
service_principal_client_id      = "CLIENT_ID"
service_principal_client_secret  = "CLIENT_SECRET"
}

Selon l’emplacement du module (dans un sous-répertoire, dans un autre référentiel GitHub, dans le registre Terraform…) et l’endroit où il est utilisé, le paramètre source peut être différent :

provider "azurerm" {
version = "~>1.30"
}

terraform {
backend "azurerm" {}
}

module "aks" {
source                          = "git@github.com:jcorioland/terraform-azure-ref-aks-module"
environment                     = "${var.environment}"
location                        = "${var.location}"
kubernetes_version              = "${var.kubernetes_version}"
service_principal_client_id     = "${var.service_principal_client_id}"
service_principal_client_secret = "${var.service_principal_client_secret}"
ssh_public_key                  = "${var.ssh_public_key}"
}

Si vous utilisez plusieurs modules, il est tout à fait possible d’utiliser les sorties d’un module comme entrées d’un autre. Ainsi, vous pourrez définir dans quel ordre les modules doivent être déployés par Terraform, en créant une dépendance entre les modules :

 

module "core" {
source      = "../core"
location    = "${var.location}"
environment = "${var.environment}"
}


module "aks" {
source                          = "git@github.com:jcorioland/terraform-azure-ref-aks-module"
environment                     = "${module.core.environment}"
location                        = "${module.core.location}"
kubernetes_version              = "${var.kubernetes_version}"
service_principal_client_id     = "${var.service_principal_client_id}"
service_principal_client_secret = "${var.service_principal_client_secret}"
ssh_public_key                  = "${var.ssh_public_key}"
}

Remarque : pour plus d’informations sur les valeurs de sortie, consultez cette page.

Une fois que vous avez créé votre configuration (qui agrège tous les modules que vous voulez importer), exécutez les commandes terraform init et terraform apply. Terraform commencera par télécharger les modules depuis leur emplacement respectif, puis appliquera la configuration, comme pour un projet sans module. C’est aussi simple que ça !

Pour en savoir plus sur les modules Terraform, rendez-vous sur cette page de la documentation Terraform.

Conclusion

Dans cette partie a été vu quelques astuces sur la manière dont vous pouvez organiser vos projets Terraform et utiliser les modules pour maximiser la réutilisation du code dans vos projets. Pour aller plus loin vous pouvez consulter la mise en œuvre de référence créé pour cette série d’articles.

Comme nous l’avons vu, vous avez de nombreuses possibilités pour réaliser ces objectifs ; les choix que vous ferez dépendront des projets.

< Partie 3 : Gestion de l’état des déploiements

Webinar

Rassemblez vos collaborateurs autour d’un objectif avec Viva Goals

Venez découvrir comment Viva Goals vous permettra d’organiser et de suivre vos objectifs grâce à des « objectifs et résultats clés » (OKR).

Je m’inscris