Les pièges de Terraform

Les pièges de Terraform
Nous allons souligner quelques pièges, y compris ceux liés aux cycles, aux expressions if et aux méthodologies de déploiement, ainsi qu'à des problèmes plus généraux concernant Terraform dans son ensemble :

  • les paramètres count et for_each ont des limitations;
  • limitations des déploiements avec un 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 s'accorde... avec le report.

Les paramètres count et for_each ont des limitations

Dans les exemples de ce chapitre, le paramètre count et l'expression for_each sont largement utilisés dans les boucles et la logique conditionnelle. Ils fonctionnent bien, mais ils ont deux importantes limitations à connaître.

  • Vous ne pouvez pas référencer de variables de sortie de ressource dans count et for_each.
  • count et for_each ne peuvent pas être utilisés dans la configuration d'un module.

Vous ne pouvez pas référencer de variables de sortie de ressource dans count et for_each.

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

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

Examinons-les un par un.

Étant donné que le paramètre count a une valeur statique, ce code fonctionnera sans problème : lorsque vous exécuterez la commande apply, il créera trois serveurs EC2. Mais si vous souhaitez déployer un serveur dans chaque zone de disponibilité (Availability Zone ou AZ) au sein de la région AWS actuelle ? Vous pouvez faire en sorte que votre code charge la liste des zones à partir de la source de données aws_availability_zones et ensuite passer « cycliquement » par chacune d'elles et créer un serveur EC2 dans chacune, en utilisant le paramètre count et l'accès au tableau par index :

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 très bien, car le paramètre count peut référencer sans problème des sources de données. Mais que se passe-t-il si le nombre de serveurs que vous devez créer dépend de la sortie d'une ressource ? Pour le démontrer, il est plus simple de prendre la ressource random_integer, qui, comme son nom l'indique, retourne un nombre 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 result 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 pour ce code, vous obtiendrez l'erreur suivante :

Erreur : Argument count invalide

   sur main.tf ligne 30, dans la ressource "aws_instance" "example_3":
   30: count = random_integer.num_instances.result

La valeur "count" dépend d'attributs de ressource qui ne peuvent pas être déterminés avant l'application, donc Terraform ne peut pas prévoir combien d'instances seront créées. Pour contourner cela, utilisez l'argument -target pour d'abord appliquer uniquement les ressources dont dépend le count.

Terraform nécessite que count et for_each soient calculés au moment de la planification, avant de créer ou de modifier des 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 (à condition que leur longueur puisse être déterminée lors de la planification), mais pas à des variables de sortie calculées de la ressource.

count et for_each ne peuvent pas être utilisés dans la configuration d'un module

Vous pourriez un jour être tenté d'ajouter le paramètre count dans les configurations de 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 essaie d'utiliser count à l'intérieur d'un module pour créer trois copies de la ressource webserver-cluster. Ou peut-être souhaitez-vous rendre la connexion du module optionnelle en fonction d'une condition booléenne en lui attribuant une valeur de 0 pour le paramètre count. Ce code peut sembler tout à fait raisonnable, mais en exécutant terraform plan, vous obtiendrez l'erreur suivante :

Erreur : Nom d'argument réservé dans le bloc de module

   sur main.tf ligne 13, dans le module "count_example":
   13: count = 3

Le nom "count" est réservé pour une utilisation dans une future version de Terraform.

Malheureusement, à la sortie de 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 possibilité à l'avenir, donc selon quand vous lisez ce livre, elle peut déjà être disponible. Pour en être sûr, lisez le journal des modifications de Terraform ici.

Limitations des déploiements à temps d'arrêt nul

L'utilisation du bloc create_before_destroy en combinaison avec ASG est une excellente solution pour organiser des déploiements sans temps d'arrêt, à une exception près : les règles d'auto-scaling ne sont pas prises en charge. Plus précisément, cela réinitialise la taille de l'ASG à min_size à chaque déploiement, ce qui peut poser problème si vous avez utilisé des règles d'auto-scaling pour augmenter le nombre de serveurs en cours d'exécution.

Par exemple, le module webserver-cluster contient quelques ressources aws_autoscaling_schedule qui, à 9 heures du matin, augmentent le nombre de serveurs dans le cluster de deux à dix. Si un déploiement est effectué, disons, à 11 heures, le nouveau groupe ASG ne se lancera pas avec dix, mais seulement avec deux serveurs et restera dans cet état jusqu'à 9 heures le lendemain.

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

  • Changer le paramètre recurrence dans aws_autoscaling_schedule de 0 9 * * * («s'exécuter à 9 heures du matin») en quelque chose comme 0-59 9-17 * * * («s'exécuter chaque minute de 9 heures à 17 heures»). Si l'ASG a déjà dix serveurs, la réexécution de cette règle d'auto-scaling ne changera rien, ce qui est précisément ce dont nous avons besoin. Mais si le groupe ASG a été déployé récemment, cette règle garantit qu'au maximum dans une minute, son nombre de serveurs atteindra dix. Ce n'est pas une approche très élégante, et de grands sauts de dix à deux serveurs et vice versa peuvent également causer des problèmes aux utilisateurs.
  • Créer un script personnalisé qui utilise l'API AWS pour déterminer le nombre de serveurs actifs dans l'ASG, l'appeler avec une source de données externe (voir la section «Source de données externe» à la page 249) et affecter à la capacité désirée du groupe ASG la valeur retournée par ce script. Ainsi, chaque nouvelle instance de l'ASG sera toujours lancée avec la même capacité que le code Terraform existant, ce qui complique son entretien.

Bien sûr, idéalement, Terraform devrait avoir un support intégré pour les déploiements sans temps d'arrêt, mais en mai 2019, l'équipe de HashiCorp ne prévoyait pas d'ajouter cette fonctionnalité (plus de détails ici).

Un plan correct peut ne pas être mise en œuvre avec succès.

Parfois, lors de l'exécution de la commande plan, un plan de déploiement tout à fait correct peut être obtenu, 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 au chapitre 2 :

resource "aws_iam_user" "existing_user" {
   # Remplacez ici par le nom de l'utilisateur IAM déjà existant,
   # pour vous entraîner à utiliser la commande terraform import
   name = "yevgeniy.brikman"
}

Maintenant, si vous exécutez la commande plan, Terraform affichera un plan de déploiement qui semble tout à fait raisonnable :

Terraform va effectuer les actions suivantes :

   # aws_iam_user.existing_user va être créé
   + resource "aws_iam_user" "existing_user" {
         + arn                  = (connu après application)
         + force_destroy   = false
         + id                    = (connu après application)
         + name               = "yevgeniy.brikman"
         + path                 = "\/"
         + unique_id         = (connu après application)
      }

Plan : 1 à ajouter, 0 à modifier, 0 à détruire.

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

Erreur : Erreur lors de la création de l'utilisateur IAM yevgeniy.brikman : EntityAlreadyExists :
L'utilisateur avec le nom yevgeniy.brikman existe déjà.

   sur main.tf ligne 10, dans resource "aws_iam_user" "existing_user" :
   10 : resource "aws_iam_user" "existing_user" {

Le problème est bien sûr que l'utilisateur IAM avec ce nom existe déjà. Et cela peut se produire non seulement avec les utilisateurs IAM, mais pratiquement avec n'importe quelle ressource. Peut-être que quelqu'un a créé cette ressource manuellement ou à l'aide de la ligne de commande, mais quoi qu'il en soit, la correspondance des identifiants entraîne des conflits. Cette erreur a de nombreuses variantes qui surprennent souvent les débutants en 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 de Terraform. Si les ressources sont créées d'une autre manière (par exemple, manuellement, en cliquant sur un bouton dans la console AWS), elles ne figureront pas dans le fichier d'état et, par conséquent, Terraform ne les prendra pas en compte lors de l'exécution de la commande plan. Au final, un plan qui semble correct à première vue sera infructueux.

On peut tirer deux leçons de cela.

  • Si vous avez déjà commencé à travailler avec Terraform, n'utilisez rien d'autre. Si une partie de votre infrastructure est gérée par Terraform, il ne doit plus être modifié manuellement. Dans le cas contraire, non seulement vous risquez d'obtenir des erreurs étranges de Terraform, mais vous saperez également de nombreux avantages de l'IaC, car le code ne sera plus une représentation fidèle de votre infrastructure.
  • Si vous avez déjà 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. Ainsi, 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. Ici, la même syntaxe que pour les liens vers les ressources est utilisée : _. (comme aws_iam_user.existing_user). Le deuxième argument est l'identifiant de la ressource à importer. Par exemple, pour la ressource aws_iam_user, l'identifiant est le nom d'utilisateur (comme yevgeniy.brikman), et pour la ressource aws_instance, l'identifiant est l'identifiant du serveur EC2 (comme i-190e22e5). Comment importer une ressource est généralement indiqué dans la documentation en bas de sa page.

    Ci-dessous se trouve la commande import, permettant de synchroniser la ressource aws_iam_user que vous avez ajoutée à votre configuration Terraform avec l'utilisateur IAM de la chapitre 2 (bien sûr, remplacez yevgeniy.brikman par votre nom) :

    $ terraform import aws_iam_user.existing_user yevgeniy.brikman

    Terraform interrogera l'API AWS pour trouver votre utilisateur IAM et établir dans le fichier d'état un lien entre lui et la ressource aws_iam_user.existing_user dans votre configuration Terraform. À partir de ce moment, lors de l'exécution de 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 avez déjà de nombreuses ressources que vous souhaitez importer dans Terraform, écrire manuellement le code et importer chacune d'elles individuellement peut s'avérer fastidieux. C'est pourquoi il vaut la peine de se pencher sur un outil tel que Terraforming (http://terraforming.dtan4.net/), qui peut automatiquement importer le code et l'état de votre compte AWS.

    Le refactoring peut avoir ses pièges

    Refactoring est une pratique courante en programmation, où vous modifiez la structure interne du code tout en laissant son comportement externe inchangé. Cela est nécessaire pour rendre le code plus compréhensible, soigné et facile à entretenir. Le refactoring est une méthodologie indispensable qui devrait être appliquée régulièrement. Mais, lorsqu'il s'agit de Terraform ou de tout autre outil IaC, il faut être extrêmement prudent quant à ce que l'on entend par « comportement externe » d'un morceau de code, sinon des problèmes imprévus surgiront.

    Par exemple, une forme courante de refactorisation consiste à remplacer les noms de variables ou de fonctions par des noms plus explicites. De nombreux IDE disposent d'un support intégré pour la refactorisation et peuvent automatiquement renommer des variables et des fonctions dans tout le projet. Dans les langages de programmation généralistes, c'est une procédure triviale à laquelle on ne pense pas, cependant, dans Terraform, il faut être extrêmement prudent, sinon vous pourriez rencontrer des interruptions de service.

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

    variable "cluster_name" {
       description = "Le nom à utiliser pour toutes les ressources du cluster"
       type          = string
    }

    Imaginez que vous avez commencé à utiliser ce module pour déployer un microservice nommé foo. Plus tard, vous avez voulu renommer votre service en bar. Ce changement peut sembler trivial, mais en réalité, il peut entraîner des interruptions de service.

    Le fait est que le module webserver-cluster utilise la variable cluster_name dans plusieurs ressources, y compris le paramètre name de deux groupes de sécurité et de 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 changez le paramètre name dans une ressource, Terraform supprimera l'ancienne version de cette ressource et créera une nouvelle à la place. Mais si cette ressource est un ALB, pendant la période entre sa suppression et le chargement de la nouvelle version, vous n'aurez pas de 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éé.

    Une autre forme de refactorisation qui pourrait vous intéresser est le changement de l'identifiant Terraform. Prenons comme exemple la ressource aws_security_group dans le module webserver-cluster :

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

    L'identifiant de cette ressource s'appelle instance. Imaginez qu'au cours de la refactorisation, vous décidiez de le changer pour un nom plus explicite (à vos yeux) cluster_instance :

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

    Que se passera-t-il alors ? Correct : une interruption de service.

    Terraform associe l'ID de chaque ressource à l'identifiant du fournisseur de cloud. Par exemple, l'iam_user est lié à l'ID de l'utilisateur IAM dans AWS, et l'aws_instance à l'ID du serveur AWS EC2. Si vous modifiez l'identifiant de la ressource (par exemple, en passant de instance à cluster_instance, comme dans le cas de aws_security_group), pour Terraform, cela semblera comme si vous aviez supprimé l'ancienne ressource et ajouté une nouvelle. En appliquant ces modifications, Terraform supprimera l'ancienne groupe de sécurité et en créera une autre, tandis que vos serveurs commenceront à rejeter tout le trafic réseau.

    Voici quatre leçons essentielles que vous devez tirer de cette discussion.

    • Utilisez toujours la commande plan. Cela vous permettra d'identifier tous ces problèmes. Examinez attentivement sa sortie et faites attention aux situations où Terraform prévoit de supprimer des ressources qu'il vaut probablement mieux ne pas supprimer.
    • 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 positive, create_before_destroy peut aider. Vous pouvez également obtenir le même résultat manuellement en effectuant deux étapes : d'abord, ajoutez la nouvelle ressource à la configuration et exécutez la commande apply, puis supprimez l'ancienne ressource de la configuration et utilisez la commande apply à nouveau.
    • Modifier des identifiants nécessite de changer l'état. Si vous souhaitez changer l'identifiant associé à une ressource (par exemple, renommer aws_security_group de instance à cluster_instance), tout en évitant de supprimer la ressource et de créer une nouvelle version, il est nécessaire de mettre à jour le fichier d'état Terraform en conséquence. Ne le faites jamais manuellement – utilisez plutôt la commande terraform state. Lors du renommage des identifiants, utilisez la commande terraform state mv qui a la syntaxe suivante :
      terraform state mv

      ORIGINAL_REFERENCE est l'expression qui fait référence à la ressource dans son état actuel, tandis que NEW_REFERENCE est l'endroit où vous souhaitez la déplacer. Par exemple, lors du renommage du groupe aws_security_group de instance à cluster_instance, il faut exécuter la commande suivante :

      $ terraform state mv 
         aws_security_group.instance 
         aws_security_group.cluster_instance

      Ainsi, vous indiquerez à Terraform que l'état qui était auparavant associé à aws_security_group.instance doit maintenant être lié à aws_security_group.cluster_instance. Si, après avoir renommé et exécuté cette commande terraform plan, aucune modification n'est affichée, cela signifie que vous avez bien fait les choses.

    • Certains paramètres ne peuvent pas être modifiés. Les paramètres de nombreux ressources sont immuables. Si vous essayez de les modifier, Terraform supprimera l'ancienne ressource et en créera une nouvelle à la place. Sur la page de chaque ressource, il est généralement précisé ce qui se passe lorsque vous modifiez tel ou tel paramètre, donc n'oubliez pas de consulter la documentation. Utilisez toujours la commande plan et évaluez la pertinence de la stratégie create_before_destroy.

    La cohérence différée se conforme… à l'attente

    L'API de certains fournisseurs de cloud, comme AWS, est asynchrone et repose sur une cohérence différée. L'asynchronicité signifie que l'interface peut immédiatement renvoyer une réponse sans attendre la fin de l'action demandée. La cohérence différée signifie qu'il peut falloir du temps pour que les modifications se propagent dans tout le système ; pendant ce temps, vos réponses peuvent être incohérentes et dépendre de la réplique de la source de données qui répond à vos appels API.

    Imaginez, par exemple, que vous effectuez un appel API à AWS demandant la création d'un serveur EC2. L'API renverra une réponse « réussie » (201 Created) presque instantanément, sans attendre la création effective du serveur. Si vous essayez de vous y connecter immédiatement, il est pratiquement certain que cela ne fonctionnera pas, car à ce moment-là, AWS initialise encore les ressources ou, en alternative, le serveur n'est pas encore démarré. De plus, si vous faites un autre appel pour obtenir des informations sur ce serveur, vous pourriez recevoir une erreur (404 Not Found). En effet, les informations sur ce serveur EC2 peuvent encore se propager dans AWS, et il faudra attendre quelques secondes pour qu'elles soient disponibles partout.

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

    $ terraform apply
    aws_subnet.private-persistence.2: InvalidSubnetID.NotFound:
    L'ID de sous-réseau 'subnet-xxxxxxx' n'existe pas

    En d'autres termes, vous créez une ressource (comme un sous-réseau) et ensuite vous essayez d'obtenir des informations à son sujet (comme l'ID du sous-réseau récemment créé), mais Terraform ne peut pas les trouver. La plupart de ces erreurs (y compris 6813) ont déjà été corrigées, mais elles apparaissent encore de temps en temps, surtout lorsque Terraform ajoute la prise en charge d'un nouveau type de ressource. C'est frustrant, mais dans la plupart des cas, cela ne cause pas de dommages. En réexécutant terraform apply, tout devrait fonctionner, car d'ici là, les informations auront déjà été diffusées dans le système.

    Ce passage est extrait du livre d'Evgeny Brikman « Terraform : infrastructure as code ».

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster