Aktualizacja 50 000 ESP32 tak, żeby żadnego nie uceglić
Spis treści7 ▾
Pierwsza aktualizacja OTA w nowym produkcie zwykle przechodzi gładko. Trafia na kilka płytek leżących na biurku, wszystkie mają dobre Wi-Fi i wszystkie stoją kilka metrów od osoby, która pisała kod. Aktualizacja, która naprawdę boli, przychodzi rok później. Flota liczy już dziesiątki tysięcy sztuk rozsianych po piwnicach, dachach i metalowych szafach, a drobna zmiana w stosie sieciowym sprawia, że kilka procent z nich nigdy więcej nie łączy się z siecią. Nikt nie pojedzie do tysiąca lokalizacji, żeby ręcznie przeflashować płytkę, więc cała ścieżka aktualizacji musi zakładać, że coś pójdzie nie tak, a mimo to zostawiać każde urządzenie w stanie, z którego da się wrócić do działania.
Poniżej opisuję, jak robię to na urządzeniach z ESP32. Większość da się przenieść na inne mikrokontrolery po niewielkich zmianach.
Dwa sloty i droga odwrotu
Podstawą jest tablica partycji. ESP-IDF obsługuje dwa sloty aplikacji, zwykle nazwane ota_0 i ota_1, oraz małą partycję otadata, z której bootloader wie, który slot uruchomić. Urządzenie zawsze działa z jednego slotu, a nowy obraz pobiera do drugiego. Jeśli pobieranie przerwie się w połowie, nic się nie zmienia i urządzenie dalej działa na starym firmware. Dopiero gdy nowy obraz jest w całości zapisany i ma zweryfikowaną sumę kontrolną, urządzenie przestawia wskaźnik bootowania i się restartuje.
To rozwiązuje problem przerwanego pobierania. Nie pomaga jednak, gdy obraz pobierze się bez zarzutu, a potem po prostu nie działa, a to właśnie taki obraz ucegla urządzenia w terenie. Na to potrzebny jest mechanizm rollbacku w bootloaderze, włączany opcją CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE. Gdy jest aktywny, świeżo zainstalowany obraz startuje w stanie oczekiwania na weryfikację. Jeśli aplikacja potwierdzi, że działa poprawnie, obraz staje się obowiązującą wersją. Jeśli urządzenie zrestartuje się przed tym potwierdzeniem, bootloader uznaje nowy obraz za nieudany i sam wraca do poprzedniego slotu.
Całość sprowadza się więc do tego, co aplikacja uznaje za poprawne działanie.
Kiedy uznać aktualizację za udaną
Widziałem firmware, który oznacza się jako poprawny już w pierwszej linii app_main. Chroni to przed obrazem, który wywala się, zanim tam dotrze, i właściwie przed niczym więcej. Urządzenie, które bez problemu startuje, ale potem już nigdy nie połączy się z brokerem, jest stracone tak samo jak to, które się wywala. Co więcej, to bardziej prawdopodobny scenariusz po aktualizacji, która dotykała Wi-Fi, certyfikatów TLS albo sterownika modemu.
Sam wolę potwierdzać obraz dopiero wtedy, gdy urządzenie zrobi to jedno, do czego zostało stworzone: połączy się z siecią, zakończy handshake TLS, dotrze do brokera MQTT i opublikuje wiadomość, której odbiór backend potwierdzi. Do tego czasu obraz pozostaje w stanie oczekiwania, a task watchdog albo zwykły timer restartuje urządzenie, jeśli trwa to zbyt długo, i w ten sposób odsyła je z powrotem na stary firmware.
static void confirm_update_if_pending(void)
{
const esp_partition_t *running = esp_ota_get_running_partition();
esp_ota_img_states_t state;
if (esp_ota_get_state_partition(running, &state) != ESP_OK ||
state != ESP_OTA_IMG_PENDING_VERIFY) {
return;
}
// Called only after the first telemetry message has been acknowledged
// by the backend, so the new image has proven it can do its job.
esp_ota_mark_app_valid_cancel_rollback();
ESP_LOGI(TAG, "update confirmed on %s", running->label);
}
Timeout trzeba dobrać z głową. Urządzenie przy słabym zasięgu komórkowym potrafi rejestrować się w sieci nawet przez parę minut, więc limit trzydziestu sekund wycofywałby zupełnie dobre aktualizacje. Zwykle daję kilka minut, a urządzenie raportuje, z jaką wersją wróciło do sieci, dzięki czemu backend odróżni rollback od urządzenia, które po prostu zamilkło.
Uruchamiaj tylko to, co podpisałeś
Wszystko, co potrafi zainstalować nowy kod przez sieć, jest jednocześnie powierzchnią ataku, a flocie, która przyjmuje niepodpisane obrazy, wystarczy jeden wyciek danych uwierzytelniających, żeby zaczęła uruchamiać cudzy firmware. W rodzinie ESP32 odpowiedzią jest Secure Boot v2. Przy każdym starcie sprawdza on podpis bootloadera i aplikacji, a także podpis każdego obrazu przychodzącego przez OTA, kluczem publicznym, którego skrót jest wypalony w eFuse'ach. Na oryginalnym ESP32 wymaga to układu w rewizji 3.0 lub nowszej, co warto sprawdzić przed pierwszym zamówieniem produkcyjnym. W parze z nim stosuje się szyfrowanie flasha, żeby firmware nie dało się po prostu odczytać z układu.
Obie te decyzje są nieodwracalne i podejmuje się je przy produkcji, dlatego lubię mieć je załatwione przed pierwszą partią. Dodanie secure boot do urządzeń, które już pracują w terenie, jest w najlepszym razie kłopotliwe, a w najgorszym niemożliwe. To samo dotyczy ochrony przed powrotem do starszej wersji (anti-rollback). ESP-IDF potrafi zapisać w eFuse'ach wersję bezpieczeństwa i odmówić uruchomienia czegokolwiek starszego, co uniemożliwia atakującemu wgranie starego obrazu ze znaną podatnością. Warto to włączyć, ale trzeba też używać tego ostrożnie, bo każde podbicie wersji na stałe wypala jeden bit. Na ESP32 to pole ma ich 32, więc wersję bezpieczeństwa podnoszę tylko w wydaniach, które łatają realną podatność.
Wdrażanie falami
Nawet z działającym rollbackiem nigdy nie wysyłam nowej wersji do całej floty naraz. Zły obraz, który czysto się wycofa, i tak kosztuje każde urządzenie jedno pobranie, restart i kilka minut offline. Do tego niektóre awarie ujawniają się dopiero po kilku godzinach, gdy wyciek pamięci zdąży urosnąć albo urządzenie przetrwa zimną noc.
Sprawdza mi się schemat, w którym najpierw aktualizuję kilkadziesiąt urządzeń wewnętrznych, potem około jednego procenta prawdziwej floty, następnie dziesięć procent, a na końcu wszystkie. Każda fala trwa na tyle długo, żeby objąć co najmniej jeden pełny cykl dobowy pracy urządzeń. Backend porównuje niewielki zestaw wskaźników zaktualizowanej grupy z resztą floty: przede wszystkim ile urządzeń wróciło z nową wersją, ile się wycofało, jak często łączą się ponownie, ile mają wolnej sterty i jak często zgłaszają crashe. Jeśli którykolwiek z nich zacznie odbiegać od normy, wdrożenie samo się zatrzymuje.
AWS IoT Jobs ma większość tego wbudowaną: tempo wdrażania może rosnąć z czasem, a reguły przerwania opierają się na odsetku urządzeń, u których aktualizacja się nie powiodła albo przekroczyła limit czasu. Jeśli masz własnego brokera, to samo zrobi niewielka usługa, która przechodzi listę urządzeń partiami i między partiami sprawdza kilka metryk. Nie wymaga to wiele kodu.
Fale lubię też dobierać celowo. Pierwsza prawdziwa grupa powinna obejmować najstarsze rewizje sprzętu, najsłabsze łącza i lokalizacje w najzimniejszych miejscach, bo właśnie tam problemy wychodzą najwcześniej. Grupa kanarkowa złożona wyłącznie ze zdrowych urządzeń w dobrych sieciach niewiele ci powie.
Ile kosztuje megabajt
Przy Wi-Fi nikt nie zastanawia się nad rozmiarem obrazu. Przy łączności komórkowej koszty szybko rosną. Gdy aplikacja na ESP32 zawiera stos TLS, klienta MQTT i logikę produktu, obraz o rozmiarze megabajta lub większym to norma. Przy półtora megabajta wysłanie go do pięćdziesięciu tysięcy urządzeń oznacza przesłanie mniej więcej siedemdziesięciu pięciu gigabajtów. Na taryfie IoT rozliczanej za megabajt pojedyncze wydanie może kosztować więcej niż miesiąc zwykłej telemetrii, a takich wydań flota i tak potrzebuje kilku w roku.
Pomaga tu kilka nawyków. Odchudzony obraz zwraca się przy każdej aktualizacji. Dzięki wznawianiu pobierania urządzenie, które straci połączenie przy dziewięćdziesięciu procentach, nie zaczyna od zera. Nowsze wersje ESP-IDF obsługują to w esp_https_ota przez opcję ota_resumption, która wznawia przerwane pobieranie żądaniem HTTP Range. Aktualizacje różnicowe (delta), w których urządzenie pobiera tylko różnicę między obecnym a nowym obrazem, potrafią zmniejszyć niewielką zmianę do ułamka pełnego rozmiaru. Komplikują jednak zarówno build, jak i firmware na urządzeniu, więc sięgnąłbym po nie dopiero wtedy, gdy rachunek za transfer wyraźnie to uzasadni.
Urządzenia bateryjne wymagają jeszcze jednej zasady: nigdy nie zaczynaj aktualizacji, gdy poziom naładowania spadł poniżej bezpiecznego progu. Zapis do flasha i kilkuminutowe utrzymywanie połączenia przez modem pobierają znacznie więcej prądu niż zwykła praca urządzenia, a bateria, która padnie w połowie, w najlepszym razie zostawi urządzenie na starym slocie.
Co musi wiedzieć backend
Każde urządzenie powinno raportować wersję firmware, na której działa, slot, z którego wystartowało, i to, czy ostatni start nastąpił po rollbacku. Mając te dane, widzisz rozkład wersji we flocie na wykresie, a na pytania w rodzaju „ile urządzeń wciąż działa na wersji z marca” dostajesz odpowiedź w kilka sekund. Zapisuję też, które wydanie zaoferowano każdemu urządzeniu, kiedy je pobrało i kiedy potwierdziło. Gdy coś idzie nie tak, ta historia zwykle najszybciej ujawnia wzorzec, na przykład że wszystkie awarie dotyczą jednej rewizji sprzętu albo jednego operatora komórkowego.
Od czego bym zaczął
Gdybym miał wskazać elementy najważniejsze w nowym produkcie, na pierwszym miejscu postawiłbym układ z dwoma slotami i rollbackiem, potwierdzanie aktualizacji dopiero po pełnej wymianie komunikatów z backendem oraz podpisane obrazy, bo dodanie ich później jest trudne albo wręcz niemożliwe. Wdrażanie etapami, wznawianie pobierania i raportowanie wersji we flocie można dobudować, gdy urządzenia już pracują w terenie.
Jeśli pracujesz nad strategią aktualizacji dla floty i chcesz ją z kimś przegadać, zawsze chętnie wymienię się doświadczeniami.