Capcane Terraform

Capcane Terraform
Să evidențiem câteva capcane, inclusiv cele legate de bucle, declarații if și tehnici de implementare, precum și probleme mai generale care afectează Terraform în general:

  • parametrii count și for_each au limitări;
  • limitarea implementărilor de timpi de nefuncționare la zero;
  • chiar și un plan bun poate eșua;
  • refactorizarea poate avea capcanele sale;
  • coerența amânată este în concordanță... cu amânarea.

Parametrii count și for_each au limitări

Exemplele din acest capitol folosesc pe scară largă parametrul count și expresia for_each în bucle și logica condiționată. Au performanțe bune, dar au două limitări importante de care trebuie să fii conștient.

  • Count și for_each nu pot face referire la nicio variabilă de ieșire a resurselor.
  • count și for_each nu pot fi utilizate în configurarea modulului.

count și for_each nu pot face referire la nicio variabilă de ieșire a resurselor

Imaginați-vă că trebuie să implementați mai multe servere EC2 și, din anumite motive, nu doriți să utilizați ASG. Codul tău ar putea fi așa:

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

Să le privim unul câte unul.

Deoarece parametrul count este setat la o valoare statică, acest cod va funcționa fără probleme: atunci când rulați comanda aplica, va crea trei servere EC2. Dar ce se întâmplă dacă ați dori să implementați un server în fiecare zonă de disponibilitate (AZ) din regiunea dvs. actuală AWS? Puteți solicita codului dvs. să încarce o listă de zone din sursa de date aws_availability_zones și apoi să parcurgeți fiecare dintre ele și să creați un server EC2 în el utilizând parametrul count și accesul la indexul matricei:

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

Acest cod va funcționa bine, deoarece parametrul de numărare poate face referire la surse de date fără probleme. Dar ce se întâmplă dacă numărul de servere pe care trebuie să le creați depinde de rezultatul unei resurse? Pentru a demonstra acest lucru, cel mai simplu mod este să utilizați resursa random_integer, care, după cum sugerează și numele, returnează un număr întreg aleator:

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

Acest cod generează un număr aleator între 1 și 3. Să vedem ce se întâmplă dacă încercăm să folosim rezultatul acestei resurse în parametrul count al resursei aws_instance:

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

Dacă executați plan terraform pe acest cod, veți primi următoarea eroare:

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 necesită ca count și for_each să fie calculate în timpul fazei de planificare, înainte ca orice resurse să fie create sau modificate. Aceasta înseamnă că count și for_each se pot referi la literale, variabile, surse de date și chiar liste de resurse (atâta timp cât lungimea acestora poate fi determinată în momentul programării), dar nu și la variabilele de ieșire a resurselor calculate.

count și for_each nu pot fi utilizate în configurarea modulului

Într-o zi ați putea fi tentat să adăugați un parametru de numărare la configurația modulului dvs.:

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

     count = 3

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

Acest cod încearcă să folosească count în interiorul unui modul pentru a crea trei copii ale resursei webserver-cluster. Sau ați putea dori să faceți conectarea unui modul opțională pe baza unei condiții booleene, setând parametrul său de numărare la 0. Acesta ar putea părea ca un cod rezonabil, dar veți primi această eroare când rulați planul 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.

Din păcate, începând cu Terraform 0.12.6, utilizarea count sau for_each într-o resursă de modul nu este acceptată. Conform notelor de lansare Terraform 0.12 (http://bit.ly/3257bv4), HashiCorp intenționează să adauge această capacitate în viitor, așa că, în funcție de momentul în care citiți această carte, este posibil ca aceasta să fie deja disponibilă. Pentru a afla cu siguranță, citiți jurnalul de modificări Terraform aici.

Limitările implementărilor Zero Downtime

Utilizarea blocului create_before_destroy în combinație cu ASG este o soluție excelentă pentru a crea implementări fără timpi de nefuncționare, cu excepția unei avertismente: regulile de autoscaling nu sunt acceptate. Sau, pentru a fi mai precis, aceasta resetează dimensiunea ASG la min_size la fiecare implementare, ceea ce ar putea fi o problemă dacă ați folosi reguli de scalare automată pentru a crește numărul de servere care rulează.

De exemplu, modulul webserver-cluster conține o pereche de resurse aws_autoscaling_schedule, care la ora 9 am crește numărul de servere din cluster de la două la zece. Dacă implementați la, să zicem, ora 11 a.m., noul ASG se va porni cu doar două servere în loc de zece și va rămâne așa până la ora 9 a.m. a doua zi.

Această limitare poate fi ocolită în mai multe moduri.

  • Schimbați parametrul de recurență din aws_autoscaling_schedule de la 0 9 * * * („rulează la 9 am”) la ceva de genul 0-59 9-17 * * * („se rulează în fiecare minut de la 9 am la 5 pm”). Dacă ASG are deja zece servere, rularea acestei reguli de autoscaling din nou nu va schimba nimic, ceea ce ne dorim. Dar dacă ASG a fost implementat doar recent, această regulă va asigura că în maximum un minut numărul serverelor sale va ajunge la zece. Aceasta nu este o abordare complet elegantă, iar salturile mari de la zece la două servere și înapoi pot cauza, de asemenea, probleme utilizatorilor.
  • Creați un script personalizat care utilizează API-ul AWS pentru a determina numărul de servere active din ASG, apelați-l folosind o sursă de date externă (consultați „Sursa de date externă” la pagina 249) și setați parametrul de capacitate dorită al ASG la valoarea returnată de scenariul. În acest fel, fiecare instanță ASG nouă va rula întotdeauna la aceeași capacitate ca și codul Terraform existent și o face mai dificil de întreținut.

Desigur, Terraform ar avea în mod ideal suport încorporat pentru implementări fără timpi de nefuncționare, dar din mai 2019, echipa HashiCorp nu avea de gând să adauge această funcționalitate (detalii - aici).

Planul corect poate fi implementat fără succes

Uneori, comanda plan produce un plan de implementare perfect corect, dar comanda apply returnează o eroare. Încercați, de exemplu, să adăugați resursa aws_iam_user cu același nume pe care l-ați folosit pentru utilizatorul IAM pe care l-ați creat mai devreme în Capitolul 2:

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

Acum, dacă rulați comanda plan, Terraform va scoate un plan de implementare aparent rezonabil:

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.

Dacă rulați comanda aplica, veți primi următoarea eroare:

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

Problema, desigur, este că un utilizator IAM cu acest nume există deja. Și acest lucru se poate întâmpla nu numai utilizatorilor IAM, ci și aproape oricărei resurse. Este posibil ca cineva să creeze această resursă manual sau folosind linia de comandă, dar în orice caz, potrivirea ID-urilor duce la conflicte. Există multe variante ale acestei erori care adesea îi iau prin surprindere pe nou-veniți la Terraform.

Punctul cheie este că comanda terraform plan ia în considerare doar acele resurse care sunt specificate în fișierul de stare Terraform. Dacă resursele sunt create într-un alt mod (de exemplu, manual făcând clic în consola AWS), acestea nu vor ajunge în fișierul de stare și, prin urmare, Terraform nu le va ține cont atunci când execută comanda planului. Drept urmare, un plan care pare corect la prima vedere se va dovedi a fi nereușit.

Sunt două lecții de învățat din asta.

  • Dacă ați început deja să lucrați cu Terraform, nu utilizați altceva. Dacă o parte a infrastructurii dvs. este gestionată folosind Terraform, nu o mai puteți modifica manual. În caz contrar, nu numai că riscați erori Terraform ciudate, dar și multe dintre beneficiile IaC, deoarece codul nu va mai fi o reprezentare exactă a infrastructurii dvs.
  • Dacă aveți deja infrastructură, utilizați comanda de import. Dacă începeți să utilizați Terraform cu infrastructura existentă, îl puteți adăuga la fișierul de stare folosind comanda terraform import. Astfel Terraform va ști ce infrastructură trebuie gestionată. Comanda de import are două argumente. Prima este adresa resursei din fișierele de configurare. Sintaxa de aici este aceeași ca și pentru linkurile de resurse: _. (cum ar fi aws_iam_user.existing_user). Al doilea argument este ID-ul resursei de importat. Să presupunem că ID-ul resursei aws_iam_user este numele de utilizator (de exemplu, yevgeniy.brikman), iar ID-ul resursei aws_instance este ID-ul serverului EC2 (cum ar fi i-190e22e5). Modul de importare a unei resurse este de obicei indicat în documentația din partea de jos a paginii acesteia.

    Mai jos este o comandă de import care sincronizează resursa aws_iam_user pe care ați adăugat-o la configurația Terraform împreună cu utilizatorul IAM în Capitolul 2 (înlocuind numele dvs. cu yevgeniy.brikman, desigur):

    $ terraform import aws_iam_user.existing_user yevgeniy.brikman

    Terraform va apela API-ul AWS pentru a găsi utilizatorul dvs. IAM și pentru a crea o asociere de fișier de stare între acesta și resursa aws_iam_user.existing_user în configurația dvs. Terraform. De acum înainte, când rulați comanda plan, Terraform va ști că utilizatorul IAM există deja și nu va încerca să o creeze din nou.

    Este demn de remarcat faptul că, dacă aveți deja o mulțime de resurse pe care doriți să le importați în Terraform, scrierea manuală a codului și importarea fiecăruia pe rând poate fi o bătaie de cap. Deci, merită să căutați un instrument precum Terraforming (http://terraforming.dtan4.net/), care poate importa automat codul și starea din contul dvs. AWS.

    Refactorizarea poate avea capcanele sale

    Refactorizarea este o practică obișnuită în programare în care modificați structura internă a codului, lăsând neschimbat comportamentul extern. Acest lucru este pentru a face codul mai clar, mai ordonat și mai ușor de întreținut. Refactorizarea este o tehnică indispensabilă care ar trebui utilizată în mod regulat. Dar când vine vorba de Terraform sau de orice alt instrument IaC, trebuie să fii extrem de atent la ce vrei să spui prin „comportamentul extern” al unei bucăți de cod, altfel vor apărea probleme neașteptate.

    De exemplu, un tip comun de refactorizare este înlocuirea numelor de variabile sau funcții cu altele mai ușor de înțeles. Multe IDE-uri au suport încorporat pentru refactorizare și pot redenumi automat variabilele și funcțiile de-a lungul proiectului. În limbajele de programare de uz general, aceasta este o procedură banală la care s-ar putea să nu te gândești, dar în Terraform trebuie să fii extrem de atent cu asta, altfel s-ar putea să întâmpinați întreruperi.

    De exemplu, modulul webserver-cluster are o variabilă de intrare cluster_name:

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

    Imaginați-vă că ați început să utilizați acest modul pentru a implementa un microserviciu numit foo. Mai târziu, doriți să redenumiți serviciul în bar. Această schimbare poate părea banală, dar în realitate poate cauza întreruperi ale serviciului.

    Faptul este că modulul webserver-cluster utilizează variabila cluster_name într-un număr de resurse, inclusiv parametrul nume a două grupuri de securitate și 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]
    }

    Dacă modificați parametrul nume pe o resursă, Terraform va șterge versiunea veche a acelei resurse și va crea una nouă în locul ei. Dar dacă resursa respectivă este un ALB, între ștergerea acesteia și descărcarea unei versiuni noi, nu veți avea un mecanism de redirecționare a traficului către serverul dvs. web. De asemenea, dacă un grup de securitate este șters, serverele dvs. vor începe să respingă orice trafic de rețea până când este creat un nou grup.

    Un alt tip de refactorizare care v-ar putea interesa este schimbarea ID-ului Terraform. Să luăm ca exemplu resursa aws_security_group din modulul webserver-cluster:

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

    Identificatorul acestei resurse se numește instanță. Imaginați-vă că în timpul refactorizării ați decis să îl schimbați cu un nume mai ușor de înțeles (în opinia dvs.) cluster_instance:

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

    Ce se va întâmpla până la urmă? Așa este: o întrerupere.

    Terraform asociază fiecare ID de resursă cu ID-ul furnizorului de cloud. De exemplu, iam_user este asociat cu ID-ul de utilizator AWS IAM, iar aws_instance este asociat cu ID-ul serverului AWS EC2. Dacă modificați ID-ul resursei (să zicem de la instanță la cluster_instance, așa cum este cazul cu aws_security_group), în Terraform va apărea ca și cum ați șters resursa veche și ați adăugat una nouă. Dacă aplicați aceste modificări, Terraform va șterge vechiul grup de securitate și va crea unul nou, în timp ce serverele dvs. încep să respingă orice trafic de rețea.

    Iată patru lecții cheie pe care ar trebui să le iei din această discuție.

    • Utilizați întotdeauna comanda plan. Poate dezvălui toate aceste probleme. Examinați-i cu atenție rezultatele și acordați atenție situațiilor în care Terraform intenționează să ștergă resurse care cel mai probabil nu ar trebui șterse.
    • Creați înainte de a șterge. Dacă doriți să înlocuiți o resursă, gândiți-vă cu atenție dacă trebuie să creați o resursă înainte de a șterge originalul. Dacă răspunsul este da, create_before_destroy vă poate ajuta. Același rezultat poate fi obținut manual prin efectuarea a doi pași: mai întâi adăugați o nouă resursă la configurație și rulați comanda apply, apoi eliminați resursa veche din configurație și utilizați din nou comanda apply.
    • Modificarea identificatorilor necesită schimbarea stării. Dacă doriți să schimbați ID-ul asociat cu o resursă (de exemplu, redenumiți aws_security_group din instanță în cluster_instance) fără a șterge resursa și a crea o nouă versiune a acesteia, trebuie să actualizați fișierul de stare Terraform în consecință. Nu faceți niciodată acest lucru manual - utilizați comanda terraform state. Când redenumești identificatorii, ar trebui să rulezi comanda terraform state mv, care are următoarea sintaxă:
      terraform state mv <ORIGINAL_REFERENCE> <NEW_REFERENCE>

      ORIGINAL_REFERENCE este o expresie care se referă la resursă în forma sa actuală, iar NEW_REFERENCE este locul în care doriți să o mutați. De exemplu, când redenumești grupul aws_security_group din instanță în cluster_instance, trebuie să rulezi următoarea comandă:

      $ terraform state mv 
         aws_security_group.instance 
         aws_security_group.cluster_instance

      Aceasta îi spune Terraform că starea care a fost asociată anterior cu aws_security_group.instance ar trebui acum să fie asociată cu aws_security_group.cluster_instance. Dacă după redenumirea și rularea acestei comenzi planul terraform nu arată nicio modificare, atunci ați făcut totul corect.

    • Unele setări nu pot fi modificate. Parametrii multor resurse sunt neschimbabili. Dacă încercați să le schimbați, Terraform va șterge resursa veche și va crea una nouă în locul ei. Fiecare pagină de resurse va indica de obicei ce se întâmplă atunci când modificați o anumită setare, așa că asigurați-vă că verificați documentația. Utilizați întotdeauna comanda plan și luați în considerare utilizarea strategiei create_before_destroy.

    Consecvența amânată este în concordanță... cu amânarea

    API-urile unor furnizori de cloud, cum ar fi AWS, sunt asincrone și au o consistență întârziată. Asincronia înseamnă că interfața poate returna imediat un răspuns fără a aștepta finalizarea acțiunii solicitate. Consecvența întârziată înseamnă că modificările pot dura timp pentru a se propaga în sistem; în timp ce acest lucru se întâmplă, răspunsurile dvs. pot fi inconsecvente și pot depinde de replica sursei de date care răspunde la apelurile dvs. API.

    Imaginați-vă, de exemplu, că efectuați un apel API către AWS, cerându-i să creeze un server EC2. API-ul va returna un răspuns „de succes” (201 Created) aproape instantaneu, fără a aștepta ca serverul însuși să fie creat. Dacă încercați să vă conectați la acesta imediat, aproape sigur va eșua, deoarece în acel moment AWS încă inițializează resurse sau, alternativ, serverul nu a pornit încă. Mai mult, dacă efectuați un alt apel pentru a obține informații despre acest server, este posibil să primiți o eroare (404 Not Found). Chestia este că informațiile despre acest server EC2 pot fi în continuare propagate în AWS înainte de a deveni disponibile peste tot, va trebui să așteptați câteva secunde.

    Ori de câte ori utilizați un API asincron cu consistență leneșă, trebuie să reîncercați periodic solicitarea până când acțiunea se finalizează și se propagă prin sistem. Din păcate, SDK-ul AWS nu oferă instrumente bune pentru aceasta, iar proiectul Terraform suferea de multe erori precum 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

    Cu alte cuvinte, creați o resursă (cum ar fi o subrețea) și apoi încercați să obțineți câteva informații despre aceasta (cum ar fi ID-ul subrețelei nou create), iar Terraform nu o poate găsi. Cele mai multe dintre aceste erori (inclusiv 6813) au fost remediate, dar încă apar din când în când, mai ales când Terraform adaugă suport pentru un nou tip de resursă. Acest lucru este enervant, dar în cele mai multe cazuri nu provoacă niciun rău. Când executați din nou terraform apply, totul ar trebui să funcționeze, deoarece până în acest moment informațiile se vor fi răspândit deja în întregul sistem.

    Acest fragment este prezentat din cartea lui Evgeniy Brikman „Terraform: infrastructură la nivel de cod”.

Sursa: www.habr.com

Adauga un comentariu