Hinweis:
Dieser Blogartikel ist älter als 5 Jahre – die genannten Inhalte sind eventuell überholt.
In diesem Artikel zeige ich den von uns eingesetzten Workflow zur Continuous Delivery von iOS Apps 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.
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 zeigt. In den einzelnen Projekten schwankt allerdings je nach Projektstand die Anzahl und Zusammensetzung der Entwickler:innen.
Dadurch wird es nötig, dass die zu entwickelnde iOS App unabhängig von bestimmten Entwickler:innen für den App Store gebaut, signiert und hochgeladen werden kann. Da die App-Entwicklung meist agil und in mehreren Iterationen abläuft, sollten Product Owner und Tester:innen in regelmäßigen Abständen lauffähige Versionen der App zur Verfügung gestellt bekommen. Auch dies darf nicht auf einer Schulter lasten und die Entwickler:innen möglichst wenig in ihrem Arbeitsprozess beschäftigen. Während der Entwicklung kann zudem kontinuierliches Testen auf den Feature-Branches sicherstellen, dass Entwickler:innen sich nur lauffähige Merge Requests von Kolleg:innen 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 Build Job kann wie folgt aussehen. Eine komplette Dokumentation ist unter Configuration of your jobs with .gitlab-ci.yml direkt bei GitLab zu finden.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# 1 before_script: - bundle install # 2 after_script: - killall Simulator || true # 3 stages: - test - build # 4 build: stage: build # 5 script: - fastlane release_build # 6 only: - /^release\/.+$/ except: - branches # 7 tags: - Xcode9.2 # 8 artifacts: paths: - fastlane/test_output/ expire_in: 10 days test: # .... |
- Um bestimmte Aktionen vor einem Job auszuführen, kann man unter dem before_script-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
mitbundle inst
installieren. - 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
- Ü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.
- Konkrete Build-Tasks werden in einem eigenen
yaml
-Root Tag mit einem eigenen Namen definiert und überstage
einem gewissen Build-Schritt zugeteilt. - 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. - Über
only
undexcept
kann man definieren, für welche Git-Referenzen man einen bestimmten Job ausführt. In diesem Beispiel wird durchexcept branches
der Job nur für Git Tags ausgeführt, die mit dem Prefixrelease
anfangen, z. B.release/1.2.5
. - 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. - 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 durchexpires_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 Build Job 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 lässt sich, dass auf einem Build Server unterschiedliche Xcode-Versionen installiert sind und ein iOS-Projekt eine bestimmte Xcode-Version benötigt.
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. Zudem werden für diesen Branch über eine fastlane lane die Unit-Tests ausgeführt. Das Resultat des Build Jobs ist für Entwickler:innen 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.
Ein Feature landet bei uns immer über einen Merge Request, der von einem/einer anderen Entwickler:in 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.
Das spart dem/der begutachtenden Entwickler:in 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.
Nach dem Merge eines Features auf develop
, 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.
Bauen einer iOS App
Der älteste und klassischste Weg eine iOS App für die Distribution zu bauen – Ad-Hoc, In-House oder App Store –, führt direkt in Xcode über Product > Archive
. Dafür müssen Entwickler:innen 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 die betroffenen Entwickler:innen nicht zur Verfügung stehen. Und die wenigstens Entwickler:innen, da leidgeprüft, möchten Xcode das Verwalten der Zertifikate überlassen. Diese Lösung eignet sich daher nur für Projekte, die von Entwickler:innen 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 Command Line Tools als Shell-Befehl durchführt. Ein klassischer Befehl zum Bauen des Archives kann dann wie folgt lauten:
1 2 3 |
xcodebuild clean archive -workspace ./iOSApp.xcworkspace -scheme ${SCHEME} -sdk iphoneos11.0 -archivePath ./ -configuration Release ONLY_ACTIVE_ARCH="NO" OTHER_CODE_SIGN_FLAGS=" DEVELOPMENT_TEAM="${DEV_TEAM}" PROVISIONING_PROFILE_SPECIFIER="iOSApp.mobileprovisioning" CODE_SIGN_IDENTITY="${SIGN_ID}" |
Und die IPA kann man aus dem Archive folgendermaßen generieren:
1 2 3 |
xcodebuild -exportArchive -exportOptionsPlist ${EXPORT_PLIST} -archivePath ./${BUILD_DIR}${ARCHIVE_DIR}${SCHEME}.xcarchive -exportPath ./${BUILD_DIR}${IPA_DIR}/ |
Der Ansatz scheint erstmal sehr praktikabel und reicht für viele Anwendungsgebiete bereits aus, da man jetzt unabhängig von Entwickler:innen die App für die Distribution bauen kann.
Ein Nachteil ist jedoch, dass 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 Entwickler:innen 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:innen 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 Build Jobs 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 Build Jobs. Jeder Block mit dem Keyword lane
ist dabei ein Job, der gezielt aufgerufen werden werden kann. Ein Fastfile kann wie folgt aussehen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# 1 fastlane_version "2.84.0" # 2 xcversion(version: "> 9.2") # 3 cocoapods(repo_update: true) # 4 lane :release_build do # 5 update_info_plist(plist_path: "./iOSApp/Info.plist", display_name: “iOSApp", app_identifier: "com.example.iOSApp") #6 run_tests(scheme: "iOSAppTests", clean: true) # 7 build_ios_app(scheme: 'iOSApp', method: 'app-store') # 8 hockey(api_token: 'xxxxxxxxxx', ipa: Actions.lane_context[SharedValues::IPA_OUTPUT_PATH], notes: "Changelog") end |
Ü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.
- Mit
fastlane_version
kann man die minimal zu verwendende Version setzen. Beachten sollte man, dass man über Ruby Gems eine passende Version installiert. - 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. - 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 2 3 |
$ fastlane release_build |
- Mit dem Keyword
lane
und einem eindeutigen Namen (release_build) definiert man einen Build Job. - Am Anfang jedes Build Jobs kann man das Projekt noch über verschiedenste Helfer-Actions für den Build konfigurieren, wie z. B. die
Info.plist
des Projekts anpassen. - Mittels run_tests kann man auf einfache Weise die Unit-Tests eines Target ausführen.
- 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.
- 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 Entwickler:innen 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.
Die Konfiguration im Projekt durchzuführen, bringt den Vorteil, dass sie 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. Dies geschieht direkt als Argument in der build_ios_app Action.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
export_options( method: "app-store", provisioningProfiles: { "com.example.bundleid" => "Provisioning Profile Name", "com.example.bundleid2" => "Provisioning Profile Name 2" } ) |
Ü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 werden. 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:innen 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:innen 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.
Danke für die ausführliche Erklärung. Ich habe meinen Code auf Gitlab gehostet und wollte auch gerne fastlane integrieren. Sobald ich das aber probiere bekomme ich beim builden folgenden Fehler:
em::Ext::BuildError: ERROR: Failed to build gem native extension.
current directory: /Library/Ruby/Gems/2.3.0/gems/unf_ext-0.0.7.5/ext/unf_ext
/System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/bin/ruby -r
./siteconf20181204-78526-131gz4g.rb extconf.rb
checking for main() in -lstdc++… *** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers. Check the mkmf.log file for more details. You may
need configuration options.
Hast du eine Ahnung was das sein könnte ?
Scheint mir zu diesem Github Issue zu passen: https://github.com/bundler/bundler/issues/3372 und mit dem Bundler Gem bzw. Dateisystem-Zugriffsrechten zu liegen.
Scheint mir zu diesem Github Issue zu passen: https://github.com/bundler/bundler/issues/3372 und mit dem Bundler Gem bzw. Dateisystem-Zugriffsrechten zu liegen.
Hallo! Danke für den sehr interessanten Artikel! Welche Rechenleistung haben die Mac Minis, damit sie als Build Server gut laufen?