Continuous Delivery von iOS Apps mit GitLab CI und fastlane

Gepostet am: 08. August 2018

Am Beispiel unserer Entwicklungsumgebung bei inovex mit GitLab als Webfrontend für die Repositoryverwaltung, GitLab CI als Continuous Integration Umgebung, sowie fastlane als Build-Tool zeige ich den von uns eingesetzten Workflow zur Continuous Delivery von iOS Apps.

Als IT-Projekthaus entwickeln wir direkt und eigenständig iOS Apps für unsere Kunden, unterstützen jedoch auch bestehende Entwicklungsteams bei internen Kundenprojekten, wie die Case-Study der waipu.tv App es aufzeigt. In den einzelnen Projekten schwankt allerdings je nach Projektstand die Anzahl und Zusammensetzung der Entwickler.

Diese Gegebenheiten machen es nötig, dass die zu entwickelnde iOS App unabhängig von einem konkreten Entwickler für den App Store gebaut, signiert und hochgeladen werden kann. Da die App-Entwicklung meist agil und in mehreren Iterationen abläuft, ist es zudem gewünscht, dass Product Owner und Tester in regelmäßigen Abständen lauffähige Versionen der App zur Verfügung gestellt bekommen. Auch dies soll nicht auf einer Schulter lasten und die Entwickler möglichst wenig in ihrem Entwicklungprozess beschäftigen. Während der Entwicklung kann zudem ein kontinuierliches Ausführen von Tests auf den Feature-Branches sicherstellen, dass Entwickler sich nur lauffähige Merge Requests von Kollegen anschauen.

Entwicklungsumgebung für die Continuous Delivery von iOS Apps

Wie Maximilian in seinem Blog-Artikel bereits erläutert hat, setzen wir bei inovex GitLab EE als Web Frontend für unsere Git-Repositories ein und haben uns entschieden, das GitLab CI Modul bei unseren Projekten für das kontinuierliche Bauen und Integrieren von Softwarekomponenten zu verwenden. Diese Kombination ermöglicht es, innerhalb des GitLab Frontends auf Ereignisse wie z.B. den Push oder Merge eines Branches zu reagieren.

Für das automatisierte kontinuierliche Bauen und Verteilen der iOS App ist man jedoch nicht auf diese Kombination beschränkt. Auch andere Lösungen, wie z.B. auf Basis von Jenkins, können verwendet werden. Dafür kann der Abschnitt Bauen der iOS App unabhängig betrachtet werden.

Konfiguration CI/CD

Um innerhalb des Git-Repositories auf Veränderung der Branches, wie einen Push von Neuerungen, zu reagieren, müssen wir dies unserer CI/CD Pipeline mitteilen. Für GitLab CI wird dies in einer .gitlab-ci.yml Konfigurationsdatei gemacht, die im Root-Verzeichnis des Repositories platziert werden muss.

Der Grundaufbau so einer Konfigurationsdatei für einen iOS Buildjob kann wie folgt aussehen. Eine komplette Dokumentation ist unter Configuration of your jobs with .gitlab-ci.yml direkt bei GitLab zu finden.

  1. Um bestimmte Aktionen vor einem Job auszuführen, kann man unter dem Tag die gewünschten Befehle auflisten. Für einen iOS Build, der Cocoapods als Dependency Management und fastlane als Buildtool einsetzt, kann man hier z.B. die notwendigen Ruby Gems aus dem Gemfile mit bundle install installieren.
  2. Analog zum before_script Tag kann man auch Skripte nach dem Ausführen eines Jobs laufen lassen. Wie hier zu sehen könnte man beispielsweise sicherstellen, dass der iOS-Simulator beendet wird, sollte er noch laufen: killall Simulator || true
  3. Über stages kann man unterschiedliche Build-Schritte definieren, die in einer gewissen Reihenfolge ausgeführt werden können. Wie hier die Schritte test und build, die einmal die Unittests ausführen und dann die iOS App für die Distribution bauen.
  4. Konkrete Build-Tasks werden in einem eigenen yaml-Root-Tag mit einem eigenen Namen definiert und über stage einem gewissen Build-Schritt zugeteilt.
  5. Im script Block kann man die für den Build-Schritt notwendigen Skripte aufrufen. In unserem Setup besteht der Aufruf nur aus der Ausführung einer fastlane lane. Mehr dazu im Abschnitt Bauen der App mit fastlane.
  6. Über only und except kann man definieren, für welche Git-Referenzen man einen bestimmten Job ausführt. In diesem Beispiel wird durch except branches der Job nur für Git Tags ausgeführt, die mit dem Prefix release anfangen, z.B. release/1.2.5.
  7. Innerhalb des tags-Blocks definiert man die Verbindung zum Build-Server. Nur Build-Server, bzw. die auf ihm laufenden Runner, die den gleichen Tag haben, werden für den Build verwendet. Im nächsten Abschnitt mehr dazu.
  8. Nach einem erfolgreichen Test oder Build ist es möglich, dass man Artefakte wie Testreports oder die IPA-Datei aus den paths-Ordnern ans GitLab Web Frontend schickt. Diese Dateien können auch zeitlich begrenzt durch expires_in Parameter persistiert werden. Diese Möglichkeit zum Speichern von Artefakten eignet sich, wenn man eine händische Nachverarbeitung, wie z.B. den manuellen App Store Upload mit dem Application Loader, durchführen möchte.

Runner

Um einen Buildjob konkret ausführen zu können, muss nun eine Verbindung zwischen dem GitLab Web Frontend mit dem GitLab-CI-Modul und einem macOS Build Server vorhanden sein.

Dies wird mit einem GitLab Runner realisiert, einem Executable, das als Service auf dem Build Server läuft und eine Verbindung zum GitLab-CI-Modul hat. Die Einrichtung wird hier erläutert.

Ein Runner kann dabei jeder beliebige macOS Rechner sein. Wir setzen beispielsweise mehrere Mac Minis für die Continuous Delivery von iOS Apps ein.

Bei der Einrichtung oder einer späteren Konfiguration im GitLab Web Frontend kann man dem GitLab Runner die tags aus Punkt 7 des vorherigen Abschnittes zuweisen, die dann der Build-Job referenzieren kann. Man kann einem Runner mehrere Tags zuweisen, so dass man auch abbilden kann, wenn auf einem Build-Server unterschiedliche Xcode-Versionen installiert sind und ein iOS-Projekt eine bestimmte Xcode-Version benötigt.

Mehrere Tags bei einem GitLab Runner.

Die Auswahl der im Build Job konkret verwendeten Xcode-Version entscheidet sich dann jedoch in der fastlane lane.

Verwendung

Als Entwicklungsworkflow in Git setzen wir je nach Projekt auf GitFlow oder eine leicht angepasste vereinfachte Variante. Innerhalb des Workflows lassen wir uns von GitLab CI unterstützen.

Während der Entwicklung eines Features auf einem dedizierten Featurebranch können wir den aktuellen Stand zum origin pushen, und es werden für diesen Branch über eine fastlane lane die Unittests ausgeführt. Das Resultat des Buildjobs ist für den Entwickler des Features direkt im GitLab Web Frontend sichtbar. Dadurch kann er sich sicher sein, dass alle notwendigen Dateien eingecheckt und Konfigurationen durchgeführt wurden, sodass die App nicht nur bei ihm erfolgreich gebaut wird.

Ergebnis eines Buildjobs

Ein Feature landet bei uns immer über einen Merge Request, der von einem anderen Entwickler begutachtet wird, im develop Branch. Den Merge Request stellen wir auch über das GitLab Web Fronted, weil uns dort direkt angezeigt wird, ob der Feature-Branche gebaut werden konnte und die Unit-Tests alle erfolgreich durchgeführt wurden.

Merge Request für Feature im GitLab Web Frontend

Dies spart dem begutachtenden Entwickler Zeit und Ärger, weil er sich nur Merge Requests anschaut die bauen und deren Unit-Test erfolgreich durchgelaufen sind. Man kann sich also auf die Begutachtung des konkreten Features konzentrieren.

Nachdem ein Feature auf develop gemerged wurde, wird in der Regel eine App-Version für die Distribution an den Product Owner gebaut und automatisch an diesen verteilt.

Ein Release für den App Store mit passendem Zertifikat und Mobile Provisioning Profile wird bei der Erstellung eines Git Tags erstellt und dann direkt an iTunes Connect übermittelt. Die Build Pipeline des Jobs enthält dann mehrere Stages.

Build Pipeline mit mehreren Stages

Bauen einer iOS App

Der älteste und klassischste Weg eine iOS App für die Distribution, Ad-Hoc, In-House oder App Store, zu bauen führt direkt in Xcode über Product > Archive. Dafür muss der Entwickler jedoch das passende Zertifikat mit privatem Key und das Mobile Provisioning Profile für die App lokal auf seinem Rechner haben. Neben der Frage des Vertrauens bei Zertifikaten von Kunden stellt sich auch die Frage, wie man diesen Ansatz besser skalieren kann, wenn der betroffene Entwickler nicht zur Verfügung steht. Und die wenigstens Entwickler, da leidgeprüft, möchten Xcode das Verwalten der Zertifikate überlassen. Diese Lösung eignet sich daher nur für Projekte, die von einem Entwickler für eine eigene App betreut werden.

Eine Erweiterung und Verbesserung davon ist, dass man die Zertifikate und Provisioning Profiles für die Distribution zentral auf einem Build Server bereitstellt und das Bauen der App über die Xcode Commandline Tools als Shell-Befehl durchführt. Ein klassischer Befehl zum Bauen des Archives kann dann wie folgt lauten:

Und die IPA kann man aus dem Archive folgendermaßen generieren:

Der Ansatz scheint erstmal sehr praktikabel und reicht für viele Anwendungsgebiete bereits aus, da man jetzt unabhängig von einem Entwickler die App für die Distribution bauen kann.

Die Nachteile sind jedoch, das man selbst komplexe Shell-Skripte erzeugt, sowohl um Unit-Tests auszuführen, als auch um die App zu bauen. Zudem gibt es durch Apple in unregelmäßigen Abständen Änderungen am Command Line Tool Interface, wie z.B. das Hinzufügen des exportOptionsPlist Flags oder das Anpassen des Formats der zugehörigen .plist Datei. Diese Änderungen muss man in seinen Skripten selbst nachpflegen.

Auch die Frage des Zertifikatsmanagements ist noch nicht zufriedenstellend geklärt. Wenn man nur eine App auf einem Build Server baut, kann man die Zertifikate und Mobile Provisioning Profile manuell dort installieren. Gibt es jedoch mehrere Build Server, auf denen unterschiedliche Projekte gebaut werden sollen, ist ein manuelles Management nicht mehr handlich und aus Kundensicht nicht mehr erwünscht.

Für beide Probleme stelle ich nachfolgend mögliche Lösungen vorstellen.

Bauen der App mit fastlane

fastlane ist eine Ansammlung von Ruby-Skripten, die mittels einer Ruby-DSL das Bauen, Signieren und Verteilen einer iOS App für einen Entwickler deutlich vereinfacht. Dabei ist es nur ein Frontend zu den oben genannten Kommandozeilenbefehlen, abstrahiert diese jedoch sehr komfortabel und kümmert sich durch kontinuierliche Updates um gewisse CLI-Änderungen sogar selbst. Die ggf. nötigen Anpassungen der fastlane-Skripte in einem längeren Projektverlauf halten sich dadurch in Grenzen und sind für Entwickler einfacher durchzuführen.

Zusätzlich zu diesen Basisfunktionalitäten werden eine Reihe von Actions angeboten, die bereits Lösungen für andere alltägliche Problemstellungen bieten. Dazu zählen z.B. das Inkrementieren von Build-Nummern oder das Erzeugen eines Icon Overlays für Testversionen. Sollte es für einen Anwendungsfall noch keine Action geben, so kann man eine eigene Action über die vorhandene Plugin-Schnittstelle selbst erstellen und analog zu den eingebauten Actions nutzen.

Das Hinzufügen von fastlane zu einem iOS-Projekt ist auf der Projektseite erklärt. Da es sich um ein Ruby Gem handelt, fügen wir es in unseren Projekten zum Gemfile des iOS-Projekts hinzu und installieren es wie Cocoapods vor jedem Build mit einem bundle install. Damit stellen wir sicher, dass der Build Server nur eine Minimalkonfiguration benötigt und alle zusätzlichen Abhängigkeiten während des Buildjobs aufgelöst werden.

Nach dem Einrichten von fastlane für ein Projekt wird eine Fastfile-Datei erstellt. In dieser Datei befinden sich die Beschreibungen der einzelnen Buildjobs. Jeder Block mit dem Keyword lane ist dabei ein Job, der gezielt aufgerufen werden werden kann. Ein Fastfile kann wie folgt aussehen.

Über alle lanes hinweg kann man globale Konfigurationen festlegen, wenn man die jeweiligen Ruby-Methoden außerhalb der lanes aufruft, wie in den Punkten 1 bis 3 zu sehen

  1. Mit fastlane_version kann man die minimal zu verwendende Version setzen. Beachten sollte man, dass man über Ruby Gems eine passende Version installiert.
  2. Die zu verwendende Xcode-Version kann man mittels xcversion setzen. Besonders wenn man mehrere Xcode-Versionen auf dem Build Server installiert hat, sollte man darauf achten, dass die richtige Version angegeben wird. fastlane setzt dabei auf ein Namensschema für die Xcode-Binary-Benamung, Xcode_x.x, für die unterschiedlichen Versionen.
  3. Vor jeder lane führen wir auch eine Installation der Dependencies aus, wobei jeweils noch mal das Cocopods-Repo nach neuen Pods und Versionen abgefragt wird. Auch dies soll sicherstellen, dass ein Build Server nur minimal konfiguriert werden muss.

Ab Punkt 4 ist eine fastlane lane definiert, die man über die Kommandozeile direkt mit ihrem Namen ansprechen kann, wie es auch in der .gitlab-ci.yml geschieht

  1. Mit dem Keyword lane und einem eindeutigen Namen (release_build) definiert man einen Buildjob.
  2. Am Anfang jedes Buildjobs kann man das Projekt noch über verschiedenste Helfer-Actions für den Build konfigurieren, wie z.B. die Info.plist des Projekts anpassen.
  3. Mittels run_tests kann man auf einfache Weise die Unit-Tests eines Target ausführen.
  4. Der eigentliche Bau und das Paketieren der IPA-Datei wird über build_ios_app  durchgeführt. Dort kann auch je nach Anwendungsfall die Methode angeben, ob man einen Ad-Hoc-, einen Enterprise-In-House, oder App Store-Build erstellen möchte.
  5. Nach der Paketierung kann man die erstellte IPA-Datei per Umgebungsvariable ansprechen und beispielsweise mit einer vordefinierten Action (hockey) an HockeyApp übertragen.

Build-Konfiguration

In einem Projekt können je Anforderungen unterschiedliche Build-Versionen anfallen: Ad-Hoc, In-House oder App Store.

Xcode bietet über die Configurations in einem App Target die Möglichkeit an, jeweils das zu verwendende Zertifikat und Mobile Provisioning Profile anzugeben.

Dabei müssen die angegebenen Zertifikate und Provisioning Profile nicht für alle Build-Konfigurationen bei den Entwicklern vorliegen. So kann man beispielsweise für die Release-Variante schon die Enterprise- oder App-Store-Konfigurationen setzen, deren Zertifikate und Profile nur auf dem Build Server zur Verfügung stehen.

Der Vorteil davon, die Konfiguration im Projekt durchzuführen, ist, dass diese unabhängig von fastlane ist und im eigentlichen Build Job nichts mehr angepasst werden muss. Über den configuration Parameter in build_ios_app kann man die für den Build passende Konfiguration setzen.

fastlane bietet auch die Möglichkeit, das zu verwendende Zertifikat und Provisioning Profile mit anzugeben. Die geschieht dann direkt als Argument in der build_ios_app Action.

Über Appfiles kann man die unterschiedlichen Konfigurationen zusätzlich noch übersichtlich verwalten. Man sollte bei der Build-Konfiguration jedoch darauf achten, dass man sich für einen Weg entscheidet, um inkonsistente Konfiguration zu vermeiden.

Zertifikatsmanagement

Die Verwaltung der Zertifikate und Mobile-Provisioning-Dateien für das Signieren der gebauten Apps ist wohl noch eins der meistbesprochenen und diskutierten Themen der iOS-Entwicklung.

Auch hier bietet fastlane eine Möglichkeit, die Zertifikate und Profile zu verwalten. Das Modul sync_code_signing ermöglicht es, für Teams die Zertifikate und Profile verschlüsselt in einem gesonderten Git-Repository zu verwalten. Bei einem neuen Build werden diese dann aus dem Repo abgerufen und auf dem ausführenden Client installiert.

Der Vorteil dieser Lösung ist, dass die Zertifikate und Profile nur von den Personen mit den nötigen Git-Rechten betrachtet und verwaltet werden können. So kann sichergestellt werden, dass beispielsweise nur der Build Server eine App-Store-Variante der App bauen kann. Ein Nachteil bei sync_code_signing ist, dass bei der Einrichtung neue Zertifikate und Profile angelegt werde. Eine Migration von bestehenden Zertifikaten und Profilen ist offiziell nicht vorgesehen.

Da wir unterschiedliche Kunden mit unterschiedlichen iOS-Projekten haben, die auch von anderen IT-Dienstleistern betreut werden können, haben wir uns entschieden, eine ähnliche Variante analog zu match umzusetzen, bei der wir jedoch bereits bestehende Zertifikate und Profile verwenden können.

Inspiriert durch den Blog-Post Travis CI for iOS speichern wir die Zertifikate und Profile verschlüsselt im Projekt-Repository oder in einem gesonderten Repo. Bei jedem Build werden dann folgende Schritte zuerst durchgeführt:

  • Entschlüsselung von Zertifikaten und Profilen
  • Erstellung eines temporären Schlüsselbundes
  • Importieren der Zertifikate in den Schlüsselbund
  • Kopieren der Provisioning-Profile in den MobileDevice Provisioning Ordner

Nach einem Build, egal ob erfolgreich oder fehlerhaft, werden sowohl der temporäre Schlüsselbund als auch die entschlüsselten Zertifikate und Profile wieder gelöscht. Die einzelnen Schritte für das Einrichten der Umgebung auf dem Build Server können einfach über die Shell-Skripte im oben genannten Blogpost umgesetzt werden.

Distribution der iOS App

Bei der Verteilung der App an In-House-Tester oder den App Store setzen wir auf die vielfältigen Actions, die fastlane direkt mitliefert.

Wenn die App zum Testen erstmal nur In-House verteilt werden soll, eignen sich Dienste wie HockeyApp oder Beta by Crashlytics, von deren Web-Oberfläche man die Apps direkt installieren kann. Für beide gibts es Actions von fastlane. Möchte man seine Distribution einfacher halten, so kann man sich auch über die s3 Action die nötigen Dateien für die In-House-Verteilung erzeugen lassen und auf einem S3 Bucket veröffentlichen.

Ein direkter iTunes Connect Upload wird über die upload_to_testflight Action ermöglicht. So kann man direkt eine App-Version an seine TestFlight-Nutzer verteilen.

Auf der Distribution-Seite finden sich Code-Beispiele für die unterschiedlichen Möglichkeiten.

Fazit

Die Kombination von GitLab CI und fastlane erlaubt es, dass sich iOS-Entwickler mehr auf die reine Entwicklung konzentrieren können, anstatt sich mit der Build-Infrastruktur und der Verteilung von Apps groß auseinander setzen zu müssen. Gerade fastlane bietet für alltägliche Probleme bereits eine Reihe von Actions an. Eine Integration von CI-Pipelines in Merge Requests unterstützen zudem schon während der Entwicklungsphase. Die durchgeführte Trennung zwischen dem CI Service und dem eigentlichen Bau und der Signierung der App würde zudem einen reibungslosen Wechsel zu anderen CI Diensten wie Travis CI oder CircleCI ermöglichen. Daher werden wir uns auch in zukünftigen Projekten für diesen Ansatz entscheiden.

2018-08-08T12:55:09+00:00