Pułapki terraformowe

Pułapki terraformowe
Zwróćmy uwagę na kilka pułapek, w tym związanych z pętlami, instrukcjami if i technikami wdrażania, a także bardziej ogólnymi problemami, które ogólnie wpływają na Terraform:

  • parametry count i for_each mają ograniczenia;
  • ograniczyć wdrożenia bez przestojów;
  • nawet dobry plan może zawieść;
  • refaktoryzacja może mieć swoje pułapki;
  • spójność odroczenia jest zgodna... z odroczeniem.

Parametry count i for_each mają ograniczenia

W przykładach w tym rozdziale szeroko wykorzystuje się parametr count i wyrażenie for_each w pętlach i logice warunkowej. Działają dobrze, ale mają dwa ważne ograniczenia, o których musisz wiedzieć.

  • Count i for_each nie mogą odwoływać się do żadnych zmiennych wyjściowych zasobów.
  • count i for_each nie mogą być użyte w konfiguracji modułu.

count i for_each nie mogą odwoływać się do żadnych zmiennych wyjściowych zasobów

Wyobraź sobie, że musisz wdrożyć kilka serwerów EC2 i z jakiegoś powodu nie chcesz używać ASG. Twój kod mógłby wyglądać następująco:

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

Przyjrzyjmy się im jeden po drugim.

Ponieważ parametr count jest ustawiony na wartość statyczną, ten kod będzie działał bez problemów: po uruchomieniu polecenia Apply utworzy trzy serwery EC2. Ale co, jeśli chcesz wdrożyć jeden serwer w każdej strefie dostępności (AZ) w bieżącym regionie AWS? Możesz poprosić swój kod o załadowanie listy stref ze źródła danych aws_availability_zones, a następnie przejście przez każdą z nich i utworzenie w nim serwera EC2, używając parametru count i dostępu do indeksu tablicy:

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

Ten kod również będzie działał poprawnie, ponieważ parametr count może bez problemu odwoływać się do źródeł danych. Ale co się stanie, jeśli liczba serwerów, które należy utworzyć, zależy od wydajności jakiegoś zasobu? Aby to zademonstrować, najłatwiej jest użyć zasobu random_integer, który, jak sama nazwa wskazuje, zwraca losową liczbę całkowitą:

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

Ten kod generuje losową liczbę z zakresu od 1 do 3. Zobaczmy, co się stanie, jeśli spróbujemy użyć danych wyjściowych tego zasobu w parametrze count zasobu aws_instance:

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

Jeśli uruchomisz plan terraform na tym kodzie, pojawi się następujący błąd:

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 wymaga obliczenia liczby i for_each na etapie planowania, przed utworzeniem lub modyfikacją jakichkolwiek zasobów. Oznacza to, że count i for_each mogą odnosić się do literałów, zmiennych, źródeł danych, a nawet list zasobów (o ile ich długość można określić w momencie planowania), ale nie do obliczonych zmiennych wyjściowych zasobów.

count i for_each nie mogą być użyte w konfiguracji modułu

Któregoś dnia możesz ulec pokusie dodania parametru count do konfiguracji modułu:

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

     count = 3

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

Ten kod próbuje użyć licznika wewnątrz modułu w celu utworzenia trzech kopii zasobu klastra serwerów WWW. Możesz też sprawić, że podłączenie modułu będzie opcjonalne w oparciu o warunek logiczny, ustawiając jego parametr count na 0. Może to wyglądać na rozsądny kod, ale podczas uruchamiania planu terraform pojawi się ten błąd:

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.

Niestety, od wersji Terraform 0.12.6 użycie count lub for_each w zasobie modułu nie jest obsługiwane. Zgodnie z informacjami o wydaniu Terraform 0.12 (http://bit.ly/3257bv4) HashiCorp planuje dodać tę funkcję w przyszłości, więc w zależności od tego, kiedy będziesz czytać tę książkę, może ona już być dostępna. Aby się o tym przekonać, przeczytaj dziennik zmian Terraform tutaj.

Ograniczenia wdrożeń bez przestojów

Używanie bloku create_before_destroy w połączeniu z ASG to świetne rozwiązanie do tworzenia wdrożeń bez przestojów, z wyjątkiem jednego zastrzeżenia: reguły autoskalowania nie są obsługiwane. Mówiąc dokładniej, przy każdym wdrożeniu resetuje to rozmiar ASG z powrotem do min_size, co może stanowić problem, jeśli używasz reguł automatycznego skalowania w celu zwiększenia liczby działających serwerów.

Przykładowo moduł webserver-cluster zawiera parę zasobów aws_autoscaling_schedule, co o godzinie 9:11 zwiększa liczbę serwerów w klastrze z dwóch do dziesięciu. Jeśli wdrożenie zostanie przeprowadzone, powiedzmy, o godzinie 9:XNUMX, nowy ASG uruchomi się z zaledwie dwoma serwerami, a nie z dziesięcioma i pozostanie w tym stanie do XNUMX:XNUMX następnego dnia.

Ograniczenie to można obejść na kilka sposobów.

  • Zmień parametr recurrence w aws_autoscaling_schedule z 0 9 * * * („uruchom o 9:0”) na coś w rodzaju 59-9 17-9 * * * („uruchom co minutę od 5:XNUMX do XNUMX:XNUMX”). Jeśli ASG ma już dziesięć serwerów, ponowne uruchomienie tej reguły automatycznego skalowania niczego nie zmieni, a tego właśnie chcemy. Jeśli jednak ASG został wdrożony dopiero niedawno, ta zasada zapewni, że w ciągu maksymalnie minuty liczba jego serwerów osiągnie dziesięć. Nie jest to do końca eleganckie podejście, a duże skoki z dziesięciu do dwóch serwerów i z powrotem mogą również powodować problemy dla użytkowników.
  • Utwórz niestandardowy skrypt, który użyje interfejsu API AWS do określenia liczby aktywnych serwerów w ASG, wywołaj go przy użyciu zewnętrznego źródła danych (patrz „Zewnętrzne źródło danych” na stronie 249) i ustaw parametr żądanej_pojemności ASG na wartość zwróconą przez scenariusz. W ten sposób każda nowa instancja ASG będzie zawsze działać z tą samą wydajnością, co istniejący kod Terraform, co utrudni jej utrzymanie.

Oczywiście w idealnym przypadku Terraform miałby wbudowaną obsługę wdrożeń bez przestojów, ale od maja 2019 r. zespół HashiCorp nie planował dodawania tej funkcjonalności (szczegóły - tutaj).

Prawidłowy plan może nie zostać wdrożony

Czasami polecenie plan tworzy całkowicie poprawny plan wdrożenia, ale polecenie Apply zwraca błąd. Spróbuj na przykład dodać zasób aws_iam_user o tej samej nazwie, której użyłeś dla użytkownika IAM utworzonego wcześniej w rozdziale 2:

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

Teraz, jeśli uruchomisz polecenie plan, Terraform wyświetli pozornie rozsądny plan wdrożenia:

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.

Jeśli uruchomisz polecenie Apply, pojawi się następujący błąd:

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

Problem polega oczywiście na tym, że użytkownik IAM o tej nazwie już istnieje. Może się to zdarzyć nie tylko użytkownikom IAM, ale niemal każdemu zasobowi. Możliwe, że ktoś utworzył ten zasób ręcznie lub przy użyciu wiersza poleceń, ale w obu przypadkach dopasowanie identyfikatorów prowadzi do konfliktów. Istnieje wiele odmian tego błędu, które często zaskakują nowicjuszy w Terraform.

Kluczową kwestią jest to, że polecenie planu terraform uwzględnia tylko te zasoby, które są określone w pliku stanu Terraform. Jeżeli zasoby zostaną utworzone w inny sposób (np. ręcznie poprzez kliknięcie w konsoli AWS), nie znajdą się one w pliku stanu i dlatego Terraform nie uwzględni ich przy wykonywaniu polecenia planu. W rezultacie plan, który na pierwszy rzut oka wydaje się słuszny, okaże się nieudany.

Można z tego wyciągnąć dwie lekcje.

  • Jeśli zacząłeś już pracować z Terraformem, nie używaj niczego innego. Jeśli część Twojej infrastruktury jest zarządzana za pomocą Terraform, nie możesz już jej modyfikować ręcznie. W przeciwnym razie nie tylko ryzykujesz dziwnymi błędami Terraform, ale także negujesz wiele zalet IaC, ponieważ kod nie będzie już dokładną reprezentacją Twojej infrastruktury.
  • Jeśli posiadasz już infrastrukturę, użyj polecenia import. Jeśli zaczynasz używać Terraform z istniejącą infrastrukturą, możesz dodać ją do pliku stanu za pomocą polecenia importu terraform. W ten sposób Terraform będzie wiedział, jaka infrastruktura wymaga zarządzania. Polecenie importu przyjmuje dwa argumenty. Pierwszy to adres zasobu w plikach konfiguracyjnych. Składnia jest tutaj taka sama jak w przypadku linków do zasobów: _. (jak aws_iam_user.existing_user). Drugi argument to identyfikator zasobu, który ma zostać zaimportowany. Załóżmy, że identyfikator zasobu aws_iam_user to nazwa użytkownika (na przykład yevgeniy.brikman), a identyfikator zasobu aws_instance to identyfikator serwera EC2 (np. i-190e22e5). Sposób importowania zasobu jest zwykle wskazany w dokumentacji na dole jego strony.

    Poniżej znajduje się polecenie importu, które synchronizuje zasób aws_iam_user dodany do konfiguracji Terraform wraz z użytkownikiem IAM w rozdziale 2 (oczywiście zastępując twoje imię i nazwisko yevgeniy.brikman):

    $ terraform import aws_iam_user.existing_user yevgeniy.brikman

    Terraform wywoła interfejs API AWS, aby znaleźć użytkownika IAM i utworzyć powiązanie pliku stanu między nim a zasobem aws_iam_user.existing_user w konfiguracji Terraform. Od tej chwili, gdy uruchomisz polecenie plan, Terraform będzie wiedział, że użytkownik IAM już istnieje i nie będzie próbował go ponownie utworzyć.

    Warto zauważyć, że jeśli masz już dużo zasobów, które chcesz zaimportować do Terraform, ręczne pisanie kodu i importowanie każdego z osobna na raz może być kłopotliwe. Warto więc zainteresować się narzędziem takim jak Terraforming (http://terraforming.dtan4.net/), które może automatycznie importować kod i stan z Twojego konta AWS.

    Refaktoryzacja może mieć swoje pułapki

    Refaktoryzacja to powszechna praktyka w programowaniu, w której zmienia się wewnętrzną strukturę kodu, pozostawiając niezmienione zachowanie zewnętrzne. Ma to na celu uczynienie kodu bardziej przejrzystym, schludnym i łatwiejszym w utrzymaniu. Refaktoryzacja jest niezbędną techniką, z której należy regularnie korzystać. Ale jeśli chodzi o Terraform lub jakiekolwiek inne narzędzie IaC, musisz być bardzo ostrożny przy tym, co rozumiesz przez „zewnętrzne zachowanie” fragmentu kodu, w przeciwnym razie pojawią się nieoczekiwane problemy.

    Na przykład powszechnym rodzajem refaktoryzacji jest zastępowanie nazw zmiennych lub funkcji bardziej zrozumiałymi. Wiele IDE ma wbudowaną obsługę refaktoryzacji i może automatycznie zmieniać nazwy zmiennych i funkcji w całym projekcie. W językach programowania ogólnego przeznaczenia jest to trywialna procedura, o której możesz nie pomyśleć, ale w Terraform musisz zachować przy tym szczególną ostrożność, w przeciwnym razie mogą wystąpić awarie.

    Na przykład moduł klastra serwerów WWW ma zmienną wejściową nazwa_klastra:

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

    Wyobraź sobie, że zacząłeś używać tego modułu do wdrożenia mikrousługi o nazwie foo. Później chcesz zmienić nazwę swojej usługi na bar. Zmiana ta może wydawać się banalna, ale w rzeczywistości może spowodować zakłócenia w świadczeniu usług.

    Faktem jest, że moduł klastra serwerów WWW używa zmiennej nazwa_klastra w wielu zasobach, w tym w parametrze name dwóch grup zabezpieczeń 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]
    }

    Jeśli zmienisz parametr nazwy zasobu, Terraform usunie starą wersję tego zasobu i utworzy w jej miejsce nową. Jeśli jednak tym zasobem jest ALB, pomiędzy jego usunięciem a pobraniem nowej wersji nie będziesz mieć mechanizmu przekierowującego ruchu na swój serwer WWW. Podobnie, jeśli grupa zabezpieczeń zostanie usunięta, serwery zaczną odrzucać wszelki ruch sieciowy do czasu utworzenia nowej grupy.

    Innym rodzajem refaktoryzacji, który może Cię zainteresować, jest zmiana identyfikatora Terraform. Weźmy jako przykład zasób aws_security_group w module webserver-cluster:

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

    Identyfikator tego zasobu nazywany jest instancją. Wyobraź sobie, że podczas refaktoryzacji zdecydowałeś się zmienić ją na bardziej zrozumiałą (Twoim zdaniem) nazwę klaster_instancja:

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

    Co się stanie na końcu? Zgadza się: zakłócenie.

    Terraform kojarzy każdy identyfikator zasobu z identyfikatorem dostawcy chmury. Na przykład iam_user jest powiązany z identyfikatorem użytkownika AWS IAM, a aws_instance jest powiązany z identyfikatorem serwera AWS EC2. Jeśli zmienisz identyfikator zasobu (powiedzmy z instancji na klaster_instancja, jak ma to miejsce w przypadku aws_security_group), na Terraform, będzie to wyglądało tak, jakbyś usunął stary zasób i dodał nowy. Jeśli zastosujesz te zmiany, Terraform usunie starą grupę zabezpieczeń i utworzy nową, a Twoje serwery zaczną odrzucać wszelki ruch sieciowy.

    Oto cztery najważniejsze wnioski, które powinieneś wyciągnąć z tej dyskusji.

    • Zawsze używaj polecenia planu. Może ujawnić wszystkie te problemy. Dokładnie przejrzyj jego wyniki i zwróć uwagę na sytuacje, w których Terraform planuje usunąć zasoby, które najprawdopodobniej nie powinny zostać usunięte.
    • Utwórz, zanim usuniesz. Jeśli chcesz zastąpić zasób, przed usunięciem oryginału dokładnie zastanów się, czy konieczne jest utworzenie zamiennika. Jeśli odpowiedź brzmi „tak”, narzędzie create_before_destroy może pomóc. Ten sam rezultat można osiągnąć ręcznie, wykonując dwa kroki: najpierw dodaj nowy zasób do konfiguracji i uruchom komendę Apply, a następnie usuń stary zasób z konfiguracji i ponownie użyj komendy Apply.
    • Zmiana identyfikatorów wymaga zmiany stanu. Jeśli chcesz zmienić identyfikator powiązany z zasobem (na przykład zmienić nazwę aws_security_group z instancji na klaster_instancja) bez usuwania zasobu i tworzenia jego nowej wersji, musisz odpowiednio zaktualizować plik stanu Terraform. Nigdy nie rób tego ręcznie — zamiast tego użyj polecenia terraform state. Zmieniając nazwę identyfikatorów, należy uruchomić komendę terraform state mv, która ma następującą składnię:
      terraform state mv <ORIGINAL_REFERENCE> <NEW_REFERENCE>

      ORIGINAL_REFERENCE to wyrażenie odnoszące się do zasobu w jego bieżącej formie, a NEW_REFERENCE to miejsce, w którym chcesz go przenieść. Na przykład, zmieniając nazwę grupy aws_security_group z instancji na klaster_instancji, należy uruchomić następującą komendę:

      $ terraform state mv 
         aws_security_group.instance 
         aws_security_group.cluster_instance

      To informuje Terraform, że stan, który był wcześniej powiązany z aws_security_group.instance, powinien teraz być powiązany z aws_security_group.cluster_instance. Jeśli po zmianie nazwy i uruchomieniu tego polecenia plan terraform nie wykazuje żadnych zmian, to wszystko zrobiłeś poprawnie.

    • Niektórych ustawień nie można zmienić. Parametry wielu zasobów są niezmienne. Jeśli spróbujesz je zmienić, Terraform usunie stary zasób i utworzy w jego miejsce nowy. Każda strona zasobów zazwyczaj wskazuje, co się stanie, gdy zmienisz określone ustawienie, dlatego pamiętaj o sprawdzeniu dokumentacji. Zawsze używaj polecenia plan i rozważ użycie strategii create_before_destroy.

    Odroczona konsystencja jest zgodna... z odroczeniem

    Interfejsy API niektórych dostawców usług w chmurze, takie jak AWS, są asynchroniczne i mają opóźnioną spójność. Asynchronia oznacza, że ​​interfejs może natychmiast zwrócić odpowiedź, bez czekania na zakończenie żądanej akcji. Opóźniona spójność oznacza, że ​​rozprzestrzenienie się zmian w całym systemie może zająć trochę czasu; w takim przypadku Twoje odpowiedzi mogą być niespójne i zależne od tego, która replika źródła danych odpowiada na wywołania API.

    Wyobraź sobie na przykład, że wykonujesz wywołanie API do AWS z prośbą o utworzenie serwera EC2. API zwróci odpowiedź „pomyślną” (201 utworzono) niemal natychmiast, bez czekania na utworzenie samego serwera. Jeśli spróbujesz połączyć się z nim od razu, prawie na pewno się to nie powiedzie, ponieważ w tym momencie AWS nadal inicjuje zasoby lub alternatywnie serwer jeszcze się nie uruchomił. Co więcej, jeśli wykonasz kolejne połączenie w celu uzyskania informacji o tym serwerze, może pojawić się błąd (404 Not Found). Rzecz w tym, że informacja o tym serwerze EC2 może jeszcze rozprzestrzenić się po całym AWS zanim będzie dostępna wszędzie, trzeba będzie poczekać kilka sekund.

    Za każdym razem, gdy używasz asynchronicznego interfejsu API z leniwą spójnością, musisz okresowo ponawiać żądanie, aż działanie zostanie zakończone i rozprzestrzenione w systemie. Niestety, pakiet AWS SDK nie zapewnia do tego żadnych dobrych narzędzi, a projekt Terraform cierpiał z powodu wielu błędów, takich jak 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

    Innymi słowy, tworzysz zasób (np. podsieć), a następnie próbujesz uzyskać o nim pewne informacje (np. identyfikator nowo utworzonej podsieci), ale Terraform nie może go znaleźć. Większość z tych błędów (w tym 6813) została naprawiona, ale nadal pojawiają się od czasu do czasu, zwłaszcza gdy Terraform dodaje obsługę nowego typu zasobów. Jest to denerwujące, ale w większości przypadków nie powoduje żadnych szkód. Po ponownym uruchomieniu terraform Apply wszystko powinno działać, ponieważ do tego czasu informacje będą już rozsiane po całym systemie.

    Ten fragment pochodzi z książki Evgeniya Brikmana „Terraform: infrastruktura na poziomie kodu”.

Źródło: www.habr.com

Dodaj komentarz