Fallstricke von Terraform

Fallstricke von Terraform
Lassen Sie uns einige Fallstricke hervorheben, darunter solche im Zusammenhang mit Schleifen, if-Anweisungen und Bereitstellungstechniken sowie allgemeinere Probleme, die Terraform im Allgemeinen betreffen:

  • Die Parameter count und for_each unterliegen Einschränkungen.
  • Begrenzen Sie Bereitstellungen ohne Ausfallzeiten.
  • selbst ein guter Plan kann scheitern;
  • Refactoring kann seine Tücken haben;
  • Aufgeschobene Kohärenz ist konsistent... mit Aufschub.

Für die Parameter count und for_each gelten Einschränkungen

Die Beispiele in diesem Kapitel nutzen den count-Parameter und den for_each-Ausdruck in Schleifen und bedingter Logik ausführlich. Sie bieten eine gute Leistung, weisen jedoch zwei wichtige Einschränkungen auf, die Sie beachten müssen.

  • Count und for_each können keine Ressourcenausgabevariablen referenzieren.
  • count und for_each können in der Modulkonfiguration nicht verwendet werden.

count und for_each können keine Ressourcenausgabevariablen referenzieren

Stellen Sie sich vor, Sie müssen mehrere EC2-Server bereitstellen und möchten ASG aus irgendeinem Grund nicht verwenden. Ihr Code könnte so aussehen:

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

Schauen wir sie uns einzeln an.

Da der Count-Parameter auf einen statischen Wert gesetzt ist, funktioniert dieser Code problemlos: Wenn Sie den Apply-Befehl ausführen, werden drei EC2-Server erstellt. Aber was wäre, wenn Sie in jeder Availability Zone (AZ) Ihrer aktuellen AWS-Region einen Server bereitstellen möchten? Sie können Ihren Code veranlassen, eine Liste von Zonen aus der Datenquelle aws_availability_zones zu laden und dann jede Zone zu durchlaufen und darin mithilfe des Count-Parameters und des Array-Indexzugriffs einen EC2-Server zu erstellen:

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" {}

Auch dieser Code funktioniert einwandfrei, da der count-Parameter problemlos auf Datenquellen verweisen kann. Aber was passiert, wenn die Anzahl der Server, die Sie erstellen müssen, von der Leistung einer Ressource abhängt? Um dies zu demonstrieren, verwenden Sie am einfachsten die Ressource random_integer, die, wie der Name schon sagt, eine zufällige Ganzzahl zurückgibt:

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

Dieser Code generiert eine Zufallszahl zwischen 1 und 3. Sehen wir uns an, was passiert, wenn wir versuchen, die Ausgabe dieser Ressource im count-Parameter der aws_instance-Ressource zu verwenden:

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

Wenn Sie Terraform Plan mit diesem Code ausführen, erhalten Sie die folgende Fehlermeldung:

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 erfordert, dass count und for_each während der Planungsphase berechnet werden, bevor Ressourcen erstellt oder geändert werden. Das bedeutet, dass count und for_each sich auf Literale, Variablen, Datenquellen und sogar Ressourcenlisten beziehen können (sofern ihre Länge zum Zeitpunkt der Planung bestimmt werden kann), jedoch nicht auf berechnete Ressourcenausgabevariablen.

count und for_each können in der Modulkonfiguration nicht verwendet werden

Eines Tages könnten Sie versucht sein, Ihrer Modulkonfiguration einen Zählparameter hinzuzufügen:

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

     count = 3

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

Dieser Code versucht, count innerhalb eines Moduls zu verwenden, um drei Kopien der Webserver-Cluster-Ressource zu erstellen. Oder Sie möchten die Verbindung eines Moduls basierend auf einer booleschen Bedingung optional machen, indem Sie dessen Zählparameter auf 0 setzen. Das sieht vielleicht nach vernünftigem Code aus, aber Sie erhalten diese Fehlermeldung, wenn Sie Terraform Plan ausführen:

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.

Leider wird ab Terraform 0.12.6 die Verwendung von count oder for_each in einer Modulressource nicht unterstützt. Laut den Versionshinweisen zu Terraform 0.12 (http://bit.ly/3257bv4) plant HashiCorp, diese Funktion in Zukunft hinzuzufügen. Je nachdem, wann Sie dieses Buch lesen, ist sie möglicherweise bereits verfügbar. Um es sicher herauszufinden, Lesen Sie hier das Terraform-Änderungsprotokoll.

Einschränkungen von Zero-Downtime-Bereitstellungen

Die Verwendung des Blocks „create_before_destroy“ in Kombination mit ASG ist eine großartige Lösung zum Erstellen von Bereitstellungen ohne Ausfallzeiten, mit einer Einschränkung: Autoscaling-Regeln werden nicht unterstützt. Genauer gesagt wird dadurch die ASG-Größe bei jeder Bereitstellung auf min_size zurückgesetzt, was ein Problem sein könnte, wenn Sie Autoscaling-Regeln verwenden, um die Anzahl der ausgeführten Server zu erhöhen.

Beispielsweise enthält das Modul „webserver-cluster“ ein Paar aws_autoscaling_schedule-Ressourcen, das um 9 Uhr morgens die Anzahl der Server im Cluster von zwei auf zehn erhöht. Wenn Sie die Bereitstellung beispielsweise um 11:9 Uhr durchführen, startet die neue ASG mit nur zwei statt zehn Servern und bleibt so bis XNUMX:XNUMX Uhr am nächsten Tag.

Diese Einschränkung kann auf verschiedene Arten umgangen werden.

  • Ändern Sie den Wiederholungsparameter in aws_autoscaling_schedule von 0 9 * * * („um 9 Uhr ausführen“) in etwa 0-59 9-17 * * * („jede Minute von 9 bis 5 Uhr ausführen“). Wenn ASG bereits über zehn Server verfügt, ändert sich durch die erneute Ausführung dieser Autoscaling-Regel nichts, was wir wollen. Wenn die ASG jedoch erst vor kurzem bereitgestellt wurde, stellt diese Regel sicher, dass die Anzahl ihrer Server in maximal einer Minute zehn erreicht. Das ist kein ganz eleganter Ansatz und auch große Sprünge von zehn auf zwei Server und zurück können den Nutzern Probleme bereiten.
  • Erstellen Sie ein benutzerdefiniertes Skript, das die AWS-API verwendet, um die Anzahl der aktiven Server in der ASG zu ermitteln, rufen Sie es über eine externe Datenquelle auf (siehe „Externe Datenquelle“ auf Seite 249) und legen Sie den Parameter „wunsch_kapazität“ der ASG auf den von zurückgegebenen Wert fest das Drehbuch. Auf diese Weise wird jede neue ASG-Instanz immer mit der gleichen Kapazität wie der vorhandene Terraform-Code ausgeführt, was die Wartung erschwert.

Natürlich hätte Terraform idealerweise integrierte Unterstützung für Bereitstellungen ohne Ausfallzeiten, aber bis Mai 2019 hatte das HashiCorp-Team keine Pläne, diese Funktionalität hinzuzufügen (Details - hier).

Der richtige Plan kann möglicherweise nicht erfolgreich umgesetzt werden

Manchmal erstellt der Befehl „plan“ einen völlig korrekten Bereitstellungsplan, der Befehl „apply“ gibt jedoch einen Fehler zurück. Versuchen Sie beispielsweise, die Ressource aws_iam_user mit demselben Namen hinzuzufügen, den Sie für den zuvor in Kapitel 2 erstellten IAM-Benutzer verwendet haben:

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

Wenn Sie nun den Befehl „plan“ ausführen, gibt Terraform einen scheinbar vernünftigen Bereitstellungsplan aus:

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.

Wenn Sie den Befehl „Apply“ ausführen, erhalten Sie die folgende Fehlermeldung:

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" {

Das Problem besteht natürlich darin, dass bereits ein IAM-Benutzer mit diesem Namen existiert. Und das kann nicht nur IAM-Benutzern passieren, sondern fast jeder Ressource. Es ist möglich, dass jemand diese Ressource manuell oder über die Befehlszeile erstellt hat, aber in beiden Fällen führen übereinstimmende IDs zu Konflikten. Es gibt viele Variationen dieses Fehlers, der Terraform-Neulinge oft überrascht.

Der entscheidende Punkt ist, dass der Befehl terraform plan nur die Ressourcen berücksichtigt, die in der Terraform-Statusdatei angegeben sind. Wenn Ressourcen auf andere Weise erstellt werden (z. B. manuell durch Klicken in der AWS-Konsole), landen sie nicht in der Statusdatei und werden daher von Terraform bei der Ausführung des Planbefehls nicht berücksichtigt. Dadurch erweist sich ein auf den ersten Blick richtig erscheinender Plan als erfolglos.

Daraus lassen sich zwei Lehren ziehen.

  • Wenn Sie bereits mit Terraform begonnen haben, verwenden Sie nichts anderes. Wenn ein Teil Ihrer Infrastruktur mit Terraform verwaltet wird, können Sie ihn nicht mehr manuell ändern. Andernfalls riskieren Sie nicht nur seltsame Terraform-Fehler, sondern machen auch viele der Vorteile von IaC zunichte, da der Code keine genaue Darstellung Ihrer Infrastruktur mehr darstellt.
  • Wenn Sie bereits über eine Infrastruktur verfügen, verwenden Sie den Importbefehl. Wenn Sie anfangen, Terraform mit bestehender Infrastruktur zu verwenden, können Sie es mit dem Befehl terraform import zur Statusdatei hinzufügen. Auf diese Weise weiß Terraform, welche Infrastruktur verwaltet werden muss. Der Importbefehl benötigt zwei Argumente. Die erste ist die Ressourcenadresse in Ihren Konfigurationsdateien. Die Syntax ist hier dieselbe wie für Ressourcenlinks: _. (wie aws_iam_user.existing_user). Das zweite Argument ist die ID der zu importierenden Ressource. Nehmen wir an, die Ressourcen-ID aws_iam_user ist der Benutzername (z. B. yevgeniy.brikman) und die Ressourcen-ID aws_instance ist die EC2-Server-ID (z. B. i-190e22e5). Wie eine Ressource importiert wird, wird normalerweise in der Dokumentation unten auf der Seite angegeben.

    Nachfolgend finden Sie einen Importbefehl, der die Ressource aws_iam_user synchronisiert, die Sie Ihrer Terraform-Konfiguration zusammen mit dem IAM-Benutzer in Kapitel 2 hinzugefügt haben (wobei natürlich yevgeniy.brikman durch Ihren Namen ersetzt wird):

    $ terraform import aws_iam_user.existing_user yevgeniy.brikman

    Terraform ruft die AWS-API auf, um Ihren IAM-Benutzer zu finden und eine Statusdateizuordnung zwischen ihm und der Ressource aws_iam_user.existing_user in Ihrer Terraform-Konfiguration zu erstellen. Wenn Sie von nun an den Befehl „plan“ ausführen, erkennt Terraform, dass der IAM-Benutzer bereits vorhanden ist, und versucht nicht, ihn erneut zu erstellen.

    Es ist erwähnenswert, dass es mühsam sein kann, den Code manuell zu schreiben und jede einzelne Ressource einzeln zu importieren, wenn Sie bereits über viele Ressourcen verfügen, die Sie in Terraform importieren möchten. Es lohnt sich also, einen Blick auf ein Tool wie Terraforming (http://terraforming.dtan4.net/) zu werfen, das automatisch Code und Status aus Ihrem AWS-Konto importieren kann.

    Refactoring kann seine Tücken haben

    Refactoring ist eine gängige Praxis in der Programmierung, bei der man die interne Struktur des Codes ändert, während das äußere Verhalten unverändert bleibt. Dadurch soll der Code klarer, übersichtlicher und leichter zu warten sein. Refactoring ist eine unverzichtbare Technik, die regelmäßig angewendet werden sollte. Aber wenn es um Terraform oder ein anderes IaC-Tool geht, muss man äußerst vorsichtig sein, was man unter dem „externen Verhalten“ eines Codestücks versteht, sonst kommt es zu unerwarteten Problemen.

    Eine gängige Art des Refactorings besteht beispielsweise darin, die Namen von Variablen oder Funktionen durch verständlichere Namen zu ersetzen. Viele IDEs verfügen über integrierte Unterstützung für Refactoring und können Variablen und Funktionen im gesamten Projekt automatisch umbenennen. In allgemeinen Programmiersprachen ist dies ein trivialer Vorgang, an den Sie vielleicht nicht denken, aber in Terraform müssen Sie damit äußerst vorsichtig sein, sonst kann es zu Ausfällen kommen.

    Beispielsweise verfügt das Modul „webserver-cluster“ über eine Eingabevariable „cluster_name“:

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

    Stellen Sie sich vor, Sie hätten begonnen, dieses Modul zu verwenden, um einen Mikrodienst namens foo bereitzustellen. Später möchten Sie Ihren Dienst in bar umbenennen. Diese Änderung mag trivial erscheinen, kann aber in Wirklichkeit zu Dienstunterbrechungen führen.

    Tatsache ist, dass das Webserver-Cluster-Modul die Variable „cluster_name“ in einer Reihe von Ressourcen verwendet, einschließlich des Namensparameters von zwei Sicherheitsgruppen und des 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]
    }

    Wenn Sie den Namensparameter einer Ressource ändern, löscht Terraform die alte Version dieser Ressource und erstellt an ihrer Stelle eine neue. Wenn es sich bei dieser Ressource jedoch um eine ALB handelt, steht Ihnen zwischen dem Löschen und dem Herunterladen einer neuen Version kein Mechanismus zur Verfügung, um den Datenverkehr auf Ihren Webserver umzuleiten. Wenn eine Sicherheitsgruppe gelöscht wird, lehnen Ihre Server ebenfalls jeglichen Netzwerkverkehr ab, bis eine neue Gruppe erstellt wird.

    Eine weitere Art der Umgestaltung, die Sie interessieren könnte, ist die Änderung der Terraform-ID. Nehmen wir als Beispiel die Ressource aws_security_group im Modul webserver-cluster:

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

    Der Bezeichner dieser Ressource wird Instanz genannt. Stellen Sie sich vor, Sie hätten sich während des Refactorings entschieden, es in einen (Ihrer Meinung nach) verständlicheren Namen „cluster_instance“ zu ändern:

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

    Was wird am Ende passieren? Genau: eine Störung.

    Terraform ordnet jede Ressourcen-ID der Cloud-Anbieter-ID zu. Beispielsweise ist iam_user mit der AWS IAM-Benutzer-ID und aws_instance mit der AWS EC2-Server-ID verknüpft. Wenn Sie die Ressourcen-ID ändern (z. B. von „Instanz“ zu „Cluster_Instanz“, wie es bei „aws_security_group“ der Fall ist), sieht es in Terraform so aus, als hätten Sie die alte Ressource gelöscht und eine neue hinzugefügt. Wenn Sie diese Änderungen anwenden, löscht Terraform die alte Sicherheitsgruppe und erstellt eine neue, während Ihre Server beginnen, jeglichen Netzwerkverkehr abzulehnen.

    Hier sind vier wichtige Lehren, die Sie aus dieser Diskussion ziehen sollten.

    • Verwenden Sie immer den Planbefehl. Es kann all diese Probleme aufdecken. Überprüfen Sie die Ausgabe sorgfältig und achten Sie auf Situationen, in denen Terraform plant, Ressourcen zu löschen, die höchstwahrscheinlich nicht gelöscht werden sollten.
    • Erstellen Sie, bevor Sie löschen. Wenn Sie eine Ressource ersetzen möchten, überlegen Sie sorgfältig, ob Sie einen Ersatz erstellen müssen, bevor Sie das Original löschen. Wenn die Antwort „Ja“ lautet, kann create_before_destroy helfen. Das gleiche Ergebnis kann manuell erreicht werden, indem zwei Schritte ausgeführt werden: Zuerst eine neue Ressource zur Konfiguration hinzufügen und den Befehl „Apply“ ausführen, dann die alte Ressource aus der Konfiguration entfernen und den Befehl „Apply“ erneut verwenden.
    • Das Ändern von Bezeichnern erfordert eine Statusänderung. Wenn Sie die einer Ressource zugeordnete ID ändern möchten (z. B. aws_security_group von „Instance“ in „Cluster_Instance“ umbenennen), ohne die Ressource zu löschen und eine neue Version davon zu erstellen, müssen Sie die Terraform-Statusdatei entsprechend aktualisieren. Tun Sie dies niemals manuell – verwenden Sie stattdessen den Befehl terraform state. Beim Umbenennen von Bezeichnern sollten Sie den Befehl terraform state mv ausführen, der die folgende Syntax hat:
      terraform state mv <ORIGINAL_REFERENCE> <NEW_REFERENCE>

      ORIGINAL_REFERENCE ist ein Ausdruck, der auf die Ressource in ihrer aktuellen Form verweist, und NEW_REFERENCE ist die Stelle, an die Sie sie verschieben möchten. Wenn Sie beispielsweise die Gruppe aws_security_group von „instance“ in „cluster_instance“ umbenennen, müssen Sie den folgenden Befehl ausführen:

      $ terraform state mv 
         aws_security_group.instance 
         aws_security_group.cluster_instance

      Dadurch wird Terraform mitgeteilt, dass der Status, der zuvor aws_security_group.instance zugeordnet war, nun aws_security_group.cluster_instance zugeordnet werden sollte. Wenn Terraform Plan nach dem Umbenennen und Ausführen dieses Befehls keine Änderungen anzeigt, haben Sie alles richtig gemacht.

    • Einige Einstellungen können nicht geändert werden. Die Parameter vieler Ressourcen sind unveränderlich. Wenn Sie versuchen, sie zu ändern, löscht Terraform die alte Ressource und erstellt an ihrer Stelle eine neue. Auf jeder Ressourcenseite wird normalerweise angegeben, was passiert, wenn Sie eine bestimmte Einstellung ändern. Lesen Sie daher unbedingt die Dokumentation. Verwenden Sie immer den Befehl „plan“ und erwägen Sie die Verwendung der Strategie „create_before_destroy“.

    Aufgeschobene Konsistenz ist konsistent ... mit Aufschub

    Die APIs einiger Cloud-Anbieter, wie z. B. AWS, sind asynchron und weisen eine verzögerte Konsistenz auf. Asynchronität bedeutet, dass die Schnittstelle sofort eine Antwort zurückgeben kann, ohne auf den Abschluss der angeforderten Aktion warten zu müssen. Verzögerte Konsistenz bedeutet, dass es einige Zeit dauern kann, bis sich Änderungen im gesamten System verbreiten. Während dies geschieht, sind Ihre Antworten möglicherweise inkonsistent und hängen davon ab, welches Datenquellenreplikat auf Ihre API-Aufrufe antwortet.

    Stellen Sie sich zum Beispiel vor, Sie rufen AWS per API auf und bitten es, einen EC2-Server zu erstellen. Die API gibt fast sofort eine „erfolgreiche“ Antwort (201 erstellt) zurück, ohne auf die Erstellung des Servers selbst warten zu müssen. Wenn Sie versuchen, sofort eine Verbindung herzustellen, schlägt dies mit ziemlicher Sicherheit fehl, da AWS zu diesem Zeitpunkt noch Ressourcen initialisiert oder der Server alternativ noch nicht gestartet ist. Wenn Sie außerdem einen weiteren Anruf tätigen, um Informationen über diesen Server zu erhalten, erhalten Sie möglicherweise eine Fehlermeldung (404 Not Found). Die Sache ist, dass die Informationen über diesen EC2-Server möglicherweise noch in AWS verbreitet werden, bevor sie überall verfügbar sind. Sie müssen einige Sekunden warten.

    Wenn Sie eine asynchrone API mit verzögerter Konsistenz verwenden, müssen Sie Ihre Anfrage regelmäßig wiederholen, bis die Aktion abgeschlossen ist und sich im System verbreitet. Leider bietet das AWS SDK hierfür keine guten Tools und das Terraform-Projekt litt früher unter vielen Fehlern wie 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

    Mit anderen Worten: Sie erstellen eine Ressource (z. B. ein Subnetz) und versuchen dann, einige Informationen darüber abzurufen (z. B. die ID des neu erstellten Subnetzes), und Terraform kann sie nicht finden. Die meisten dieser Fehler (einschließlich 6813) wurden behoben, sie treten jedoch immer noch von Zeit zu Zeit auf, insbesondere wenn Terraform Unterstützung für einen neuen Ressourcentyp hinzufügt. Das ist ärgerlich, schadet aber in den meisten Fällen nicht. Wenn Sie terraform apply erneut ausführen, sollte alles funktionieren, da sich die Informationen zu diesem Zeitpunkt bereits im gesamten System verbreitet haben.

    Dieser Auszug stammt aus dem Buch von Evgeniy Brikman „Terraform: Infrastruktur auf Codeebene“.

Source: habr.com

Kommentar hinzufügen