Systemy CI dla rozwoju iOS: transformacja Intela na ARM
Opublikowany: 2024-02-14W stale zmieniającym się krajobrazie technologicznym firmy muszą dostosowywać się do wiatru zmian, aby pozostać istotnymi i konkurencyjnymi. Jedną z takich transformacji, która szturmem podbiła świat technologii, jest przejście z architektury Intel x86_64 na architekturę iOS ARM, czego przykładem jest przełomowy chip Apple M1 firmy Apple. W tym kontekście systemy CI dla iOS stały się kluczowym czynnikiem branym pod uwagę przez firmy radzące sobie z tą zmianą, zapewniając, że procesy tworzenia i testowania oprogramowania pozostają wydajne i aktualne zgodnie z najnowszymi standardami technologicznymi.
Apple ogłosiło wprowadzenie chipów M1 prawie trzy lata temu i od tego czasu było jasne, że firma przyjmie architekturę ARM i ostatecznie zrezygnuje ze wsparcia dla oprogramowania opartego na procesorach Intel. Aby zachować kompatybilność między architekturami, Apple wprowadziło nową wersję Rosetty, swojego zastrzeżonego frameworka do tłumaczenia binarnego, który okazał się niezawodny w przeszłości podczas znaczącej transformacji architektury z PowerPC na Intel w 2006 roku. Transformacja wciąż trwa i widzieliśmy Xcode traci obsługę Rosetty w wersji 14.3.
W Miquido kilka lat temu dostrzegliśmy potrzebę migracji z Intela na ARM. Przygotowania rozpoczęliśmy w połowie 2021 roku. Jako software house z wieloma klientami, aplikacjami i projektami realizowanymi jednocześnie, stanęliśmy przed kilkoma wyzwaniami, którym musieliśmy sprostać. Ten artykuł może być Twoim przewodnikiem, jeśli Twoja firma boryka się z podobną sytuacją. Opisane sytuacje i rozwiązania przedstawiono z perspektywy rozwoju iOS – ale możesz znaleźć spostrzeżenia odpowiednie także dla innych technologii. Kluczowym elementem naszej strategii przejścia było zapewnienie pełnej optymalizacji naszego systemu CI dla iOS pod kątem nowej architektury, podkreślając znaczenie systemów CI dla iOS w utrzymywaniu wydajnych przepływów pracy i wysokiej jakości wyników w obliczu tak znaczącej zmiany.
Problem: migracja architektury Intel do iOS ARM
Proces migracji został podzielony na dwie główne gałęzie.
1. Wymiana istniejących komputerów programistów opartych na procesorach Intel na nowe Macbooki M1
Proces ten miał być stosunkowo prosty. Ustaliliśmy politykę stopniowej wymiany wszystkich komputerów Macbook Intel firmy deweloperskiej w ciągu dwóch lat. Obecnie 95% naszych pracowników korzysta z Macbooków opartych na architekturze ARM.
Jednak podczas tego procesu napotkaliśmy pewne nieoczekiwane wyzwania. W połowie 2021 r. niedobór komputerów Mac M1 spowolnił proces wymiany. Do końca 2021 roku udało nam się wymienić zaledwie kilka Macbooków z prawie 200 oczekujących. Oszacowaliśmy, że pełna wymiana wszystkich komputerów Mac z procesorem Intel na Macbooki M1 zajmie około dwóch lat, włączając w to inżynierów innych niż iOS.
Na szczęście Apple wypuściło swoje nowe chipy M1 Pro i M2. W rezultacie przenieśliśmy naszą uwagę z zastępowania procesorów Intel na komputery Mac M1 na zastępowanie ich chipami M1 Pro i M2.
Oprogramowanie, które nie było gotowe na zmianę, spowodowało frustrację programistów
Pierwsi inżynierowie, którzy otrzymali nowe Macbooki M1, mieli trudności, ponieważ większość oprogramowania nie była gotowa na przejście na nową architekturę Apple iOS ARM. Najbardziej dotknięte zostały narzędzia innych firm, takie jak Rubygems i Cocoapods, które są narzędziami do zarządzania zależnościami, które opierają się na wielu innych Rubygems. Niektóre z tych narzędzi nie były wówczas kompilowane dla architektury iOS ARM, więc większość oprogramowania musiała być uruchamiana przy użyciu Rosetty, co powodowało problemy z wydajnością i frustrację.
Jednak twórcy oprogramowania pracowali nad rozwiązaniem większości tych problemów w miarę ich pojawiania się. Przełomowy moment nastąpił wraz z wydaniem Xcode 14.3, który nie miał już wsparcia Rosetty. Był to wyraźny sygnał dla wszystkich twórców oprogramowania, że Apple naciska na migrację architektury Intel do iOS ARM. Zmusiło to większość zewnętrznych twórców oprogramowania, którzy wcześniej polegali na Rosetcie, do migracji swojego oprogramowania do ARM. Obecnie 99% oprogramowania innych firm używanego na co dzień w Miquido działa bez oprogramowania Rosetta.
2. Wymiana systemu CI Miquido na iOS
Wymiana systemu ciągłej integracji iOS w Miquido okazała się bardziej skomplikowanym zadaniem niż zwykła wymiana maszyn. Na początek proszę przyjrzeć się naszej ówczesnej infrastrukturze:
Mieliśmy instancję chmurową Gitlab i podłączonych do niej 9 komputerów Mac Mini z procesorami Intel. Maszyny te służyły do wykonywania zadań, a Gitlab był odpowiedzialny za orkiestrację. Za każdym razem, gdy zadanie CI było umieszczane w kolejce, Gitlab przypisywał je do pierwszego dostępnego modułu uruchamiającego, który spełniał wymagania projektu określone w pliku gitlab-ci.yml. Gitlab utworzy skrypt zadania zawierający wszystkie polecenia kompilacji, zmienne, ścieżki itp. Skrypt ten został następnie przeniesiony do modułu uruchamiającego i wykonany na tej maszynie.
Chociaż ta konfiguracja może wydawać się solidna, napotkaliśmy problemy z wirtualizacją ze względu na słabą obsługę procesorów Intel. W rezultacie zdecydowaliśmy się nie używać wirtualizacji takiej jak Docker i wykonywać zadania na samych maszynach fizycznych. Próbowaliśmy stworzyć wydajne i niezawodne rozwiązanie oparte na Dockerze, ale ograniczenia wirtualizacji, takie jak brak akceleracji GPU, spowodowały, że wykonanie zadań trwało dwukrotnie dłużej niż na maszynach fizycznych. Doprowadziło to do większych kosztów ogólnych i szybkiego zapełniania się kolejek.
Ze względu na umowę SLA macOS mogliśmy skonfigurować jednocześnie tylko dwie maszyny wirtualne. Dlatego zdecydowaliśmy się rozszerzyć pulę fizycznych modułów uruchamiających i skonfigurować je tak, aby wykonywały zadania Gitlab bezpośrednio w ich systemie operacyjnym. Jednak to podejście miało też kilka wad.
Wyzwania w procesie kompilacji i zarządzaniu modułami wykonawczymi
- Brak izolacji kompilacji poza piaskownicą katalogu kompilacji.
Moduł uruchamiający wykonuje każdą kompilację na komputerze fizycznym, co oznacza, że kompilacje nie są odizolowane od piaskownicy katalogu kompilacji. Ma to swoje zalety i wady. Z jednej strony możemy użyć pamięci podręcznej systemu, aby przyspieszyć kompilację, ponieważ większość projektów korzysta z tego samego zestawu zależności stron trzecich.
Z drugiej strony pamięć podręczna staje się niemożliwa do utrzymania, ponieważ pozostałości z jednego projektu mogą mieć wpływ na każdy inny projekt. Jest to szczególnie ważne w przypadku pamięci podręcznych obejmujących cały system, ponieważ te same moduły uruchamiające są używane zarówno do programowania Flutter, jak i React Native. W szczególności React Native wymaga wielu zależności buforowanych przez NPM.
- Potencjalny bałagan w narzędziach systemowych.
Chociaż żadne zadanie nie zostało wykonane z uprawnieniami sudo, nadal mogli uzyskać dostęp do niektórych narzędzi systemowych lub użytkownika, takich jak Ruby. Stanowiło to potencjalne zagrożenie zepsucia niektórych z tych narzędzi, zwłaszcza że macOS używa języka Ruby w przypadku niektórych starszych programów, w tym niektórych starszych funkcji Xcode. Wersja systemowa Ruby nie jest czymś, z czym chciałbyś zadzierać.
Jednak wprowadzenie rbenv stwarza kolejną warstwę złożoności, z którą trzeba sobie poradzić. Należy pamiętać, że Rubygems są instalowane w zależności od wersji Ruby, a niektóre z tych klejnotów wymagają określonych wersji Ruby. Prawie wszystkie narzędzia innych firm, których używaliśmy, były zależne od Ruby, a głównymi aktorami były Cocoapods i Fastlane.
- Zarządzanie tożsamościami podpisującymi.
Zarządzanie wieloma tożsamościami podpisującymi z różnych kont programistycznych klientów może być wyzwaniem, jeśli chodzi o pęki kluczy systemowych w modułach biegaczy. Tożsamość podpisująca jest bardzo wrażliwym danymi, ponieważ pozwala nam współprojektować aplikację, czyniąc ją podatną na potencjalne zagrożenia.
Aby zapewnić bezpieczeństwo, tożsamości powinny być umieszczane w piaskownicy między projektami i chronione. Jednak proces ten może stać się koszmarem, biorąc pod uwagę dodatkową złożoność wprowadzoną przez macOS w implementacji pęku kluczy.
- Wyzwania w środowiskach wieloprojektowych.
Nie wszystkie projekty zostały utworzone przy użyciu tych samych narzędzi, szczególnie Xcode. Niektóre projekty, zwłaszcza te w fazie wsparcia, były utrzymywane przy użyciu ostatniej wersji Xcode, w której projekt był tworzony. Oznacza to, że jeśli w ramach tych projektów wymagane były jakiekolwiek prace, IK musiała być w stanie je zbudować. W rezultacie biegacze musieli obsługiwać wiele wersji Xcode w tym samym czasie, co skutecznie zawężało liczbę biegaczy dostępnych do konkretnego zadania.
5. Wymagany dodatkowy wysiłek.
Wszelkie zmiany dokonane na biegaczach, takie jak instalacja oprogramowania, muszą zostać przeprowadzone na wszystkich biegaczach jednocześnie. Chociaż mieliśmy do tego narzędzie do automatyzacji, utrzymanie skryptów automatyzacji wymagało dodatkowego wysiłku.
Indywidualne rozwiązania infrastrukturalne dla różnorodnych potrzeb klientów
Miquido to software house, który współpracuje z wieloma klientami o różnych potrzebach. Dostosowujemy nasze usługi do specyficznych wymagań każdego klienta. Często hostujemy bazę kodu i niezbędną infrastrukturę dla małych firm lub start-upów, ponieważ może im brakować zasobów lub wiedzy, aby ją utrzymać.
Klienci korporacyjni zazwyczaj posiadają własną infrastrukturę do hostowania swoich projektów. Niektóre jednak nie mają takiej możliwości lub są zobowiązane przepisami branżowymi do korzystania ze swojej infrastruktury. Wolą też nie korzystać z usług SaaS innych firm, takich jak Xcode Cloud lub Codemagic. Zamiast tego chcą rozwiązania, które pasuje do ich istniejącej architektury.
Aby dostosować się do potrzeb tych klientów, często hostujemy projekty w naszej infrastrukturze lub konfigurujemy tę samą konfigurację ciągłej integracji iOS w ich infrastrukturze. Jednakże zachowujemy szczególną ostrożność w przypadku poufnych informacji i plików, takich jak podpisywanie tożsamości.
Wykorzystanie Fastlane do wydajnego zarządzania kompilacją
W tym przypadku Fastlane okazuje się przydatnym narzędziem. Składa się z różnych modułów zwanych akcjami, które pomagają usprawnić proces i rozdzielić go pomiędzy różnymi klientami. Jedna z tych akcji, zwana dopasowaniem, pomaga zachować tożsamości podpisywania programów i produkcji, a także profile udostępniania. Działa również na poziomie systemu operacyjnego, oddzielając te tożsamości w oddzielnych pękach kluczy na czas kompilacji i przeprowadza czyszczenie po kompilacji, co jest wyjątkowo pomocne, ponieważ wszystkie nasze kompilacje uruchamiamy na maszynach fizycznych.
Początkowo zwróciliśmy się do Fastlane z konkretnego powodu, ale odkryliśmy, że ma on dodatkowe funkcje, które mogą być dla nas przydatne.
- Kompilacja Prześlij do Testflight
W przeszłości interfejs API AppStoreConnect nie był publicznie dostępny dla programistów. Oznaczało to, że jedynym sposobem przesłania kompilacji do Testflight było użycie Xcode lub Fastlane. Fastlane było narzędziem, które zasadniczo zeskrobało API ASC i przekształciło je w akcję zwaną pilotem . Jednak ta metoda często psuła się wraz z następną aktualizacją Xcode. Jeśli programista chciał przesłać swoją kompilację do Testflight za pomocą wiersza poleceń, Fastlane było najlepszą dostępną opcją.
- Łatwe przełączanie między wersjami Xcode
Mając więcej niż jedną instancję Xcode na jednej maszynie, konieczne było wybranie, który Xcode ma zostać użyty do kompilacji. Niestety Apple sprawiło, że przełączanie się pomiędzy wersjami Xcode było niewygodne – trzeba w tym celu użyć opcji „xcode-select”, co dodatkowo wymaga uprawnień sudo. Fastlane również to obejmuje.
- Dodatkowe narzędzia dla programistów
Fastlane zapewnia wiele innych przydatnych narzędzi, w tym wersjonowanie i możliwość przesyłania wyników kompilacji do webhooków.
Wady Fastlane
Dostosowanie Fastlane do naszych projektów było rozsądne i solidne, więc poszliśmy w tym kierunku. Z powodzeniem stosowaliśmy go przez kilka lat. Jednak na przestrzeni tych lat zidentyfikowaliśmy kilka problemów:
- Fastlane wymaga znajomości języka Ruby.
Fastlane jest narzędziem napisanym w języku Ruby i wymaga dobrej znajomości języka Ruby, aby móc z niego efektywnie korzystać. Jeśli w konfiguracji Fastlane lub w samym narzędziu występują błędy, debugowanie ich za pomocą irb lub pry może być dość trudne.
- Uzależnienie od wielu klejnotów.
Sam Fastlane opiera się na około 70 klejnotach. Aby zmniejszyć ryzyko uszkodzenia systemu Ruby, w projektach korzystano z lokalnych klejnotów pakietu. Pobranie wszystkich tych klejnotów wymagało dużej ilości czasu.
- Problemy z systemem Ruby i rubygems.
W rezultacie wszystkie wspomniane wcześniej problemy z systemem Ruby i rubygems mają zastosowanie również tutaj.
- Redundancja dla projektów Flutter.
Projekty Flutter były również zmuszone do korzystania z dopasowania Fastlane tylko po to, aby zachować kompatybilność z projektami iOS i chronić pęki kluczy biegacza. Było to absurdalnie niepotrzebne, ponieważ Flutter ma wbudowany własny system kompilacji, a wspomniane wcześniej obciążenie zostało wprowadzone jedynie w celu zarządzania tożsamościami podpisującymi i profilami udostępniania.
Większość tych problemów rozwiązano po drodze, ale potrzebowaliśmy solidniejszego i niezawodnego rozwiązania.
Pomysł: Adaptacja nowych, solidniejszych narzędzi ciągłej integracji dla iOS
Dobra wiadomość jest taka, że Apple uzyskał pełną kontrolę nad swoją architekturą chipów i opracował nowy framework wirtualizacyjny dla systemu macOS. Ta platforma umożliwia użytkownikom tworzenie, konfigurowanie i uruchamianie maszyn wirtualnych z systemem Linux lub macOS, które uruchamiają się szybko i charakteryzują się wydajnością natywną – i naprawdę mam na myśli natywną.
Wyglądało to obiecująco i mogło stanowić kamień węgielny dla naszych nowych narzędzi do ciągłej integracji dla systemu iOS. Był to jednak tylko wycinek kompletnego rozwiązania. Mając narzędzie do zarządzania maszynami wirtualnymi, potrzebowaliśmy także czegoś, co mogłoby wykorzystywać ten framework w koordynacji z naszymi modułami uruchamiającymi Gitlab.
Dzięki temu większość naszych problemów związanych ze słabą wydajnością wirtualizacji stanie się przestarzała. Pozwoliłoby nam to również automatycznie rozwiązać większość problemów, które chcieliśmy rozwiązać za pomocą Fastlane.
Opracowanie dostosowanego rozwiązania do zarządzania tożsamością podpisującą na żądanie
Pozostał nam już ostatni problem do rozwiązania – zarządzanie tożsamością podpisów. Nie chcieliśmy w tym celu używać Fastlane, ponieważ wydawało się to nadmierne w stosunku do naszych potrzeb. Zamiast tego szukaliśmy rozwiązania, które byłoby bardziej dostosowane do naszych wymagań. Nasze potrzeby były proste: proces zarządzania tożsamością musiał być wykonywany na żądanie, wyłącznie na czas kompilacji, bez żadnych wstępnie zainstalowanych tożsamości w pęku kluczy i musiał być kompatybilny z dowolną maszyną, na której miałby działać.
Problem dystrybucji i brak stabilnego API AppstoreConnect stały się przestarzałe, gdy Apple wypuściło swoje „altool”, które umożliwiło komunikację pomiędzy użytkownikami a ASC.
Wpadliśmy więc na pomysł i musieliśmy znaleźć sposób na połączenie tych trzech aspektów w jedną całość:
- Znalezienie sposobu na wykorzystanie platformy wirtualizacji firmy Apple.
- Sprawienie, że będzie działać z modułami uruchamiającymi Gitlab.
- Znalezienie rozwiązania do zarządzania tożsamością podpisów w wielu projektach i biegaczach.
Rozwiązanie: Rzut oka na nasze podejście (w zestawie narzędzia)
Zaczęliśmy szukać rozwiązań, które rozwiążą wszystkie wspomniane wcześniej problemy.
- Korzystanie ze środowiska wirtualizacji firmy Apple.
Na pierwszą przeszkodę dość szybko znaleźliśmy rozwiązanie: natknęliśmy się na narzędzie do tarty Cirrus Labs. Od pierwszej chwili wiedzieliśmy, że to będzie nasz wybór.
Do najważniejszych zalet korzystania z narzędzia do tart oferowanego przez Cirrus Lab należą:
- Możliwość tworzenia vmów z surowych obrazów .ipsw.
- Możliwość tworzenia maszyn wirtualnych przy użyciu gotowych szablonów (z zainstalowanymi niektórymi narzędziami narzędziowymi, takimi jak Brew lub Xcode), dostępnych na stronie GitHub Cirrus Labs.
- Narzędzie Tarta wykorzystuje paker do dynamicznego wspierania budowania obrazu.
- Narzędzie Tart obsługuje obrazy zarówno w systemie Linux, jak i MacOS.
- Narzędzie wykorzystuje wyjątkową cechę systemu plików APFS, która umożliwia powielanie plików bez konieczności rezerwowania dla nich miejsca na dysku. W ten sposób nie trzeba przydzielać miejsca na dysku na 3-krotność oryginalnego rozmiaru obrazu. Potrzebujesz tylko wystarczającej ilości miejsca na dysku dla oryginalnego obrazu, podczas gdy klon zajmuje tylko tyle miejsca, ile wynosi różnica między nim a obrazem oryginalnym. Jest to niezwykle pomocne, zwłaszcza że obrazy macOS są zwykle dość duże.
Na przykład działający obraz macOS Ventura z zainstalowanym Xcode i innymi narzędziami wymaga co najmniej 60 GB miejsca na dysku. W normalnych okolicznościach obraz i dwa jego klony zajmowałyby do 180 GB miejsca na dysku, co stanowi znaczną ilość. A to dopiero początek, ponieważ możesz chcieć mieć więcej niż jeden oryginalny obraz lub zainstalować wiele wersji Xcode na jednej maszynie wirtualnej, co jeszcze bardziej zwiększy rozmiar.
- Narzędzie pozwala na zarządzanie adresami IP oryginalnych i sklonowanych maszyn wirtualnych, umożliwiając dostęp SSH do maszyn wirtualnych.
- Możliwość krzyżowego montowania katalogów między maszyną hosta a maszynami wirtualnymi.
- Narzędzie jest przyjazne dla użytkownika i ma bardzo prosty interfejs CLI.
Nie ma prawie niczego, czego brakuje temu narzędziu, jeśli chodzi o wykorzystanie go do zarządzania maszynami wirtualnymi. Prawie nic poza jedną rzeczą: chociaż obiecująca, wtyczka pakująca do tworzenia obrazów w locie była zbyt czasochłonna, więc zdecydowaliśmy się jej nie używać.
Próbowaliśmy tarty i zadziałała fantastycznie. Jego działanie było podobne do natywnego, a zarządzanie było łatwe.
Po pomyślnym zintegrowaniu tarty z imponującymi wynikami, następnie skupiliśmy się na sprostaniu innym wyzwaniom.
- Znalezienie sposobu na połączenie tarty z prowadnicami Gitlab.
Po rozwiązaniu pierwszego problemu stanęliśmy przed pytaniem, jak połączyć tartę z prowadnicami Gitlab.
Zacznijmy od opisania, co właściwie robią biegacze Gitlab:
Musieliśmy dodać do diagramu dodatkową łamigłówkę, która polegała na przydzielaniu zadań od hosta biegacza do maszyny wirtualnej. Zadanie GitLab to skrypt powłoki, który przechowuje kluczowe zmienne, wpisy PATH i polecenia.
Naszym celem było przeniesienie tego skryptu na maszynę wirtualną i uruchomienie go.
Zadanie to okazało się jednak trudniejsze, niż początkowo sądziliśmy.
Biegacz
Standardowe moduły wykonawcze Gitlab, takie jak Docker lub SSH, są proste w konfiguracji i wymagają niewielkiej lub żadnej konfiguracji. Potrzebowaliśmy jednak większej kontroli nad konfiguracją, co skłoniło nas do zbadania niestandardowych modułów wykonawczych dostarczonych przez GitLab.
Niestandardowe moduły executorów są świetną opcją w przypadku niestandardowych konfiguracji, ponieważ każdy krok modułu uruchamiającego (przygotowanie, wykonanie, czyszczenie) jest opisany w formie skryptu powłoki. Brakowało tylko narzędzia wiersza poleceń, które mogłoby wykonywać potrzebne nam zadania i być wykonywane w skryptach konfiguracyjnych programu uruchamiającego.
Obecnie dostępnych jest kilka narzędzi, które dokładnie to robią – na przykład executor tart CirrusLabs Gitlab. To narzędzie jest dokładnie tym, czego wówczas szukaliśmy. Jednak jeszcze nie istniało, a po przeprowadzeniu badań nie znaleźliśmy żadnego narzędzia, które mogłoby pomóc nam w realizacji naszego zadania.
Napisanie własnego rozwiązania
Ponieważ nie mogliśmy znaleźć idealnego rozwiązania, sami je napisaliśmy. W końcu jesteśmy inżynierami! Pomysł wydawał się solidny, a my mieliśmy wszystkie niezbędne narzędzia, więc przystąpiliśmy do rozwoju.
Zdecydowaliśmy się użyć Swift i kilku bibliotek open source dostarczonych przez Apple: Swift Argument Parser do obsługi wykonywania wiersza poleceń i Swift NIO do obsługi połączenia SSH z maszynami wirtualnymi. Rozpoczęliśmy rozwój i w ciągu kilku dni otrzymaliśmy pierwszy działający prototyp narzędzia, które ostatecznie przekształciło się w MQVMRunner.
Na wysokim poziomie narzędzie działa w następujący sposób:
- (Krok przygotowania)
- Przeczytaj zmienne podane w gitlab-ci.yml (nazwa obrazu i dodatkowe zmienne).
- Wybierz żądaną bazę maszyn wirtualnych
- Sklonuj żądaną bazę maszyny wirtualnej.
- Skonfiguruj katalog montowany krzyżowo i skopiuj do niego skrypt zadania Gitlab, ustawiając dla niego niezbędne uprawnienia.
- Uruchom klon i sprawdź połączenie SSH.
- W razie potrzeby skonfiguruj wszelkie wymagane zależności (takie jak wersja Xcode).
- (Wykonaj krok)
- Uruchom zadanie Gitlab, wykonując skrypt z katalogu zamontowanego krzyżowo na przygotowanym klonie maszyny wirtualnej poprzez SSH.
- (Etap czyszczenia)
- Usuń sklonowany obraz.
Wyzwania w rozwoju
Podczas opracowywania napotkaliśmy kilka problemów, które spowodowały, że nie wszystko poszło tak gładko, jak byśmy sobie tego życzyli.
- Zarządzanie adresami IP.
Zarządzanie adresami IP to kluczowe zadanie, z którym należy postępować ostrożnie. W prototypie obsługa SSH została zaimplementowana przy użyciu bezpośrednich i zakodowanych na stałe poleceń powłoki SSH. Jednakże w przypadku powłok nieinteraktywnych zalecane jest uwierzytelnianie kluczem. Dodatkowo zaleca się dodanie hosta do pliku znane_hosty, aby uniknąć przerw. Niemniej jednak, ze względu na dynamiczne zarządzanie adresami IP maszyn wirtualnych, istnieje możliwość podwojenia wpisu dla konkretnego adresu IP, co może prowadzić do błędów. Dlatego musimy dynamicznie przypisywać znane_hosty do konkretnego zadania, aby zapobiec takim problemom.
- Czyste rozwiązanie Swift.
Biorąc to pod uwagę oraz fakt, że zakodowane na stałe polecenia powłoki w kodzie Swift nie są zbyt eleganckie, pomyśleliśmy, że miło byłoby użyć dedykowanej biblioteki Swift i zdecydowaliśmy się na Swift NIO. Rozwiązaliśmy część problemów, ale jednocześnie wprowadziliśmy kilka nowych, jak np. czasami logi umieszczone na stdout były przesyłane *po* zakończeniu kanału SSH w związku z zakończeniem wykonywania polecenia – i jak bazowaliśmy na wyników w dalszej pracy, wykonanie losowo zakończyło się niepowodzeniem.
- Wybór wersji Xcode.
Ponieważ wtyczka Packer nie nadawała się do dynamicznego budowania obrazu ze względu na czasochłonność, zdecydowaliśmy się na pojedynczą bazę maszyn wirtualnych z preinstalowanymi wieloma wersjami Xcode. Musieliśmy znaleźć sposób, aby programiści mogli określić potrzebną wersję Xcode w pliku gitlab-ci.yml – i opracowaliśmy niestandardowe zmienne, których można używać w dowolnym projekcie. Następnie MQVMRunner wykona `xcode-select` na sklonowanej maszynie wirtualnej, aby skonfigurować odpowiednią wersję Xcode.
I wiele, wiele innych
Usprawnienie migracji projektów i ciągła integracja przepływu pracy w systemie iOS z Mac Studios
Skonfigurowaliśmy to w dwóch nowych studiach Mac i rozpoczęliśmy migrację projektów. Chcieliśmy, aby proces migracji dla naszych programistów był jak najbardziej przejrzysty. Nie mogliśmy zrobić tego całkowicie płynnie, ale w końcu doszliśmy do punktu, w którym musieli zrobić tylko kilka rzeczy w gitlab-ci.yml:
- Tagi biegaczy: używać Mac Studios zamiast Intel.
- Nazwa obrazu: parametr opcjonalny, wprowadzony ze względu na przyszłą kompatybilność w przypadku, gdy potrzebujemy więcej niż jednej podstawowej maszyny wirtualnej. W tej chwili zawsze domyślnie jest to pojedyncza podstawowa maszyna wirtualna, którą mamy.
- Wersja Xcode: parametr opcjonalny; jeśli nie zostanie podany, zostanie użyta najnowsza dostępna wersja.
Narzędzie spotkało się z bardzo dobrymi wstępnymi opiniami, dlatego zdecydowaliśmy się udostępnić je jako oprogramowanie typu open source. Dodaliśmy skrypt instalacyjny, aby skonfigurować Gitlab Custom Runner oraz wszystkie wymagane akcje i zmienne. Korzystając z naszego narzędzia, w ciągu kilku minut możesz skonfigurować własnego runnera GitLab – jedyne, czego potrzebujesz, to tarta i baza VM, na której będą wykonywane zadania.
Ostateczna struktura ciągłej integracji dla systemu iOS wygląda następująco:
3. Rozwiązanie umożliwiające efektywne zarządzanie tożsamością
Ciężko nam było znaleźć skuteczne rozwiązanie do zarządzania tożsamościami podpisującymi naszych klientów. Było to szczególnie trudne, ponieważ podpisywanie tożsamości to dane wysoce poufne, których nie należy przechowywać w niezabezpieczonym miejscu dłużej niż to konieczne.
Ponadto chcieliśmy ładować te tożsamości tylko w czasie kompilacji, bez żadnych rozwiązań międzyprojektowych. Oznaczało to, że tożsamość nie powinna być dostępna poza piaskownicą aplikacji (lub kompilacji). Ten ostatni problem rozwiązaliśmy już, przechodząc na maszyny wirtualne. Jednak nadal musieliśmy znaleźć sposób na przechowywanie i ładowanie tożsamości podpisującej do maszyny wirtualnej tylko na czas kompilacji.
Problemy z dopasowaniem Fastlane
W tamtym czasie nadal korzystaliśmy z dopasowania Fastlane, które przechowuje zaszyfrowane tożsamości i zabezpieczenia w oddzielnym repozytorium, ładuje je podczas procesu kompilacji do osobnej instancji pęku kluczy i usuwa tę instancję po kompilacji.
To podejście wydaje się wygodne, ale wiąże się z pewnymi problemami:
- Do działania wymaga całej konfiguracji Fastlane.
Fastlane to Rubygem i tutaj obowiązują wszystkie kwestie wymienione w pierwszym rozdziale.
- Pobieranie repozytorium w czasie kompilacji.
Trzymaliśmy nasze tożsamości w oddzielnym repozytorium, które zostało sprawdzone podczas procesu kompilacji, a nie procesu instalacji. Oznaczało to, że musieliśmy ustanowić oddzielny dostęp do repozytorium tożsamości, nie tylko dla Gitlaba, ale dla konkretnych modułów uruchamiających, podobnie jak w przypadku zależności prywatnych stron trzecich.
- Trudno zarządzać poza meczem.
Jeśli używasz Match do zarządzania tożsamościami lub udostępniania, nie ma potrzeby ręcznej interwencji. Ręczna edycja, odszyfrowywanie i szyfrowanie profili, aby później mogły z nimi nadal działać dopasowania, jest żmudne i czasochłonne. Użycie Fastlane do przeprowadzenia tego procesu zwykle skutkuje całkowitym wymazaniem konfiguracji udostępniania aplikacji i utworzeniem nowej.
- Trochę trudne do debugowania.
W przypadku jakichkolwiek problemów z podpisaniem kodu może być trudno określić tożsamość i dopasowanie właśnie zainstalowanej konfiguracji, ponieważ konieczne będzie ich wcześniejsze odkodowanie.
- Obawy dotyczące bezpieczeństwa.
Dopasuj konta programistów, do których uzyskano dostęp, korzystając z podanych danych uwierzytelniających, aby wprowadzić zmiany w ich imieniu. Mimo że Fastlane jest oprogramowaniem typu open source, niektórzy klienci odmówili jego stosowania ze względów bezpieczeństwa.
- I wreszcie, pozbycie się Match usunęłoby największą przeszkodę na naszej drodze do całkowitego pozbycia się Fastlane.
Nasze początkowe wymagania były następujące:
- Wczytanie wymaga podpisania tożsamości z bezpiecznego miejsca, najlepiej w formie innego niż zwykły tekst, i umieszczenia jej w pęku kluczy.
- Ta tożsamość powinna być dostępna dla Xcode.
- Najlepiej, aby zmienne hasła tożsamości, nazwy pęku kluczy i hasła pęku kluczy były możliwe do ustawienia na potrzeby debugowania.
Match miał wszystko, czego potrzebowaliśmy, ale wdrożenie Fastlane tylko po to, aby używać Match, wydawało się przesadą, szczególnie w przypadku rozwiązań wieloplatformowych z własnym systemem kompilacji. Chcieliśmy czegoś podobnego do Match, ale bez ciężkiego ciężaru Ruby, który niósł.
Tworzenie własnego rozwiązania
Pomyśleliśmy więc – napiszmy to sami! Zrobiliśmy to z MQVMRunnerem, więc możemy to zrobić również tutaj. My również wybraliśmy do tego Swift, głównie dlatego, że mogliśmy uzyskać wiele niezbędnych API za darmo, korzystając z frameworku Apple Security.
Oczywiście nie poszło też tak gładko, jak oczekiwano.
- Istnieją ramy bezpieczeństwa.
Najłatwiejszą strategią było wywołanie poleceń bash, tak jak robi to Fastlane. Jednakże mając dostępną platformę Security, pomyśleliśmy, że bardziej eleganckie będzie wykorzystanie jej do programowania.
- Brak doświadczenia.
Nie mieliśmy dużego doświadczenia ze frameworkiem Security dla macOS i okazało się, że różni się on znacznie od tego, do czego byliśmy przyzwyczajeni na iOS. W wielu przypadkach obróciło się to przeciwko nam, gdy nie zdawaliśmy sobie sprawy z ograniczeń macOS lub zakładaliśmy, że działa on tak samo jak na iOS – większość tych założeń była błędna.
- Straszna dokumentacja.
Dokumentacja frameworku Apple Security jest, delikatnie mówiąc, skromna. To bardzo stare API, którego początki sięgają pierwszych wersji OSX i czasami odnosiliśmy wrażenie, że od tamtej pory nie było ono aktualizowane. Duża część kodu nie jest udokumentowana, ale przewidywaliśmy jej działanie, czytając kod źródłowy. Na szczęście dla nas jest to oprogramowanie typu open source.
- Wycofanie bez zamienników.
Duża część tego frameworka jest przestarzała; Apple próbuje odejść od typowego pęku kluczy w stylu macOS (wiele pęków kluczy dostępnych za pomocą hasła) i wdrożyć pęk kluczy w stylu iOS (pojedynczy pęk kluczy, synchronizowany przez iCloud). Dlatego w 2014 r. wycofano go z systemu macOS Yosemite, ale w ciągu ostatnich dziewięciu lat nie wymyślono żadnego zamiennika. Zatem jedyny dostępny dla nas interfejs API jest na razie przestarzały, ponieważ nie ma jeszcze nowego.
Założyliśmy, że tożsamości podpisujące mogą być przechowywane jako ciągi zakodowane w standardzie Base64 w zmiennych Gitlab dla poszczególnych projektów. Jest bezpieczny, oparty na projekcie i jeśli zostanie ustawiony jako zmienna maskowana, można go odczytać i wyświetlić w dziennikach kompilacji jako zwykły tekst.
Mieliśmy więc dane identyfikacyjne. Musieliśmy tylko umieścić go w pęku kluczy. Korzystanie z Security API Po kilku próbach i utrudnionym przejrzeniu dokumentacji frameworku Security przygotowaliśmy prototyp czegoś, co później stało się MQSwiftSign.
Nauka systemu zabezpieczeń macOS, ale na własnej skórze
Aby opracować nasze narzędzie, musieliśmy dogłębnie zrozumieć sposób działania pęku kluczy macOS. Wiązało się to ze zbadaniem, w jaki sposób pęk kluczy zarządza elementami, ich dostępem i uprawnieniami oraz strukturą danych pęku kluczy. Odkryliśmy na przykład, że pęk kluczy to jedyny plik macOS, którego system operacyjny ignoruje zestaw ACL. Dodatkowo dowiedzieliśmy się, że lista ACL dla określonych elementów pęku kluczy to zwykła lista tekstowa zapisana w pliku pęku kluczy. Po drodze stanęliśmy przed wieloma wyzwaniami, ale też wiele się nauczyliśmy.
Jednym z istotnych wyzwań, jakie napotkaliśmy, były podpowiedzi. Nasze narzędzie zostało zaprojektowane przede wszystkim do działania na systemach CI iOS, co oznaczało, że nie mogło być interaktywne. Nie mogliśmy poprosić użytkowników o potwierdzenie hasła w CI.
Jednak system zabezpieczeń macOS jest dobrze zaprojektowany, co uniemożliwia edytowanie lub odczytywanie poufnych informacji, w tym tożsamości podpisującej, bez wyraźnej zgody użytkownika. Aby uzyskać dostęp do zasobu bez potwierdzenia, program uzyskujący dostęp musi znajdować się na liście kontroli dostępu zasobu. Jest to rygorystyczny wymóg, którego żaden program nie może złamać, nawet programy Apple dostarczane z systemem. Jeśli jakikolwiek program musi odczytać lub edytować wpis pęku kluczy, użytkownik musi podać hasło pęku kluczy, aby go odblokować i opcjonalnie dodać go do listy ACL wpisu.
Pokonywanie wyzwań związanych z uprawnieniami użytkowników
Musieliśmy więc znaleźć sposób, aby Xcode mógł uzyskać dostęp do tożsamości skonfigurowanej przez nasz pęk kluczy bez pytania użytkownika o pozwolenie za pomocą pytania o hasło. W tym celu możemy zmienić listę kontroli dostępu elementu, ale wymaga to również zgody użytkownika – i oczywiście tak jest. W przeciwnym razie podważyłoby to cały sens posiadania listy ACL. Próbowaliśmy ominąć to zabezpieczenie – staraliśmy się uzyskać taki sam efekt, jak przy pomocy polecenia `security set-key-partition-list`.
Po dokładnym zapoznaniu się z dokumentacją frameworka nie znaleźliśmy żadnego API umożliwiającego edycję listy ACL bez monitowania użytkownika o podanie hasła. Najbliższą rzeczą, jaką znaleźliśmy, jest „SecKeychainItemSetAccess”, która za każdym razem powoduje wyświetlenie monitu w interfejsie użytkownika. Następnie ponownie zagłębiliśmy się, ale tym razem w najlepszą dokumentację, jaką jest sam kod źródłowy. Jak Apple to wdrożyło?
Okazało się, że – jak można było się spodziewać – korzystali z prywatnego API. Metoda o nazwie `SecKeychainItemSetAccessWithPassword` robi w zasadzie to samo, co `SecKeychainItemSetAccess`, ale zamiast pytać użytkownika o hasło, hasło jest podawane jako argument funkcji. Oczywiście – jako prywatny interfejs API nie jest wymieniony w dokumentacji, ale Apple nie ma dokumentacji dla takich interfejsów API, jakby nie mogło pomyśleć o stworzeniu aplikacji do użytku osobistego lub korporacyjnego. Ponieważ narzędzie miało być przeznaczone wyłącznie do użytku wewnętrznego, nie wahaliśmy się skorzystać z prywatnego API. Jedyne, co należało zrobić, to połączyć metodę C z Swiftem.
Zatem ostateczny przebieg prototypu wyglądał następująco:
- Utwórz tymczasowy odblokowany pęk kluczy z wyłączoną automatyczną blokadą.
- Pobierz i zdekoduj dane tożsamości podpisu zakodowane w formacie Base64 ze zmiennych środowiskowych (przekazanych przez Gitlab).
- Zaimportuj tożsamość do utworzonego pęku kluczy.
- Ustaw odpowiednie opcje dostępu dla zaimportowanej tożsamości, aby Xcode i inne narzędzia mogły ją odczytać na potrzeby współprojektowania.
Dalsze ulepszenia
Prototyp działał dobrze, dlatego zidentyfikowaliśmy kilka dodatkowych funkcji, które chcielibyśmy dodać do narzędzia. Naszym celem było ostateczne zastąpienie Fastlane; wdrożyliśmy już akcję „dopasuj”. Jednak Fastlane nadal oferował dwie cenne funkcje, których jeszcze nie mieliśmy – instalację profilu udostępniania i tworzenie pliku eksportu.plist.
Instalacja profilu udostępniania
Instalacja profilu udostępniania jest dość prosta — sprowadza się do wyodrębnienia identyfikatora UUID profilu i skopiowania pliku do `~/Library/MobileDevice/Provisioning Profiles/` z UUID jako nazwą pliku — i to wystarczy, aby Xcode mógł go poprawnie zobaczyć. Nie jest niczym niezwykłym dodanie do naszego narzędzia prostej wtyczki umożliwiającej przeglądanie dostarczonego katalogu i robienie tego dla każdego znalezionego w nim pliku .mobileprovision.
Tworzenie pliku Export.plist
Jednak utworzenie pliku eksport.plist jest nieco trudniejsze. Aby wygenerować prawidłowy plik IPA, Xcode wymaga od użytkowników dostarczenia pliku plist zawierającego określone informacje zebrane z różnych źródeł – plik projektu, listę uprawnień, ustawienia obszaru roboczego itp. Powód, dla którego Xcode może zbierać te dane tylko za pomocą kreatora dystrybucji, ale nie przez CLI jest mi nieznane. Mieliśmy je jednak zebrać przy pomocy Swift API, mając jedynie referencje do projektu/obszaru roboczego i niewielką dawkę wiedzy na temat budowy pliku projektu Xcode.
Wynik był lepszy niż się spodziewaliśmy, dlatego postanowiliśmy dodać go jako kolejną wtyczkę do naszego narzędzia. Wydaliśmy go również jako projekt open source dla szerszej publiczności. W tej chwili MQSwiftSign jest wielofunkcyjnym narzędziem, które z powodzeniem może zastąpić podstawowe działania fastlane wymagane do budowy i dystrybucji aplikacji na iOS i używamy go w każdym naszym projekcie w Miquido.
Końcowe przemyślenia: sukces
Przejście z architektury Intel na architekturę iOS ARM było trudnym zadaniem. Napotkaliśmy wiele przeszkód i spędziliśmy dużo czasu na opracowywaniu narzędzi ze względu na brak dokumentacji. Ostatecznie jednak stworzyliśmy solidny system:
- Dwóch biegaczy do zarządzania zamiast dziewięciu;
- Uruchamianie oprogramowania, które jest całkowicie pod naszą kontrolą, bez mnóstwa narzutów w postaci rubygemów – byliśmy w stanie pozbyć się Fastlane lub dowolnego oprogramowania stron trzecich w naszych konfiguracjach kompilacji;
- DUŻO wiedzy i zrozumienia rzeczy, na które zwykle nie zwracamy uwagi – jak bezpieczeństwo systemu macOS i sam framework zabezpieczeń, faktyczna struktura projektu Xcode i wiele, wiele innych.
Chętnie Cię zachęcę – Jeśli masz problemy z konfiguracją modułu uruchamiającego GitLab dla wersji iOS, wypróbuj nasz MQVMRunner. Jeśli potrzebujesz pomocy w budowaniu i dystrybucji aplikacji przy użyciu jednego narzędzia i nie chcesz polegać na rubygemach, wypróbuj MQSwiftSign. Działa na mnie, może zadziałać również na Ciebie!