The pitfalls of Terraform

The pitfalls of Terraform
Laten we een aantal valkuilen bespreken, inclusief de zaken die verband houden met lussen, if-uitspraken en implementatiemethoden, evenals meer algemene problemen die betrekking hebben op Terraform in het algemeen:

  • de parameters count en for_each hebben beperkingen;
  • beperkingen van implementaties met nul downtime;
  • zelfs een goed plan kan mislukken;
  • herstructurering kan zijn eigen valkuilen hebben;
  • uitgestelde consistentie gaat... samen met uitstel.

De parameters count en for_each hebben beperkingen

In de voorbeelden van dit hoofdstuk worden de parameter count en de for_each-uitdrukking actief gebruikt in lussen en voorwaardelijke logica. Ze presteren goed, maar er zijn twee belangrijke beperkingen waar je van op de hoogte moet zijn.

  • In count en for_each kunnen geen uitvoervariabelen van de resource worden aangeroepen.
  • count en for_each kunnen niet in de configuratie van een module worden gebruikt.

In count en for_each kunnen geen uitvoervariabelen van de resource worden aangeroepen.

Stel je voor dat je meerdere EC2-servers moet implementeren en om een of andere reden wil je geen ASG gebruiken. Je code kan er als volgt uitzien:

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

Laten we ze een voor een bekijken.

Omdat de parameter count een statische waarde heeft, zal deze code probleemloos werken: wanneer je de apply-opdracht uitvoert, worden er drie EC2-servers aangemaakt. Maar wat als je één server in elke beschikbaarheidszone (Availability Zone of AZ) binnen de huidige AWS-regio wilt implementeren? Je kunt ervoor zorgen dat je code de lijst met zones laadt uit de datasource aws_availability_zones en vervolgens 'cyclic' door elk van hen heen gaat en daar een EC2-server aanmaakt, met behulp van de parameter count en toegang tot de array via de index:

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

Deze code zal ook prima werken, omdat de parameter count probleemloos kan verwijzen naar datasources. Maar wat gebeurt er als het aantal servers dat je moet maken afhankelijk is van de uitvoer van een bepaalde resource? Om dit te demonstreren, is het het eenvoudigst om de resource random_integer te nemen, die, zoals je aan de naam kunt raden, een willekeurig geheel getal retourneert:

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

Deze code genereert een willekeurig getal van 1 tot 3. Laten we eens kijken wat er gebeurt als we proberen de uitvoer result van deze bron te gebruiken in de parameter count van de bron aws_instance:

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

Als je deze code terraform plan uitvoert, krijg je de volgende foutmelding:

Error: Ongeldig count-argument

   op main.tf regel 30, in resource "aws_instance" "example_3":
   30: count = random_integer.num_instances.result

De waarde "count" is afhankelijk van bronnen attributen die pas bij de toepassing kunnen worden bepaald, dus Terraform kan niet voorspellen hoeveel instanties er zullen worden aangemaakt. Gebruik in plaats daarvan het -target argument om eerst alleen de bronnen toe te passen waar count van afhankelijk is.

Terraform vereist dat count en for_each worden berekend in de planningsfase, voordat er bronnen worden aangemaakt of gewijzigd. Dit betekent dat count en for_each kunnen verwijzen naar literalen, variabelen, gegevensbronnen en zelfs lijsten met bronnen (op voorwaarde dat hun lengte in de planningsfase kan worden bepaald), maar niet naar berekende uitvoer variabelen van bronnen.

count en for_each kunnen niet worden gebruikt in de moduleconfiguratie

Je kunt ooit de verleiding voelen om de parameter count toe te voegen aan moduleconfiguraties:

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

     count = 3

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

Deze code probeert count binnen de module te gebruiken om drie kopieën van de bron webserver-cluster aan te maken. Of misschien wil je de aansluiting van de module optioneel maken, afhankelijk van een of andere booleaanse voorwaarde, door de parameter count een waarde van 0 te geven. Zo'n code lijkt heel redelijk, maar na het uitvoeren van terraform plan krijg je de volgende foutmelding:

Error: Gereserveerde argumentnaam in moduleblok

   op main.tf regel 13, in module "count_example":
   13: count = 3

De naam "count" is gereserveerd voor gebruik in een toekomstige versie van Terraform.

Helaas wordt bij de release van Terraform 0.12.6 het gebruik van count of for_each in de modulebron niet ondersteund. Volgens de release-opmerkingen van Terraform 0.12 (http://bit.ly/3257bv4) is HashiCorp van plan deze mogelijkheid in de toekomst toe te voegen, dus afhankelijk van wanneer je dit boek leest, kan het al beschikbaar zijn. Om er zeker van te zijn, kun je hier het wijzigingslog van Terraform lezen.

Beperkingen op implementaties met nul downtime

Het gebruik van de create_before_destroy-blok in combinatie met ASG is een uitstekende oplossing voor het organiseren van deploys met nul downtime, afgezien van één nuancering: autoscaling-regels worden hierbij niet ondersteund. Of, om preciezer te zijn, dit reset de ASG-grootte terug naar min_size bij elke deploy, wat een probleem kan worden als je autoscaling-regels hebt gebruikt om het aantal actieve servers te vergroten.

Bijvoorbeeld, de webserver-clustermodule bevat een paar aws_autoscaling_schedule-resources die om 9 uur het aantal servers in de cluster van twee naar tien verhogen. Als je een deploy zou uitvoeren, laten we zeggen om 11 uur, zal de nieuwe ASG-groep niet met tien, maar met slechts twee servers opstarten en in deze staat blijven tot 9 uur de volgende dag.

Deze beperking kan op verschillende manieren worden omzeild.

  • Verander de recurrence-parameter in aws_autoscaling_schedule van 0 9 * * * ('start om 9 uur') naar iets als 0-59 9-17 * * * ('start elke minuut van 9 uur tot 17 uur'). Als er al tien servers in de ASG zijn, zal het opnieuw uitvoeren van deze autoscaling-regel niets veranderen, wat we willen. Maar als de ASG-groep zeer recent is uitgerold, garandeert deze regel dat binnen één minuut het aantal servers zal toenemen tot tien. Dit is niet de meest elegante aanpak, en grote sprongen van tien naar twee servers en weer terug kunnen ook problemen veroorzaken voor gebruikers.
  • Maak een aangepaste script dat de AWS-API gebruikt om het aantal actieve servers in de ASG te bepalen, roep dit aan met behulp van een externe gegevensbron (zie 'Externe gegevensbron' op pagina 249) en wijs de waarde die door dit script is teruggegeven toe aan de desired_capacity-parameter van de ASG-groep. Op deze manier zal elke nieuwe instantie van de ASG altijd worden opgestart met dezelfde capaciteit, wat het onderhoud van de bestaande Terraform-code lastiger maakt.

Natuurlijk, ideaal gezien zou Terraform ingebouwde ondersteuning moeten hebben voor deploys met nul downtime, maar op het moment van mei 2019 had het team van HashiCorp niet van plan om deze functionaliteit toe te voegen (meer details - hier).

Een correcte planning kan mislukt worden uitgevoerd

Soms bij het uitvoeren van de plan-opdracht krijg je een redelijk goed implementatieplan, maar de apply-opdracht geeft een foutmelding. Probeer bijvoorbeeld een aws_iam_user-resource toe te voegen met dezelfde naam die je eerder hebt gebruikt voor de IAM-gebruiker die je in hoofdstuk 2 hebt aangemaakt.

resource "aws_iam_user" "existing_user" {
   # Vul hier de naam in van een bestaande IAM-gebruiker,
   # om te oefenen met de terraform import-opdracht
   name = "yevgeniy.brikman"
}

Nu, als je de plan-opdracht uitvoert, zal Terraform ogenschijnlijk een redelijk implementatieplan tonen:

Terraform zal de volgende acties uitvoeren:

   # aws_iam_user.existing_user zal worden aangemaakt
   + resource "aws_iam_user" "existing_user" {
         + arn                  = (bekend na apply)
         + force_destroy   = false
         + id                    = (bekend na apply)
         + name               = "yevgeniy.brikman"
         + path                 = "\/"
         + unique_id         = (bekend na apply)
      }

Plan: 1 toe te voegen, 0 te wijzigen, 0 te vernietigen.

Als je de apply-opdracht uitvoert, krijg je de volgende foutmelding:

Error: Error creating IAM User yevgeniy.brikman: EntityAlreadyExists:
User with name yevgeniy.brikman already exists.

   op main.tf regel 10, in resource "aws_iam_user" "existing_user":
   10: resource "aws_iam_user" "existing_user" {

Het probleem is natuurlijk dat er al een IAM-gebruiker met die naam bestaat. Dit kan niet alleen met IAM-gebruikers gebeuren, maar met vrijwel elke resource. Iemand kan deze resource handmatig of via de commandoregel hebben aangemaakt, maar hoe het ook zij, het samenvallen van ID's leidt tot conflicten. Deze foutmelding kent een aantal varianten die vaak nieuwkomers in Terraform verrassen.

Het belangrijkste punt is dat de terraform plan-opdracht alleen rekening houdt met de resources die in het Terraform-statusbestand zijn opgegeven. Als resources op een andere manier zijn aangemaakt (bijvoorbeeld handmatig of door op een knop in de AWS-console te klikken), zullen zij niet in het statusbestand komen en zal Terraform ze dus niet meenemen bij het uitvoeren van de plan-opdracht. Uiteindelijk zal een plan dat op het eerste gezicht correct lijkt, mislukken.

Hieruit kunnen twee lessen worden getrokken.

  • Als je al met Terraform bent begonnen, gebruik dan niets anders. Als een deel van je infrastructuur met Terraform wordt beheerd, mag je deze niet meer handmatig wijzigen. Anders loop je niet alleen het risico op vreemde Terraform-fouten, maar ondermijn je ook veel voordelen van IaC, omdat de code geen nauwkeurige weergave meer zal zijn van je infrastructuur.
  • Als je al enige infrastructuur hebt, gebruik dan de import-opdracht. Als je Terraform gaat gebruiken met al bestaande infrastructuur, kan deze aan het statusbestand worden toegevoegd met de opdracht terraform import. Zo weet Terraform welke infrastructuur moet worden beheerd. De import-opdracht accepteert twee argumenten. De eerste is het adres van de bron in je configuratiebestanden. Hier geldt dezelfde syntaxis als in verwijzingen naar bronnen: _. (zoals aws_iam_user.existing_user). Het tweede argument is de identificatie van de bron die je wilt importeren. Laten we zeggen dat de ID van de bron aws_iam_user de gebruikersnaam is (bijvoorbeeld yevgeniy.brikman), en de ID van de bron aws_instance de identificatie van de EC2-server is (zoals i-190e22e5). Hoe je een bron importeert, wordt meestal in de documentatie onderaan de pagina vermeld.

    Hieronder staat de import-opdracht die de synchronisatie van de aws_iam_user-bron mogelijk maakt, die je aan je Terraform-configuratie hebt toegevoegd samen met de IAM-gebruiker in hoofdstuk 2 (natuurlijk moet je in plaats van yevgeniy.brikman je eigen naam invullen):

    $ terraform import aws_iam_user.existing_user yevgeniy.brikman

    Terraform zal de AWS API raadplegen om je IAM-gebruiker te vinden en een relatie te creëren tussen hem en de aws_iam_user.existing_user-bron in je Terraform-configuratie in het statusbestand. Vanaf dat moment zal Terraform weten dat de IAM-gebruiker al bestaat en zal hij niet proberen deze opnieuw te creëren wanneer je de plan-opdracht uitvoert.

    Het is belangrijk op te merken dat als je al veel bronnen hebt die je in Terraform wilt importeren, het handmatig schrijven van code en het importeren van elk individueel item een tijdrovende klus kan zijn. Daarom is het raadzaam om te kijken naar een tool zoals Terraforming (http://terraforming.dtan4.net/), die automatisch code en status kan importeren vanuit een AWS-account.

    Refactoring kan zijn eigen valkuilen hebben.

    Refactoring is een veelvoorkomende praktijk in de programmering waarbij je de interne structuur van de code verandert zonder het externe gedrag te wijzigen. Dit is nodig om de code begrijpelijker, netter en onderhoudsvriendelijker te maken. Refactoring is een onmisbare techniek die regelmatig moet worden toegepast. Maar als het gaat om Terraform of andere IaC-tools, moet je uiterst voorzichtig omgaan met wat wordt bedoeld met 'extern gedrag' van de code, anders kunnen zich onvoorziene problemen voordoen.

    Bijvoorbeeld, een veelvoorkomend type refactoring is het vervangen van variabelen- of functienamen door meer begrijpelijke namen. Veel IDE's hebben ingebouwde ondersteuning voor refactoring en kunnen automatisch variabelen en functies in het hele project hernoemen. In algemene programmeertalen is dit een triviale procedure, waar je niet bij stil hoeft te staan, maar in Terraform moet je hier uiterst voorzichtig mee zijn, anders kun je verstoringen in de werking tegenkomen.

    Bijvoorbeeld, de module webserver-cluster heeft een invoervariabele cluster_name:

    variable "cluster_name" {
       description = "De naam die voor alle clusterresources gebruikt moet worden"
       type          = string
    }

    Stel je voor dat je deze module bent gaan gebruiken om een microservice met de naam foo uit te rollen. Later wil je je service hernoemen naar bar. Deze wijziging lijkt triviaal, maar kan in werkelijkheid leiden tot verstoringen in de werking.

    Het punt is dat de module webserver-cluster de variabele cluster_name gebruikt in een groot aantal resources, waaronder de naamparameter van twee beveiligingsgroepen en de 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]
    }

    Als je de naamparameter in een resource wijzigt, verwijdert Terraform de oude versie van die resource en maakt in plaats daarvan een nieuwe aan. Maar als die resource een ALB is, heb je gedurende de periode tussen de verwijdering en het laden van de nieuwe versie geen mechanisme om verkeer naar je webserver te leiden. Evenzo, als een beveiligingsgroep wordt verwijderd, zullen je servers al het netwerkverkeer afwijzen totdat er een nieuwe groep is aangemaakt.

    Een ander type refactoring dat je mogelijk interesseert, is het wijzigen van de Terraform-id. Laten we als voorbeeld de resource aws_security_group in de module webserver-cluster nemen:

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

    De id van deze resource wordt instance genoemd. Stel je voor dat je tijdens het refactoren besluit deze te wijzigen in een meer begrijpelijke naam (volgens jou) cluster_instance:

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

    Wat gebeurt er uiteindelijk? Juist: een verstoring in de werking.

    Terraform koppelt de ID van elke bron aan de ID van de cloudprovider. Bijvoorbeeld, iam_user is gekoppeld aan de IAM-gebruikers-ID in AWS, en aws_instance is gekoppeld aan de AWS EC2 server-ID. Als je de ID van een bron wijzigt (bijvoorbeeld van instance naar cluster_instance, zoals in het geval van aws_security_group), ziet Terraform dit als een verwijdering van de oude bron en het toevoegen van een nieuwe. Bij het toepassen van deze wijzigingen zal Terraform de oude beveiligingsgroep verwijderen en een andere aanmaken, terwijl ondertussen je servers alle netwerkverkeer zullen afwijzen.

    Hier zijn vier belangrijke lessen die je uit deze discussie kunt halen.

    • Gebruik altijd de commando plan. Hiermee kun je al deze problemen identificeren. Bekijk de uitvoer zorgvuldig en let op situaties waarin Terraform van plan is om bronnen te verwijderen die waarschijnlijk niet verwijderd zouden moeten worden.
    • Creëer voordat je verwijdert. Als je een bron wilt vervangen, denk goed na of je een vervanging moet creëren voordat je de originele verwijdert. Als het antwoord ja is, kan create_before_destroy hierbij helpen. Je kunt hetzelfde resultaat handmatig bereiken door in twee stappen te werken: voeg eerst de nieuwe bron toe aan de configuratie en voer de commando apply uit, en verwijder vervolgens de oude bron uit de configuratie en gebruik opnieuw de commando apply.
    • Het wijzigen van identificaties vereist een wijziging van de status. Als je de ID wilt wijzigen die aan een bron is gekoppeld (bijvoorbeeld het hernoemen van aws_security_group van instance naar cluster_instance), zonder de bron te verwijderen en een nieuwe versie aan te maken, moet je het Terraform-statusbestand dienovereenkomstig bijwerken. Doe dit nooit handmatig - gebruik in plaats daarvan de commando terraform state. Bij het hernoemen van identificaties moet je de commando terraform state mv uitvoeren, die de volgende syntaxis heeft:
      terraform state mv

      ORIGINAL_REFERENCE is de verwijzing naar de bron in zijn huidige vorm, en NEW_REFERENCE is de plaats waar je het wilt verplaatsen. Bijvoorbeeld, bij het hernoemen van de groep aws_security_group van instance naar cluster_instance, moet je de volgende commando uitvoeren:

      $ terraform state mv 
         aws_security_group.instance 
         aws_security_group.cluster_instance

      Zo laat je Terraform weten dat de toestand die eerder betrekking had op aws_security_group.instance nu verbonden moet worden met aws_security_group.cluster_instance. Als terraform plan na het hernoemen en uitvoeren van deze opdracht geen wijzigingen aangeeft, heb je alles correct gedaan.

    • Bepaalde parameters kunnen niet worden gewijzigd. De parameters van veel resources zijn onveranderlijk. Als je probeert ze te wijzigen, zal Terraform de oude resource verwijderen en er een nieuwe voor in de plaats maken. Op de pagina van elke resource wordt doorgaans aangegeven wat er gebeurt bij het wijzigen van bepaalde parameters, dus vergeet niet om de documentatie te raadplegen. Gebruik altijd de plan-opdracht en overweeg de haalbaarheid van de strategie 'create_before_destroy'.

    Uitgestelde consistentie stemt overeen... met uitstel

    De API van sommige cloudproviders, zoals AWS, is asynchroon en heeft uitgestelde consistentie. Asynchroniciteit betekent dat de interface onmiddellijk een antwoord kan teruggeven zonder te wachten op de voltooiing van de gevraagde actie. Uitgestelde consistentie betekent dat het enige tijd kan duren voordat wijzigingen door het hele systeem zijn verspreid; terwijl dit gebeurt, kunnen je antwoorden inconsistent zijn en afhankelijk zijn van welke replica van de gegevensbron reageert op je API-verzoeken.

    Stel je voor dat je bijvoorbeeld een API-aanroep doet naar AWS met het verzoek om een EC2-server te maken. De API geeft vrijwel onmiddellijk een 'succesvol' antwoord (201 Created) terug, zonder te wachten op het daadwerkelijke maken van de server. Als je meteen probeert verbinding te maken, lukt het vrijwel zeker niet, omdat AWS op dat moment nog steeds de resources initialiseert of, als alternatief, de server nog niet is opgestart. Bovendien kan het zijn dat je een fout (404 Not Found) ontvangt als je een andere aanroep doet om informatie over deze server op te halen. Het punt is dat de gegevens over deze EC2-server nog steeds door AWS kunnen worden verspreid; het kan enkele seconden duren voordat ze overal beschikbaar zijn.

    Bij elk gebruik van een asynchrone API met uitgestelde consistentie moet je periodiek je verzoek herhalen totdat de actie is voltooid en zich door het systeem heeft verspreid. Helaas biedt de AWS SDK hiervoor geen goede tools, en het Terraform-project heeft eerder te maken gehad met tal van fouten zoals 6813 (https://github.com/hashicorp/terraform/issues/6813):

    $ terraform apply
    aws_subnet.private-persistence.2: InvalidSubnetID.NotFound:
    Het subnet-ID 'subnet-xxxxxxx' bestaat niet.

    Met andere woorden, je creëert een resource (zoals een subnet) en probeert vervolgens informatie daarover op te vragen (zoals de ID van het net zojuist gemaakte subnet), maar Terraform kan deze niet vinden. De meeste van dergelijke fouten (inclusief 6813) zijn al verholpen, maar ze komen af en toe nog voor, vooral wanneer er ondersteuning voor een nieuw type resource aan Terraform wordt toegevoegd. Dit is vervelend, maar in de meeste gevallen zijn de gevolgen minimaal. Bij het opnieuw uitvoeren van terraform apply zou alles moeten werken, omdat op dat moment de informatie al door het systeem is verspreid.

    Dit fragment is ontleend aan het boek van Eugene Brinkman. «Terraform: infrastructuur als code».

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster