Blog
Eingabevalidierung - fordern Sie Ihre Zeit von Terraform zurück!

Ich verwende Terraform jetzt seit über zwei Jahren und habe von Anfang an festgestellt, dass eine angemessene Eingabevalidierung notwendig ist. Obwohl Terraform mit HCL 2.0 Variablentypen und sogar Regeln für die Validierung von Eingabevariablen in Terraform CLI v0.13.0 hinzugefügt hat, scheint dies immer noch eine Herausforderung zu sein.
Warum Eingabeüberprüfung?
Es gibt mehrere Gründe, warum die Eingabevalidierung eine gute Idee ist, aber der wichtigste ist die Zeitersparnis!
Es ist schon oft vorgekommen, dass eine plan Ausgabe für mich gut aussah und Terraform selbst keine Fehler fand, aber wenn ich apply ausführte, brach der Code trotzdem ab. Nicht alle Anbieter scheinen Ihnen im Voraus zu sagen, dass eine bestimmte Kombination von Einstellungen nicht korrekt ist. Es kann auch vorkommen, dass der Name der Ressource nicht validiert wird, aber das wird in der Phase plan nicht erkannt. Insbesondere bei der Bereitstellung von GKE-Clustern oder Diensten wie Composer in der Google Cloud können Sie mehr als 30 Minuten warten, bis diese Art von Fehlern auftaucht. Und dann können Sie weitere 30 Minuten warten, bis die Ressourcen wieder deleted sind.
Sie sehen also, dass Sie viel Zeit sparen können, wenn Sie im Voraus über diese Einstellungsfehler informiert werden.
Benutzerdefinierte Assertions verwenden
Eines der ersten Dinge, die wir in unseren benutzerdefinierten Modulen implementiert haben, ist die Verwendung von Assertions. Die Sicherstellung, dass Terraform eine korrekte Fehlermeldung anzeigt, war eine Herausforderung, da es dafür keine vordefinierte Funktion gibt. Hinzu kommt, dass das Hinzufügen von benutzerdefinierten Prüfungen, um aus dem Code auszubrechen, von HCL nicht unterstützt wird. Beispiel:
# variables.tf
variable "prefix" {
description = "Company naming prefix, ensures uniqueness of bucket names"
type = string
default = "binx"
}
variable "project" {
description = "Company project name."
type = string
default = "blog"
}
variable "environment" {
description = "Company environment for which the resources are created (e.g. dev, tst, acc, prd, all)."
type = string
default = "dev"
}
variable "purpose" {
description = "Bucket purpose, will be used as part of the bucket name"
type = string
}
# main.tf
locals {
bucket_name = format("%s-%s-%s-%s", var.prefix, var.project, var.environment, var.purpose)
}
resource "google_storage_bucket" "demo" {
provider = google-beta
name = local.bucket_name
}
Wenn Sie terraform plan mit diesem Beispielcode ausführen und eine purpose mit ungültigen Zeichen eingeben, erhalten Sie die folgende Ausgabe:
var.purpose
Bucket purpose, will be used as part of the bucket name
Enter a value: test^&asd
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# google_storage_bucket.demo will be created
+ resource "google_storage_bucket" "demo" {
+ bucket_policy_only = (known after apply)
+ force_destroy = false
+ id = (known after apply)
+ location = "US"
+ name = "binx-blog-dev-test^&asd"
+ project = (known after apply)
+ self_link = (known after apply)
+ storage_class = "STANDARD"
+ uniform_bucket_level_access = (known after apply)
+ url = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Wie Sie sehen können, prüft Terraform die Zeichen im Bucket-Namen nicht und lässt Sie glauben, dass alles in Ordnung ist. Wenn Sie jedoch versuchen, diesen Code unter apply einzugeben, erhalten Sie bei der Erstellung des Buckets eine Fehlermeldung, dass der Name ungültig ist:
google_storage_bucket.demo: Creating...
╷
│ Error: googleapi: Error 400: Invalid bucket name: 'binx-blog-dev-test^&asd', invalid
│
│ with google_storage_bucket.demo,
│ on test.tf line 30, in resource "google_storage_bucket" "demo":
│ 30: resource "google_storage_bucket" "demo" {
│
╵
Wir möchten dies eigentlich in der plan Phase erkennen, um den Entwickler darauf hinzuweisen, dass er seinen Code korrigieren soll, bevor er ihn in Master für das Rollout zusammenführt.
Wie haben wir also dieses Problem gelöst?
Es scheint, dass wir die Funktion file missbrauchen können, um eine angemessene Fehlermeldung anzuzeigen, wenn unsere Prüfungen fehlschlagen. Das Schöne an dieser Funktion ist, dass sie Ihnen eine Fehlermeldung anzeigt, wenn die Datei, die Sie zu laden versuchen, nicht existiert. Wir können dies zu unserem Vorteil nutzen, indem wir eine Fehlerbeschreibung als Dateinamen angeben.
Beispiel:
# main.tf
locals {
regex_bucket_name = "(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])" # See https://cloud.google.com/storage/docs/naming-buckets
bucket_name = format("%s-%s-%s-%s", var.prefix, var.project, var.environment, var.purpose)
bucket_name_check = length(regexall("^${local.regex_bucket_name}$", local.bucket_name)) == 0 ? file(format("Bucket [%s]'s generated name [%s] does not match regex ^%s$", var.purpose, local.bucket_name, local.regex_bucket_name)) : "ok"
}
resource "google_storage_bucket" "demo" {
provider = google-beta
name = local.bucket_name
}
Wenn wir das obige Beispiel verwenden und Terraform plan mit der gleichen purpose Eingabe wie zuvor ausführen, erhalten wir die folgende Meldung:
│ Error: Invalid function argument
│
│ on main.tf line 5, in locals:
│ 5: bucket_name_check = length(regexall("^${local.regex_bucket_name}$", local.bucket_name)) == 0 ? file(format("Bucket [%s]'s generated name [%s] does not match regex ^%s$", var.purpose, local.bucket_name, local.regex_bucket_name)) : "ok"
│ ├────────────────
│ │ local.bucket_name is "binx-blog-dev-test^&asd"
│ │ local.regex_bucket_name is "(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])"
│ │ var.purpose is "test^&asd"
│
│ Invalid value for "path" parameter: no file exists at Bucket [test^&asd]'s generated name [binx-blog-dev-test^&asd] does not match regex ^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9]).)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result
│ from an attribute of that resource.
Wie Sie sehen können, versucht Terraform, eine Datei namens:
Bucket [test^&asdf]'s generated name [lot-blog-dev-test^&asdf] does not match regex:
^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9]).)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$
Dieser Dateiname ist eigentlich die Fehlermeldung, die wir anzeigen wollen. Offensichtlich existiert diese Datei nicht, so dass der Vorgang fehlschlägt und diese Meldung auf der Konsole angezeigt wird. Schön! Wir haben also eine Möglichkeit, dem Entwickler im Voraus mitzuteilen, dass die eingegebene Datei nicht gültig ist. Aber wie können wir die Meldung noch deutlicher machen? Wir können Zeilenumbrüche verwenden! Sehen Sie sich das folgende Beispiel an:
# main.tf
locals {
regex_bucket_name = "(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])" # See https://cloud.google.com/storage/docs/naming-buckets
assert_head = "nn-------------------------- /!\ CUSTOM ASSERTION FAILED /!\ --------------------------nn"
assert_foot = "nn-------------------------- /!\ ^^^^^^^^^^^^^^^^^^^^^^^ /!\ --------------------------nn"
bucket_name = format("%s-%s-%s-%s", var.prefix, var.project, var.environment, var.purpose)
bucket_name_check = length(regexall("^${local.regex_bucket_name}$", local.bucket_name)) == 0 ? file(format("%sBucket [%s]'s generated name [%s] does not match regex:n^%s$%s", local.assert_head, var.purpose, local.bucket_name, local.regex_bucket_name, local.assert_foot)) : "ok"
}
resource "google_storage_bucket" "demo" {
provider = google-beta
name = local.bucket_name
}
Wir erhalten nun die folgende Fehlerausgabe:
│ Error: Invalid function argument
│
│ on main.tf line 8, in locals:
│ 8: bucket_name_check = length(regexall("^${local.regex_bucket_name}$", local.bucket_name)) == 0 ? file(format("%sBucket [%s]'s generated name [%s] does not match regex:n^%s$%s", local.assert_head, var.purpose, local.bucket_name, local.regex_bucket_name, local.assert_foot)) : "ok"
│ ├────────────────
│ │ local.assert_foot is "nn-------------------------- /!\ ^^^^^^^^^^^^^^^^^^^^^^^ /!\ --------------------------nn"
│ │ local.assert_head is "nn-------------------------- /!\ CUSTOM ASSERTION FAILED /!\ --------------------------nn"
│ │ local.bucket_name is "binx-blog-dev-test^&asd"
│ │ local.regex_bucket_name is "(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])"
│ │ var.purpose is "test^&asd"
│
│ Invalid value for "path" parameter: no file exists at
│
│ -------------------------- /! CUSTOM ASSERTION FAILED /! --------------------------
│
│ Bucket [test^&asd]'s generated name [binx-blog-dev-test^&asd] does not match regex:
│ ^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9]).)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$
│
│ -------------------------- /! ^^^^^^^^^^^^^^^^^^^^^^^ /! --------------------------
│
│ ; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource.
Konklusion
Wie Sie sehen, eröffnet die Verwendung der Funktion file zur Generierung von Assertions viele Möglichkeiten für die Eingabevalidierung. Weitere Beispiele für die Verwendung dieser Funktion finden Sie in unseren eigenen Terraform-Modulen no file exists mitteilt, während Sie gar nicht versuchen, irgendwelche Dateien zu lesen.
Verfasst von

Lotte-Sara Laan
Unsere Ideen
Weitere Blogs
Contact



