409: Sprawa zasobu, który już istniał
Krótka historia detektywistyczna ze świata Infrastructure as Code.
┌────────────────────────────────────────────────────────────┐ │ TABLICA ŚLEDCZA │ ├────────────────────────────────────────────────────────────┤ │ │ │ PLAN TERRAFORMA API DOSTAWCY MÓWI │ │ "Utworzę to" ✗ "409 Conflict — to │ │ (state w ogóle nie już istnieje" │ │ ma o tym zapisu) │ │ │ │ ┌───────────────────────────┐ │ │ │ PODEJRZANY: zasób, │ │ │ │ obecny w rzeczywistości, │ │ │ │ ale nieobecny w state │ │ │ └───────────────────────────┘ │ │ │ │ MOTYW: ktoś zbudował go ręcznie, tygodnie temu, │ │ zanim w ogóle dotarł tu potok │ │ │ └────────────────────────────────────────────────────────────┘
Zgłoszenie
Zaczęło się od rutynowego wdrożenia. Funkcja, która już bezpiecznie trafiła do jednego środowiska, była wreszcie promowana do kolejnego: mały, dobrze przetestowany kawałek Infrastructure-as-Code, ukryty za flagą funkcji, przełączoną z false na true. Nic w nim nie wyglądało ryzykownie.
Potok wdrożeniowy ruszył. Dziesięć zasobów powstało bez problemu: nowe konta serwisowe, nowe uprawnienia, kilka drobnych zmian konfiguracji. A potem, na jedenastym, wszystko stanęło:
Error: 409 Conflict: The requested stream conflicts with an existing stream with module.<redacted>.event_stream.hook, on event_stream.tf line 30
Plan Terraforma mówił czarno na białym, że ten zasób zostanie utworzony: nie miał o nim żadnego zapisu w pliku state. A jednak dostawca po drugiej stronie odmawiał, upierając się, że coś o tym samym kształcie już istnieje.
Coś się nie zgadzało.
Rozdział 1 — „Czy już tego nie widzieliśmy?”
Inżynier badający sprawę miał nieodparte wrażenie, że dokładnie ten błąd, w tym samym brzmieniu i przy tym samym rodzaju zasobu, wygląda znajomo. Na pewno już raz został gdzieś niedawno rozwiązany?
Przeszukanie wewnętrznych notatek zespołu dało sporo pokrewnego kontekstu: jak ta funkcja była wdrażana stopniowo, środowisko po środowisku, za flagą; jak rutynowy plan w tych repozytoriach bywa pełen niezwiązanych, już scalonych zmian czekających na osobny krok promocji. Ale nic o tym konkretnym błędzie. Nic o poprawce.
Kusiło, żeby wyciągnąć oczywisty wniosek: to coś nowego, nikt na to nie trafił, rozwiążmy to od zera. Ten wniosek został zapisany jako notatka robocza i od razu podważony. „Jestem prawie pewien, że już to naprawiałem” – padła odpowiedź. „Tylko nie pamiętam jak”.
To jedno zdanie zmieniło cały kształt śledztwa. Jeśli wewnętrzne notatki nie miały odpowiedzi, może miał ją sam kod.
Rozdział 2 — Czytanie pamięci samego pliku
Zamiast traktować notatki jako ostateczne słowo, śledztwo zwróciło się ku jedynemu miejscu, które nigdy nie zapomina: historii wersji pliku, na który wskazywał błąd.
$ git log --follow -- event_stream.tf refactor: rename the resource label fix: adopt pre-existing resource instead of re-creating it feat: switch to the new event-stream resource type ...
I było. Tygodnie wcześniej, we wcześniejszym środowisku tego samego wdrożenia, dokładnie ten sam 409 już się pojawił i już został naprawiony. Opis commita opowiadał całą historię w czterech zdaniach:
terraform apply kończył się błędem 409: zasób już istnieje na koncie tego środowiska, utworzony poza potokiem, kiedy wdrożenie czekało na zależność. Terraform nie miał o nim zapisu, więc ciągle próbował utworzyć duplikat, zamiast go przejąć.
Duplikat. To był brakujący element. Gdzieś zasób o dokładnie tym kształcie został utworzony ręcznie, nie przez Terraform, kiedy inny zespół, chcąc jak najszybciej zacząć własne testy integracyjne, nie chciał czekać, aż wdrożenie infrastruktury go dogoni.
To jednak rodziło oczywiste pytanie: jeśli raz już to naprawiono, dlaczego ten sam błąd pojawiał się znowu, w innym środowisku, dzisiaj?
Rozdział 3 — Ten sam błąd, inny adres
Odpowiedź, kiedy padła na głos, okazała się wręcz zawstydzająco prosta: każde środowisko w tym systemie to całkowicie osobne konto z własnym, osobnym plikiem state. Naprawienie problemu w Środowisku 1 uczy state Środowiska 1 o zasobie, który istnieje na koncie Środowiska 1. Nie ma to żadnego wpływu na Środowisko 2, które jest zupełnie innym kontem, z własnym zupełnie pustym state i, jak się okazało, z własnym zupełnie prawdziwym, ręcznie utworzonym duplikatem, który tam czekał.
Środowisko 1 (naprawione tygodnie temu) Środowisko 2 (dziś)
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ State Terraforma: pusty │ │ State Terraforma: pusty │
│ Konto dostawcy: MA zasób │ │ Konto dostawcy: MA zasób │
│ (utworzony ręcznie, │ │ (utworzony ręcznie, │
│ poza potokiem) │ │ poza potokiem, niezależnie) │
└─────────────┬───────────────┘ └─────────────┬───────────────┘
│ terraform import │ terraform import
▼ ▼
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ State Terraforma: teraz │ │ State Terraforma: teraz │
│ wie o zasobie │ │ wie o zasobie │
└─────────────────────────────┘ └─────────────────────────────┘
Dwa niezależne zespoły, dwa niezależne środowiska i ten sam bardzo ludzki skrót, użyty dwa razy: „nie chcemy czekać na potok, na razie utworzymy to sami”. Nikt właściwie nie zrobił nic złego. Po prostu nikt nie powiedział o tym Terraformowi.
Zanim przyjęto tę teorię, sprawdzono jeszcze jedną drogę: bezpośrednie wywołanie administracyjnego API dostawcy, z użyciem poświadczeń już dostępnych na koncie, żeby potwierdzić dokładną tożsamość zduplikowanego zasobu. Ta droga okazała się ślepą uliczką: dostępne poświadczenia pozwalały samej aplikacji korzystać z usługi, a nie nią administrować, i żądanie wróciło ze stanowczym błędem uprawnień. To była rozsądna próba, nie zadziałała i nic się nie stało: panel webowy dostawcy podał prawdziwy identyfikator zasobu w jakieś dziesięć sekund, bez żadnego specjalnego dostępu.
Rozdział 4 — Poprawka i sprzątanie, o którym nie wolno zapomnieć
Sama poprawka, kiedy przyczyna była już jasna, okazała się wręcz rozczarowująco prosta: terraform import. To narzędzie stworzone dokładnie na taką sytuację: uczy plik state o zasobie, który już legalnie istnieje w rzeczywistości, nie dotykając samego zasobu.
Proces, niemal wierne odbicie poprawki z wcześniejszego środowiska:
- Potwierdzić, że konfiguracja działającego zasobu faktycznie zgadza się z tym, czego oczekuje kod Terraforma (zgadzała się: wcześniejsza poprawka już poszerzyła konfigurację, żeby pasowała, więc tym razem nie trzeba było zmieniać kodu, wystarczył import).
- Dodać mały, wyraźnie opisany, tymczasowy job w potoku, którego jedynym zadaniem jest uruchomienie
terraform importna konkretnym ID zasobu, i oznaczyć go jako uruchamiany wyłącznie ręcznie, nigdy automatycznie. - Uruchomić go raz. Patrzeć, jak się udaje.
- Od razu otworzyć kolejną zmianę, która ten job z powrotem usuwa. Jednorazowy job importu pozostawiony w potoku to naładowana broń: dzień po użyciu jest niegroźny, ale potrafi być bardzo mylącą niespodzianką, jeśli ktoś przypadkiem uruchomi go miesiące później na zasobie, który jest już poprawnie śledzony.
Oba kroki trafiły jako dwie małe, osobne zmiany do przeglądu: jedna przejmuje zasób, druga po sobie sprząta. Dokładnie tak samo jak poprawka we wcześniejszym środowisku, tygodnie wcześniej.
Podsumowanie — Wskazówki dla kolegów śledczych
Jeśli zarządzasz zasobami chmurowymi lub SaaS przez Terraform (albo dowolne deklaratywne narzędzie IaC) w wielu środowiskach, kilka rzeczy warto mieć pod ręką:
- „Nigdy tego nie widzieliśmy” to stwierdzenie o twoich notatkach, a nie o rzeczywistości. Zanim uznasz, że błąd to nieznany teren, sprawdź faktyczną historię wersji pliku, którego dotyczy. Jednorazową poprawkę wrzuconą prosto do kodu i nigdzie indziej nieopisaną łatwo przeoczyć, a
git log/git blamena konkretnym pliku bywa szybszy i bardziej wiarygodny niż jakiekolwiek przeszukiwanie wiki. - Konflikt 409 typu „już istnieje” przy pustym pliku state prawie zawsze oznacza, że zasób utworzono poza potokiem: kolega, inny zespół albo wcześniejszy ręczny test, i nikt nie powiedział o tym twojemu infrastructure-as-code. Rozwiązaniem jest
terraform import: naucz plik state rzeczywistości, zamiast walczyć z rzeczywistością, żeby pasowała do błędnego state. - Naprawienie tego raz nie naprawia tego wszędzie. Jeśli twoje środowiska to naprawdę osobne konta lub tenanty z osobnym state, ten sam skrót „ktoś najpierw zbudował to ręcznie” może zdarzyć się niezależnie w każdym z nich. Spodziewaj się powtórki poprawki i nie bądź zaskoczony, kiedy nastąpi.
- Tymczasowe joby w potoku potrzebują od razu commita sprzątającego. Jeśli poprawka wymaga jednorazowego ręcznego kroku, wypuść jego usunięcie niemal równocześnie, jako kolejną zmianę. Nie pozwól, żeby „uruchom to raz” po cichu zamieniło się w „to może kiedyś zostać przypadkiem uruchomione ponownie”.