Les pièges de Terraform

Les pièges de Terraform
Soulignons quelques pièges, notamment ceux liés aux boucles, aux instructions if et aux techniques de déploiement, ainsi que des problèmes plus généraux qui affectent Terraform en général :

  • les paramètres count et for_each ont des limitations ;
  • limiter les déploiements à temps d'arrêt nul ;
  • même un bon plan peut échouer ;
  • le refactoring peut avoir ses pièges ;
  • la cohérence différée est cohérente... avec le report.

Les paramètres count et for_each ont des limites

Les exemples de ce chapitre utilisent largement le paramètre count et l'expression for_each dans les boucles et la logique conditionnelle. Ils fonctionnent bien, mais ils présentent deux limites importantes dont vous devez être conscient.

  • Count et for_each ne peuvent référencer aucune variable de ressource de sortie.
  • count et for_each ne peuvent pas être utilisés dans la configuration du module.

count et for_each ne peuvent référencer aucune variable de sortie de ressource

Imaginez que vous deviez déployer plusieurs serveurs EC2 et que, pour une raison quelconque, vous ne souhaitiez pas utiliser ASG. Votre code pourrait ressembler à ceci :

resource "aws_instance" "example_1" {
   count             = 3
   ami                = "ami-0c55b159cbfafe1f0"
   instance_type = "t2.micro"
}

Regardons-les un par un.

Puisque le paramètre count est défini sur une valeur statique, ce code fonctionnera sans problème : lorsque vous exécuterez la commande apply, il créera trois serveurs EC2. Mais que se passe-t-il si vous souhaitez déployer un serveur dans chaque zone de disponibilité (AZ) de votre région AWS actuelle ? Vous pouvez demander à votre code de charger une liste de zones à partir de la source de données aws_availability_zones, puis de parcourir chacune d'entre elles et d'y créer un serveur EC2 à l'aide du paramètre count et de l'accès à l'index du tableau :

resource "aws_instance" "example_2" {
   count                   = length(data.aws_availability_zones.all.names)
   availability_zone   = data.aws_availability_zones.all.names[count.index]
   ami                     = "ami-0c55b159cbfafe1f0"
   instance_type       = "t2.micro"
}

data "aws_availability_zones" "all" {}

Ce code fonctionnera également correctement, puisque le paramètre count peut référencer des sources de données sans aucun problème. Mais que se passe-t-il si le nombre de serveurs que vous devez créer dépend du rendement d’une ressource ? Pour démontrer cela, le moyen le plus simple est d'utiliser la ressource random_integer, qui, comme son nom l'indique, renvoie un entier aléatoire :

resource "random_integer" "num_instances" {
  min = 1
  max = 3
}

Ce code génère un nombre aléatoire entre 1 et 3. Voyons ce qui se passe si nous essayons d'utiliser la sortie de cette ressource dans le paramètre count de la ressource aws_instance :

resource "aws_instance" "example_3" {
   count             = random_integer.num_instances.result
   ami                = "ami-0c55b159cbfafe1f0"
   instance_type = "t2.micro"
}

Si vous exécutez Terraform Plan sur ce code, vous obtiendrez l'erreur suivante :

Error: Invalid count argument

   on main.tf line 30, in resource "aws_instance" "example_3":
   30: count = random_integer.num_instances.result

The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the count depends on.

Terraform exige que count et for_each soient calculés pendant la phase de planification, avant la création ou la modification de ressources. Cela signifie que count et for_each peuvent faire référence à des littéraux, des variables, des sources de données et même des listes de ressources (tant que leur longueur peut être déterminée au moment de la planification), mais pas aux variables de sortie de ressources calculées.

count et for_each ne peuvent pas être utilisés dans la configuration du module

Un jour, vous pourriez être tenté d'ajouter un paramètre count à la configuration de votre module :

module "count_example" {
     source = "../../../../modules/services/webserver-cluster"

     count = 3

     cluster_name = "terraform-up-and-running-example"
     server_port = 8080
     instance_type = "t2.micro"
}

Ce code tente d'utiliser count à l'intérieur d'un module pour créer trois copies de la ressource du cluster de serveur Web. Ou vous souhaiterez peut-être rendre la connexion d'un module facultative en fonction d'une condition booléenne en définissant son paramètre count sur 0. Cela peut ressembler à du code raisonnable, mais vous obtiendrez cette erreur lors de l'exécution du plan Terraform :

Error: Reserved argument name in module block

   on main.tf line 13, in module "count_example":
   13: count = 3

The name "count" is reserved for use in a future version of Terraform.

Malheureusement, depuis Terraform 0.12.6, l'utilisation de count ou for_each dans une ressource de module n'est pas prise en charge. Selon les notes de version de Terraform 0.12 (http://bit.ly/3257bv4), HashiCorp prévoit d'ajouter cette fonctionnalité à l'avenir, donc selon le moment où vous lirez ce livre, elle sera peut-être déjà disponible. Pour le savoir avec certitude, lisez le journal des modifications de Terraform ici.

Limites des déploiements sans temps d'arrêt

L'utilisation du bloc create_before_destroy en combinaison avec ASG est une excellente solution pour créer des déploiements sans temps d'arrêt, à une exception près : les règles de mise à l'échelle automatique ne sont pas prises en charge. Ou pour être plus précis, cela réinitialise la taille ASG à min_size à chaque déploiement, ce qui pourrait poser un problème si vous utilisiez des règles de mise à l'échelle automatique pour augmenter le nombre de serveurs en cours d'exécution.

Par exemple, le module webserver-cluster contient une paire de ressources aws_autoscaling_schedule, qui à 9 heures du matin augmente le nombre de serveurs dans le cluster de deux à dix. Si vous déployez, disons, à 11 heures du matin, le nouvel ASG démarrera avec seulement deux serveurs au lieu de dix et le restera jusqu'à 9 heures du matin le lendemain.

Cette limitation peut être contournée de plusieurs manières.

  • Modifiez le paramètre de récurrence dans aws_autoscaling_schedule de 0 9 * * * (« exécuter à 9 heures du matin ») à quelque chose comme 0-59 9-17 * * * (« exécuter toutes les minutes de 9 heures du matin à 5 heures »). Si ASG dispose déjà de dix serveurs, exécuter à nouveau cette règle d'autoscaling ne changera rien, ce que nous souhaitons. Mais si l'ASG n'a été déployé que récemment, cette règle garantira qu'en une minute maximum le nombre de ses serveurs atteindra dix. Ce n’est pas une approche tout à fait élégante, et des sauts importants de dix à deux serveurs et inversement peuvent également causer des problèmes aux utilisateurs.
  • Créez un script personnalisé qui utilise l'API AWS pour déterminer le nombre de serveurs actifs dans l'ASG, appelez-le à l'aide d'une source de données externe (voir « Source de données externe » à la page 249) et définissez le paramètre wanted_capacity de l'ASG sur la valeur renvoyée par le script. De cette façon, chaque nouvelle instance ASG fonctionnera toujours à la même capacité que le code Terraform existant, ce qui rend sa maintenance plus difficile.

Bien sûr, Terraform aurait idéalement une prise en charge intégrée pour les déploiements sans temps d'arrêt, mais en mai 2019, l'équipe HashiCorp n'avait pas l'intention d'ajouter cette fonctionnalité (détails - ici).

Le bon plan peut être mis en œuvre sans succès

Parfois, la commande plan produit un plan de déploiement parfaitement correct, mais la commande apply renvoie une erreur. Essayez, par exemple, d'ajouter la ressource aws_iam_user avec le même nom que celui que vous avez utilisé pour l'utilisateur IAM que vous avez créé précédemment dans le chapitre 2 :

resource "aws_iam_user" "existing_user" {
   # Подставьте сюда имя уже существующего пользователя IAM,
   # чтобы попрактиковаться в использовании команды terraform import
   name = "yevgeniy.brikman"
}

Désormais, si vous exécutez la commande plan, Terraform affichera un plan de déploiement apparemment raisonnable :

Terraform will perform the following actions:

   # aws_iam_user.existing_user will be created
   + resource "aws_iam_user" "existing_user" {
         + arn                  = (known after apply)
         + force_destroy   = false
         + id                    = (known after apply)
         + name               = "yevgeniy.brikman"
         + path                 = "/"
         + unique_id         = (known after apply)
      }

Plan: 1 to add, 0 to change, 0 to destroy.

Si vous exécutez la commande apply, vous obtiendrez l'erreur suivante :

Error: Error creating IAM User yevgeniy.brikman: EntityAlreadyExists:
User with name yevgeniy.brikman already exists.

   on main.tf line 10, in resource "aws_iam_user" "existing_user":
   10: resource "aws_iam_user" "existing_user" {

Le problème, bien sûr, est qu’un utilisateur IAM portant ce nom existe déjà. Et cela peut arriver non seulement aux utilisateurs IAM, mais à presque toutes les ressources. Il est possible que quelqu'un ait créé cette ressource manuellement ou à l'aide de la ligne de commande, mais dans tous les cas, la correspondance des ID entraîne des conflits. Il existe de nombreuses variantes de cette erreur qui surprennent souvent les nouveaux arrivants sur Terraform.

Le point clé est que la commande terraform plan ne prend en compte que les ressources spécifiées dans le fichier d'état Terraform. Si les ressources sont créées d'une autre manière (par exemple manuellement en cliquant dans la console AWS), elles ne finiront pas dans le fichier d'état et Terraform ne les prendra donc pas en compte lors de l'exécution de la commande plan. En conséquence, un plan qui semble correct à première vue s'avérera infructueux.

Il y a deux leçons à en tirer.

  • Si vous avez déjà commencé à travailler avec Terraform, n'utilisez rien d'autre. Si une partie de votre infrastructure est gérée via Terraform, vous ne pouvez plus la modifier manuellement. Sinon, vous risquez non seulement d'étranges erreurs Terraform, mais vous annulez également de nombreux avantages de l'IaC puisque le code ne sera plus une représentation précise de votre infrastructure.
  • Si vous disposez déjà d’une infrastructure, utilisez la commande import. Si vous commencez à utiliser Terraform avec une infrastructure existante, vous pouvez l'ajouter au fichier d'état à l'aide de la commande terraform import. De cette façon, Terraform saura quelle infrastructure doit être gérée. La commande import prend deux arguments. Le premier est l'adresse de la ressource dans vos fichiers de configuration. La syntaxe ici est la même que pour les liens de ressources : _. (comme aws_iam_user.existing_user). Le deuxième argument est l'ID de la ressource à importer. Disons que l'ID de ressource aws_iam_user est le nom d'utilisateur (par exemple, yevgeniy.brikman) et que l'ID de ressource aws_instance est l'ID du serveur EC2 (comme i-190e22e5). Comment importer une ressource est généralement indiqué dans la documentation en bas de sa page.

    Vous trouverez ci-dessous une commande d'importation qui synchronise la ressource aws_iam_user que vous avez ajoutée à votre configuration Terraform avec l'utilisateur IAM au chapitre 2 (en remplaçant votre nom par yevgeniy.brikman, bien sûr) :

    $ terraform import aws_iam_user.existing_user yevgeniy.brikman

    Terraform appellera l'API AWS pour rechercher votre utilisateur IAM et créera une association de fichier d'état entre celui-ci et la ressource aws_iam_user.existing_user dans votre configuration Terraform. Désormais, lorsque vous exécuterez la commande plan, Terraform saura que l'utilisateur IAM existe déjà et n'essaiera pas de le créer à nouveau.

    Il convient de noter que si vous souhaitez déjà importer de nombreuses ressources dans Terraform, écrire manuellement le code et importer chacune d'entre elles à la fois peut s'avérer fastidieux. Cela vaut donc la peine de se tourner vers un outil comme Terraforming (http://terraforming.dtan4.net/), qui peut importer automatiquement le code et l'état depuis votre compte AWS.

    La refactorisation peut avoir ses pièges

    Refactorisation est une pratique courante en programmation où vous modifiez la structure interne du code tout en laissant le comportement externe inchangé. Il s’agit de rendre le code plus clair, plus soigné et plus facile à maintenir. Le refactoring est une technique indispensable qui doit être utilisée régulièrement. Mais lorsqu’il s’agit de Terraform ou de tout autre outil IaC, il faut être extrêmement prudent sur ce que l’on entend par « comportement externe » d’un morceau de code, sinon des problèmes inattendus surgiront.

    Par exemple, un type courant de refactorisation consiste à remplacer les noms de variables ou de fonctions par des noms plus compréhensibles. De nombreux IDE prennent en charge la refactorisation et peuvent automatiquement renommer les variables et les fonctions tout au long du projet. Dans les langages de programmation à usage général, il s'agit d'une procédure triviale à laquelle vous ne pensez peut-être pas, mais dans Terraform, vous devez être extrêmement prudent, sinon vous risquez de rencontrer des pannes.

    Par exemple, le module webserver-cluster a une variable d'entrée cluster_name :

    variable "cluster_name" {
       description = "The name to use for all the cluster resources"
       type          = string
    }

    Imaginez que vous ayez commencé à utiliser ce module pour déployer un microservice appelé foo. Plus tard, vous souhaitez renommer votre service en bar. Ce changement peut paraître anodin, mais en réalité il peut provoquer des interruptions de service.

    Le fait est que le module webserver-cluster utilise la variable cluster_name dans un certain nombre de ressources, y compris le paramètre name de deux groupes de sécurité et l'ALB :

    resource "aws_lb" "example" {
       name                    = var.cluster_name
       load_balancer_type = "application"
       subnets = data.aws_subnet_ids.default.ids
       security_groups      = [aws_security_group.alb.id]
    }

    Si vous modifiez le paramètre de nom sur une ressource, Terraform supprimera l'ancienne version de cette ressource et en créera une nouvelle à la place. Mais si cette ressource est un ALB, entre sa suppression et le téléchargement d'une nouvelle version, vous ne disposerez d'aucun mécanisme pour rediriger le trafic vers votre serveur Web. De même, si un groupe de sécurité est supprimé, vos serveurs commenceront à rejeter tout trafic réseau jusqu'à ce qu'un nouveau groupe soit créé.

    Un autre type de refactorisation qui pourrait vous intéresser consiste à modifier l'ID Terraform. Prenons comme exemple la ressource aws_security_group dans le module webserver-cluster :

    resource "aws_security_group" "instance" {
      # (...)
    }

    L'identifiant de cette ressource est appelé instance. Imaginez que lors du refactoring, vous décidiez de le changer en un nom plus compréhensible (à votre avis) cluster_instance :

    resource "aws_security_group" "cluster_instance" {
       # (...)
    }

    Que va-t-il se passer à la fin ? C'est vrai : une perturbation.

    Terraform associe chaque ID de ressource à l'ID du fournisseur cloud. Par exemple, iam_user est associé à l'ID utilisateur AWS IAM et aws_instance est associé à l'ID du serveur AWS EC2. Si vous modifiez l'ID de ressource (par exemple d'instance en cluster_instance, comme c'est le cas avec aws_security_group), en Terraform, il apparaîtra comme si vous aviez supprimé l'ancienne ressource et en avait ajouté une nouvelle. Si vous appliquez ces modifications, Terraform supprimera l'ancien groupe de sécurité et en créera un nouveau, tandis que vos serveurs commenceront à rejeter tout trafic réseau.

    Voici quatre leçons clés que vous devriez retenir de cette discussion.

    • Utilisez toujours la commande plan. Il peut révéler tous ces accrocs. Examinez attentivement sa sortie et faites attention aux situations dans lesquelles Terraform envisage de supprimer des ressources qui ne devraient probablement pas être supprimées.
    • Créez avant de supprimer. Si vous souhaitez remplacer une ressource, réfléchissez bien à la nécessité de créer un remplacement avant de supprimer l'original. Si la réponse est oui, create_before_destroy peut vous aider. Le même résultat peut être obtenu manuellement en effectuant deux étapes : ajoutez d'abord une nouvelle ressource à la configuration et exécutez la commande apply, puis supprimez l'ancienne ressource de la configuration et utilisez à nouveau la commande apply.
    • Changer les identifiants nécessite de changer d’état. Si vous souhaitez modifier l'ID associé à une ressource (par exemple, renommer aws_security_group d'instance en cluster_instance) sans supprimer la ressource ni créer une nouvelle version de celle-ci, vous devez mettre à jour le fichier d'état Terraform en conséquence. Ne faites jamais cela manuellement – ​​utilisez plutôt la commande terraform state. Lorsque vous renommez des identifiants, vous devez exécuter la commande terraform state mv, qui a la syntaxe suivante :
      terraform state mv <ORIGINAL_REFERENCE> <NEW_REFERENCE>

      ORIGINAL_REFERENCE est une expression qui fait référence à la ressource dans sa forme actuelle et NEW_REFERENCE est l'endroit où vous souhaitez la déplacer. Par exemple, lorsque vous renommez le groupe aws_security_group d'instance en cluster_instance, vous devez exécuter la commande suivante :

      $ terraform state mv 
         aws_security_group.instance 
         aws_security_group.cluster_instance

      Cela indique à Terraform que l'état qui était précédemment associé à aws_security_group.instance doit désormais être associé à aws_security_group.cluster_instance. Si après avoir renommé et exécuté cette commande, terraform plan n'affiche aucun changement, alors vous avez tout fait correctement.

    • Certains paramètres ne peuvent pas être modifiés. Les paramètres de nombreuses ressources sont immuables. Si vous essayez de les modifier, Terraform supprimera l'ancienne ressource et en créera une nouvelle à sa place. Chaque page de ressources indique généralement ce qui se passe lorsque vous modifiez un paramètre particulier, alors assurez-vous de consulter la documentation. Utilisez toujours la commande plan et envisagez d'utiliser la stratégie create_before_destroy.

    La cohérence différée est cohérente... avec le report

    Les API de certains fournisseurs de cloud, comme AWS, sont asynchrones et ont une cohérence retardée. L'asynchronie signifie que l'interface peut immédiatement renvoyer une réponse sans attendre la fin de l'action demandée. Une cohérence retardée signifie que les changements peuvent mettre du temps à se propager dans tout le système ; Pendant ce temps, vos réponses peuvent être incohérentes et dépendre du réplica de source de données qui répond à vos appels d'API.

    Imaginez, par exemple, que vous effectuiez un appel API à AWS pour lui demander de créer un serveur EC2. L'API renverra une réponse « réussie » (201 Created) presque instantanément, sans attendre la création du serveur lui-même. Si vous essayez de vous y connecter immédiatement, cela échouera presque certainement car à ce stade, AWS est encore en train d'initialiser les ressources ou, alternativement, le serveur n'a pas encore démarré. De plus, si vous effectuez un autre appel pour obtenir des informations sur ce serveur, vous risquez de recevoir une erreur (404 Not Found). Le fait est que les informations sur ce serveur EC2 peuvent encore se propager dans AWS avant d'être disponibles partout, vous devrez attendre quelques secondes.

    Chaque fois que vous utilisez une API asynchrone avec une cohérence paresseuse, vous devez réessayer périodiquement votre demande jusqu'à ce que l'action se termine et se propage dans le système. Malheureusement, le SDK AWS ne fournit pas de bons outils pour cela, et le projet Terraform souffrait de nombreux bugs comme 6813 (https://github.com/hashicorp/terraform/issues/6813) :

    $ terraform apply
    aws_subnet.private-persistence.2: InvalidSubnetID.NotFound:
    The subnet ID 'subnet-xxxxxxx' does not exist

    En d'autres termes, vous créez une ressource (comme un sous-réseau), puis essayez d'obtenir des informations à son sujet (comme l'ID du sous-réseau nouvellement créé), et Terraform ne peut pas la trouver. La plupart de ces bugs (dont 6813) ont été corrigés, mais ils surviennent encore de temps en temps, notamment lorsque Terraform ajoute la prise en charge d'un nouveau type de ressource. C'est ennuyeux, mais dans la plupart des cas, cela ne cause aucun dommage. Lorsque vous exécutez à nouveau terraform apply, tout devrait fonctionner, car à ce moment-là, les informations se seront déjà répandues dans tout le système.

    Cet extrait est présenté du livre d'Evgeniy Brikman "Terraform : infrastructure au niveau du code".

Source: habr.com

Ajouter un commentaire