Insidie ​​della terraformazione

Insidie ​​della terraformazione
Evidenziamo alcune insidie, comprese quelle relative ai loop, alle istruzioni if ​​e alle tecniche di distribuzione, nonché problemi più generali che interessano Terraform in generale:

  • i parametri count e for_each hanno limitazioni;
  • limitare a zero le implementazioni con tempi di inattività;
  • anche un buon piano può fallire;
  • il refactoring può avere le sue insidie;
  • la coerenza differita è coerente... con il differimento.

I parametri count e for_each presentano limitazioni

Gli esempi in questo capitolo fanno ampio uso del parametro count e dell'espressione for_each nei cicli e nella logica condizionale. Funzionano bene, ma hanno due importanti limitazioni di cui devi essere consapevole.

  • Count e for_each non possono fare riferimento ad alcuna variabile di output della risorsa.
  • count e for_each non possono essere utilizzati nella configurazione del modulo.

count e for_each non possono fare riferimento ad alcuna variabile di output della risorsa

Immagina di dover distribuire diversi server EC2 e per qualche motivo non desideri utilizzare ASG. Il tuo codice potrebbe essere così:

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

Osserviamoli uno per uno.

Poiché il parametro count è impostato su un valore statico, questo codice funzionerà senza problemi: quando esegui il comando apply, creerà tre server EC2. Ma cosa succederebbe se volessi distribuire un server in ciascuna zona di disponibilità (AZ) nella tua attuale regione AWS? Puoi fare in modo che il tuo codice carichi un elenco di zone dall'origine dati aws_availability_zones e quindi scorrere ciascuna di esse e creare un server EC2 al suo interno utilizzando il parametro count e l'accesso all'indice dell'array:

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

Anche questo codice funzionerà correttamente, poiché il parametro count può fare riferimento a origini dati senza problemi. Ma cosa succede se il numero di server che devi creare dipende dall'output di alcune risorse? Per dimostrarlo, il modo più semplice è utilizzare la risorsa random_integer che, come suggerisce il nome, restituisce un numero intero casuale:

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

Questo codice genera un numero casuale compreso tra 1 e 3. Vediamo cosa succede se proviamo a utilizzare l'output di questa risorsa nel parametro count della risorsa aws_instance:

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

Se esegui Terraform Plan su questo codice, riceverai il seguente errore:

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 richiede che count e for_each vengano calcolati durante la fase di pianificazione, prima che qualsiasi risorsa venga creata o modificata. Ciò significa che count e for_each possono fare riferimento a valori letterali, variabili, origini dati e persino elenchi di risorse (purché la loro lunghezza possa essere determinata al momento della pianificazione), ma non a variabili di output delle risorse calcolate.

count e for_each non possono essere utilizzati nella configurazione del modulo

Un giorno potresti essere tentato di aggiungere un parametro di conteggio alla configurazione del tuo modulo:

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

     count = 3

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

Questo codice tenta di utilizzare count all'interno di un modulo per creare tre copie della risorsa cluster di server web. Oppure potresti voler rendere facoltativa la connessione di un modulo in base ad alcune condizioni booleane impostando il suo parametro di conteggio su 0. Potrebbe sembrare un codice ragionevole, ma riceverai questo errore durante l'esecuzione del piano 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.

Sfortunatamente, a partire da Terraform 0.12.6, l'utilizzo di count o for_each in una risorsa del modulo non è supportato. Secondo le note sulla versione di Terraform 0.12 (http://bit.ly/3257bv4), HashiCorp prevede di aggiungere questa funzionalità in futuro, quindi, a seconda di quando leggerai questo libro, potrebbe essere già disponibile. Per scoprirlo con certezza, leggi il registro delle modifiche di Terraform qui.

Limitazioni delle distribuzioni con tempi di inattività pari a zero

L'utilizzo del blocco create_before_destroy in combinazione con ASG è un'ottima soluzione per creare distribuzioni senza tempi di inattività, ad eccezione di un avvertimento: le regole di scalabilità automatica non sono supportate. O per essere più precisi, reimposta la dimensione ASG su min_size su ogni distribuzione, il che potrebbe rappresentare un problema se si utilizzassero regole di scalabilità automatica per aumentare il numero di server in esecuzione.

Ad esempio, il modulo webserver-cluster contiene una coppia di risorse aws_autoscaling_schedule, che alle 9 del mattino aumenta il numero di server nel cluster da due a dieci. Se esegui la distribuzione, diciamo, alle 11:9, il nuovo ASG si avvierà con solo due server anziché dieci e rimarrà tale fino alle XNUMX:XNUMX del giorno successivo.

Questa limitazione può essere aggirata in diversi modi.

  • Modificare il parametro di ricorrenza in aws_autoscaling_schedule da 0 9 * * * ("esegui alle 9:0") a qualcosa come 59-9 17-9 * * * ("esegui ogni minuto dalle 5:XNUMX alle XNUMX:XNUMX"). Se ASG ha già dieci server, eseguire nuovamente questa regola di scalabilità automatica non cambierà nulla, che è ciò che vogliamo. Ma se l'ASG è stato installato solo di recente, questa regola farà sì che nel giro di un minuto al massimo il numero dei suoi server raggiunga i dieci. Questo non è un approccio del tutto elegante e anche i grandi salti da dieci a due server e viceversa possono causare problemi agli utenti.
  • Crea uno script personalizzato che utilizza l'API AWS per determinare il numero di server attivi nell'ASG, chiamalo utilizzando un'origine dati esterna (vedi "Origine dati esterna" a pagina 249) e imposta il parametro desiderata_capacità dell'ASG sul valore restituito da il copione. In questo modo, ogni nuova istanza ASG funzionerà sempre alla stessa capacità del codice Terraform esistente e ne renderà più difficile la manutenzione.

Naturalmente, Terraform avrebbe idealmente il supporto integrato per distribuzioni senza tempi di inattività, ma a partire da maggio 2019 il team di HashiCorp non aveva intenzione di aggiungere questa funzionalità (dettagli - qui).

Il piano corretto potrebbe essere implementato senza successo

A volte il comando plan produce un piano di distribuzione perfettamente corretto, ma il comando apply restituisce un errore. Prova, ad esempio, ad aggiungere la risorsa aws_iam_user con lo stesso nome utilizzato per l'utente IAM creato in precedenza nel Capitolo 2:

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

Ora, se esegui il comando plan, Terraform genererà un piano di distribuzione apparentemente ragionevole:

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.

Se esegui il comando apply otterrai il seguente errore:

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

Il problema, ovviamente, è che esiste già un utente IAM con quel nome. E questo può accadere non solo agli utenti IAM, ma a quasi tutte le risorse. È possibile che qualcuno abbia creato questa risorsa manualmente o utilizzando la riga di comando, ma in ogni caso la corrispondenza degli ID porta a conflitti. Esistono molte varianti di questo errore che spesso colgono di sorpresa i nuovi arrivati ​​​​su Terraform.

Il punto chiave è che il comando terraform plan prende in considerazione solo le risorse specificate nel file di stato Terraform. Se le risorse vengono create in altro modo (ad esempio manualmente cliccando nella console AWS), non finiranno nel file di stato e quindi Terraform non ne terrà conto durante l'esecuzione del comando plan. Di conseguenza, un piano che a prima vista sembra corretto si rivelerà infruttuoso.

Ci sono due lezioni da imparare da questo.

  • Se hai già iniziato a lavorare con Terraform, non utilizzare nient'altro. Se parte della tua infrastruttura è gestita tramite Terraform, non puoi più modificarla manualmente. In caso contrario, non solo rischierai strani errori Terraform, ma annullerai anche molti dei vantaggi di IaC poiché il codice non sarà più una rappresentazione accurata della tua infrastruttura.
  • Se disponi già di un'infrastruttura, utilizza il comando import. Se stai iniziando a utilizzare Terraform con un'infrastruttura esistente, puoi aggiungerlo al file di stato utilizzando il comando terraform import. In questo modo Terraform saprà quale infrastruttura deve essere gestita. Il comando import accetta due argomenti. Il primo è l'indirizzo della risorsa nei file di configurazione. La sintassi qui è la stessa dei collegamenti alle risorse: _. (come aws_iam_user.existing_user). Il secondo argomento è l'ID della risorsa da importare. Supponiamo che l'ID risorsa aws_iam_user sia il nome utente (ad esempio, yevgeniy.brikman) e l'ID risorsa aws_instance sia l'ID del server EC2 (come i-190e22e5). Come importare una risorsa è solitamente indicato nella documentazione in fondo alla sua pagina.

    Di seguito è riportato un comando di importazione che sincronizza la risorsa aws_iam_user che hai aggiunto alla configurazione Terraform insieme all'utente IAM nel capitolo 2 (sostituendo ovviamente il tuo nome con yevgeniy.brikman):

    $ terraform import aws_iam_user.existing_user yevgeniy.brikman

    Terraform chiamerà l'API AWS per trovare il tuo utente IAM e creare un'associazione di file di stato tra esso e la risorsa aws_iam_user.existing_user nella configurazione Terraform. D'ora in poi, quando eseguirai il comando plan, Terraform saprà che l'utente IAM esiste già e non tenterà di crearlo di nuovo.

    Vale la pena notare che se disponi già di molte risorse che desideri importare in Terraform, scrivere manualmente il codice e importarne una alla volta può essere una seccatura. Quindi vale la pena esaminare uno strumento come Terraforming (http://terraforming.dtan4.net/), che può importare automaticamente codice e stato dal tuo account AWS.

    Il refactoring può avere le sue insidie

    Refactoring è una pratica comune nella programmazione in cui si modifica la struttura interna del codice lasciando invariato il comportamento esterno. Questo per rendere il codice più chiaro, più ordinato e più facile da mantenere. Il refactoring è una tecnica indispensabile che dovrebbe essere utilizzata regolarmente. Ma quando si tratta di Terraform o di qualsiasi altro strumento IaC, bisogna stare estremamente attenti a cosa si intende per “comportamento esterno” di un pezzo di codice, altrimenti sorgeranno problemi imprevisti.

    Ad esempio, un tipo comune di refactoring consiste nel sostituire i nomi delle variabili o delle funzioni con nomi più comprensibili. Molti IDE dispongono del supporto integrato per il refactoring e possono rinominare automaticamente variabili e funzioni in tutto il progetto. Nei linguaggi di programmazione generici, questa è una procedura banale a cui potresti non pensare, ma in Terraform devi stare estremamente attento, altrimenti potresti riscontrare interruzioni.

    Ad esempio, il modulo webserver-cluster ha una variabile di input nome_cluster:

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

    Immagina di aver iniziato a utilizzare questo modulo per distribuire un microservizio chiamato foo. Successivamente, rinominerai il tuo servizio in bar. Questo cambiamento può sembrare banale, ma in realtà può causare disservizi.

    Il fatto è che il modulo webserver-cluster utilizza la variabile cluster_name in una serie di risorse, incluso il parametro name di due gruppi di sicurezza e 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]
    }

    Se modifichi il parametro name su una risorsa, Terraform eliminerà la vecchia versione di quella risorsa e ne creerà una nuova al suo posto. Ma se quella risorsa è un ALB, tra l'eliminazione e il download di una nuova versione, non avrai un meccanismo per reindirizzare il traffico al tuo server web. Allo stesso modo, se un gruppo di sicurezza viene eliminato, i tuoi server inizieranno a rifiutare qualsiasi traffico di rete fino alla creazione di un nuovo gruppo.

    Un altro tipo di refactoring che potrebbe interessarti è la modifica dell'ID Terraform. Prendiamo come esempio la risorsa aws_security_group nel modulo webserver-cluster:

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

    L'identificatore di questa risorsa è chiamato istanza. Immagina che durante il refactoring tu abbia deciso di cambiarlo in un nome più comprensibile (secondo te) cluster_instance:

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

    Cosa accadrà alla fine? Esatto: uno sconvolgimento.

    Terraform associa ogni ID risorsa all'ID del provider cloud. Ad esempio, iam_user è associato all'ID utente AWS IAM e aws_instance è associato all'ID del server AWS EC2. Se modifichi l'ID della risorsa (ad esempio da istanza a cluster_instance, come nel caso di aws_security_group), Terraform sembrerà come se avessi eliminato la vecchia risorsa e ne avessi aggiunta una nuova. Se applichi queste modifiche, Terraform eliminerà il vecchio gruppo di sicurezza e ne creerà uno nuovo, mentre i tuoi server inizieranno a rifiutare qualsiasi traffico di rete.

    Ecco quattro lezioni chiave che dovresti trarre da questa discussione.

    • Utilizzare sempre il comando plan. Può rivelare tutti questi ostacoli. Esamina attentamente il suo output e presta attenzione alle situazioni in cui Terraform prevede di eliminare risorse che molto probabilmente non dovrebbero essere eliminate.
    • Crea prima di eliminare. Se desideri sostituire una risorsa, valuta attentamente se è necessario creare una sostituzione prima di eliminare l'originale. Se la risposta è sì, create_before_destroy può aiutare. Lo stesso risultato può essere ottenuto manualmente eseguendo due passaggi: prima aggiungere una nuova risorsa alla configurazione ed eseguire il comando apply, quindi rimuovere la vecchia risorsa dalla configurazione e utilizzare nuovamente il comando apply.
    • La modifica degli identificatori richiede la modifica dello stato. Se desideri modificare l'ID associato a una risorsa (ad esempio, rinominare aws_security_group da istanza a cluster_instance) senza eliminare la risorsa e crearne una nuova versione, devi aggiornare di conseguenza il file di stato Terraform. Non farlo mai manualmente: usa invece il comando terraform state. Quando rinomini gli identificatori, dovresti eseguire il comando terraform state mv, che ha la seguente sintassi:
      terraform state mv <ORIGINAL_REFERENCE> <NEW_REFERENCE>

      ORIGINAL_REFERENCE è un'espressione che fa riferimento alla risorsa nella sua forma corrente e NEW_REFERENCE è dove desideri spostarla. Ad esempio, quando si rinomina il gruppo aws_security_group da istanza a cluster_instance, è necessario eseguire il comando seguente:

      $ terraform state mv 
         aws_security_group.instance 
         aws_security_group.cluster_instance

      Ciò indica a Terraform che lo stato precedentemente associato ad aws_security_group.instance dovrebbe ora essere associato ad aws_security_group.cluster_instance. Se dopo aver rinominato ed eseguito questo comando terraform plan non mostra alcuna modifica, hai fatto tutto correttamente.

    • Alcune impostazioni non possono essere modificate. I parametri di molte risorse sono immutabili. Se provi a modificarli, Terraform eliminerà la vecchia risorsa e ne creerà una nuova al suo posto. Ogni pagina delle risorse in genere indicherà cosa succede quando modifichi una particolare impostazione, quindi assicurati di controllare la documentazione. Utilizza sempre il comando plan e considera l'utilizzo della strategia create_before_destroy.

    La coerenza differita è coerente... con il differimento

    Le API di alcuni fornitori di servizi cloud, come AWS, sono asincrone e hanno una coerenza ritardata. Asincronia significa che l'interfaccia può restituire immediatamente una risposta senza attendere il completamento dell'azione richiesta. La coerenza ritardata significa che le modifiche potrebbero richiedere tempo per propagarsi in tutto il sistema; mentre ciò accade, le tue risposte potrebbero essere incoerenti e dipendere da quale replica dell'origine dati sta rispondendo alle tue chiamate API.

    Immagina, ad esempio, di effettuare una chiamata API ad AWS chiedendogli di creare un server EC2. L'API restituirà una risposta di “successo” (201 Creato) quasi istantaneamente, senza attendere la creazione del server stesso. Se provi a connetterti subito, quasi sicuramente fallirà perché a quel punto AWS sta ancora inizializzando le risorse o, in alternativa, il server non si è ancora avviato. Inoltre, se effettui un'altra chiamata per ottenere informazioni su questo server, potresti ricevere un errore (404 Not Found). Il fatto è che le informazioni su questo server EC2 potrebbero ancora essere propagate in AWS prima che diventino disponibili ovunque, dovrai attendere qualche secondo.

    Ogni volta che utilizzi un'API asincrona con coerenza lazy, devi ritentare periodicamente la richiesta finché l'azione non viene completata e si propaga attraverso il sistema. Sfortunatamente, l'SDK AWS non fornisce strumenti validi per questo e il progetto Terraform soffriva di molti bug come 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

    In altre parole, crei una risorsa (come una sottorete) e poi provi a ottenere alcune informazioni su di essa (come l'ID della sottorete appena creata) e Terraform non riesce a trovarla. La maggior parte di questi bug (incluso 6813) sono stati risolti, ma continuano a presentarsi di tanto in tanto, soprattutto quando Terraform aggiunge il supporto per un nuovo tipo di risorsa. Questo è fastidioso, ma nella maggior parte dei casi non causa alcun danno. Quando esegui nuovamente terraform apply, tutto dovrebbe funzionare, poiché a questo punto le informazioni saranno già diffuse in tutto il sistema.

    Questo estratto è presentato dal libro di Evgeniy Brikman "Terraform: infrastrutture a livello di codice".

Fonte: habr.com

Aggiungi un commento