🚀

ExpoアプリをEASではなくApp Distributionに自動配布し費用を抑える方法

2024/12/08に公開

最近、React Nativeを使ったアプリ開発に挑戦しているのですが、なるべく費用を抑えるために試行錯誤しています!

React Nativeでアプリを開発するときにほとんどのケースでExpoを利用すると思います。ExpoはReact Nativeの開発をサポートするためのフレームワークで、開発体験がとてもいいです。
しかし、Expoで作成したアプリをテスト用などに配布する場合、Expoの公式サービスであるEASを利用することができますが、無料プランだとアプリのビルド数に制限があり、1ヶ月に30ビルドまでとなっています。
iOSのビルドとAndroidのビルドでそれぞれカウントされるため、実際には15ビルドまでとなりとても厳しいです。

https://expo.dev/pricing

30ビルドは、CI/CDを導入しているとすぐに使い切ってしまうことがあるため、無料でアプリを配布する方法を探していました。そこで、FirebaseのApp Distributionを利用することで、無料でアプリを配布することができることを知りました。

https://firebase.google.com/docs/app-distribution?hl=ja

この記事では、ExpoのアプリをApp Distribution経由で配布する方法を紹介します。App DistributionはFirebaseの機能の一つで、アプリの配布を行うことができます。無料プランでもアプリの配布が可能で、ビルド数に制限がないため、Expoのアプリを無料で配布することができます。

Expoの設定

まずは、Expo側の設定を行います。
App Distributionで配布しますがビルドのためにEASも利用します。

https://expo.dev/

Expoのコンソールにアクセスし、プロジェクトを作成します。

tokenの取得

次に、管理画面よりtokenを取得しておきます。

https://expo.dev/settings/access-tokens

ローカルでの作業

次に、ローカルでの作業を行います。
ローカルではeas-cliを使ってプロジェクトをExpoの管理画面で設定したそれとあわせておきましょう

まずはExpoアプリを作成するために、以下のコマンドを実行します。

$ npx create-expo-app@latest

これでExpoアプリの雛形が作成されます。

次に、Expoのアプリをeas-cliでビルドするために、eas-cliをインストールします。

$ npm install -g eas-cli
$ eas login
$ eas build:configure

eas build:configureコマンドを実行すると、app.config.jsファイルが自動で作成されます。

Android用のkeystoreおよび、iOS用の証明書の設定

アプリをビルドするには、Android用のkeystoreおよび、iOS用の証明書が必要です。それぞれの設定を行います。
Expoでは、eas credentialsコマンドを使ってAndroid用のkeystoreおよび、iOS用の証明書を設定することができます。
便利ですね!!

$ eas credentials

この状態で、eas build --localコマンドを実行すると、ローカルでアプリのビルドができるようになります。

$ eas build --local

環境変数周りの設定

また、Expoではローカルの開発に.env.localファイルを使って環境変数を設定することができます。

FIREBASE_API_KEY=xxxx
FIREBASE_APP_ID=xxxx

このファイルはgitに含めないように.gitignoreに追加しておきましょう。

設定した環境変数は先ほど作成したapp.config.jsに記載しておきます。
extraという項目に自由に環境変数を追加することができます。

export const expoConfig: ExpoConfig = {
  ios: {
    bundleIdentifier: "com.example.app",
    config: {
      googleServicesFile: "./GoogleService-Info.plist"
    }
  },
  android: {
    package: "com.example.app",
    config: {
      googleServicesFile: "./google-services.json"
    }
  },
  extra: {
    FIREBASE_API_KEY: process.env.FIREBASE_API_KEY,
    FIREBASE_DOMAIN,: process.env.FIREBASE_DOMAIN,
    FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
  }
}

設定した環境変数はアプリ内で参照することができます。

import Constants from "expo-constants";

// Firebaseの設定
const firebaseConfig = {
  apiKey: Constants.expoConfig?.extra?.FIREBASE_API_KEY ?? "",
  authDomain: Constants.expoConfig?.extra?.FIREBASE_DOMAIN ?? "",
  projectId: Constants.expoConfig?.extra?.FIREBASE_PROJECT_ID ?? "",
};

Firebaseプロジェクトの作成

つぎに、Firebaseプロジェクトを作成します。Firebaseのコンソールにアクセスし、新しいプロジェクトを作成します。

https://console.firebase.google.com/

次に、プロジェクトの設定に移動します。

マイアプリより、iOS用とAndroid用のアプリを追加します。

それぞれのアプリのパッケージ名を入力します。

ここは、Expoのapp.config.jsに記載されているios.bundleIdentifierandroid.packageを入力します。

export const expoConfig: ExpoConfig = {
  ...
  ios: {
    bundleIdentifier: "com.example.app"
  },
  android: {
    package: "com.example.app"
  } 
}

App Distributionの設定

次に、FirebaseのApp Distributionの設定を行います。Firebaseのコンソールにアクセスし、App Distributionを選択します。

ここで、「開始」をクリックします。

GitHub Actionsの設定

最後に、GitHub Actionsを使ってアプリをApp Distributionに配布する設定を行います。

冒頭にも記載した通り、今回はExpoのアプリをExpoの管理画面上ではなく、FirebaseのApp Distributionを使って無料で配布するので、Expoのプロジェクトで環境変数を設定しておく方法は使えません。

ですので、先ほどローカルで設定した環境変数をGitHub→Settings→Secrets→Actionsに設定しておきます。
取得したExpoのtokenも同様にSecretsに設定しておきます。

設定ファイルの調整

プロジェクトでGitHub Actionsの設定ファイルを作成します。

僕は以下のようにAndroid用とiOS用のjobをわけています。

eas build --localコマンドをGitHub Actionsで実行するのがミソで、--localオプションをつけている限りは無料でビルドすることができます。
ただし、--localオプションをつけると、環境変数などはExpoで設定したものではなく、ローカルの設定が使われるため、.envファイルを作成する必要があります。
さらにeas build --local時にAPP_VARIANTdevelopmentproductionを指定しておくことで、app.config.jsの設定を本番用と開発用で切り替えることができるのでおすすめです。

- name: Build on EAS (Android)
  run: eas build --platform android --non-interactive --no-wait --local
  env:
    APP_VARIANT: development

また、eas buildにはjavanodeが必要なので、それらの設定も行っています。

- name: Set up JDK
  uses: actions/setup-java@v3
  with:
    java-version: 17
    distribution: "zulu"
各設定ファイル
./.github/workflows/deploy.yml
name: Distribute Android and iOS Apps with App Distribution

on:
  workflow_dispatch:
  push:
    branches:
      - development

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build-android:
    name: Build and Distribute Android App
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
        with:
          fetch-depth: 2
      - name: setup common
        uses: ./.github/actions/setup
        env:
          EXPO_ACCESS_TOKEN: ${{ secrets.EXPO_ACCESS_TOKEN }}
      - name: create env file
        run: |
          touch .env
          echo "ANDROID_CLIENT_ID=${{ secrets.ANDROID_CLIENT_ID }}" >> .env
          echo "FIREBASE_API_KEY=${{ secrets.FIREBASE_API_KEY }}" >> .env
          echo "FIREBASE_APP_ID=${{ secrets.FIREBASE_APP_ID }}" >> .env
          echo "FIREBASE_DOMAIN=${{ secrets.FIREBASE_DOMAIN }}" >> .env
          echo "FIREBASE_PROJECT_ID=${{ secrets.FIREBASE_PROJECT_ID }}" >> .env
          echo "IOS_CLIENT_ID=${{ secrets.IOS_CLIENT_ID }}" >> .env
      - name: Build on EAS (Android)
        run: eas build --platform android --non-interactive --no-wait --local
        env:
          APP_VARIANT: development
      - name: Find APK file
        run: |
          APK_PATH=$(find ./ -name "*.apk" | head -n 1)
          echo "EAS_BUILD_OUTPUT_PATH=$APK_PATH" >> $GITHUB_ENV
      - name: Deploy to Firebase App Distribution (iOS)
        uses: ./.github/actions/distribute
        env:
          EAS_BUILD_OUTPUT_PATH: ${{ env.EAS_BUILD_OUTPUT_PATH }}
          FIREBASE_APP_ID_STAGING: ${{ secrets.FIREBASE_APP_ID_STAGING }}
          FIREBASE_SERVICE_CREDENTIALS_JSON: ${{ secrets.FIREBASE_SERVICE_CREDENTIALS_JSON }}
  build-ios:
    name: Build and Distribute iOS App
    runs-on: macos-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
        with:
          fetch-depth: 2
      - name: setup common
        uses: ./.github/actions/setup
        env:
          EXPO_ACCESS_TOKEN: ${{ secrets.EXPO_ACCESS_TOKEN }}
      - name: create env file
        run: |
          touch .env
          echo "ANDROID_CLIENT_ID=${{ secrets.ANDROID_CLIENT_ID }}" >> .env
          echo "FIREBASE_API_KEY=${{ secrets.FIREBASE_API_KEY }}" >> .env
          echo "FIREBASE_APP_ID=${{ secrets.FIREBASE_APP_ID }}" >> .env
          echo "FIREBASE_DOMAIN=${{ secrets.FIREBASE_DOMAIN }}" >> .env
          echo "FIREBASE_PROJECT_ID=${{ secrets.FIREBASE_PROJECT_ID }}" >> .env
          echo "IOS_CLIENT_ID=${{ secrets.IOS_CLIENT_ID }}" >> .env
      - name: Build on EAS (iOS)
        run: eas build --platform ios --non-interactive --no-wait --local
        env:
          APP_VARIANT: development
      - name: Find IPA file
        run: |
          IPA_PATH=$(find ./ -name "*.ipa" | head -n 1)
          echo "EAS_BUILD_OUTPUT_PATH=$IPA_PATH" >> $GITHUB_ENV
      - name: Deploy to Firebase App Distribution (iOS)
        uses: ./.github/actions/distribute
        env:
          EAS_BUILD_OUTPUT_PATH: ${{ env.EAS_BUILD_OUTPUT_PATH }}
          FIREBASE_APP_ID_STAGING: ${{ secrets.FIREBASE_APP_ID_IOS_STAGING }}
          FIREBASE_SERVICE_CREDENTIALS_JSON: ${{ secrets.FIREBASE_SERVICE_CREDENTIALS_JSON }}
./.github/actions/setup/action.yaml
name: "setup common"
description: "Setup common settings"
runs:
  using: composite
  steps:
    - name: Set up Node.js
      uses: actions/setup-node@v3
      with:
        node-version: 20
        registry-url: "https://registry.npmjs.org"
    - name: Set up JDK
      uses: actions/setup-java@v3
      with:
        java-version: 17
        distribution: "zulu"
    - name: Setup Expo and EAS
      uses: expo/expo-github-action@v8
      with:
        eas-version: latest
        token: ${{ env.EXPO_ACCESS_TOKEN }}
    - name: Install dependencies
      run: npm ci
      shell: bash
./.github/actions/distribute/action.yaml
name: "distribute"
description: "Distribute app to testers"
runs:
  using: composite
  steps:
    - name: Install Firebase CLI
      run: curl -sL https://firebase.tools | bash
      shell: bash
    - name:
      id: create-json
      uses: jsdaniell/create-json@v1.2.2
      with:
        name: "credentials.json"
        json: ${{ env.FIREBASE_SERVICE_CREDENTIALS_JSON }}
        dir: "."
    - name: Deploy to Firebase App Distribution
      env:
        GOOGLE_APPLICATION_CREDENTIALS: credentials.json
      run: |
        firebase appdistribution:distribute \
          ${{ env.EAS_BUILD_OUTPUT_PATH }} \
          --app ${{ env.FIREBASE_APP_ID_STAGING }} \
          --groups testers
      shell: bash

これで、GitHub Actionsを使ってExpoのアプリをApp Distributionに配布する設定が完了しました。

テスターの設定

次に、テスターの設定を行います。FirebaseのApp Distributionの管理画面からテスターを追加します。

テスターを追加したあと、GitHub Actionsを実行します。

無事にGitHub Actionsが成功すると、FirebaseのApp Distributionにアプリが配布されます。

無事にアプリが配布されると、テスターに招待メールが送られます。

Androidの場合

Androidの場合は、メールに記載されたリンクをクリックすることでアプリをインストールすることができます。
App Testerアプリがインストールされていることを確認してください。

iOSの場合

iOSの場合は、メールに記載されたリンクをクリックし「Install Firebase profile」をクリックします。
Profileをインストールすると設定アプリに「Firebase App Distribution」が追加されているので、そこからアプリをインストールすることができます。

アプリをインストールしたタイミングではまだ、「Firebase App Distribution」からアプリがダウンロードできません。

Apple Developerでの設定

そこで、iOSの場合は、テスターのUDIDをApple Developerに登録する作業が必要です。
テスターがアプリをインストールすると、App Distributionより開発者にテスターのUDIDが届くので、そのUDIDをApple Developerに登録します。

https://developer.apple.com/account/resources/devices/list

次に作ったdeviceをもとにProvisioning Profileを作成します。
「AdHoc」のProvisioning Profileを作成し、ダウンロードします。

https://developer.apple.com/account/resources/profiles/list

ExpoでのProvisioning Profileの設定

ダウンロードしたProvisioning ProfileはExpoのCredentialsより登録します。

再度GitHub Actionsを実行

再度GitHub Actionsを実行し、iOSのアプリをApp Distributionに配布します。
成功すると「ダウンロード」ボタンがApp Distributionアプリに表示されるので、そこからアプリをインストールすることができます。

まとめ

以上で、ExpoのアプリをApp Distributionに配布する方法を紹介しました。
無料プランでもアプリの配布が可能で、ビルド数に制限がないため、Expoのアプリを無料で配布することができます。

ただ、Expoでそのままアプリを配布する場合と比べるとはるかに手間がかかりますね。
予算に余裕がある場合は、EASをそのまま使ってアプリを配布することをおすすめします。

Discussion