Pułapki Terraform

Pułapki Terraform
Podkreślimy kilka pułapek, w tym te związane z cyklami, wyrażeniami if oraz metodami wdrażania, a także bardziej ogólnymi problemami dotyczącymi Terraform w całości:

  • parametry count i for_each mają ograniczenia;
  • ograniczenia wdrożeń z zerowym czasem przestoju;
  • nawet dobry plan może okazać się nieudany;
  • refaktoryzacja może mieć swoje pułapki;
  • opóźniona spójność współczesnych... z opóźnieniem.

Parametry count i for_each mają ograniczenia

W przykładach tej części, parametr count oraz wyrażenie for_each są aktywnie stosowane w cyklach i logice warunkowej. Dobrze się sprawdzają, ale mają dwa ważne ograniczenia, o których należy wiedzieć.

  • W count i for_each nie można odnosić się do żadnych zmiennych wyjściowych zasobu.
  • count i for_each nie można używać w konfiguracji modułu.

W count i for_each nie można odnosić się do żadnych zmiennych wyjściowych zasobu

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

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

Rozważmy je po kolei.

Ponieważ parametrowi count przypisano statyczną wartość, ten kod zadziała bez problemów: kiedy wykonasz polecenie apply, utworzy trzy serwery EC2. Ale co jeśli chcesz wdrożyć po jednym serwerze w każdej strefie dostępności (Availability Zone czy AZ) w ramach bieżącego regionu AWS? Możesz sprawić, aby twój kod załadował listę stref z źródła danych aws_availability_zones i następnie 'cyklicznie' przeszedł przez każdą z nich, tworząc w niej serwer EC2, korzystając z parametru count oraz dostępu do tablicy przez indeks:

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 doskonale działać, ponieważ parametr count może bez problemu odnosić się do źródeł danych. Ale co się stanie, jeśli liczba serwerów, które musisz utworzyć, zależy od wyjścia jakiegoś zasobu? Aby to zademonstrować, najłatwiej wziąć zasób random_integer, który, jak można się domyślić z nazwy, zwraca losową liczbę całkowitą:

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

Ten kod generuje losową liczbę od 1 do 3. Zobaczymy, co się stanie, jeśli spróbujemy użyć wyniku result 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 wykonasz plan terraform dla tego kodu, otrzymasz 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

Wartość "count" zależy od atrybutów zasobów, które nie mogą być określone aż do zastosowania, więc Terraform nie może przewidzieć, ile instancji zostanie utworzonych. Aby to obejść, użyj argumentu -target, aby najpierw zastosować tylko zasoby, od których zależy count.

Terraform wymaga, aby count i for_each były obliczane na etapie planowania, przed utworzeniem lub zmianą jakichkolwiek zasobów. Oznacza to, że count i for_each mogą odnosić się do liter, zmiennych, źródeł danych, a nawet list zasobów (pod warunkiem, że ich długość można określić w trakcie planowania), ale nie do obliczanych zmiennych wyjściowych zasobów.

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

Może się zdarzyć, że zechcesz dodać parametr count w konfiguracjach 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żywać count wewnątrz modułu, aby utworzyć trzy kopie zasobu webserver-cluster. A może chcesz uczynić połączenie modułu opcjonalnym w zależności od jakiegoś warunku logicznego, przypisując jego parametrowi count wartość 0. Taki kod wydaje się całkiem rozsądny, jednak po wykonaniu terraform plan otrzymasz taki błąd:

Error: Reserved argument name in module block

   on main.tf line 13, in module "count_example":
   13: count = 3

Nazwa "count" jest zarezerwowana do użytku w przyszłej wersji Terraform.

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

Ograniczenia wdrożeń z zerowym czasem przestoju

Użycie bloku create_before_destroy w połączeniu z ASG jest doskonałym rozwiązaniem do organizacji wdrożeń bez przestojów, z jednym wyjątkiem: zasady automatycznego skalowania nie są wspierane. A dokładniej mówiąc, przy każdym wdrożeniu rozmiar ASG wraca do min_size, co może być problematyczne, jeśli korzystano z zasad automatycznego skalowania w celu zwiększenia liczby uruchomionych serwerów.

Na przykład, moduł webserver-cluster zawiera kilka zasobów aws_autoscaling_schedule, które o 9:00 zwiększają liczbę serwerów w klastrze z dwóch do dziesięciu. Jeśli wdrożysz to na przykład o 11:00, nowa grupa ASG uruchomi się z zaledwie dwoma serwerami, a nie dziesięcioma, i pozostanie w tym stanie do 9:00 następnego dnia.

To ograniczenie można obejść na kilka sposobów.

  • Zmień parametr recurrence w aws_autoscaling_schedule z 0 9 * * * („uruchamiaj o 9:00”) na coś w rodzaju 0-59 9-17 * * * („uruchamiaj co minutę od 9:00 do 17:00”). Jeśli w ASG jest już dziesięć serwerów, ponowne zastosowanie tej zasady automatycznego skalowania nic nie zmieni, co nas właśnie interesuje. Ale jeśli grupa ASG została wdrożona całkiem niedawno, to zasada ta gwarantuje, że maksymalnie w ciągu minuty liczba jej serwerów osiągnie dziesięć. To nie jest zbyt eleganckie podejście, a duże skoki z dziesięciu do dwóch serwerów i z powrotem mogą powodować problemy u użytkowników.
  • Stwórz niestandardowy skrypt, który wykorzystuje API AWS do określenia liczby aktywnych serwerów w ASG, wywołaj go za pomocą zewnętrznego źródła danych (patrz punkt „Zewnętrzne źródło danych” na s. 249) i przypisz parametrowi desired_capacity grupy ASG wartość zwróconą przez ten skrypt. Dzięki temu każdy nowy egzemplarz ASG zawsze uruchomi się z tą samą pojemnością, co nasz kod Terraform i komplikuje jego konserwację.

Oczywiście, w idealnym świecie Terraform powinien mieć wbudowaną obsługę wdrożeń bez przestojów, ale na maj 2019 roku zespół HashiCorp nie planował dodania tej funkcjonalności (szczegóły — tutaj).

Poprawny plan może zostać nieudanie wdrożony

Czasami, gdy wykonujesz polecenie plan, otrzymujesz całkowicie prawidłowy plan wdrożenia, jednak 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 przez Ciebie wcześniej w rozdziale 2:

resource "aws_iam_user" "existing_user" {
   # Podstaw tutaj nazwę już istniejącego użytkownika IAM,
   # aby poćwiczyć korzystanie z polecenia terraform import
   name = "yevgeniy.brikman"
}

Teraz, jeśli wykonasz polecenie plan, Terraform wygeneruje na pierwszy rzut oka całkiem rozsądny plan wdrożenia:

Terraform wykona następujące działania:

   # aws_iam_user.existing_user zostanie utworzony
   + resource "aws_iam_user" "existing_user" {
         + arn                  = (znane po zastosowaniu)
         + force_destroy   = false
         + id                    = (znane po zastosowaniu)
         + name               = "yevgeniy.brikman"
         + path                 = "\/"
         + unique_id         = (znane po zastosowaniu)
      }

Plan: 1 do dodania, 0 do zmiany, 0 do zniszczenia.

Jeśli wykonasz polecenie apply, otrzymasz następujący błąd:

Error: Błąd podczas tworzenia użytkownika IAM yevgeniy.brikman: EntityAlreadyExists:
Użytkownik o nazwie yevgeniy.brikman już istnieje.

   na main.tf linii 10, w zasobie "aws_iam_user" "existing_user":
   10: resource "aws_iam_user" "existing_user" {

Problem, oczywiście, polega na tym, że użytkownik IAM o tej nazwie już istnieje. I może to dotyczyć nie tylko użytkowników IAM, ale prawie każdego zasobu. Może ktoś utworzył ten zasób ręcznie lub za pomocą wiersza poleceń, ale tak czy inaczej, powtórzenie identyfikatorów prowadzi do konfliktów. Istnieje wiele wariantów tego błędu, które często zaskakują nowicjuszy w Terraform.

Kluczowym punktem jest to, że polecenie terraform plan uwzględnia tylko te zasoby, które są wskazane w pliku stanu Terraform. Jeśli zasoby zostały utworzone w inny sposób (na przykład ręcznie, klikając przycisk w konsoli AWS), nie trafią do pliku stanu i w związku z tym Terraform nie będzie ich uwzględniać podczas wykonywania polecenia plan. W efekcie poprawny na pierwszy rzut oka plan okaże się nieudany.

Z tego można wyciągnąć dwie lekcje.

  • Jeśli już zacząłeś pracować z Terraform, nie korzystaj z niczego innego. Jeśli część Twojej infrastruktury jest zarządzana za pomocą Terraform, nie powinno się jej już zmieniać ręcznie. W przeciwnym razie nie tylko ryzykujesz otrzymanie dziwnych błędów Terraform, ale także przekreślasz wiele korzyści z IaC, ponieważ kod już nie będzie dokładnym odwzorowaniem Twojej infrastruktury.
  • Jeśli masz już jakąś infrastrukturę, użyj polecenia import. Jeśli zaczynasz używać Terraform z istniejącą już infrastrukturą, można ją dodać do pliku stanu za pomocą polecenia terraform import. Dzięki temu Terraform będzie wiedział, jaką infrastrukturą ma zarządzać. Polecenie import przyjmuje dwa argumenty. Pierwszym jest adres zasobu w twoich plikach konfiguracyjnych. Używa się tutaj tego samego składni, co w odwołaniach do zasobów: _. (jak np. aws_iam_user.existing_user). Drugim argumentem jest identyfikator zasobu, który chcesz zaimportować. Na przykład, jako ID zasobu aws_iam_user występuje nazwa użytkownika (np. yevgeniy.brikman), a ID zasobu aws_instance to identyfikator serwera EC2 (np. i-190e22e5). Jak zaimportować zasób, zazwyczaj wskazuje dokumentacja na dole jego strony.

    Poniżej podano polecenie import, które umożliwia synchronizację zasobu aws_iam_user, który dodałeś do swojej konfiguracji Terraform wraz z użytkownikiem IAM w rozdziale 2 (oczywiście zamiast yevgeniy.brikman należy wstawić swoją nazwę):

    $ terraform import aws_iam_user.existing_user yevgeniy.brikman

    Terraform skontaktuje się z API AWS, aby znaleźć twojego użytkownika IAM i utworzyć w pliku stanu połączenie między nim a zasobem aws_iam_user.existing_user w twojej konfiguracji Terraform. Od tego momentu, wykonując polecenie plan, Terraform będzie wiedział, że użytkownik IAM już istnieje i nie spróbuje go utworzyć ponownie.

    Należy zauważyć, że jeśli masz już wiele zasobów, które chcesz zaimportować do Terraform, ręczne zapisanie kodu i import każdego z nich z osobna może być uciążliwe. Dlatego warto zwrócić uwagę na takie narzędzie, jak Terraforming (http://terraforming.dtan4.net/), które może automatycznie importować kod i stan z konta AWS.

    Refaktoryzacja może mieć swoje pułapki

    Refaktoryzacja to powszechna praktyka w programowaniu, polegająca na zmianie wewnętrznej struktury kodu, pozostawiając jego zewnętrzne zachowanie bez zmian. Jest to potrzebne, aby uczynić kod bardziej zrozumiałym, schludnym i łatwym w utrzymaniu. Refaktoryzacja jest niezbędną metodą, którą należy regularnie stosować. Jednak gdy mowa o Terraform lub jakimkolwiek innym narzędziu IaC, należy być bardzo ostrożnym w rozumieniu, co oznacza „zewnętrzne zachowanie” fragmentu kodu, w przeciwnym razie mogą pojawić się nieprzewidziane problemy.

    Na przykład powszechnym rodzajem refaktoryzacji jest zastępowanie nazw zmiennych lub funkcji bardziej zrozumiałymi. Wiele IDE ma wbudowane wsparcie dla refaktoryzacji i może automatycznie zmieniać nazwy zmiennych i funkcji w całym projekcie. W ogólnych językach programowania jest to trywialna procedura, o której nie ma potrzeby się zastanawiać, jednak w Terraform należy to robić z wielką ostrożnością, w przeciwnym razie można napotkać przerwy w działaniu.

    Na przykład, w module webserver-cluster znajduje się zmienna wejściowa cluster_name:

    variable "cluster_name" {
       description = "Nazwa do użycia dla wszystkich zasobów klastra"
       type          = string
    }

    Wyobraź sobie, że zacząłeś używać tego modułu do wdrożenia mikrousługi o nazwie foo. Później chciałeś zmienić nazwę swojego serwisu na bar. Ta zmiana może wydawać się trywialna, ale w rzeczywistości może spowodować przerwy w działaniu.

    Chodzi o to, że moduł webserver-cluster używa zmiennej cluster_name w wielu zasobach, w tym w parametrze name dwóch grup bezpieczeństwa 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 name w jakimś zasobie, Terraform usunie starą wersję tego zasobu i stworzy nową. Ale jeśli zasobem jest ALB, w okresie między jego usunięciem a wprowadzeniem nowej wersji, nie będziesz miał mechanizmu do przekierowywania ruchu do swojego serwera WWW. Podobnie, jeśli usunięta zostanie grupa bezpieczeństwa, twoje serwery zaczną odrzucać jakikolwiek ruch sieciowy, dopóki nie zostanie utworzona nowa grupa.

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

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

    Identyfikator tego zasobu nazywa się instance. Wyobraź sobie, że podczas refaktoryzacji postanowiłeś zmienić go na bardziej zrozumiałą (twoim zdaniem) nazwę cluster_instance:

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

    Co się w końcu stanie? Zgadza się: przerwa w działaniu.

    Terraform łączy identyfikator każdego zasobu z identyfikatorem dostawcy chmury. Na przykład, iam_user jest powiązany z identyfikatorem użytkownika IAM w AWS, a aws_instance – z identyfikatorem serwera AWS EC2. Jeśli zmienisz identyfikator zasobu (powiedzmy, z instance na cluster_instance, jak w przypadku aws_security_group), dla 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ę bezpieczeństwa i utworzy nową, podczas gdy Twoje serwery zaczną odrzucać każdy ruch sieciowy.

    Oto cztery podstawowe lekcje, które powinieneś wyciągnąć z tej dyskusji.

    • Zawsze używaj polecenia plan. Dzięki niemu możesz zidentyfikować wszystkie te problemy. Starannie przeglądaj jego wyniki i zwracaj uwagę na sytuacje, w których Terraform planuje usunąć zasoby, których prawdopodobnie nie powinno się usuwać.
    • Twórz przed usunięciem. Jeśli chcesz zastąpić zasób, dobrze się zastanów, czy należy utworzyć nowy przed usunięciem oryginału. Jeśli odpowiedź brzmi tak, w tym może pomóc create_before_destroy. Osiągniesz taki sam efekt ręcznie, wykonując dwa kroki: najpierw dodaj nowy zasób do konfiguracji i uruchom polecenie apply, a następnie usuń stary zasób z konfiguracji i znów skorzystaj z polecenia apply.
    • Zmiana identyfikatorów wymaga zmiany stanu. Jeśli chcesz zmienić identyfikator powiązany z zasobem (na przykład, zmieniając nazwę aws_security_group z instance na cluster_instance), unikając przy tym usunięcia zasobu i utworzenia jego nowej wersji, musisz odpowiednio zaktualizować plik stanu Terraform. Nigdy nie rób tego ręcznie — zamiast tego użyj polecenia terraform state. Przy zmianie identyfikatorów należy wykonać polecenie terraform state mv, które ma następującą składnię:
      terraform state mv

      ORIGINAL_REFERENCE to wyrażenie odnoszące się do zasobu w jego obecnej postaci, a NEW_REFERENCE to miejsce, do którego chcesz go przenieść. Na przykład, przy zmianie nazwy grupy aws_security_group z instance na cluster_instance należy wykonać następujące polecenie:

      $ terraform state mv 
         aws_security_group.instance 
         aws_security_group.cluster_instance

      W ten sposób poinformujesz Terraform, że stan, który wcześniej dotyczył aws_security_group.instance, teraz powinien być połączony z aws_security_group.cluster_instance. Jeśli po zmianie nazwy i uruchomieniu tej komendy terraform plan nie pokaże żadnych zmian, oznacza to, że wszystko zrobiłeś poprawnie.

    • Niektóre parametry nie mogą być zmieniane. Parametry wielu zasobów są niezmienne. Jeśli spróbujesz je zmienić, Terraform usunie stary zasób i stworzy nowy. Na stronie każdego zasobu zazwyczaj podano, co się dzieje przy zmianie danego parametru, więc nie zapomnij sprawdzić dokumentacji. Zawsze używaj komendy plan i rozważaj zasadność zastosowania strategii create_before_destroy.

    Odwrotna spójność jest zgodna… z opóźnieniem

    API niektórych dostawców chmury, takich jak AWS, są asynchroniczne i mają opóźnioną spójność. Asynchroniczność oznacza, że interfejs może od razu zwrócić odpowiedź, nie czekając na zakończenie żądanego działania. Opóźniona spójność oznacza, że rozprzestrzenienie zmian w całym systemie może zająć czas; podczas gdy to się dzieje, twoje odpowiedzi mogą być niespójne i zależeć od tego, która replika źródła danych odpowiada na twoje wywołania API.

    Wyobraź sobie na przykład, że robisz wywołanie API do AWS z prośbą o utworzenie serwera EC2. API zwróci "pomyślną" odpowiedź (201 Created) niemal natychmiast, nie czekając na utworzenie samego serwera. Jeśli spróbujesz od razu się do niego podłączyć, prawie na pewno się nie uda, ponieważ w tym momencie AWS wciąż inicjalizuje zasoby lub, co gorsza, serwer jeszcze się nie uruchomił. Co więcej, jeśli wykonasz kolejne wywołanie, aby uzyskać informacje o tym serwerze, może przyjść błąd (404 Not Found). Faktem jest, że informacje o tym serwerze EC2 mogą wciąż rozprzestrzeniać się po AWS, aby stały się dostępne wszędzie, trzeba będzie poczekać kilka sekund.

    Za każdym razem, gdy korzystasz z asynchronicznego API z opóźnioną spójnością, musisz okresowo powtarzać swoje zapytanie, aż akcja zostanie zakończona i rozprzestrzeni się w systemie. Niestety, AWS SDK nie dostarcza do tego żadnych dobrych narzędzi, a projekt Terraform miał wcześniej wiele problemów, takich jak 6813 (https://github.com/hashicorp/terraform/issues/6813):

    $ terraform apply
    aws_subnet.private-persistence.2: InvalidSubnetID.NotFound:
    Subnet ID 'subnet-xxxxxxx' nie istnieje

    Innymi słowy, tworzysz zasób (na przykład podsieć) i następnie próbujesz uzyskać o nim pewne informacje (takie jak ID właśnie utworzonej podsieci), a Terraform nie może ich znaleźć. Większość z takich błędów (w tym 6813) została już naprawiona, ale od czasu do czasu nadal się pojawiają, szczególnie gdy Terraform dodaje wsparcie dla nowego typu zasobów. To jest irytujące, ale w większości przypadków nie niesie żadnego ryzyka. Po ponownym wykonaniu terraform apply wszystko powinno działać, ponieważ w tym momencie informacje już się rozprzestrzenią po systemie.

    Ten fragment pochodzi z książki Eugeniusza Brikmana „Terraform: infrastruktura jako kod”.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster