Bonnes pratiques relatives à l'utilisation de Terraform

Restez organisé à l'aide des collections Enregistrez et classez les contenus selon vos préférences.

Ce document fournit des consignes et des recommandations pour un développement efficace avec Terraform dans un contexte avec plusieurs membres d'équipe et plusieurs flux de travail.

Ce guide n'est pas une introduction à Terraform. Pour une présentation de l'utilisation de Terraform avec Google Cloud, consultez la page Premiers pas avec Terraform.

Consignes générales de style et de structure

Les recommandations suivantes couvrent la structure et le style de base de vos configurations Terraform. Les recommandations s'appliquent aux modules Terraform réutilisables et aux configurations racine.

Suivre une structure de module standard

  • Les modules Terraform doivent respecter la structure de module standard.
  • Démarrez chaque module avec un fichier main.tf où les ressources sont placées par défaut.
  • Dans chaque module, incluez un fichier README.md au format Markdown. Dans le fichier README.md, incluez la documentation de base du module.
  • Placez les exemples dans un dossier examples/, avec un sous-répertoire distinct pour chaque exemple. Pour chaque exemple, incluez un fichier README.md détaillé.
  • Créez des regroupements logiques de ressources avec leurs propres fichiers et des noms descriptifs comme network.tf, instances.tf ou loadbalancer.tf.
    • Évitez d'attribuer à chaque ressource son propre fichier. Regroupez les ressources en fonction de leur objectif partagé. Par exemple, combinez google_dns_managed_zone et google_dns_record_set dans dns.tf.
  • Dans le répertoire racine du module, n'incluez que Terraform (*.tf) et les fichiers de métadonnées du dépôt (tels que README.md et CHANGELOG.md).
  • Placez toute documentation supplémentaire dans un sous-répertoire docs/.

Adopter une convention d'attribution de noms

  • Nommez tous les objets de configuration en utilisant des traits de soulignement pour délimiter plusieurs mots. Cette pratique garantit la cohérence avec la convention d'attribution de noms pour les types de ressources, les types de sources de données et les autres valeurs prédéfinies. Cette convention ne s'applique pas aux arguments de noms.

    Recommandations :

    resource "google_compute_instance" "web_server" {
      name = "web-server"
    }
    

    Option déconseillée :

    resource "google_compute_instance" "web-server" {
      name = "web-server"
    }
    
  • Pour simplifier les références à une ressource qui est la seule de son type (par exemple, un équilibreur de charge unique pour un module entier), nommez la ressource main.

    • Il faut un surcroît de travail mental pour se souvenir de some_google_resource.my_special_resource.id par rapport à some_google_resource.main.id.
  • Pour différencier les ressources d'un même type les unes des autres (par exemple, primary et secondary), utilisez des noms de ressources pertinents.

  • Utilisez des noms de ressource au singulier.

  • Dans le nom de la ressource, ne répétez pas le type de ressource. Exemple :

    Recommandations :

    resource "google_compute_global_address" "main" { ... }
    

    Option déconseillée :

    resource "google_compute_global_address" "main_global_address" { … }
    

Utiliser les variables avec soin

  • Déclarez toutes les variables dans variables.tf.
  • Attribuez aux variables des noms descriptifs pertinents pour leur utilisation ou leur objectif.
    • Les entrées, les variables locales et les sorties représentant des valeurs numériques (tailles de disque ou de RAM, par exemple) doivent être nommées avec des unités (comme ram_size_gb). Les API Google Cloud ne disposant pas d'unités standards, le fait de nommer les variables avec des unités permet d'indiquer clairement l'unité d'entrée attendue pour les responsables de configuration.
    • Pour les unités de stockage, utilisez des préfixes d'unité binaires (multiples de 1 024) (kilo, mega, giga). Pour toutes les autres unités de mesure, utilisez des préfixes décimaux (multiples de 1 000). Cette pratique correspond à celle utilisée dans Google Cloud.
    • Pour simplifier la logique conditionnelle, attribuez des noms positifs aux variables booléennes (par exemple, enable_external_access).
  • Les variables doivent comporter une description. Les descriptions sont automatiquement incluses dans la documentation générée automatiquement d'un module publié. Pour les nouveaux développeurs, les descriptions ajoutent un contexte supplémentaire que les noms descriptifs seuls ne peuvent pas fournir.
  • Attribuez des types définis aux variables.
  • Le cas échéant, indiquez les valeurs par défaut :
    • Pour les variables dont les valeurs sont indépendantes de l'environnement (telles que la taille du disque), fournissez les valeurs par défaut.
    • Pour les variables ayant des valeurs spécifiques à l'environnement (telles que project_id), ne fournissez pas de valeurs par défaut. De cette façon, le module appelant doit fournir des valeurs significatives.
  • N'utilisez des valeurs par défaut vides pour les variables (chaînes ou listes vides, par exemple) que si l'utilisation d'une variable vide est une préférence valide que les API sous-jacentes ne rejettent pas.
  • Soyez prudent lorsque vous utilisez des variables. Ne définissez des paramètres que pour les valeurs qui changent pour chaque instance ou chaque environnement. Lorsque vous décidez d'exposer une variable, assurez-vous de disposer d'un cas d'utilisation concret pour la modification de cette variable. S'il est peu probable qu'une variable soit nécessaire, ne l'exposez pas.
    • L'ajout d'une variable avec une valeur par défaut est rétrocompatible.
    • La suppression d'une variable n'est pas rétrocompatible.
    • Dans les cas où un littéral est réutilisé à plusieurs endroits, vous pouvez utiliser une valeur locale sans l'exposer en tant que variable.

Exposer les sorties

  • Organisez toutes les sorties dans un fichier outputs.tf.
  • Fournissez des descriptions pertinentes pour toutes les sorties.
  • Documentez les descriptions de sorties dans le fichier README.md. Générez automatiquement des descriptions sur le commit avec des outils tels que terraform-docs.
  • Générez toutes les valeurs utiles que les modules racine peuvent avoir besoin de référencer ou de partager. En particulier pour les modules Open Source ou hautement utilisés, exposez toutes les sorties ayant un potentiel de consommation.
  • Ne transmettez pas de sorties directement via des variables d'entrée, car cela empêche leur ajout correct au graphe de dépendance. Pour vous assurer que des dépendances implicites sont créées, assurez-vous que les sorties renvoient des attributs de ressources. Au lieu de référencer directement une variable d'entrée pour une instance, transmettez l'attribut comme indiqué ici :

    Recommandations :

    output "name" {
      description = "Name of instance"
      value       = google_compute_instance.main.name
    }
    

    Option déconseillée :

    output "name" {
      description = "Name of instance"
      value       = var.name
    }
    

Utiliser des sources de données

  • Placez les sources de données à côté des ressources qui y font référence. Par exemple, si vous récupérez une image à utiliser lors du lancement d'une instance, placez-la avec l'instance plutôt que de collecter des ressources de données dans son propre fichier.
  • Si le nombre de sources de données devient important, envisagez de les déplacer vers un fichier data.tf dédié.
  • Pour extraire les données relatives à l'environnement actuel, utilisez l'interpolation de variables ou de ressources.

Limiter l'utilisation des scripts personnalisés

  • N'utilisez des scripts que si cela est vraiment nécessaire. L'état des ressources créées via des scripts n'est ni pris en compte, ni géré par Terraform.
    • Évitez les scripts personnalisés, si possible. Utilisez-les uniquement lorsque les ressources Terraform ne sont pas compatibles avec le comportement souhaité.
    • Tous les scripts personnalisés utilisés doivent avoir une raison clairement documentée d'existence et, idéalement, un plan d'abandon.
  • Terraform peut appeler des scripts personnalisés via des approvisionneurs, y compris l'approvisionneur local-exec.
  • Placez les scripts personnalisés appelés par Terraform dans un répertoire scripts/.

Inclure les scripts d'aide dans un répertoire distinct

  • Organisez les scripts d'aide qui ne sont pas appelés par Terraform dans un répertoire helpers/.
  • Décrivez les scripts d'aide dans le fichier README.md avec des explications et des exemples d'appels.
  • Si les scripts d'aide acceptent des arguments, incluez une vérification des arguments et une sortie --help.

Placer les fichiers statiques dans un répertoire distinct

  • Les fichiers statiques référencés par Terraform mais non exécutés (tels que les scripts de démarrage chargés sur des instances Compute Engine) doivent être organisés dans un répertoire files/.
  • Placez les documents HereDocs dans des fichiers externes, séparés de leur HCL. Référencez-les en utilisant la fonction file().
  • Pour les fichiers lus à l'aide de la fonction templatefile de Terraform, utilisez l'extension de fichier .tftpl.
    • Les modèles doivent être placés dans un répertoire templates/.

Protéger les ressources avec état

Pour les ressources avec état, telles que les bases de données, assurez-vous que la protection contre la suppression est activée. Exemple :

resource "google_sql_database_instance" "main" {
  name = "primary-instance"
  settings {
    tier = "D0"
  }

  lifecycle {
    prevent_destroy = true
  }
}

Utiliser la mise en forme intégrée

Tous les fichiers Terraform doivent être conformes aux normes de terraform fmt.

Limiter la complexité des expressions

  • Limitez la complexité des expressions interpolées individuelles. Si de nombreuses fonctions sont nécessaires dans une seule expression, envisagez de la diviser en plusieurs expressions en utilisant des valeurs locales.
  • N'exécutez jamais plusieurs opérations ternaires sur une seule ligne. Utilisez plutôt plusieurs valeurs locales pour créer la logique.

Utiliser count pour les valeurs conditionnelles

Pour instancier une ressource de manière conditionnelle, utilisez le méta-argument count. Exemple :

variable "readers" {
  description = "..."
  type        = list
  default     = []
}

resource "resource_type" "reference_name" {
  // Do not create this resource if the list of readers is empty.
  count = length(var.readers) == 0 ? 0 : 1
  ...
}

Soyez prudent lorsque vous utilisez des variables spécifiées par l'utilisateur pour définir la variable count pour les ressources. Si un attribut de ressource est fourni pour une variable de ce type (comme project_id) et que cette ressource n'existe pas encore, Terraform ne peut pas générer de plan. Au lieu de cela, Terraform rapporte une erreur value of count cannot be computed. Dans ce cas, utilisez une variable enable_x distincte pour calculer la logique conditionnelle.

Utiliser for_each pour les ressources itérées

Si vous souhaitez créer plusieurs copies d'une ressource en vous basant sur une ressource d'entrée, utilisez le méta-argument for_each.

Publier les modules dans un registre

Modules réutilisables

Pour les modules destinés à être réutilisés, suivez les consignes suivantes en plus des consignes précédentes.

Activer les API requises dans les modules

Les modules Terraform peuvent activer tous les services requis en utilisant la ressource google_project_service ou le module project_services. Inclure l'activation des API facilite les démonstrations.

  • Si l'activation d'une API est incluse dans un module, elle doit pouvoir être désactivée en exposant une variable enable_apis dont la valeur par défaut est true.
  • Si l'activation d'une API est incluse dans un module, elle doit définir disable_services_on_destroy sur false, car cet attribut peut poser des problèmes lorsque vous utilisez plusieurs instances du module.

    Exemple :

    module "project-services" {
      source  = "terraform-google-modules/project-factory/google//modules/project_services"
      version = "~> 12.0"
    
      project_id  = var.project_id
      enable_apis = var.enable_apis
    
      activate_apis = [
        "compute.googleapis.com",
        "pubsub.googleapis.com",
      ]
      disable_services_on_destroy = false
    }
    

Inclure un fichier de propriétaires

Pour tous les modules partagés, incluez un fichier OWNERS (ou CODEOWNERS sur GitHub), qui indique qui est responsable du module. Avant toute demande d'extraction, un propriétaire doit l'approuver.

Publier des versions avec tags

Parfois, des modules nécessitent des modifications destructives et vous devez communiquer les effets aux utilisateurs afin qu'ils puissent rattacher leurs configurations à une version spécifique.

Assurez-vous que les modules partagés suivent SemVer 2.0.0 lorsque de nouvelles versions sont taguées ou publiées.

Lorsque vous référencez un module, utilisez une contrainte de version pour rattacher la version majeure. Exemple :

module "gke" {
  source  = "terraform-google-modules/kubernetes-engine/google"
  version = "~> 20.0"
}

Ne déclarez pas des fournisseurs ou des backends.

Les modules partagés ne doivent pas déclarer de fournisseurs ou de backends. Déclarez plutôt les fournisseurs et les backends dans les modules racine.

Pour les modules partagés, définissez les versions minimales requises du fournisseur dans un bloc required_providers, comme suit :

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = ">= 4.0.0"
    }
  }
}

Sauf indication contraire, supposez que les nouvelles versions du fournisseur fonctionneront normalement.

Exposer les libellés en tant que variable

Permet une flexibilité dans l'ajout de libellés aux ressources via l'interface du module. Envisagez de fournir une variable labels avec un mappage vide comme valeur par défaut :

variable "labels" {
  description = "A map of labels to apply to contained resources."
  default     = {}
  type        = "map"
}

Exposer les sorties de toutes les ressources

Les variables et les sorties vous permettent de déduire les dépendances entre les modules et les ressources. Sans aucune sortie, les utilisateurs ne peuvent pas trier correctement votre module en fonction de leurs configurations Terraform.

Pour chaque ressource définie dans un module partagé, incluez au moins une sortie qui référence la ressource.

Utiliser des sous-modules intégrés pour les logiques complexes

  • Les modules intégrés vous permettent d'organiser des modules Terraform complexes en unités plus petites et de dédupliquer les ressources communes.
  • Placez les modules intégrés dans modules/$modulename.
  • Traitez les modules intégrés comme privés, ne devant pas être utilisés par des modules externes, sauf indication contraire dans la documentation du module partagé.
  • Terraform ne suit pas les ressources refactorisées. Si vous commencez avec plusieurs ressources dans le module de premier niveau, puis que vous les transférez dans des sous-modules, Terraform tente de recréer toutes les ressources refactorisées. Pour atténuer ce comportement, utilisez des blocs moved lors de la refactorisation.
  • Les sorties définies par les modules internes ne sont pas automatiquement exposées. Pour partager les sorties des modules internes, réexportez-les.

Modules racines Terraform

Les configurations racines (modules racine) sont les répertoires de travail à partir desquels vous exécutez la CLI Terraform. Assurez-vous que les configurations racines respectent les normes suivantes (et les consignes Terraform précédentes, le cas échéant). Les recommandations explicites pour les modules racine remplacent les consignes générales.

Réduire le nombre de ressources de chaque module racine

Il est important d'éviter que la configuration racine unique ne devienne trop volumineuse, avec un trop grand nombre de ressources stockées dans le même répertoire et dans le même état. Toutes les ressources d'une configuration racine spécifique sont actualisées à chaque exécution de Terraform. Cela peut ralentir l'exécution si de trop nombreuses ressources sont incluses dans un seul état. En règle générale : n'incluez pas plus de 100 ressources (idéalement, quelques douzaines tout au plus) dans un seul état.

Utiliser des répertoires distincts pour chaque application

Pour gérer des applications et des projets indépendamment les uns des autres, placez les ressources de chaque application et de chaque projet dans leurs propres répertoires Terraform. Un service peut représenter une application particulière ou un service commun tel qu'un réseau partagé. Imbriquez tout le code Terraform d'un service donné dans un seul répertoire (avec des sous-répertoires).

Diviser les applications en sous-répertoires spécifiques à l'environnement

Lors du déploiement de services dans Google Cloud, divisez la configuration Terraform du service en deux répertoires de premier niveau: un répertoire modules qui contient la configuration réelle du service, et un répertoire environments contenant la configuration racine de chaque environnement.

-- SERVICE-DIRECTORY/
   -- OWNERS
   -- modules/
      -- <service-name>/
         -- main.tf
         -- variables.tf
         -- outputs.tf
         -- provider.tf
         -- README
      -- ...other…
   -- environments/
      -- dev/
         -- backend.tf
         -- main.tf

      -- qa/
         -- backend.tf
         -- main.tf

      -- prod/
         -- backend.tf
         -- main.tf

Utiliser des répertoires d'environnement

Chaque répertoire d'environnement (dev, qa, prod) correspond à un espace de travail Terraform par défaut et déploie une version du service dans cet environnement. Utilisez uniquement l'espace de travail par défaut. Les espaces de travail seuls sont insuffisants pour modéliser différents environnements.

Pour partager du code entre plusieurs environnements, référencez les modules. En général, il peut s'agir d'un module de service qui inclut la configuration Terraform partagée de base pour le service. Dans les modules de service, codez en dur les entrées courantes et ne demandez des entrées spécifiques à l'environnement que sous forme de variables.

Ce répertoire d'environnement doit contenir les fichiers suivants :

  • Un fichier backend.tf qui déclare l'emplacement de l'état du backend Terraform (généralement Cloud Storage)
  • Un fichier main.tf qui instancie le module de service

Exposer les sorties via un état distant

Exportez, sous forme de sorties, les informations d'un module racine dont d'autres modules racine peuvent dépendre. Veillez en particulier à réexporter les sorties de modules imbriqués utiles en tant qu'états distants.

Les autres environnements et applications Terraform ne peuvent référencer que des sorties au niveau du module racine.

En utilisant un état distant, vous pouvez référencer les sorties du module racine. Pour autoriser l'utilisation par d'autres applications dépendantes à des fins de configuration, exportez les informations vers un état distant lié aux points de terminaison d'un service.

Parfois, par exemple lorsque vous appelez un module de service partagé à partir de répertoires d'environnement, il convient de réexporter l'intégralité du module enfant, comme suit :

output "service" {
  value       = module.service
  description = "The service module outputs"
}

Épingler aux versions de fournisseurs mineures

Dans les modules racine, déclarez chaque fournisseur et épinglez-le à une version mineure. Cela permet la mise à niveau automatique avec les nouveaux correctifs publiés, tout en conservant une cible fiable. Par souci de cohérence, nommez le fichier de versions versions.tf.

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 4.0.0"
    }
  }
}

Stocker les variables dans un fichier tfvars

Pour les modules racines, fournissez les variables en utilisant un fichier de variables .tfvars. Par souci de cohérence, nommez vos fichiers de variables terraform.tfvars.

Ne spécifiez pas de variables en utilisant d'autres options de ligne de commande var-files ou var='key=val'. Les options de ligne de commande sont éphémères et s'oublient facilement. L'utilisation d'un fichier de variables par défaut est plus prévisible.

Enregistrer le fichier .terraform.lock.hcl

Pour les modules racine, le fichier de verrou de dépendance .terraform.lock.hcl doit être enregistré dans le contrôle de source. Cela vous permet de suivre et d'examiner les modifications apportées aux fournisseurs pour une configuration donnée.

Communication entre les configurations

Un problème courant dans l'utilisation de Terraform est le partage d'informations entre différentes configurations Terraform (potentiellement gérées par différentes équipes). En règle générale, des informations peuvent être partagées entre des configurations sans avoir à les stocker dans un seul répertoire de configuration (ou même dans un seul dépôt).

La méthode recommandée pour partager des informations entre différentes configurations Terraform consiste à utiliser un état distant pour faire référence à d'autres modules racine. Cloud Storage ou Terraform Enterprise sont les backends d'état privilégiés.

Pour interroger des ressources non gérées par Terraform, utilisez des sources de données provenant du fournisseur Google. Par exemple, le compte de service Compute Engine par défaut peut être récupéré en utilisant une source de données. N'utilisez pas de sources de données pour interroger des ressources gérées par une autre configuration Terraform. Cela peut créer, sur les noms et structures de ressources, des dépendances implicites dont les opérations normales Terraform peuvent involontairement perturber le fonctionnement.

Utiliser les ressources Google Cloud

Les bonnes pratiques pour le provisionnement de ressources Google Cloud avec Terraform sont intégrées aux modules Cloud Foundation gérés par Google. Cette section répète certaines de ces bonnes pratiques.

Préconcevoir des images de machine virtuelle

En général, nous vous recommandons de créer des images de machine virtuelle en utilisant un outil comme Packer. Il suffit ensuite de lancer des machines à l'aide des images préconçues.

Si les images préconçues ne sont pas disponibles, Terraform peut transférer de nouvelles machines virtuelles vers un outil de gestion de la configuration en utilisant un bloc provisioner. Nous vous recommandons d'éviter cette méthode et de ne l'utiliser qu'en dernier recours. Pour nettoyer l'ancien état associé à l'instance, les approvisionneurs nécessitant une logique de suppression doivent utiliser un bloc provisioner avec when = destroy.

Terraform doit fournir les informations de configuration des VM pour la gestion de la configuration avec des métadonnées d'instance.

Gérer la gestion de l'authentification et des accès (IAM)

Lorsque vous provisionnez des associations IAM avec Terraform, plusieurs ressources différentes sont disponibles :

  • google_*_iam_policy (par exemple, google_project_iam_policy)
  • google_*_iam_binding (par exemple, google_project_iam_binding)
  • google_*_iam_member (par exemple, google_project_iam_member)

google_*_iam_policy et google_*_iam_binding créent des associations IAM faisant autorité, où les ressources Terraform servent de seule source fiable pour les autorisations pouvant être attribuées à la ressource concernée.

Si les autorisations changent en dehors de Terraform, à la prochaine exécution, Terraform écrase toutes les autorisations afin de représenter la stratégie telle que définie dans votre configuration. Cela peut être judicieux pour les ressources entièrement gérées par une configuration Terraform spécifique. Cependant, cela signifie que les rôles automatiquement gérés par Google Cloud sont supprimés, ce qui risque de perturber le bon fonctionnnement de certains services.

Pour éviter cela, nous vous recommandons d'utiliser directement les ressources google_*_iam_member ou le module IAM de Google.

Contrôle des versions

Comme pour les autres formes de code, stockez le code d'infrastructure dans le contrôle des versions afin de conserver l'historique et de faciliter les rollbacks.

Utiliser une stratégie d'embranchement par défaut

Pour tous les dépôts contenant du code Terraform, utilisez par défaut la stratégie suivante :

  • La branche main est la branche de développement principale et représente le dernier code approuvé. La branche main est protégée.
  • Le développement s'effectue sur des branches de fonctionnalités et de correction de bugs qui partent de la branche main.
    • Nommez les branches de fonctionnalités feature/$feature_name.
    • Nommez les branches de correction de bugs fix/$bugfix_name.
  • Une fois la fonctionnalité ou la correction de bug terminée, refusionnez-la dans la branche main avec une demande d'extraction.
  • Pour éviter les conflits de fusion, redéfinissez les branches avant de les fusionner.

Utiliser des branches d'environnement pour les configurations racine

Pour les dépôts qui incluent des configurations racine déployées directement sur Google Cloud, une stratégie de déploiement sécurisé est requise. Nous vous recommandons de disposer d'une branche distincte pour chaque environnement. Ainsi, les modifications apportées à la configuration Terraform peuvent être promues en fusionnant les modifications entre les différentes branches.

Branche distincte pour chaque environnement

Autoriser une visibilité étendue

Rendre le code source et les dépôts Terraform largement visibles et accessibles dans tous les corps d'ingénierie, des propriétaires (par exemple, SRE) aux parties prenantes de l'infrastructure (par exemple, les développeurs). Ainsi, les parties prenantes peuvent mieux comprendre l'infrastructure dont elles dépendent.

Encouragez les parties prenantes de l'infrastructure à envoyer des demandes de fusion dans le cadre du processus de demande de modification.

Ne jamais intégrer de secret dans un commit

N'intégrez jamais de secrets dans un commit pour le contrôle des sources, y compris dans la configuration Terraform. Importez-les plutôt dans un système tel que Secret Manager, puis référencez-les en utilisant des sources de données.

Gardez à l'esprit que ces valeurs sensibles peuvent toujours se retrouver dans le fichier d'état et être également exposées en tant que sorties.

Organiser les dépôts en fonction des limites de l'équipe

Bien que vous puissiez utiliser des répertoires distincts pour gérer les limites logiques entre les ressources, ce sont les limites organisationnelles et logistiques qui déterminent la structure du dépôt. En général, suivez le principe de conception selon lequel les configurations ayant différentes exigences d'approbation et de gestion doivent être séparées dans différents dépôts de gestion de code source. Pour illustrer ce principe, voici quelques configurations de dépôt possibles :

  • Un seul dépôt central : dans ce modèle, tout le code Terraform est géré de manière centralisée par une équipe de plate-forme unique. Ce modèle fonctionne mieux lorsqu'une équipe d'infrastructure dédiée est responsable de la gestion du cloud et approuve les modifications demandées par d'autres équipes.

  • Dépôts d'équipe : dans ce modèle, chaque équipe est responsable de son propre dépôt Terraform, dans lequel elle gère tout ce qui est lié à l'infrastructure dont elle est propriétaire. Par exemple, l'équipe de sécurité peut disposer d'un dépôt dans lequel tous les contrôles de sécurité sont gérés, et les équipes de développement peuvent disposer de leur propre dépôt Terraform pour déployer et gérer leur application.

    L'organisation des dépôts en fonction des limites d'équipe constitue la meilleure structure pour la plupart des scénarios d'entreprise.

  • Dépôts découplés : dans ce modèle, chaque composant Terraform logique est placé dans son propre dépôt. Par exemple, la mise en réseau peut disposer d'un dépôt dédié alors qu'un autre peut être utilisé pour la création et la gestion de projets. Cette approche est plus adaptée aux environnements hautement décentralisés, dans lesquels les responsabilités changent fréquemment entre les équipes.

Exemple de structure de dépôt

Vous pouvez combiner ces principes pour répartir la configuration Terraform sur différents types de dépôts :

  • Élémentaire
  • Spécifique aux application et aux équipes
Dépôt élémentaire

Un dépôt élémentaire contenant les principaux composants centraux tels que les dossiers ou la configuration IAM d'organisation. Ce dépôt peut être géré par l'équipe cloud centrale.

  • Dans ce dépôt, incluez un répertoire pour chaque composant principal (dossiers, réseaux, etc.).
  • Dans les répertoires de composants, incluez un dossier distinct pour chaque environnement (en appliquant les conseils de structure de répertoire mentionnées précédemment).

Structure de dépôt élémentaire

Dépôts spécifiques aux applications et aux équipes

Déployez les dépôts spécifiques à une application et à une équipe séparément pour chaque équipe afin de gérer sa configuration Terraform unique spécifique à l'application.

Structure du dépôt spécifique à l'application et à l'équipe

Opérations

Pour sécuriser votre infrastructure, vous devez disposer d'un processus stable et sécurisé permettant d'appliquer les mises à jour Terraform.

Planifier toujours en premier

Commencez toujours par générer un plan pour les exécutions Terraform. Enregistrez le plan dans un fichier de sortie. Une fois qu'un propriétaire d'infrastructure l'a approuvé, exécutez le plan. Même lorsque les développeurs prototypent localement des modifications, ils doivent générer un plan et passer en revue les ressources à ajouter, modifier et supprimer avant d'appliquer le plan.

Mettre en œuvre un pipeline automatisé

Pour garantir un contexte d'exécution cohérent, exécutez Terraform via des outils automatisés. Si un système de compilation (tel que Jenkins) est déjà utilisé et largement adopté, utilisez-le pour exécuter automatiquement les commandes terraform plan et terraform apply. Si aucun système existant n'est disponible, adoptez Cloud Build ou Terraform Cloud.

Utiliser les identifiants du compte de service pour l'intégration continue

Lorsque Terraform est exécuté à partir d'une machine dans un pipeline CI/CD, il doit hériter des identifiants de compte de service du service exécutant le pipeline. Dans la mesure du possible, nous vous recommandons d'exécuter vos pipelines CI sur Google Cloud car Cloud Build, Google Kubernetes Engine ou Compute Engine vous permettent d'utiliser des outils comme Workload Identity pour injecter des identifiants sans télécharger les clés de compte de service. Pour les pipelines exécutés ailleurs, préférez la fédération d'identité de charge de travail pour obtenir des identifiants. N'utilisez des clés de compte de service téléchargées qu'en dernier recours.

Éviter d'importer des ressources existantes

Dans la mesure du possible, évitez d'importer des ressources existantes (en utilisant terraform import), car cela peut rendre difficile la compréhension de la provenance et de la configuration des ressources créées manuellement. Créez plutôt des ressources via Terraform et supprimez les anciennes.

Si la suppression d'anciennes ressources est trop laborieuse, utilisez la commande terraform import avec une approbation explicite. Une fois qu'une ressource a été importée dans Terraform, vous pouvez la gérer exclusivement avec Terraform.

Google fournit un outil permettant d'importer vos ressources Google Cloud dans l'état Terraform. Pour plus d'informations, consultez la section Importer vos ressources Google Cloud dans l'état Terraform.

Ne pas modifier manuellement l'état Terraform

Le fichier d'état Terraform est essentiel pour maintenir le mappage entre la configuration Terraform et les ressources Google Cloud. Sa corruption peut entraîner de graves problèmes d'infrastructure. Lorsque des modifications de l'état Terraform sont nécessaires, utilisez la commande terraform state.

Examiner régulièrement les versions épinglées

Épingler des versions garantit la stabilité mais empêche l'intégration de correctifs de bugs et d'autres améliorations dans votre configuration. Par conséquent, examinez régulièrement les versions épinglées pour Terraform, les fournisseurs Terraform et les modules.

Vous pouvez automatiser ce processus avec un outil comme Dependabot.

Utiliser les identifiants par défaut de l'application lors de l'exécution en local

Lorsque les développeurs itèrent sur la configuration Terraform en local, ils doivent s'authentifier en exécutant gcloud auth application-default login pour générer les identifiants par défaut de l'application. Ne téléchargez pas de clés de compte de service, car elles sont plus difficiles à gérer et à sécuriser.

Définir des alias sur Terraform

Pour faciliter le développement en local, vous pouvez ajouter des alias au profil d'interface système :

  • alias tf="terraform"
  • alias terrafrom="terraform"

Sécurité

Terraform nécessite un accès sensible à votre infrastructure cloud pour fonctionner. Le respect de ces bonnes pratiques peut vous aider à réduire les risques associés et à améliorer votre sécurité globale dans le cloud.

Utiliser l'état à distance

Pour les clients Google Cloud, nous vous recommandons d'utiliser le backend d'état Cloud Storage. Cette approche verrouille l'état pour permettre une collaboration en équipe. Elle sépare également l'état et toutes les informations potentiellement sensibles du contrôle des versions.

Assurez-vous que seuls le système de compilation et les administrateurs hautement privilégiés peuvent accéder au bucket utilisé pour l'état distant.

Pour éviter de valider accidentellement l'état de développement dans le contrôle des sources, utilisez gitignore pour les fichiers d'état Terraform.

Chiffrer l'état

Bien que les buckets Google Cloud soient chiffrés au repos, vous pouvez utiliser des clés de chiffrement fournies par le client pour fournir une couche de protection supplémentaire. Pour ce faire, utilisez la variable d'environnement GOOGLE_ENCRYPTION_KEY. Même si aucun secret ne doit figurer dans le fichier d'état, chiffrez toujours l'état pour renforcer la sécurité.

Ne pas stocker de secrets dans l'état

De nombreuses ressources et de nombreux fournisseurs de données dans Terraform stockent les valeurs des secrets en texte brut dans le fichier d'état. Dans la mesure du possible, évitez de stocker des secrets dans l'état. Voici quelques exemples de fournisseurs qui stockent des secrets en texte brut :

Marquer les sorties sensibles

Au lieu de tenter de chiffrer manuellement les valeurs sensibles, utilisez les capacités intégrées de Terraform pour la gestion des états sensibles. Lorsque vous exportez des valeurs sensibles en sortie, assurez-vous qu'elles sont marquées comme sensibles.

Assurer la séparation des tâches

Si vous ne pouvez pas exécuter Terraform à partir d'un système automatisé où si aucun utilisateur n'a accès, respectez la séparation des tâches en séparant les autorisations et les répertoires. Par exemple, un projet réseau correspond à un compte de service ou à un utilisateur de réseau Terraform dont l'accès est limité à ce projet.

Exécuter des vérifications préalables

Lorsque vous exécutez Terraform dans un pipeline automatisé, utilisez un outil tel que gcloud terraform vet pour vérifier la sortie du plan par rapport aux règles avant son application. Cela permet de détecter les régressions de sécurité avant qu'elles ne se produisent.

Effectuer des audits continus

Une fois la commande terraform apply exécutée, exécutez des contrôles de sécurité automatisés. Ces contrôles vous permettent de vous assurer que l'infrastructure ne sombre pas dans un état non sécurisé. Les outils suivants sont des options valides pour ce type de test :

Tests

Le test des modules et des configurations Terraform suit parfois des modèles et des conventions différents de ceux des tests utilisés pour du code d'application. Bien que les tests de code d'application impliquent principalement de tester la logique métier des applications elles-mêmes, le test complet du code d'infrastructure nécessite le déploiement de ressources cloud réelles pour minimiser le risque de défaillances de production. Vous devez prendre en compte quelques éléments lors de l'exécution de tests Terraform :

  • L'exécution d'un test Terraform crée, modifie et supprime une infrastructure réelle. Vos tests peuvent donc prendre beaucoup de temps et être coûteux.
  • Vous ne pouvez pas effectuer de tests unitaires pour une architecture de bout en bout. La meilleure approche consiste à diviser votre architecture en modules et à les tester individuellement. Les avantages de cette approche sont les suivants : le développement itératif est plus rapide, car l'environnement d'exécution des tests est plus rapide, les coûts pour chaque test sont réduits et le risque d'échecs liés aux facteurs échappant à votre contrôle est également réduit.
  • Évitez de réutiliser l'état si possible. Il peut arriver d'avoir à effectuer des tests avec des configurations partageant des données avec d'autres configurations. Idéalement, chaque test doit être indépendant et ne pas réutiliser l'état entre différents tests.

Utiliser d'abord des méthodes de test moins coûteuses

Vous disposez de plusieurs méthodes pour tester Terraform. Par ordre croissant de coût, de temps d'exécution et de profondeur, ces méthodes incluent les suivantes :

  • Analyse statique: test de la syntaxe et de la structure de votre configuration sans déployer de ressources, à l'aide d'outils tels que les compilateurs, les fonctions linters et les simulations. Pour ce faire, utilisez terraform validate.
  • Tests d'intégration des modules : pour vous assurer que les modules fonctionnent correctement, testez-les individuellement. Les tests d'intégration des modules impliquent le déploiement du module dans un environnement de test et la vérification de la création des ressources attendues. Plusieurs frameworks de test facilitent l'écriture de tests, comme suit :
  • Tests de bout en bout : en étendant l'approche des tests d'intégration à un environnement complet, vous pouvez vérifier que plusieurs modules fonctionnent correctement ensemble. Dans cette approche, déployez tous les modules qui composent l'architecture dans un environnement de test récent. Idéalement, l'environnement de test est aussi semblable que possible à votre environnement de production. Bien que plus coûteuse, cette méthode offre le plus haut niveau de confiance pour garantir que les modifications ne nuisent pas à votre environnement de production.

Procéder par étapes

Assurez-vous que vos tests se compilent les uns les autres de manière itérative. Envisagez d'abord d'exécuter des tests plus petits pour avancer progressivement vers des tests plus complexes, en adoptant une approche basée sur les échecs rapides.

Randomiser les ID de projet et les noms de ressources

Pour éviter les conflits de noms, assurez-vous que vos configurations ont un ID de projet global unique et des noms de ressources qui ne se chevauchent pas au sein de chaque projet. Pour ce faire, utilisez des espaces de noms pour vos ressources. Terraform dispose d'un fournisseur aléatoire intégré.

Utiliser un environnement distinct pour les tests

Pendant les tests, de nombreuses ressources sont créées et supprimées. Assurez-vous que l'environnement est isolé des projets de développement ou de production afin d'éviter toute suppression accidentelle lors du nettoyage des ressources. La meilleure approche consiste à demander à chaque test de créer un nouveau projet ou un nouveau dossier. Pour éviter toute erreur de configuration, envisagez de créer des comptes de service spécifiques pour chaque exécution de test.

Nettoyer toutes les ressources

Tester le code d'infrastructure signifie que vous déployez des ressources réelles. Pour éviter des frais, envisagez de mettre en œuvre une étape de nettoyage.

Pour détruire tous les objets distants gérés par une configuration spécifique, utilisez la commande terraform destroy. Certains frameworks de test disposent d'une étape de nettoyage intégrée. Par exemple, si vous utilisez Terratest, ajoutez defer terraform.Destroy(t, terraformOptions) à votre test. Si vous utilisez Kitchen-Terraform, supprimez votre espace de travail en utilisant terraform kitchen delete WORKSPACE_NAME.

Après avoir exécuté la commande terraform destroy, exécutez également des procédures de nettoyage supplémentaires pour supprimer toutes les ressources que Terraform n'a pas pu détruire. Pour ce faire, supprimez tous les projets utilisés pour exécuter le test ou utilisez un outil tel que le module project_cleanup.

Optimiser le temps d'exécution des tests

Pour optimiser le temps d'exécution des tests, utilisez les approches suivantes :

  • Exécuter des tests en parallèle. Certains frameworks de tests permettent d'exécuter simultanément plusieurs tests Terraform.
    • Par exemple, avec Terratest, vous pouvez pour cela ajouter t.Parallel() après la définition de la fonction de test.
  • Tester par étapes. Séparez vos tests en configurations indépendantes pouvant être testées séparément. Cette approche vous évite d'avoir à passer toutes les étapes lors de test et accélère le cycle de développement itératif.
    • Par exemple, dans la Kitchen-Terraform, divisez les tests en suites distinctes. Pendant l'itération, exécutez chaque suite indépendamment.
    • De même, avec Terratest, encapsulez chaque étape de votre test avec stage(t, STAGE_NAME, CORRESPONDING_TESTFUNCTION). Définissez des variables d'environnement indiquant les tests à exécuter. Par exemple, SKIPSTAGE_NAME="true"
    • Le framework de test du plan est compatible avec l'exécution par étapes.

Étape suivante