仙台からこんにちは。asmzです。
今回は弊社のiOSアプリ開発全般で整備しているCI/CD環境の構成と、その実現のために利用しているモバイルアプリ向けCI/CDプラットフォームサービス「Bitrise」の活用方法についてご紹介したいと思います。
はじめに
弊社には、自社VODサービスである「VideoMarket」や現在私が担当している「MIRAIL」、また動画配信プラットフォームとしてアライアンス企業様へ提供している動画再生ライブラリなど、いくつかのiOSアプリプロジェクトが存在します。
しかし、1年半くらい前まではそれらのプロジェクトにはCI環境が整備されておらず、ベータ版の配布やアプリのリリース作業などは全て手動で行われており、多くの作業コストやビルド設定誤りリスクなどを抱えていました。
このような問題が積み重なっていくと長期的にサービスを継続・グロースしていくに当たっての大きな障壁となりうることから、1年半前より少しずつ既存のプロジェクトにCI環境を整備し、その後の新規アプリ開発においても開発の序盤でCI環境を整備するように取り組んできました。
CI/CD環境構成
先述の通り社内にはいくつかのiOSアプリのプロジェクトが存在しますが、ここでは例として私が担当しているMIRAIL iOSアプリのCI環境構成をご紹介します。

プロジェクトによって細部は異なりますが、基本的な流れは概ね以下の通りです。
- BitbucketへのPushやプルリクエスト作成
- BitbucketからBitriseへWebhook
- Bitriseワークフロー実行
- リポジトリClone、環境設定など
- fastlaneでProvisioning Profile取得
- fastlaneでビルドやユニットテスト実行
- Fabric Betaやfastlane deliverでアプリ配布
- Slack通知
Bitriseとは
この環境構成を実現するにあたり、弊社では「Bitrise」というCIサービスを利用しています。
BitriseとはiOSやAndroidなどモバイルアプリケーション向けのCI/CDプラットフォームサービスです。
iOSアプリのビルドが出来るCIサービスは他にもいくつかありますが、証明書・Provisioning Profile関連の連携やWeb管理画面でのビルドプロセスの構築のしやすさ、対応するXcodeバージョンの豊富さや新バージョンへの対応の早さなど、iOS向けのサポートが手厚いことなどを主な選定理由としてBitriseを採用しました。
Bitriseの「Workflow」と「Step」と「Trigger」
Bitriseでは、一連のビルドプロセスのことを「Workflow」、そのWorkflowを構成する1つ1つの細かいタスクのことを「Step」と呼びます。
また、用意した複数WorkflowのうちどのWorkflowを実行するかという「起動条件」のことを「Trigger」と呼びます。MIRAILでは以下のように規定ブランチへのPush(or Merge)やプルリクエスト作成をTriggerとして、各種Workflowを実行するよう設定しています。

[主なTrigger設定]
| Trigger | 実行するWorkflow処理内容 |
|---|---|
feature/*ブランチへのPush |
UnitTest実行 |
developブランチへのPush |
開発環境用アプリをビルドし、Fabric Betaでベータ版配布 |
release/*ブランチからmaster向きのPR作成 |
本番環境用アプリをビルドし、fastlane deliverでAppStoreConnectへアップ |
masterブランチへのタグ付け |
リリースノート追記(後述) |
BitriseのカスタムStep活用例
先にご紹介したようなCI環境を整備するにあたり、一部Bitrise既存のStepでは対応できないようなケースがありました。
ただ、Bitriseでは「Script」Stepを使ってある程度自由にカスタムStepを組むことが可能となっていますので、ここではいくつか独自に用意したカスタムStepをご紹介します。


↑のように「Script」Stepは「Script content」のところに自分で自由にシェルスクリプトを書くことができる(ので、割となんでも出来ちゃう)
CI環境内にSwaggerでモックサーバを立ち上げて自動テスト
あまりこういうやり方することは多くないかと思うんですが、API仕様を管理しているswagger.yamlをリポジトリから取得し、Swagger CodegenでNode.jsのモックサーバソースコードを生成、それを使ってCI環境内にモックサーバを立ち上げることが出来たりします。
[Script StepのScript content記述例]
※諸事情ありまして、Swagger Codegenのバージョンがやや古いです。。
# Get swagger.yaml
git clone git@bitbucket.org:{swagger.yamlを管理している社内リポジトリ名}.git
# Generate Mock Server sources
cd {swagger.yamlを管理している社内リポジトリ名}
wget http://central.maven.org/maven2/io/swagger/swagger-codegen-cli/2.3.1/swagger-codegen-cli-2.3.1.jar -O swagger-codegen-cli.jar
java -jar swagger-codegen-cli.jar generate -i ./path/to/swagger.yaml -l nodejs-server -o ./out # 一時ディレクトリにソース生成
cd ./out
npm install
# Start Mock Server
npm install -g forever
forever start ./index.js # foreverでデーモン化してモックサーバ起動
iOSプロジェクトのテストコードにはlocalhost向けの通信処理を用意しておき、モックサーバ立ち上げた後のStepでそのテスト実行すれば、実際にモックサーバ向けの通信が走ります。

リリース後のGitタグ付け時に自動でリリースノート追記
アプリのストア公開まで完了した後、masterブランチにGitタグを付与し、かつそのバージョンではどのような改修をしたかを簡単にまとめたリリースノートを社内ドキュメントに残していたのですが、作業自体を1つにまとめて自動化しました。
なお弊社では全社的なナレッジ共有システムとしてConfluenceを使用していますので、このStepではCI環境内でのGitコマンドよりタグのコメント文を取得し、Confluence APIを叩いてConfluenceの新規ページに突っ込む、という事を行なっています。
[Script StepのScript content記述例]
PAGE_TITLE="${BITRISE_GIT_TAG}_ios"
ANCESTOR_ID={親ページのページIDを指定(その子ページとして作成される)}
RELEASE_DATE=`date +%Y/%m/%d`
# Generate release notes
NOTE=`git tag -n100 $BITRISE_GIT_TAG | perl -pe 's/\n/\\\n/g' | perl -pe 's/\\\n/<br\/>/g'` # 改行コードの整理
NOTE=`echo "Released At: $RELEASE_DATE<br/>Release Version: $XPI_VERSION ($XPI_BUILD)<br/><br/>Note:<br/> $NOTE"`
# Install jq
brew install jq
# Post to Confluence
curl -s -X POST 'https://yourdomain.atlassian.net/wiki/rest/api/content' \
-u "${ATLASSIAN_ACCOUNT_EMAIL}:${ATLASSIAN_API_TOKEN}" \
-H 'Accept: application/json' -H 'Content-Type: application/json' \
-d "{\"title\": \"${PAGE_TITLE}\",\"type\":\"page\",\"space\":{\"key\":\"YOUR_SPACE_KEY\"},\"status\":\"current\",\"ancestors\":[{\"id\":\"${ANCESTOR_ID}\"}],\"body\":{\"storage\":{\"value\":\"${NOTE}\",\"representation\":\"storage\"}}}" \
| jq '(._links.base)+=(._links.webui)' | jq -r ._links.base > page_url.log
# Export page URL
envman add --key=RELEASE_NOTE_URL --value="$(cat ./page_url.log)" # 取得したURLはSlack通知などで利用
ここでは要点のみ記載していますが、実際には既に同タイトルのページが存在するかどうかチェックし、存在する場合は更新、のような処理も入れたりしています。
実際にPOSTされるとこのような感じになります。

ちなみに手前味噌でアレですが、Confluence APIの簡単な仕様についてはこちらに少しまとめてありますので、ご興味あればご覧ください。
ビルド成果物を自動でGoogleドライブへアップロード
こちらはアプリではなくアライアンス企業様へ提供するiOSライブラリプロジェクトなどで主に利用しているのですが、ライブラリをビルドして生成されたFrameworkファイルを自動でGoogleドライブへ引き上げたい要件があったため用意したStepです。
実際にはGoogleドライブのチームドライブを使用したかったため、コマンドラインからチームドライブへのアップロードに対応している「Rclone」というツールを用いて実現させました。
Rcloneの簡単な仕様については、またしても手前味噌でアレですがこちらに少しまとめてありますので、ご興味あればご覧ください。
で、実際のScriptは以下のような感じになります
[Script StepのScript content記述例]
※このStepの前に「~/.config/rclone/rclone.conf」にRclone設定ファイルを別途配置しておく
# Install rclone
brew install rclone
# Setup path
SCHEME={RcloneのSCHEME名を指定}
DIST_DIR=$(TZ=Asia/Tokyo date +%Y%m%d_%H%M)_$BITRISE_GIT_MESSAGE
FROM_PATH=$BITRISE_SOURCE_DIR/DerivedData/Artifacts # iOS Frameworkファイル出力先を指定
TO_PATH=$SCHEME:/$DIST_DIR
# Execute rclone
rclone sync -n $FROM_PATH $TO_PATH && \ # dry-run
rclone sync $FROM_PATH $TO_PATH
なお、このScript自体はiOSのFrameworkファイルに特化した処理になっている訳ではなく、仕組み的には任意のファイルをGoogleドライブに引き上げることが可能ですので、他にも何らかアイデア次第で応用は効くかなと思います。
おわりに
以上、弊社iOSアプリのCI環境についての紹介でした。今後同じような環境整備される方いらっしゃいましたら、何らか参考にして頂ければ幸いです。
なお、今回書ききれませんでしたが、MIRAILではAndroidアプリでもBitriseを用いて概ね同等レベルのCI環境を整備しています。iOSではコマンドラインでのビルド作業効率化のためにfastlaneを導入していましたが、Androidの場合はGradleの機能が十分充実しておりコマンド実行も容易なため、BitriseからGradleコマンドを叩くようなWorkflowとしています。
また、弊社東京チームでは現在Flutterを採用した開発も行っており、そちらもBitriseを用いてCI環境を構築しておりますので、今後FlutterでのCI環境整備ノウハウなどもこのブログを通じて共有できればと思っています。
PR
2019/9/5〜9/7に開催されるiOS関連技術をコアのテーマとした技術カンファレンス「iOSDC Japan 2019」において、弊社もシルバープラン & ボトルウォータースポンサーとして協賛しております!
私は残念ながらプロポーザル採択されなかったのですが、一般参加者として参加しますので、もし見かけたらお声がけください!