スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

Quipper での CodePush を使った OTA 配信とその自動化

この記事は React Native Advent Calendar 2017 6 日目の記事です。5 日目は Quipper 同僚の @hotchemi によるQuipperにおけるReact Native活用事例でした。

こんにちは、Quipper で Software Engineer をやっている @yuya-takeyama です。 入社以来ほとんど Web のサーバサイド・クライアントサイドをやってきましたが、最近は React Native アプリのプロジェクトで TypeScript を書いています。

昨日の記事でも軽く触れてますが、Quipper で最近運用が始まった OTA (Over The Air) によるアプリ配信の運用とその自動化について詳しく紹介します。

なお、実際に動くコード例として以下のリポジトリを用意しました。

CircleCI の設定や、スクリプトについてはコピペして対象のアプリ名等を手直しすればそのまま使えると思うので、よければお試しください。 (なお、現在は CircleCI との連携を止めています)

OTA (Over The Air) とは

この記事においては React Native における OTA についてなので、アプリ中の JS 部分 (JS Bundle) だけを HTTP 経由で配信することを指します。

React Native を知らない方向けに説明すると、JS 部分というのはアプリケーション層のほぼ全体と言えます。

コンパイルされるコードとしてはもちろん Objective-CJava によるネイティブのコードも含まれますが、それらは各プラットフォームの UI コンポーネントをブリッジする部分のようなフレームワーク層であり、開発者が触るアプリケーション層はほぼ全て JavaScript で実装可能です。 つまり、ちょっとした不具合の修正だったり、スタイルの微調整などをいつでも行うことができます。

逆に、ネイティブのコードを含むモジュール等を追加したような場合は、CodePush での更新はできません。

今回紹介する CodePush は 2 年前にはすでにサービスが始まっており、以下の記事で紹介されています。

最近の CodePush 事情

もともと CodePush は Microsoft が運営する独立したサービスでしたが、つい先日 Visual Studio App Center というサービスに統合されました。

アプリのリリースとほぼ同タイミングだったのでちょっと驚きましたが、SDK はそのままでも使えるので特に問題ありませんでした。運営も変わらず Microsoft です。

App Center が何かというと CI とか MBaaS とかが全部入りのサービスで、React Native に限らず Cordova によるアプリや通常の Android/iOS ネイティブアプリ等も対応しています。

今のところビルドとテストは CircleCI、クラッシュリポートは Sentry、プッシュ通知は Firebase という感じで使い分けており、今は CodePush 以外の機能は使っていません。

CodePush のセットアップ

セットアップについては公式のドキュメント通りで問題ないのでそちらを参照してください。

完全に新規のアプリにインストールするときはほぼ問題ないと思いますが、react-native-navigation のようなネイティブのコードを含むモジュールが既に入っている場合はうまくいかないこともあるので、注意が必要です。

react-native link コマンドで自動修正が行われた AppDelegate.m 等のファイルはよく注意が必要です。

私たちの場合は以下のようなコードになってしまっていたため、CodePush の分岐に絶対に入らないようになってしまっていました。

  #ifdef DEBUG
    #ifdef DEBUG
        jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
    #else
        // 本当はここの分岐に入って欲しいけど、よく見ると絶対にこの分岐には入らない
        jsCodeLocation = [CodePush bundleURL];
    #endif
  #else
    jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
  #endif

CodePush の変更適用後、アプリを再起動するとロールバックしてしまうという現象に悩まされましたが、以下のように直すことで解消できました。

  #ifdef DEBUG
    jsCodeLocation = [CodePush bundleURL];
  #else
    jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
  #endif

教訓としては react-native link は過度に信用しないように、ということで。

CodePush による自動 OTA Updates のフロー

基本的には Git Flow とほぼ変わらなくて、リリース方法が CodePush とバイナリリリースとで分かれているので、その部分だけ手を加えています。

master ブランチは fastlane でのバイナリの自動リリースに使うつもりですが、頻度は高くないので後回しにしてます。

ビルドやデプロイは先に書いた通り CircleCI を使って、GitHub でのマージをトリガーに実行しています。

App Center の機能が便利そうならそっちを使ってみてもいいかもしれませんが、今のところは特に困っていることもないので考えていません。

必要なツールのインストール

CodePush へのアプリの追加や Deployment と呼ばれる環境の追加、デプロイの実行等は基本的に appcenter-cli と呼ばれるツールが必要です。手動操作用と自動化用にグローバル・ローカル両方にインストールします。

# グローバルインストール
$ npm install -g appcenter-cli

# プロジェクトへのローカルインストール
$ yarn add -D appcenter-cli

ところでこの appcenter-cli ですが、ここ数日の統合のタイミングでドカドカと Pull Request が作られてまとめて取り込まれたせいもあってか、割と致命的な不具合が見つかっては直されている状態なので、正直品質はかなりアレかもしれません。必要に応じては旧 code-push-cli を使うこともあるかもしれません。OSS なので自分で直して Pull Request を送るのも良いと思います。

App Center への登録

登録は GitHub アカウントでのログイン等で簡単にできます。

Organization の作成

会社で運用する上では Organization を作っておくのが良いでしょう。Web 上のコンソールから作成します。

App の作成

作った Organization に紐付く形で App を作成します。これも Web 上のコンソールから。

間違って個人アカウントに紐づけてしまっても、各 App ごとの Settings 画面から Transfer app to organization というメニューで移管が可能なので問題ありません。

アクセストークンの作成

デプロイを実行するためのアクセストークンを作成します。

$ appcenter tokens create -d '用途の説明'

アクセストークンは後からの参照はできません。CircleCI であれば環境変数として登録しておきます。

わからなくなった場合は古いトークンを削除して再発行し直すのがいいので、メモする必要はないでしょう。

Deployment の作成

Deployment というのは CodePush においては環境のことだと思ってください。デフォルトでは Production と Staging というのが作られます。

Quipper では以下のように使っています。

  • Development: develop ブランチにマージしたものを自動でデプロイ
  • Release: リリーステスト用。release/codepush/v* にマージすると自動デプロイ
  • Production: 本番用。deploy/codepush にマージすると自動デプロイ

必要に応じて以下のように作成します。

$ appcenter codepush deployment add --app OurOrg/AwesomeApp NewDeploymentName

デプロイのためのスクリプトの用意

自動デプロイのために以下のコマンドを用意します。

  • yarn codepush:login: アクセストークンによるログイン
  • yarn codepush:deploy: CodePush へのデプロイの実行

今回は上述の通り CircleCI の環境変数にアクセストークンを持たせているので、$CODEPUSH_ACCESS_TOKEN として参照します。

{
  "scripts": {
    "codepush:login": "appcenter login --token $CODEPUSH_ACCESS_TOKEN",
    "codepush:deploy": "./scripts/deploy_codepush Production"
  }
}

yarn codepush:deploy./scripts/deploy_codepush というシェルスクリプトに切り出してみました。

ここでは --target-binary-option というオプションを指定しています。これはネイティブモジュールの追加・修正があった場合、CodePush だけではうまく変更が適用できないので、Semantic Versioning でバイナリリリースのバージョンを指定するという機能です。

TARGET_BINARY_VERSION というファイルでバージョンを指定しています。

# The content of this file is specified as `--target-binary-version` in the deploy script `./scripts/deploy_codepush`.
# When you introduced a new native module, please release the binary first and update this file.
# You can specify it by following Semantic Versioning.
# ref: https://docs.microsoft.com/en-us/appcenter/distribution/codepush/cli#target-binary-version-parameter
1.2.0

本来は Semantic Versioning で 1.2.x~1.2.0 といった指定ができるはずですが、今は不具合があるので 1.2.0 のような固定バージョンを指定する必要があります。以下の Pull Request がリリースされれば解消されるはずです。 (マージ後、v1.0.6 としてリリースされました)

なお、現在のプロジェクトでは TypeScript を使っているので、実際には事前に yarn build コマンドで JS ファイルへのトランスパイルを実行しています。

CircleCI での自動デプロイの設定

CircleCI 2.0 の Workflow を使っています。

あとは release/codepush/v* といった名前のブランチを作ればリリース用の JS Bundle がデプロイされ、QA を通れば deploy/codepush にマージすることでユーザ向けにデプロイされます。

リリース用 PR の自動作成

せっかくなのでそのリリース用のブランチ・Pull Request の作成も自動化します。

こんなスクリプトを用意していみました。

これは手元で手動実行してもいいですが、Jenkins からボタン一発で実行するようにセットアップしています。

環境変数 $CODEPUSH_ACCESS_TOKEN をセットして yarn codepush:login を実行した後、環境変数 $GITHUB_ACCESS_TOKENGitHub のアクセストークンが必要です。

また、初回だけは deploy/codepush ブランチを事前に用意して置く必要があります。

実行すると以下のような Pull Request が作成されます。

スクリプト内では以下のようなことを行なっています。

  1. Production の現行のバージョンを取得
  2. バージョンをインクリメントして次のバージョンを算出
  3. 次のバージョンを元に release/codepush/v* といったブランチを develop から作成
  4. deploy/codepush との差分を元に、コミット一覧からマージした Pull Request の一覧を作成
  5. Pull Request を作成

これで、必要な機能が develop ブランチにマージされて入れば、そのあとのリリースのプロジェクトは開発者以外でも行えるようになりました。

あとは開発に集中して高速のリリースを回して行きましょう!