Na początku wszystko jest dość proste.
Jest formularz.
Jest API.
Jest baza.
Jest kilka funkcji albo jeden mały serwis.
Klikasz.
Działa.
Jeszcze raz.
Działa.
Deployment przeszedł. Endpoint odpowiada. Dane pojawiły się tam, gdzie powinny.
Można uznać, że MVP jest gotowe.
I bardzo często rzeczywiście jest.
Problem zaczyna się chwilę później.
Kiedy ten sam przycisk przestaje klikać osoba, która dokładnie wie, jak działa system.
A zaczyna ktoś, kto właśnie powierzył mu swoje dane.
Wtedy MVP nadal może być małe.
Nie potrzebuje Kubernetesa. Nie potrzebuje multiregionalnej dostępności. Nie potrzebuje pięciu środowisk, service mesha ani platform teamu.
Ale przestaje być demo.
I to zmienia więcej, niż mogłoby się wydawać.
Demo wybacza. Produkcja pamięta.
Jeżeli podczas developmentu Lambda zwróci 500, zwykle wiadomo, co zrobić.
Logi.
Poprawka.
Jeszcze jeden deploy.
Jeżeli testowy rekord w bazie wygląda źle, można go usunąć.
Jeżeli środowisko developerskie przestanie działać na pół godziny, jest irytująco.
I tyle.
Większość takich problemów ma jedną wspólną cechę.
Łatwo je odwrócić.
Można uruchomić proces jeszcze raz.
Można cofnąć kod.
Można wyczyścić dane testowe.
Można zacząć od początku.
Produkcja dodaje do systemu coś nowego.
Konsekwencję.
Request może wykonać się dwa razy.
Webhook może przyjść ponownie.
Worker może zakończyć połowę procesu i zniknąć dokładnie pomiędzy dwoma krokami.
Migracja może zmienić dane, których nie da się już łatwo odtworzyć.
Użytkownik może zobaczyć komunikat „gotowe”, chociaż gdzieś trzy usługi dalej operacja właśnie przestała być gotowa.
I wtedy pytanie:
„Czy działa?”
robi się trochę za małe.
Znacznie ciekawsze staje się inne:
Co się stanie, kiedy nie zadziała?
A zaraz za nim:
Jak trudno będzie odwrócić skutki?
To właśnie ta druga odpowiedź jest dla mnie jednym z ważniejszych kryteriów gotowości MVP.
Nie kompletność infrastruktury.
Nie liczba wdrożonych zabezpieczeń.
Nie to, czy diagram wygląda już wystarczająco „produkcyjnie”.
Odwracalność.
Kod zazwyczaj łatwo cofnąć
Wyobraźmy sobie prostą aplikację działającą na ECS.
Nowa wersja trafia na produkcję.
Po kilku minutach okazuje się, że coś jest nie tak.
Jeżeli poprzedni image nadal istnieje, konfiguracja jest znana, a deployment nie zmienił czegoś nieodwracalnego po drodze, sytuacja jest nieprzyjemna.
Ale wciąż dość nudna.
Wracamy do poprzedniej wersji.
Podobnie może być z Lambdą.
Albo z prostym frontendem.
Kod bardzo często należy do tej wygodnej kategorii problemów, które możemy naprawić kolejną wersją kodu.
Co jest dobrą wiadomością.
I właśnie dlatego nie każde MVP potrzebuje od początku najbardziej rozbudowanego deployment strategy, jakie potrafimy sobie wyobrazić.
Czasem wystarczy wiedzieć dwie rzeczy:
co było poprzednią dobrą wersją
i
jak do niej wrócić.
Nie musi to jeszcze oznaczać blue/green deploymentu, progressive delivery i automatycznego rollbacku opartego o kilkanaście metryk.
Być może kiedyś.
Na początku ważniejsze jest, żeby droga powrotna w ogóle istniała.
Dane są mniej wyrozumiałe
Kod można wdrożyć ponownie.
Z danymi bywa gorzej.
Jeżeli błędna wersja aplikacji przez godzinę zapisuje niewłaściwy stan, rollback kodu nie cofnie tego, co już wydarzyło się w bazie.
Jeżeli migracja usunie informację, której później będziemy potrzebować, poprzedni Docker image niewiele pomoże.
Jeżeli użytkownik usunie coś, czego w rzeczywistości nie powinniśmy usuwać natychmiast, cofnięcie commita również nie przywróci jego danych.
To jest moment, w którym baza przestaje być tylko prostokątem na diagramie.
Staje się odpowiedzialnością.
Nie oznacza to od razu cross-region replication, skomplikowanego disaster recovery i infrastruktury przygotowanej na koniec świata.
Czasem wystarczy zwykły backup.
Tylko tutaj pojawia się mało efektowny szczegół.
Backup, którego nigdy nie próbowaliśmy odtworzyć, jest bardziej obietnicą niż mechanizmem odzyskiwania.
AWS Well-Architected od lat podkreśla właśnie ten element: samo wykonywanie kopii nie kończy problemu. Recovery również trzeba sprawdzać.
Bo moment awarii jest dość słabym momentem na pierwsze odkrycie, że czegoś jednak nie zapisaliśmy.
Albo zapisaliśmy.
Tylko nie to, czego potrzebowaliśmy.
Przy pierwszych użytkownikach nie musi więc istnieć ogromny proces backupowy.
Ale dobrze byłoby wiedzieć:
- co naprawdę możemy stracić,
- jak długo możemy to tracić,
- i czy umiemy to później odzyskać.
To już wystarcza, żeby wiele decyzji architektonicznych zaczęło wyglądać inaczej.
Retry jest prosty. Dopóki operacja niczego nie zmienia.
Serverless bardzo dobrze pokazuje jeszcze inny rodzaj problemu.
Mamy prosty flow.
API Gateway → Lambda → SQS → worker
Request trafia do API.
Lambda robi swoje.
Wiadomość ląduje w kolejce.
Worker wykonuje operację.
Pięknie.
Potem dodajemy retry.
Jeszcze lepiej.
System jest bardziej odporny.
Tylko retry ma jedną dość istotną cechę.
Nie oznacza:
„dokończ to, czego poprzednim razem nie udało się zrobić”.
Oznacza:
„wykonaj tę operację ponownie”.
Dopóki operacja odczytuje dane, różnica może być niewielka.
Jeżeli wysyła mail, tworzy zamówienie, generuje fakturę, pobiera płatność albo zmienia stan biznesowy — zaczyna być trochę ciekawiej.
Wyobraźmy sobie, że worker wykonał operację po stronie zewnętrznego systemu.
Sekundę później stracił połączenie.
Nie wiemy, czy odpowiedź do nas nie dotarła, czy sama operacja nie została wykonana.
Co robimy?
Retry.
I właśnie dlatego idempotency nie jest czymś, co pojawia się dopiero wtedy, kiedy system obsługuje milion requestów.
Amazon Builders’ Library opisuje ten problem bardzo praktycznie: retry jest jednym z podstawowych mechanizmów budowania odpornych systemów, ale staje się bezpieczny dopiero wtedy, kiedy operacja jest zaprojektowana tak, aby jej powtórzenie nie tworzyło kolejnego skutku ubocznego.
To prowadzi do prostej obserwacji.
Retry nie naprawia operacji.
Retry ją powtarza.
To, czy powtórzenie jest bezpieczne, jest już odpowiedzialnością architektury.
I do tego wcale nie potrzeba dużej skali.
Dziesięciu użytkowników wystarczy, żeby jeden z nich dwa razy dostał tę samą fakturę.
Najgorsze błędy nie zawsze powodują awarię
Są błędy bardzo wygodne.
Serwis przestaje odpowiadać.
Alarm się uruchamia.
Wykres robi się czerwony.
Wiadomo, że mamy problem.
Znacznie ciekawsze są te, w których wszystko wygląda dobrze.
Pipeline — green.
Healthcheck — 200.
CPU spokojne.
Target group healthy.
Grafana wygląda, jakby można ją było wkleić do prezentacji.
Tylko użytkownik nadal nie może zrobić tego, po co przyszedł.
I tutaj observability zaczyna znaczyć coś więcej niż posiadanie dashboardu.
Nie chodzi o to, żeby od pierwszego dnia zbudować centrum dowodzenia jakie posiada NASA.
Chodzi o możliwość odpowiedzenia na kilka prostych pytań.
Czy najważniejszy flow użytkownika rzeczywiście się zakończył?
Jeżeli nie to gdzie się zatrzymał?
Czy potrafimy znaleźć konkretną operację kilka minut później?
Czy dowiemy się o problemie sami, czy pierwszy monitoring nadal będzie miał adres e-mail klienta?
Przy prostym systemie odpowiedzią może być:
structured logging,
correlation ID,
kilka dobrze wybranych metryk,
jeden sensowny alarm.
To może być wystarczające.
Bo observability w MVP nie musi dawać pełnego obrazu systemu.
Powinna przede wszystkim dawać możliwość zrozumienia tego, co właśnie poszło nie tak.
Jest to szczególnie istotne właśnie przez odwracalność.
Nawet najlepszy rollback niewiele daje, jeżeli przez trzy dni nie wiadomo, że jest potrzebny.
Są też rzeczy, których rollback nie cofnie
Security można potraktować podobnie.
Łatwo tutaj popaść w jedną z dwóch skrajności.
Pierwsza:
„to tylko MVP”.
Druga:
próba zbudowania przed pierwszym użytkownikiem security programu większego niż sam produkt.
Nie potrzebujemy żadnej z nich.
Wystarczy zauważyć, że część zdarzeń jest po prostu bardzo słabo odwracalna.
Ujawnionego sekretu nie da się „odujawnić”.
Danych wysłanych w niewłaściwe miejsce nie cofniemy wdrożeniem poprzedniego image.
Zbyt szerokie uprawnienia mogą przez miesiące niczego nie zepsuć.
A potem nagle zepsuć znacznie więcej, niż powinny.
Dlatego security w MVP również można rozpatrywać przez koszt konsekwencji.
Nie przez liczbę produktów security w architekturze.
Credentials poza repozytorium.
Sensowne granice IAM.
Brak przypadkowo publicznych zasobów.
Świadomość, gdzie znajdują się dane użytkowników.
To nadal może być bardzo proste.
Minimalizm nie oznacza przecież braku granic.
Czasem oznacza właśnie ustawienie tylko tych, które naprawdę mają znaczenie.
Nie wszystko zasługuje na taką samą uwagę
To chyba najważniejsza konsekwencja całego podejścia.
Przy MVP nie chcę maksymalizować jakości każdego elementu.
To prowadziłoby dokładnie tam, skąd próbowałem uciec.
Do architektury gotowej na problemy, których jeszcze nie mamy.
Bardziej interesuje mnie nierówne rozłożenie uwagi.
Mało energii tam, gdzie decyzję łatwo zmienić.
Dużo więcej tam, gdzie skutki mogą zostać z nami na długo.
Runtime można zmienić.
Sposób deploymentu można poprawić.
Dashboard można przebudować.
Lambda może kiedyś zostać zastąpiona kontenerem.
Nazwa kolejki raczej nie zdecyduje o przyszłości produktu.
Ale utracone dane?
Podwójnie wykonana operacja finansowa?
Ujawniony sekret?
Migracja, której nie potrafimy cofnąć?
To są trochę inne problemy.
Nie dlatego, że są bardziej „enterprise”.
Dlatego, że mają inny koszt błędu.
Własne SaaS-y bardzo szybko sprowadzają tę dyskusję na ziemię
Przy budowaniu własnych produktów można długo rozmawiać o docelowej architekturze.
Czy coś powinno zostać serverless.
Czy lepiej przenieść fragment do ECS.
Czy potrzebujemy osobnego środowiska.
Czy Terraform powinien mieć jeden state czy kilka.
To są ciekawe decyzje.
Ale bardzo szybko okazuje się, że część z nich może spokojnie poczekać.
Jeżeli wybiorę niewłaściwy runtime, prawdopodobnie za jakiś czas go zmienię.
Jeżeli pipeline nie jest jeszcze idealny, mogę go przebudować.
Jeżeli logi nie wyglądają tak dobrze, jak bym chciał, mogę do nich wrócić.
Znacznie bardziej interesują mnie miejsca, w których system zaczyna robić coś w imieniu użytkownika.
Przechowywać jego dane.
Wykonywać proces asynchroniczny.
Zmieniać stan.
Komunikować się z innym systemem.
Podejmować operację, której powtórzenie nie jest obojętne.
Tam prostota nadal jest ważna.
Tylko przestaje oznaczać:
„zróbmy jak najmniej”.
Zaczyna oznaczać:
„zróbmy tylko tyle, żeby konsekwencje były dla nas zrozumiałe”.
To trochę inna definicja minimalizmu.
I znacznie bardziej użyteczna.
Czego więc naprawdę potrzebuje MVP?
Nie mam checklisty.
To prawdopodobnie dobra wiadomość.
MVP obsługujące publiczne dane pogodowe ma zupełnie inny profil ryzyka niż system przechowujący informacje finansowe.
Proces, który można uruchomić jeszcze raz, różni się od procesu, który po każdym wykonaniu wystawia fakturę.
Aplikacja wewnętrzna dla pięciu osób to nie to samo co publiczny SaaS.
Dlatego zamiast pytać:
„Czy to jest już production-grade?”
wolę kilka mniej efektownych pytań.
Co tutaj może się nie udać?
Które błędy możemy łatwo cofnąć?
Które zostawiają po sobie trwały ślad?
Skąd dowiemy się, że coś poszło źle?
Jak wrócimy do ostatniego znanego dobrego stanu?
Czy potrafimy odzyskać to, czego nie możemy stracić?
Czy powtórzenie operacji jest bezpieczne?
Odpowiedzi zwykle dość szybko pokazują, gdzie potrzebujemy trochę więcej uwagi.
I równie często pokazują, czego jeszcze wcale nie musimy budować.
MVP powinno pozostać niedokończone
To właściwie jego rola.
Nie musi mieć finalnej architektury.
Nie musi używać docelowego runtime.
Nie musi przewidywać organizacji, której jeszcze nie ma.
Nie musi rozwiązywać problemów skali, która być może nigdy się nie pojawi.
Powinno zostawić miejsce na zmianę.
Jeżeli coś możemy łatwo zmienić później, często właśnie tak powinniśmy zrobić.
Ale istnieje różnica pomiędzy:
świadomie niedokończonym systemem
a
systemem, w którym nikt jeszcze nie zastanowił się nad konsekwencjami.
Pierwszy zachowuje możliwość ruchu.
Drugi liczy trochę na szczęście.
Pierwszy użytkownik zmienia architekturę bardziej niż pierwszy tysiąc
Nie dlatego, że generuje duży ruch.
Najczęściej nie generuje prawie żadnego.
CPU nawet tego nie zauważy.
Zmienia się coś innego.
Od tego momentu system przestaje istnieć wyłącznie dla ludzi, którzy go zbudowali.
Pojawiają się cudze dane.
Cudzy czas.
Czasem cudze pieniądze.
I oczekiwanie, że jeśli aplikacja powiedziała „gotowe”, to prawdopodobnie naprawdę jest gotowe.
System nadal może być bardzo mały.
Kilka Lambd.
Jeden serwis ECS.
Jedna baza.
Jedna kolejka.
Prosty pipeline.
Nie ma powodu komplikować go wcześniej niż trzeba.
Ale przed pierwszym prawdziwym użytkownikiem warto wiedzieć, gdzie prostota jest świadomą decyzją, a gdzie zaczyna być tylko odkładaniem konsekwencji.
Nie wszystko w MVP musi być gotowe na skalę.
Rzeczy, których skutków nie da się łatwo odwrócić, powinny być gotowe trochę wcześniej.
Reszta może spokojnie poczekać.