📓

カナリーにおけるReact Native + Expo を用いたOTA運用戦略

に公開

はじめに

カナリーでは、React Native + Expo を用いてモバイルアプリの開発を行っています。

その中でも私たちのチームは、「OTA(Over-the-Air)アップデート」という仕組みを活用することで、ユーザーに対してアプリの修正を迅速に届けられる体制を整えています。

本記事では、我々がどのように OTA アップデートを運用しているかについて紹介します。

OTA アップデートとは?

OTA アップデートとは、アプリのバイナリを更新せずに、JavaScript バンドルや静的アセットのみを差し替えて配信する仕組みです。Expo の EAS Update 機能を用いることで、ストアの審査を経ずに修正を即座にリリースすることが可能です。

審査・反映に通常は半日〜1日程度かかるところ、OTA を使えば即時でのリリースが可能です。

OTA 対象ファイルの範囲

Expo を用いた OTA アップデートで配信可能なファイルは、JavaScript バンドルと静的アセット(画像・フォントなど)に限定されます。一方で、ネイティブコードの変更や一部の設定ファイルの変更は、App Store / Google Play での通常のアプリ更新が必要になります。

OTA アップデートで反映されるもの

  • .js, .ts, .tsx ファイル(ロジック・UI の変更)
  • 静的アセット(assets/ 配下の画像、フォントなど)
  • translations/ ディレクトリなど、コードにバンドルされる JSON 等

OTA アップデートで反映されないもの

  • ネイティブコード
    • 例: ネイティブライブラリの追加・更新、MainActivity.javaの変更など
  • app.json / app.config.js の一部
    • 例: アプリアイコン、スプラッシュ画像、バンドル ID など
  • ビルド設定に依存するファイル
    • 例: Info.plist, AndroidManifest.xml など

カナリーチームの OTA 運用ルール

原則

  • 本番環境では、OTA アップデートは原則禁止としています。

例外:緊急対応・軽微な修正

以下の場合に限り、OTA アップデートを利用します:

  • 緊急バグ修正

その際は、PdM および テックリード の許可を得ることを必須としています。

このように限定している理由は、ストアポリシーを遵守するためです。

App Store

Apple のApp Review ガイドライン 2.5.2では、

アプリの機能を導入したり変更したりするコードをエリア外からダウンロード、インストール、実行することは許可されません。

と記載されており、後から機能追加を行うような OTA 更新は許可されていません。

Google Play

一方で、Google Play Console では OTA アップデートが明示的に禁止されているわけではありません。

Google Play のポリシーでは、実行可能コード(.dex/.so など)の外部取得は NG ですが、次のような例外が認められています

Google Play で販売または配布されるアプリについては、Google Play の更新機能以外の方法によりアプリ自体の変更、差し替え、更新を行うことはできません。同様に、Google Play 以外の提供元から実行コード(dex、JAR、.so などのファイル)をダウンロードすることもできません。この制限は、Android API への間接アクセスを提供する仮想マシンまたはインタープリタ(WebView またはブラウザ内の JavaScript など)で実行されるコードには適用されません。

React Native + Expo のように JavaScript ベースで構築されたアプリはこの例外に該当するため、ポリシー上も比較的柔軟に対応可能です。

OTA 運用ルール

次は実際に OTA アップデートをどのように使用しているかを説明します。

Dev 環境

開発中は、PR にコードを push するだけで OTA が反映されるよう、GitHub Actions を用いた自動化を構築しています。

フロー

  1. PR 作成
  2. devcheck ラベルを付与し、dev 環境生成
  3. PR に修正コードを push
  4. GitHub Actions により OTA アップデートが自動実行される

実際の GitHub Actions ワークフロー

name: Publish Dev

on:
  workflow_dispatch:
  pull_request:
    types:
      - 'synchronize'

jobs:
  publish:
    if: contains(github.event.pull_request.labels.*.name, 'devcheck')
    name: Install and publish
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version-file: '.tool-versions'
          cache-dependency-path: 'yarn.lock'
          cache: yarn

      - name: Setup Expo and EAS
        uses: expo/expo-github-action@v8
        with:
          eas-version: 12.3.0
          token: ${{ secrets.EXPO_TOKEN }}

      - name: Setting Git User
        run: |
          git config user.name github-actions
          git config user.email github-actions@github.com

      - name: Replace channel
        run: |
          BRANCH_NAME=${GITHUB_HEAD_REF} npx zx ./scripts/zx/replace_dev_release_channel.mjs
          git add eas.json
          git commit -m "replace channel"

      - name: Install dependencies
        run: yarn install --frozen-lockfile

      - name: EAS update
        run: |
          BRANCH_NAME=$(echo "${GITHUB_HEAD_REF}" | tr '[:upper:]' '[:lower:]' | sed 's/\//-/g')
          eas update --branch ${BRANCH_NAME} --message "EAS update"
          eas channel:edit ${BRANCH_NAME} --branch ${BRANCH_NAME}

この workflow の特徴は以下の通りです

  • PR に devcheck ラベルが付いていることをトリガーに OTA を実行
  • replace_dev_release_channel.mjseas.json のチャンネルを適切に書き換え
    • replace_dev_release_channel.mjs
      /**
       * eas.jsonのdev:releaseのchannelをブランチ名で置き換えるためのスクリプト
       */
      
      const _channel = process.env.BRANCH_NAME;
      const toChannelString = (value) => {
        const INVALID_STRING = '/';
        const VALID_STRING = '-';
        return value.replaceAll(INVALID_STRING, VALID_STRING).toLowerCase();
      };
      
      const channel = toChannelString(_channel);
      
      const easConfig = await fs.readJson('./eas.json');
      easConfig.build['dev:release'].channel = channel;
      await fs.writeJson('./eas.json', easConfig, { spaces: 2 });
      
  • eas update で OTA チャンネルに即時配信

このように、自動化された仕組みによって、PR を push するだけでアプリに反映される環境を実現しています。

加えて、開発環境において OTA アップデートを活用する最大のメリットは、修正内容を実機で確認する際にビルドし直す必要がないことです。通常のビルドでは、1回の修正を確認するために数分から十数分かかることもありますが、OTA を使えば PR を更新するだけで差分が即時に反映されます。

この仕組みは、デザイナーとの UI 調整のやり取りでも非常に有効です。

たとえば、「このボタンの余白をあと2px広げてほしい」や「もう少し文字を大きくしてほしい」といった軽微な修正を行った場合でも、再ビルドせずにその場で OTA によって即時反映できるため、デザイナーが実機で確認しながらフィードバックを返すことが可能になります。

結果として、ビルド時間を短縮しつつ、開発者とデザイナー間のコミュニケーションコストも削減でき、よりスムーズな UI 改善サイクルが実現されています。

Prod 環境

本番環境では、緊急時のみ、手動で OTA を適用できるようにしています。

  1. PdM・テックリードの承認を得た上で実施
  2. GitHub Actions により OTA アップデートが自動実行される

実際の GitHub Actions ワークフロー

name: Publish Release

on:
  workflow_dispatch:

jobs:
  publish:
    name: Install and publish
    runs-on: macos-latest-xl
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version-file: '.tool-versions'
          cache-dependency-path: 'yarn.lock'
          cache: yarn

      - name: Setup Expo and EAS
        uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}

      - name: Install dependencies
        run: yarn install --frozen-lockfile

      - name: EAS update
        run: |
          eas update --branch "production" --message "EAS update"
          eas channel:edit "production" --branch "production"

      - name: Notify slack on success
        if: success()
        env:
          SLACK_WEB_HOOK_URL: ${{ secrets.PJ_PORTAL_DEVELOPERS_APP_DEPLOYMENT_PROD_WEBHOOK }}
          CHANNEL: ${{ github.event.inputs.version }}
          IS_SUCCEEDED: true
        run: npx zx ./scripts/zx/notify_published.mjs

      - name: Notify slack on failure
        if: failure()
        env:
          SLACK_WEB_HOOK_URL: ${{ secrets.PJ_PORTAL_DEVELOPERS_APP_DEPLOYMENT_PROD_WEBHOOK }}
          CHANNEL: ${{ github.event.inputs.version }}
          IS_SUCCEEDED: false
        run: npx zx ./scripts/zx/notify_published.mjs

このワークフローの特徴:

  • workflow_dispatch により手動実行
  • production ブランチに OTA 更新を実施し、Expo チャンネルを明示的に紐付け
  • 成功・失敗に応じて Slack 通知を送信

このように、安全性を確保しつつ、緊急対応を可能にする運用体制を構築しています。

まとめ

本記事では、Expo を活用した OTA アップデートの仕組みと、カナリーチームでの具体的な運用方法をご紹介しました。

通常、アプリの修正にはストア審査を伴うため、リリースに時間がかかります。しかし、OTA アップデートを適切に活用すれば、緊急の修正や軽微な改善を即座にユーザーに届けることができます。

今後もストアポリシーを遵守しつつ、OTA アップデートを適切に活用することで、より良いサービスを提供していきたいと考えています。

Canary Tech Blog

Discussion