前回はビルド設定とスキーマにより本番環境とテスト環境を数クリックで切り替えられる仕組みを説明しました。
今回はTestFlightによるアプリ配布と、Jenkinsを組み合わせてアプリ開発者以外でもアプリ配布ができるようになる仕組みの構築について説明します。
はじめに
背景と問題点
私がRettyで仕事を始めたとき、アプリを配布する仕組みは特になく、アプリの評価が必要になるたびに実機を借りてインストールを行い、評価をしていました。その後、会社のメンバーやインターンの数が増えてくるにしたがって、以下のような問題が出てきました。
- 評価を行うメンバーの全端末にそれぞれインストールするのが煩雑になってきた
- 開発者の数は増えたがiOS開発者は増えなかったため、インストール依頼の負荷がiOS開発者にかかるようになった
- 開発者の増加にともない開発環境が増え、ビルド設定とスキーマでカバーしきれなくなっていた*1
このような問題を解決するため、Rettyでは以下のようなアプローチをとりました。
- TestFlightのようなOver The Air(以下、OTA)のサービスを利用し、アプリを全端末や特定のグループに対して一斉配信するようにした
- Jenkinsを導入し、アプリ開発者でなくても好きなタイミングでアプリを配布できるようにした
- Jenkinsからシェルスクリプトを使って設定を自動的に書き換えて、ビルド設定とスキーマに設定されていない環境向けのアプリも作れるようにした
ここから、それぞれのアプローチについて詳しく説明します。
[*1] Rettyでは開発環境を1人につき1つづつ払い出しているため、2014.7.27現在では30弱の開発環境が存在しています
TestFlightとは?
TestFlightとは、OTAと呼ばれる仕組みのサービスの1つで、iPhoneをMacにつないで実機インストールしなくても、Web上からアプリをダウンロード、インストールするための仕組みです。いまのところ無償で使えます(2014年8月現在)。
iOS5以降では、AppleによるiOSのバージョンアップもOTAで配布されるようになりました。TestFlightは配布対象となるグループの管理機能とアプリ配布のためのAPIを備えているため、簡単に、かつコマンドラインで必要な人にだけアプリを配布できます。

図1: TestFlightトップページ
Jenkinsとは?
Jenkinsとは、もともとは継続的インテグレーションを行うためのソフトウェアです。プラグインをインストールすることで機能を拡張し、ジョブの作り方によっては自動または手動によるさまざまな処理を実行できます。これを利用してXcodeのビルド・アーカイブを実行し、TestFlightで配布するまでの処理を行います。

図2: Jenkinsトップページ
TestFlightとJenkinsの導入前後で変わったこと(Rettyの場合)
ご紹介した2つの策を適用した前後で、Rettyには以下のような変化がありました。
BEFORE
- アプリが必要になるたび個別にアプリを実機インストールしていた
- アプリ開発者以外はアプリのインストールができなかった
- ビルド設定とスキーマに記載のない環境のアプリは基本的に作らない方針で、必要な場合には個別に設定を書き換えてビルドとインストールを行っていた
AFTER
- 必要な配布対象をあらかじめグルーピングしておき、まとめてアプリをOTAで配布・インストールできるようになった
- アプリ開発者でなくても数クリックでアプリを配布できるようになった
- Jenkinsにビルドに必要な設定項目を記入しておくことで、スキーマには存在しない開発環境のアプリでも作成できるようになった
TestFlightの導入
TestFlightの導入にあたって必要な項目を挙げます。細かい部分についてはWeb上にも多くの情報があるためポイントを絞って紹介します。
アカウントの作成
チームの登録:まずTestFlightにアプリを配布するためのチームを登録する必要があります。チームでは複数のアプリ・テストユーザを管理できます。TestFlightには、こちらからサインアップできます。
テストユーザの登録:テストユーザはInvite Peopleのページから登録できます。招待したい人のメールアドレスを入力してテストユーザを招待します。全てのテストユーザーを管理者が招待するのは煩雑なため、招待用 のURLを作成し、そこからユーザー登録することも可能です。招待用URLはこちらのページから作成できます(図3)。
アプリの登録:作成したアプリをTestFlightにアップロードしたタイミングで自動的にアプリが追加されます。また手動でipaファイルをアップロードすることでも追加されます。アプリはBundleIDごとに管理されるため、違う BundleIDのアプリを配布した場合には別のアプリとして管理されます。

図3: 招待用URL確認画面
配布グループの作成
アプリを配布をするときには、全員に配布したい場合もあれば、特定のグループの人たちにだけ配布したい場合もあります。このような時にはDistribution Listというグループを作ることで、指定したグループに属する人達だけにアプリを配布できるようになります。Rettyの場合は、ケースに応じて以下のようなグループを作成しています。
- developer:Web開発者
- designer:デザイナー
- ios developer:iOS開発者
- all:全員
Distribution Listはこちらの画面から追加できます。
コマンドラインによるTestFlightでのアプリ配布
TestFlightにはUpload APIがあり、これを利用すると管理画面を開かなくてもコマンドラインから直接アプリを配布できます。今回の問題を解決するために、このAPIを使います。Upload APIについてはこちらのページに説明があるのでご参照ください。
またUpload APIでアプリを配布するには、「どのチームに対して」「誰が」配布したのかを明確にするためにチームトークンとAPIトークンというものが必要になります。チームトークンとAPIトークンはそれぞれ下記のURLから確認できます。
- チームトークン:https://www.testflightapp.com/dashboard/team/edit/
- APIトークン:https://www.testflightapp.com/account/#api
Upload APIのページに説明もありますが、下記のようなcurlコマンドを実行することでアプリを配布できます。最終的にはこのコマンドをJenkinsから実行してアプリを配布します。
curl http://testflightapp.com/api/builds.json -F file=@testflightapp.ipa -F dsym=@testflightapp.app.dSYM.zip -F api_token='API_TOKEN' -F team_token='TEAM_TOKEN' -F notes='app comment' -F notify=True -F distribution_lists='developer'
ワイルドカード Provisioning Profile
前節では、コマンドラインからアプリの配布をする方法を説明しました。この方法で、Jenkinsを使ってアプリを配布できるようにはなります。しかし「開発者の増加にともなって増えた開発環境を切り替える」という課題がまだ残っています。
TestFlightで配布するアプリはProvisioning Profileによってprovisionされたものでなければならないのですが、Facebookと連携する場合や、環境ごとに別アプリとしてインストールしたい場合には、BundleIDを一意のものにはできません。*2
この問題は、ワイルドカードProvisioning Profileを作ることによって解決できます。
[*2] BundleIDを変えておくとiPhoneに別アプリとしてインストールすることができます
ワイルドカード Provisioning Profileとは?
一般にアプリをAppStoreにリリースする際のDistribution Profileは、BundleIDが完全一致したProvisioning Profileを作ります。その場合は、当然ですが、そのBundleID以外のアプリをprovisionすることはできません。
ワイルドカードProvisioning Profileとは、複数のBundleIDに対応できるProvisioning Profileのことです。ただし、PUSH通知を飛ばせなくなる等の制限があるため、目的によっては別のアプローチが必要になります。
Rettyの場合、本番環境に接続する用のアプリはPUSH通知のテストも行うため、そちらは完全一致するBundleIDを使い、開発環境に接続するためのアプリにはBundleIDの利用数とProfile管理の手間を減らすためにワイルドカードProvisioning Profileを使う、という使い分けをしています。
ワイルドカード Provisioning Profileの作成と設定
では実際にワイルドカード Provisioning Profileを作ってみましょう。
Member Centerにログインし、Certificates,Identifiers & Profilesを開きます。
- Identifiersを選択してAdd IDsを開き、「+」ボタンをクリックします
- Register iOS App IDのページが開いたら、図11のようにリリースしているアプリのApp ID Prefixを選択します(通常はTeam ID)。App ID Suffixには「Wildcard App ID」を選び、アスタリスク(*)を記入します
- Provisioning Profileを選択し、Add iOS Provisioning Profileから Adhoc Distribution Profileの作成を選択します。App IDからは先ほど作成したものを選択してします
- Xcodeで先ほど作成したProvisioning Profileをダウンロードし、対象のビルド設定に適用します(図4)

図4: 作成したワイルドカード Provisioning Profileを対象のビルド設定に適用
Jenkins Jobの作成と設定
Jenkinsのインストール
iPhoneアプリの配布をJenkinsから行うために、MacへJenkinsをインストールする必要があります。homebrewコマンドを使うことで簡単にインストールできます。
> brew install jenkins
使用するプラグイン
配布するiOSアプリを生成するため、今回は以下のプラグインをインストールしています。Xcodeプラグインが含まれていませんが、Rettyで管理対象としているディレクトリの構成が少し特殊で、そのまま適用できなかったため使用していません。
- conditional-buildstep
- Environment Injector Plugin
- Flexible Publish Plugin
- Git Client Plugin
- Git Plugin
- Next Build Number Plugin
- Run Condition Plugin
- Token Macro Plugin
これらのプラグインを使った設定などは、この後の節で説明していきます。
ジョブの作成(本番環境のアプリ)
まずは本番環境に接続するアプリを配布するためのジョブを作ります。後ほどこの内容をベースにして開発環境を切り替えて、アプリを配布するジョブを作ります。
ジョブの作成
「新規ジョブ作成」をクリックし、「フリースタイル・プロジェクトのビルド」を選択します。名前は適切なものを付けましょう。(図5)

図5: Jenkinsジョブの新規作成
Gitからの最新バージョンのチェックアウト
新規ジョブが作成されるとジョブの設定画面が開きます。まずはソースコードをサーバから取得するための設定をしていきましょう。Rettyの場合はGithubでソースコード管理をしているため、Githubからソースコードをチェックアウトできるようにします。
「ソースコード管理」からGitを選択し、「Repository URL」 にGithubの URLを入力します。「Credentials」にはGithubに登録したssh公開鍵の設定を選びます(Githubのsshキー設定はGithubのサイトで行います、図6)。
Jenkinsへの鍵登録はトップ画面の「認証情報」->「グローバルドメイン」->「認証情報の追加」から行います(図7)。また「Branches to build」でブランチを選択することもできますが、今回は特に変更せずデフォルト(*/master)のままにしておきます。

図6: Githubのリポジトリ設定

図7: 公開鍵の設定
環境変数の追加
Jenkinsには実行時にシステムから与えられる環境変数がありますが、Environment Injector Pluginをインストールすることで自分で設定した環境変数を柔軟に設定して、ジョブを実行できます。
今回は下記のような設定を行います。
「ビルド」欄の「ビルド手順の追加」から「環境変数のインジェクト」を選択し、入力欄に以下の設定を記載します。
# アプリケーション名 APP_NAME=Retty # 対象SDK SDK=iphoneos # Xcodeのワークスペースファイルの場所 WORKSPACE_FILE_PATH=${APP_NAME}.xcworkspace # ビルド時に利用するスキーマの名前(スキーマについては前回の連載を参照してください) SCHEME_NAME=${APP_NAME} ##TestFlight用の設定 # .app, .ipaファイルの出力先ディレクトリ設定 #($WORKSPACEはJenkins固有の環境変数) OUT_IPA_DIR=$WORKSPACE/testflight # .appファイルの出力ファイル名設定 PRODUCT_NAME=${OUT_IPA_DIR}/${APP_NAME}.app # .ipaファイルの出力ファイル名設定 IPA_FILE_NAME=${OUT_IPA_DIR}/${APP_NAME}.ipa #dSYMの出力ファイル名設定 DSYM_DIR_NAME=${OUT_IPA_DIR}/${APP_NAME}.app.dSYM # dSYMのzip出力ファイル名設定 ZIP_DSYM_FILE_NAME=${OUT_IPA_DIR}/${APP_NAME}.app.dSYM.zip # TestFlightのアプリトークン API_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # TestFlightのチームトークン TEAM_TOKEN=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
CocoaPodsのインストール
Rettyではライブラリの管理にCocoaPodsを使っています。そのためビルド前にライブラリをインストールしておく必要があります。CocoaPodsのプラグインもありますが、Rettyではディレクトリ構成が特殊なため(図8)シェルスクリプトを使って実行しています。

図8: RettyのGithubファイル構成
「ビルド」欄の「ビルド手順の追加」から「シェルの実行」を選択し、入力欄に下記の設定を記載します。
#CocoaPods # $WORKSPACEはJenkinsがGitをチェックアウトする場所になります # podfileが Rettyディレクトリ配下にあるため、一旦cdで移動します cd $WORKSPACE/Retty pod install
Xcodeビルドの実行
それではいよいよ実際にビルドを実行しますXcodeのビルドは、コマンドツールxcodebuildを使っても同じことができます。時々きちんとCleanしてからでないとうまく変更が反映されない場合があるため、ビルド前にいったんプロジェクトのCleanを行います。
「ビルド」欄の「ビルド手順の追加」から「シェルの実行」を選択し、入力欄に以下の設定を記載します。
cd $WORKSPACE/Retty # ビルド前に一回プロジェクトをCleanする xcodebuild clean -scheme ${SCHEME_NAME} -workspace ${WORKSPACE_FILE_PATH} # スキーマ、SDK、ビルド設定、出力先を指定して xcodebuild -scheme ${SCHEME_NAME} \ -workspace ${WORKSPACE_FILE_PATH} \ -sdk ${SDK} \ -configuration Adhoc\ CONFIGURATION_BUILD_DIR=${OUT_IPA_DIR}
ipaファイルとzip化したdSYMファイルの作成
プロジェクトをビルドしただけでは .app形式しか出力されないため、配布が可能な .ipa形式にする必要があります。 .ipa形式のファイルはxcrunコマンドを使うことで作成できます。またTestFlightアップロード時にはzip化したdSYMファイルも必要になるのでこのタイミングで作成します。
「ビルド」欄の「ビルド手順の追加」から「シェルの実行」を選択し、入力欄に下記の設定を記載します。
cd $WORKSPACE/Retty #ipaファイルの作成 xcrun -sdk "${SDK}" PackageApplication "${PRODUCT_NAME}" -o "${IPA_FILE_NAME}" #zip化したdSYMファイルの作成 zip -r $ZIP_DSYM_FILE_NAME $DSYM_DIR_NAME
TestFlightでのアプリ配布
いよいよ最後にTestFlightの配信処理を行います。コマンドラインからcurlを実行するため、もう一度「ビルド」欄の「ビルド手順の追加」から「シェルの実行」を選択し、入力欄に下記の設定を記載します。
# TestFlight upload API # allという名前の Distribution List(配布リスト)があることを想定しています curl -v http://testflightapp.com/api/builds.json\ -F file=@$IPA_FILE_NAME\ -F dsym=@$ZIP_DSYM_FILE_NAME\ -F api_token="$API_TOKEN" \ -F team_token="$TEAM_TOKEN" \ -F notes="Jenkins app auto build" \ -F notify=True\ -F distribution_lists="all"
それではアプリの設定を保存してビルド実行ボタンを押してみましょう。ビルドが開始されてアプリが全員に配布されます。(図9)

図9: ビルドの成功
アプリ配布先をビルド毎に変えるための設定
ここまでの設定で基本的なアプリ配布ができるようになりました。ただし、配布先が全体に固定されてしまっていて不便なため、少し改良します。
ジョブの設定画面を再度開き、画面上のほうにある「ビルドのパラメータ化」にチェックを入れ「パラメータの追加」から「選択」を選びます。すると入力欄が新しくできるので、名前には DISTRIBUTION_LIST、TestFlightで作成した配布リストを改行区切りで入力します。説明はあってもなくても大丈夫です(図10)。

図10: 配布先の設定追加
このように設定すると先ほどまでは「ビルド実行」となっていたボタンが「パラメータ付きビルド」という表示に変わります。ボタンを押すとビルドが実行される前にパラメータの設定を入力できる画面に変わり、先ほど設定した配布先の設定項目が追加されていることが確認できます。(図11)

図11: 実行時に配布先を設定
また、ここで設定した内容は名前と同じ環境変数に入るため、先ほどのTestFlight実行部分を下記のように書き換えることで選択した配布先に対してアプリを配布できるようになります。
# TestFlight upload API curl -v http://testflightapp.com/api/builds.json\ -F file=@$IPA_FILE_NAME\ -F dsym=@$ZIP_DSYM_FILE_NAME\ -F api_token="$API_TOKEN" \ -F team_token="$TEAM_TOKEN" \ -F notes="Jenkins app auto build" \ -F notify=True\ -F distribution_lists="$DISTRIBUTION_LIST"
ジョブの作成(開発環境のアプリ)
いよいよ開発環境用のアプリを配布するためのジョブを作成します。
開発環境用のアプリ配布ジョブについては本番環境用のアプリを作成する時とほぼ同じ内容なので、重複する部分については割愛します。
新規ジョブの追加
今回は前節のものと基本的に同じジョブを作るので、手間を軽減するために既存のジョブをコピーして作成します。
新規ジョブ作成画面で「既存ジョブのコピー」を選択し、先ほど作ったジョブの名前を入力すると、そのジョブを元にしたコピーが作成されます(図12)。

図12: 既存のジョブをコピーして新規作成
ビルドパラメータの追加
前節の最後に記載した配布先の追加と同様に、ビルド前に指定できるパラメータを追加して接続対象の開発環境を切り替えるための仕掛けをします(表1)。Rettyの場合では以下のビルドパラメータを追加しましたが、導入するアプリの特性に応じて変更してみてください。
表1: ビルドパラメータ名とその内容
ビルドパラメータ名 | 内容 | 解説 |
---|---|---|
HOST_NAME | 接続先サーバホスト名(BundleIDと兼用) | 接続先の指定と、環境ごとに異なるアプリとしてインストールするため |
APP_DISPLAY_NAME | アプリの表示名 | 接続環境の異なるアプリの判別をしやすくするため |
FACEBOOK_APP_ID | FacebookアプリID | Facebook認証のテストをしたいときに指定が必要 |
APP_SCHEME | URL Scheme | アプリ毎にURLスキーマを変えないと別のアプリを起動してしまうため |
アプリ設定の置換と設定の巻き戻し
ビルドパラメータを追加し、アプリビルド時の接続環境を切り替える準備が整いました。実際にビルドフェーズで設定部分を置き換えていきましょう。
設定の置き換えにはsedコマンドを使うため、「ビルド」欄の「ビルド手順の追加」から「シェルの実行」を選択し、入力欄に下記の設定を記載します。また、実行の順番はCocoaPodsのライブラリの取得前に変更します。
# Info.plistの編集 # ホスト名は想定のものになるのでみなさんの環境に合わせて変更してください sed -i -e "s/develop.retty.me/${DEVELOP_HOST_NAME}/g" Retty/Retty-develop-Info.plist sed -i -e "s/123456789012345/${FACEBOOK_APP_ID}/g" Retty/Retty-develop-Info.plist sed -i -e "s/rettydevelop/${APP_SCHEME}/g" Retty/Retty-develop-Info.plist sed -i -e "s/\${PRODUCT_NAME}/${APP_DISPLAY_NAME}/g" Retty/Retty-develop-Info.plist # 設定ファイル の編集 sed -i -e "s/develop.retty.me/${DEVELOP_HOST_NAME}/g" Retty/Defines.h sed -i -e "s/123456789012345/${FACEBOOK_APP_ID}/g" Retty/Defines.h sed -i -e "s/rettydevelop/${APP_SCHEME}/g" Retty/Defines.h
前回の記事では「sedによる環境設定の切り替えは毎回コマンドを打たなければならないので面倒だ」と書きましたが、今回はJenkinsによって自動実行をするので、sedで変更した内容を元に戻す処理(巻き戻し)についても自動実行されるように設定しておけば問題ありません。
「ビルド」欄の「ビルド手順の追加」から「シェルの実行」を選択し、入力欄に下記の設定を記載します。この巻き戻しはジョブの最後に実行される必要があるので手順も一番最後にもっていきます。
# Git管理されたファイルの変更を全てリセット git reset HEAD --hard
TestFlightでのアプリ配布
それでは、実際にアプリを配布してみましょう。「パラメータ付きビルド」のボタンを押して接続環境固有の情報を入力し「ビルド」ボタンを押します。
正常にアプリが配布されれば無事設定完了です。あとは必要なだけ開発環境用のアプリを作って配布をしましょう。(図13)

図13: 開発環境アプリを複数入れた状態
まとめ
いかがでしたでしょうか? 普段の細かいルーチンワークですが、自動化の仕組みができることによりそれにかける時間コストの削減や、自分がいなくても回ることで仕事の効率化が進みますので、ぜひチャレンジしてもらえたらと思います。
次回は、Retty社内のネットワークの設定と工夫についてご紹介したいと思います。