Evidenziamo alcune difficoltà, comprese quelle legate ai cicli, alle espressioni if e alle metodologie di distribuzione, così come a problemi più generali che riguardano Terraform nel suo insieme:
- i parametri count e for_each hanno restrizioni;
- le restrizioni sulle distribuzioni con tempo di inattività nullo;
- anche un buon piano può rivelarsi non riuscito;
- il refactoring può avere le sue insidie;
- la coerenza ritardata è in accordo... con il rinvio.
I parametri count e for_each hanno restrizioni
Negli esempi di questo capitolo, il parametro count e l'espressione for_each vengono attivamente utilizzati in cicli e logica condizionale. Funzionano bene, ma hanno due importanti limitazioni delle quali è necessario essere a conoscenza.
- Né count né for_each possono fare riferimento a qualsiasi variabile di uscita della risorsa.
- count e for_each non possono essere utilizzati nella configurazione del modulo.
Né count né for_each possono fare riferimento a qualsiasi variabile di uscita della risorsa
Immagina di dover distribuire più server EC2 e per qualche motivo non vuoi usare ASG. Il tuo codice potrebbe essere così:
resource "aws_instance" "example_1" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}
Esaminiamoli uno ad uno.
Poiché al parametro count è stato assegnato un valore statico, questo codice funzionerà senza problemi: quando eseguirai il comando apply, creerà tre server EC2. Ma se desideri distribuire un server in ciascuna zona di disponibilità (Availability Zone o AZ) nel current regione AWS? Puoi fare in modo che il tuo codice carichi l'elenco delle zone da una fonte dati aws_availability_zones e poi scorra 'cyclicamente' ognuna di esse per creare un server EC2 in essa, utilizzando il parametro count e l'accesso all'array per indice:
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à perfettamente, poiché il parametro count può fare riferimento senza problemi alle fonti dati. Ma cosa succede se il numero di server da creare dipende dall'uscita di una risorsa? Per dimostrarlo, è più semplice prendere la risorsa random_integer, che, come si può intuire dal nome, restituisce un numero intero casuale:
resource "random_integer" "num_instances" {
min = 1
max = 3
}Questo codice genera un numero casuale da 1 a 3. Vediamo cosa succede se proviamo a utilizzare l'output result 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 questo codice terraform plan, si otterrà 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
Il valore "count" dipende dagli attributi delle risorse che non possono essere determinati fino all'applicazione, quindi Terraform non può prevedere quante istanze verranno create. Per aggirare questo problema, usa l'argomento -target per applicare prima solo le risorse da cui dipende il count.Terraform richiede che count e for_each vengano calcolati nella fase di pianificazione, prima della creazione o modifica di qualsiasi risorsa. Ciò significa che count e for_each possono riferirsi a letterali, variabili, sorgenti dati e persino elenchi di risorse (a condizione che la loro lunghezza possa essere determinata durante la pianificazione), ma non a variabili di output calcolate della risorsa.
count e for_each non possono essere utilizzati nella configurazione del modulo
Potrebbe sorgere la tentazione di aggiungere il parametro count nelle configurazioni del 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 cerca di utilizzare count all'interno di un modulo per creare tre copie della risorsa webserver-cluster. O forse vuoi rendere l'inclusione del modulo opzionale a seconda di qualche condizione booleana, assegnando al suo parametro count il valore 0. Un codice del genere potrebbe sembrare ragionevole, tuttavia, in seguito all'esecuzione di terraform plan si otterrà il seguente errore:
Error: Reserved argument name in module block
on main.tf line 13, in module "count_example":
13: count = 3
Il nome "count" è riservato per un utilizzo in una futura versione di Terraform.Sfortunatamente, al momento del rilascio di Terraform 0.12.6, l'uso di count o for_each nella risorsa modulo non è supportato. Secondo le note di rilascio di Terraform 0.12 (http:\/\/bit.ly\/3257bv4) HashiCorp prevede di aggiungere questa funzionalità in futuro, quindi, a seconda di quando leggi questo libro, potrebbe già essere disponibile. Per saperne di più, .
Limitazioni delle distribuzioni senza tempo di inattività
L'uso del blocco create_before_destroy insieme ad ASG è una soluzione eccellente per organizzare distribuzioni senza tempi di inattività, a meno che non si consideri un piccolo dettaglio: le regole di autoscaling non sono supportate. Oppure, per essere più precisi, questo ripristina la dimensione dell'ASG a min_size ad ogni distribuzione, il che può diventare problematico se si sono utilizzate regole di autoscaling per aumentare il numero di server attivi.
Ad esempio, il modulo webserver-cluster contiene un paio di risorse aws_autoscaling_schedule, che alle 9 del mattino aumentano il numero di server nel cluster da due a dieci. Se viene eseguita una distribuzione, diciamo, alle 11 del mattino, il nuovo gruppo ASG si caricherà non con dieci, ma solo con due server e rimarrà in questo stato fino alle 9 del giorno successivo.
Questa limitazione può essere aggirata in vari modi.
- Modificare il parametro recurrence in aws_autoscaling_schedule da 0 9 * * * («eseguire alle 9 del mattino») a qualcosa come 0-59 9-17 * * * («eseguire ogni minuto dalle 9 del mattino alle 5 del pomeriggio»). Se nell'ASG ci sono già dieci server, rieseguire questa regola di autoscaling non cambierà nulla, il che è esattamente ciò che ci serve. Ma se il gruppo ASG è stato distribuito di recente, questa regola garantirà che, al massimo dopo un minuto, il numero dei suoi server raggiunga dieci. Questo non è esattamente un approccio elegante, e i grandi salti da dieci a due server e viceversa possono anche causare problemi agli utenti.
- Creare uno script personalizzato che utilizza l'API AWS per determinare il numero di server attivi nell'ASG, chiamarlo tramite una fonte di dati esterna (vedi la sezione «Fonte di dati esterna» a pagina 249) e assegnare al parametro desired_capacity del gruppo ASG il valore restituito da questo script. In questo modo, ogni nuova istanza dell'ASG sarà sempre avviata con la stessa capacità, il che complica la manutenzione del nostro codice Terraform.
Certo, in ideale sarebbe presente nel Terraform il supporto integrato per distribuzioni senza tempi di inattività, ma a maggio 2019 il team di HashiCorp non prevedeva di aggiungere questa funzionalità ().
Un piano corretto potrebbe non essere implementato con successo
A volte, eseguendo il comando plan, si ottiene un piano di distribuzione del tutto coerente, tuttavia il comando apply restituisce un errore. Provate, ad esempio, ad aggiungere la risorsa aws_iam_user con lo stesso nome che avete utilizzato per l'utente IAM creato in precedenza nel capitolo 2:
resource "aws_iam_user" "existing_user" {
# Inserisci qui il nome dell'utente IAM esistente,
# per esercitarti nell'utilizzo del comando terraform import
name = "yevgeniy.brikman"
}Ora, se esegui il comando plan, Terraform produrrà un piano di distribuzione che a prima vista sembra del tutto ragionevole:
Terraform eseguirà le seguenti azioni:
# aws_iam_user.existing_user verrà creato
+ resource "aws_iam_user" "existing_user" {
+ arn = (conosciuto dopo l'applicazione)
+ force_destroy = false
+ id = (conosciuto dopo l'applicazione)
+ name = "yevgeniy.brikman"
+ path = "\/"
+ unique_id = (conosciuto dopo l'applicazione)
}
Piano: 1 da aggiungere, 0 da modificare, 0 da distruggere.Se esegui il comando apply, si verificherà il seguente errore:
Error: Errore nella creazione dell'utente IAM yevgeniy.brikman: EntityAlreadyExists:
L'utente con il nome yevgeniy.brikman esiste già.
su main.tf riga 10, nella risorsa "aws_iam_user" "existing_user":
10: resource "aws_iam_user" "existing_user" {Il problema, ovviamente, è che un utente IAM con quel nome esiste già. E questo può succedere non solo con gli utenti IAM, ma anche con quasi qualsiasi risorsa. È possibile che qualcuno abbia creato questa risorsa manualmente o tramite la riga di comando, ma, in ogni caso, la corrispondenza degli identificatori porta a conflitti. Esistono molte varianti di questo errore che spesso colgono di sorpresa i nuovi utenti di Terraform.
Il punto cruciale è che il comando terraform plan considera solo le risorse che sono indicate nel file di stato di Terraform. Se le risorse sono state create in un altro modo (ad esempio manualmente, cliccando nel pannello di controllo AWS), non verranno incluse nel file di stato e, di conseguenza, Terraform non le terrà in considerazione quando esegue il comando plan. Di conseguenza, un piano che sembra valido a prima vista si rivelerà inefficace.
Da questo si possono trarre due insegnamenti.
- Se hai già iniziato a lavorare con Terraform, non utilizzare altro. Se una parte della tua infrastruttura è gestita tramite Terraform, non è più possibile modificarla manualmente. Altrimenti, non solo rischi di ottenere strani errori da Terraform, ma annulli anche molti vantaggi dell'IaC, poiché il codice non sarà più una rappresentazione accurata della tua infrastruttura.
- Se hai già un'infrastruttura, utilizza il comando import. Se inizi a utilizzare Terraform con un'infrastruttura esistente, puoi aggiungerla al file di stato con il comando terraform import. In questo modo, Terraform saprà quale infrastruttura gestire. Il comando import accetta due argomenti. Il primo è l'indirizzo della risorsa nei tuoi file di configurazione. Qui il sintassi è la stessa delle referenze alle risorse: _. (come aws_iam_user.existing_user). Il secondo argomento è l'identificatore della risorsa da importare. Ad esempio, per la risorsa aws_iam_user, l'ID è il nome utente (per esempio, yevgeniy.brikman), mentre per la risorsa aws_instance, l'ID è l'identificatore del server EC2 (come i-190e22e5). Come importare una risorsa è generalmente indicato nella documentazione in fondo alla sua pagina.
Di seguito è riportato il comando import che permette di sincronizzare la risorsa aws_iam_user che hai aggiunto alla tua configurazione Terraform insieme all'utente IAM nel capitolo 2 (ovviamente, al posto di yevgeniy.brikman dovrai inserire il tuo nome):
$ terraform import aws_iam_user.existing_user yevgeniy.brikmanTerraform si rivolgerà all'API AWS per trovare il tuo utente IAM e creare nel file di stato un collegamento tra lui e la risorsa aws_iam_user.existing_user nella tua configurazione Terraform. Da questo momento in poi, quando esegui il comando plan, Terraform saprà che l'utente IAM esiste già e non cercherà di crearne un altro.
È importante notare che, se hai già molte risorse che desideri importare in Terraform, scrivere il codice manualmente e importare ognuna di esse singolarmente può diventare un compito noioso. Pertanto, è consigliabile considerare uno strumento come Terraforming (http://terraforming.dtan4.net/), che può importare automaticamente il codice e lo stato dal tuo account AWS.
Il refactoring può avere le sue insidie
Refactoring è una pratica comune nella programmazione in cui cambi la struttura interna del codice, mantenendo invariato il comportamento esterno. Questo è necessario per rendere il codice più chiaro, ordinato e facile da mantenere. Il refactoring è una metodologia indispensabile da applicare regolarmente. Tuttavia, quando si parla di Terraform o di qualsiasi altro strumento IaC, è fondamentale prestare molta attenzione a ciò che si intende per “comportamento esterno” di un segmento di codice, altrimenti potrebbero sorgere problemi imprevisti.
Ad esempio, un tipo comune di refactoring è la sostituzione dei nomi delle variabili o delle funzioni con nomi più chiari. Molti IDE offrono supporto integrato per il refactoring e possono rinominare automaticamente variabili e funzioni in tutto il progetto. Nei linguaggi di programmazione generali, è una procedura banale che non richiede particolare attenzione, ma in Terraform è necessario essere estremamente cauti, altrimenti si possono verificare interruzioni nel funzionamento.
Per esempio, il modulo webserver-cluster ha una variabile di ingresso cluster_name:
variable "cluster_name" { description = "Il nome da utilizzare per tutte le risorse del cluster" type = string }Immagina di aver iniziato a utilizzare questo modulo per distribuire un microservizio chiamato foo. In seguito, potresti voler rinominare il tuo servizio in bar. Questo cambiamento potrebbe sembrare banale, ma in realtà potrebbe causare interruzioni nel funzionamento.
Il fatto è che il modulo webserver-cluster utilizza la variabile cluster_name in un certo numero di risorse, inclusi i parametri name di due gruppi di sicurezza e 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 cambi il parametro name in una risorsa, Terraform eliminerà la vecchia versione di quella risorsa e ne creerà una nuova. Ma se quella risorsa è un ALB, durante il periodo tra la sua eliminazione e il caricamento della nuova versione non avrai un meccanismo per reindirizzare il traffico al tuo server web. Allo stesso modo, se viene eliminato un gruppo di sicurezza, i tuoi server inizieranno a rifiutare qualsiasi traffico di rete fino a quando non verrà creato un nuovo gruppo.
Un altro tipo di refactoring che potrebbe interessarti è la modifica dell'identificatore di Terraform. Prendiamo come esempio la risorsa aws_security_group nel modulo webserver-cluster:
resource "aws_security_group" "instance" { # (...) }L'identificatore di questa risorsa si chiama instance. Immagina che durante il refactoring tu abbia deciso di cambiarlo in un nome più chiaro (a tuo avviso) cluster_instance:
resource "aws_security_group" "cluster_instance" { # (...) }E cosa succederà alla fine? Esatto: interruzione del servizio.
Terraform associa l'ID di ogni risorsa con l'identificatore del fornitore di cloud. Ad esempio, l'iam_user è associato all'ID dell'utente IAM in AWS, mentre aws_instance è legato all'ID del server AWS EC2. Se si cambia l'identificatore della risorsa (ad esempio, da instance a cluster_instance, come nel caso di aws_security_group), per Terraform sembrerà che tu abbia rimosso la vecchia risorsa e aggiunto una nuova. Se applichi queste modifiche, Terraform eliminerà il vecchio gruppo di sicurezza e ne creerà un altro, mentre i tuoi server inizieranno a rifiutare qualsiasi traffico di rete.
Ecco quattro lezioni principali che dovresti trarre da questa discussione.
- Usa sempre il comando plan. Può rivelare tutte queste problematiche. Controlla attentamente il suo output e fai attenzione alle situazioni in cui Terraform pianifica di rimuovere risorse che probabilmente non dovrebbero essere rimosse.
- Crea prima di eliminare. Se desideri sostituire una risorsa, rifletti attentamente se è necessario creare una sostituzione prima di eliminare l'originale. Se la risposta è affermativa, può aiutarti create_before_destroy. Lo stesso risultato può essere ottenuto manualmente, eseguendo due passaggi: prima aggiungi la nuova risorsa alla configurazione e lancia il comando apply, poi rimuovi la vecchia risorsa dalla configurazione e utilizza nuovamente il comando apply.
- Cambiare gli identificatori richiede la modifica dello stato. Se desideri cambiare l'identificatore associato a una risorsa (ad esempio, rinominare aws_security_group da instance a cluster_instance), evitando così di eliminare la risorsa e creare la sua nuova versione, è necessario aggiornare di conseguenza il file di stato di Terraform. Non farlo mai manualmente: usa invece il comando terraform state. Quando rinomini gli identificatori, esegui il comando terraform state mv, che ha la seguente sintassi:
terraform state mvORIGINAL_REFERENCE è un'espressione che si riferisce alla risorsa nella sua forma attuale, mentre NEW_REFERENCE è il luogo in cui desideri spostarla. Ad esempio, rinominando il gruppo aws_security_group da instance a cluster_instance, devi eseguire il seguente comando:
$ terraform state mv aws_security_group.instance aws_security_group.cluster_instanceCosì comunicherai a Terraform che lo stato, che in precedenza riguardava aws_security_group.instance, ora deve essere associato a aws_security_group.cluster_instance. Se dopo aver rinominato e lanciato questo comando terraform plan non mostra alcuna modifica, significa che hai fatto tutto correttamente.
- Alcuni parametri non possono essere modificati. I parametri di molte risorse sono immutabili. Se provi a modificarli, Terraform eliminerà la vecchia risorsa e ne creerà una nuova al suo posto. Sulla pagina di ciascuna risorsa è solitamente indicato cosa accade quando si modifica un certo parametro, quindi non dimenticare di controllare la documentazione. Usa sempre il comando plan e considera la convenienza di applicare la strategia create_before_destroy.
La coerenza ritardata si allinea… con il ritardo
Le API di alcuni fornitori cloud, come AWS, sono asincrone e hanno coerenza ritardata. L'asincronia significa che l'interfaccia può restituire immediatamente una risposta, senza attendere il completamento dell'azione richiesta. La coerenza ritardata implica che potrebbero essere necessarie alcune tempistiche per propagare le modifiche attraverso tutto il sistema; durante questo processo, le tue risposte potrebbero non essere coerenti e dipendere da quale replica della fonte dei dati risponde alle tue chiamate API.
Immagina, ad esempio, di effettuare una chiamata API ad AWS per creare un server EC2. L'API restituirà una risposta "riuscita" (201 Created) praticamente immediatamente, senza attendere la creazione effettiva del server. Se provi immediatamente a connetterti, è molto probabile che non funzioni, poiché in quel momento AWS sta ancora inizializzando le risorse o, in alternativa, il server non si è ancora avviato. Inoltre, se effettui un'altra richiesta per ottenere informazioni su quel server, potresti ricevere un errore (404 Not Found). Infatti, le informazioni su quel server EC2 potrebbero ancora dover essere propagate in AWS, e potrebbe essere necessario attendere alcuni secondi affinché diventino disponibili ovunque.
Ogni volta che utilizzi un'API asincrona con coerenza ritardata, devi ripetere periodicamente la tua richiesta finché l'azione non si completa e non si propaga nel sistema. Purtroppo, l'AWS SDK non offre strumenti adeguati per questo, e il progetto Terraform ha sofferto in passato di numerosi bug come il 6813 (https://github.com/hashicorp/terraform/issues/6813):
$ terraform apply aws_subnet.private-persistence.2: InvalidSubnetID.NotFound: L'ID della subnet 'subnet-xxxxxxx' non esisteIn altre parole, stai creando una risorsa (ad esempio, una subnet) e poi stai cercando di ottenere alcune informazioni su di essa (come l'ID della subnet appena creata), ma Terraform non riesce a trovarle. La maggior parte di questi errori (compreso il 6813) è già stata corretta, ma di tanto in tanto si presentano ancora, soprattutto quando viene aggiunto a Terraform il supporto per un nuovo tipo di risorsa. È frustrante, ma nella maggior parte dei casi non comporta alcun danno. Ripetendo l'esecuzione di terraform apply dovrebbe funzionare, poiché a quel punto le informazioni saranno già state diffuse nel sistema.
Questo brano è estratto dal libro di Evgenij Brikman .
Fonte: habr.com
