第18章 EAS Buildでビルドして配布する
eas.jsonの設定からTestFlightや内部テストへの配布、ストア公開までをクラウドビルドでたどります。
ここまで作ってきたアプリを、外に出します。XcodeとAndroid Studioをローカルで動かしてビルドする道もありますが、本書では**EAS (Expo Application Services)**を使ってクラウドでビルドし、そのままTestFlight/内部テストに流し、ストアに公開するまでを通します。
ローカルビルドより設定が短く済み、macOS以外でもiOSビルドが作れて、ビルドサーバーの面倒を見なくていいので、個人開発でも会社の開発でもまずEASから入るのが今は無難です。

上段はネイティブバイナリを作ってストアへ届ける経路、下段は互換性を確認したうえでJSとアセットだけを届ける経路です。EAS UpdateとFingerprintは本章後半と第21章で扱います。
準備するもの
EASを使うには、ExpoアカウントとEAS CLIが要ります。
npm install -g eas-cli
eas login
ブラウザが開いてログインを求められます。GitHubアカウント連携でも、Expo独自アカウントでも構いません。
ストアに出すには、それぞれの開発者プログラムへの加入も必要です。
- Apple Developer Program: 年間99ドル
- Google Play Console: 一度きり25ドル
このあたりはExpoではなくApple/Googleの世界の話なので、本書では触れません。学習目的でビルドだけしたい場合、EAS Buildだけなら無料枠の範囲で試せます。
eas.jsonでプロファイルを定義する
eas.jsonはビルドや申請の設定をまとめるファイルです。プロジェクト直下に作ります。
{
"cli": {
"version": ">= 12.0.0",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"channel": "development"
},
"preview": {
"distribution": "internal",
"channel": "preview",
"ios": {
"simulator": false
}
},
"store-preview": {
"channel": "preview",
"autoIncrement": true
},
"production": {
"channel": "production",
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}
4つのプロファイルを置いています。
- development: Development Build用。Expo Goの代わりにシミュレータや実機にインストールして開発するためのビルド
- preview: Ad Hoc配布テスト用。事前に登録した実機に直接インストールする中間ビルド(TestFlightは通せません)
- store-preview: TestFlight/内部テストトラック配布用の中間ビルド。ストア配布形式(署名・審査経路)はproductionと同じで、チャンネルだけ
previewに分けています - production: ストア審査に出す本番ビルド
previewとstore-previewを混同しないでください。previewはdistribution: "internal"を指定しているため、iOSではAd Hoc署名になります。Ad Hocはプロビジョニングプロファイルに登録した実機へ直接インストールする配布方式で、ExpoのInternal Distributionが指すのもこの形です。TestFlightはApp Store向けの配布経路なので、Ad Hoc署名のビルドはそもそも提出できません。TestFlightや内部テストトラックに配りたい場合は、distributionを指定しない(=デフォルトのstoreのまま)store-previewのようなプロファイルを使ってください。
appVersionSource: "remote"は、ビルド番号(iOSのbuildNumber、AndroidのversionCode)をEASに任せて自動採番させる設定です。autoIncrement: trueと組み合わせると、本番ビルドが走るたびに自動でインクリメントされます。手動で番号を上げる手間が消えます。
環境変数とシークレット管理
「APIエンドポイントを開発と本番で切り替えたい」「Sentry連携用の鍵をリポジトリに置きたくない」など、Web側の.env感覚で扱いたい値が出てきます。.envとEXPO_PUBLIC_の基本は第5章で扱ったので、ここでは配布を見据えた整理をします。Expo + EASでは扱う層を3つに分けて考えます。
| 層 | 仕組み | 公開されるか |
|---|---|---|
| クライアントに公開してよい値 | process.env.EXPO_PUBLIC_* |
◎ バンドルに焼き込まれる |
| クライアントから読みたい設定値 | app.config.tsのextra |
○ ほぼ公開と同じ |
| ビルド時だけ必要な秘密の値 | EAS環境変数(visibility: sensitive/secret) |
× Workerの環境変数として注入 |
EXPO_PUBLIC_で公開前提の値を扱う
EXPO_PUBLIC_で始まる環境変数は、ビルド時にMetroが値を読み出してバンドルに焼き込みます。「公開API URL」「Firebaseのクライアント設定」のような、リバースエンジニアされても困らない値だけを置いてください。
# .env (リポジトリにコミットして良い値だけ)
EXPO_PUBLIC_API_URL=https://api.example.com
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
プロファイルごとに値を切り替えたい場合は、eas.jsonの各プロファイルにenvを書きます。
{
"build": {
"preview": { "env": { "EXPO_PUBLIC_API_URL": "https://api.staging.example.com" } },
"production": { "env": { "EXPO_PUBLIC_API_URL": "https://api.example.com" } }
}
}
app.config.tsのextraで動的に値を流し込む
Constants経由でランタイムから読みたい値や、ビルド時のprocess.envを参照して切り替えたい値は、app.config.tsのextraに書きます。これも結局バンドルに含まれるので、機密扱いの値は入れません。
// app.config.ts
import type { ExpoConfig } from "expo/config";
export default ({ config }: { config: ExpoConfig }) => ({
...config,
extra: {
eas: { projectId: "xxx" },
sentryDsn: process.env.SENTRY_DSN,
},
});
import Constants from "expo-constants";
const dsn = Constants.expoConfig?.extra?.sentryDsn;
app.config.tsの実行時にprocess.envを読めるので、CI側で渡した値をそのまま流し込めます。
EAS環境変数で本当の秘密の値を渡す
ストア提出用のサービスアカウント鍵、Sentryのソースマップアップロードトークンなど「クライアントには出ない/ビルド時だけ必要」な値は、sensitiveまたはsecretな可視性を指定したEAS環境変数として登録します(旧eas secret:*コマンドは非推奨で、環境変数機能に統合されています)。
eas env:create --name SENTRY_AUTH_TOKEN \
--value xxx \
--environment production \
--visibility sensitive
eas env:list --environment production
--visibilityにはplain(平文で参照可能)、sensitive(登録後は値を読み返せない)、secret(ビルドログにも出さないよりさらに厳格な扱い)があります。Sentryのトークンのような値はsensitiveで十分です。登録した値は指定した--environment(development/preview/production)に紐づくEAS BuildのWorkerでだけ環境変数として読めます。.envにもapp.config.tsにも書く必要はなく、リポジトリには値が一切残りません。Service Account JSONのようにファイルそのものを渡したい場合は--type fileで登録できます。
Webの.env感覚との違い
整理すると、Web開発で書く.envの値は、モバイルでは2つに分かれます。
- ブラウザに送られて困らない値 →
EXPO_PUBLIC_*かextraでクライアント層に置いてよい - サーバから出さない値 → クライアントに置かず、サーバ側で扱うか
EAS環境変数(sensitive/secret)でビルド時注入のみにする
「リバースエンジニアリングで取り出せる前提でクライアント層を設計する」が原則です。困ったら、まずサーバ側の薄い中継層に逃がせるかを検討してください。
初期化する
eas.jsonを作ったら、Expoのプロジェクトとして登録します。
eas init
プロンプトに沿うとExpoのアカウントに紐づいたプロジェクトが作られ、app.jsonのextra.eas.projectIdに識別子が書き込まれます。
iOSのバンドルIDとAndroidのパッケージ名も、ストアに出す前に決めておきます。app.jsonに追記します。
{
"expo": {
"name": "sample-todo-app",
"ios": {
"bundleIdentifier": "jp.kazutoyo.sampletodoapp"
},
"android": {
"package": "jp.kazutoyo.sampletodoapp"
}
}
}
com.example.xxxは本番では使えません。自分のドメインの逆順(jp.kazutoyo.sampletodoappなど)か、所属組織のドメインの逆順を使ってください。一度ストアに出したIDは変えられないので、最初に決めるところに少し時間をかける価値があります。
Development Buildを作る
開発フェーズで一番恩恵があるのが、Development Buildです。Expo Goには入っていないネイティブモジュール(本書のサンプルだとStorybookのオンデバイスツールなど)を組み込んだ、自分専用のExpo Goのようなものです。Development BuildはmacOSならnpx expo run:ios --deviceでローカルビルドもできますが、本節ではOSを問わず使えるEAS Buildの経路を扱います。
Development Buildの実体はexpo-dev-clientというパッケージで、これを組み込んだアプリをEASでビルドします。まだ入れていなければ先にインストールしておきます。
npx expo install expo-dev-client
インストールすると、npx expo startでDevelopment Build/Expo Goのどちらに接続するか選べるようになります。準備ができたら、実際のビルドに進みます。
eas build --profile development --platform ios
eas build --profile development --platform android
# 両方まとめて流したいときは
eas build --profile development --platform all
実行するとEASのサーバー側でクラウドビルドが走り、終わるとQRコードとインストールURLが表示されます。
このクラウドビルドの利点は、Windows/LinuxからでもiOS実機用のDevelopment Buildを作れることです。iOSシミュレータをローカルで起動するにはmacOS + Xcodeが必要ですが、実機での開発・確認だけならMacは必須ではありません。iOS実機へ入れる場合はApple Developer ProgramとUDID登録が必要です。

ブラウザで開くとEASのダッシュボードに飛び、ビルドの進捗、ログ、生成物のダウンロードリンクが見られます。

iOS実機にインストールするには、Apple Developer Programの登録と、実機のUDIDをプロビジョニングプロファイルに登録する手順が要ります。EAS CLIが対話的に手伝ってくれるので、初回は指示に沿って進めるのが楽です。
シミュレータだけで試したい場合は、profileに"ios": { "simulator": true }を入れたビルドを別途作ります。EASはシミュレータ用のバイナリ(.tar.gz)を生成してくれるので、解凍してxcrun simctl install booted ./SampleTodoApp.appで入ります。最近はeas build:run --platform ios --latestを使うと、最新ビルドのダウンロードからシミュレータへのインストールまで1コマンドで済みます。
TestFlight/内部テストに配る
Development Buildは「自分の開発機で動く版」です。ステークホルダーや社外のテスターに配るには、TestFlight(iOS)とInternal Testing Track(Android)を使います。手順を整理します。
iOS: TestFlight
- App Store Connectで対応するアプリのレコードを作る(バンドルID、表示名、カテゴリなど)
eas build --profile store-preview --platform iosでビルド(そのまま本番として出す場合はproductionプロファイルでも構いません)- ビルド完了後、
eas submit --platform iosを実行
eas submitはEASが代行してApp Store Connectへipaをアップロードする仕組みです。1回目はApp Store Connect APIキーの登録を求められます。指示通りにキーを発行して登録しておけば、以後は引数を渡すだけでアップロードできます。
アップロードからTestFlightで配れる状態になるまで5〜30分ほどかかります。「Processing」のステータスから「Ready to Submit」または「Ready to Test」に変わったら、テスター情報を登録すれば配信開始です。

Android: Internal Testing
- Google Play Consoleでアプリを作成(パッケージ名、デフォルト言語など)
eas build --profile production --platform androidでAABを生成eas submit --platform androidでアップロード
Androidは初回のみ少し手順が複雑です。
- Google Play Consoleで「Internal app sharing」または「Internal testing」のトラックを開設
- サービスアカウントJSONを作って、
eas.jsonに登録(eas submitが初回ガイドしてくれる) - アプリ署名鍵はGoogle Playの「Play App Signing」に任せる(EASがアップロード鍵を管理)
ストアに公開する
テスター内で問題がなければ、本番リリースです。やることはざっくり3つに分かれます。
メタデータを揃える
ストアに出す前に、画面外の情報を揃えます。Apple/Google両方で共通で必要なのは次の項目です。
- アイコン(1024×1024)
- スクリーンショット(iOSは6.9インチ(iPhone 16 Pro Maxなど)が基準としてほぼ必須。必要に応じて6.5インチも用意すると古い画面サイズもカバーできる)
- アプリ名(30文字程度の制限)
- サブタイトル / 短い説明(iOS: 30文字、Android: 80文字)
- 詳細説明(4000文字程度)
- プロモーション画像 / フィーチャーグラフィック(Androidの1024×500)
- プライバシーポリシーのURL
- カテゴリ、対象年齢、コンテンツレーティング
スクリーンショットは実機やシミュレータで撮れます。デバイスフレームを足したり、キャッチコピーを入れたりは好みですが、最初は素のスクショで十分です。

素のスクリーンショットでもここまでは揃います。デバイスフレームやキャッチコピーを足すかどうかは好みで判断してください。
Apple: 審査に提出
App Store Connectで対象ビルドを選び、必要事項を埋めて「審査に提出」を押します。審査は近年は1〜2日で結果が返ることが多いです。リジェクトされた場合は理由がメッセージで届くので、それに沿って修正してビルドを差し替えます。
審査の判断基準はApp Store Reviewガイドラインとして公開されています(日本語版あり)。リジェクトの通知は「Guideline 2.1」のように条項番号で理由を示してくるので、手元にこのページを開いておくと、通知を読んだときに何を直せばいいかがすぐ分かります。初めて提出する前に一度ざっと目を通しておくのがおすすめです。
よく引っかかるのは次あたりです。
- 権限の説明文不足:
app.jsonのinfoPlistにある説明文が空だとリジェクトされる - アカウント機能のテスト不足: ログインが必要ならテスト用アカウントを審査側に提供する
- アプリの内容が薄い: 「Hello World」レベルだとGuideline 4.2(Minimum Functionality)で落ちる
サンプルのTODOアプリも、リマインダー(ローカル通知)などモバイルらしい機能を1つは載せた状態で出すのが安全です。単機能のリストとチェックボックスだけだと「Webサイトで代替できる」と判断されてリジェクトを受けやすく、ホームウィジェットや位置リマインダーなど、モバイル文脈での意義を1つ加えるだけで通りやすさが大きく変わります。
Google: 段階公開
Google Playは「製品版」トラックに昇格させる形で公開します。即時100%公開もできますが、段階的公開を使うと最初は1%から始めて、問題がなければ徐々に上げていけます。何か起きたときに被害を抑えられるので、本番では段階公開がおすすめです。
審査はAppleより緩めに見えますが、自動チェックとレビュアー確認の両方があります。SDKのバージョン要件(targetSdkVersionの最低値)など、定期的に締め付けが上がるので最新情報の確認をおすすめします。
OTA更新を活用する
EASにはストア提出を経由せずにJSコードを更新できるEAS Updateという仕組みがあります。アプリのネイティブ部分は変えず、JSバンドルだけを差し替えるしくみです。
npx expo install expo-updates
eas update --channel production --message "テキスト修正"
channelはビルド時にeas.jsonで指定したものに合わせます。たとえばproductionチャンネルでビルドしたアプリにはproductionにpushしたアップデートだけが届きます。
文言の修正やUIの微調整、ちょっとしたバグ修正なら、ストア審査を待たずに数十秒で反映できます。逆にネイティブ依存(新しいExpoモジュールの追加、app.jsonの変更など)が絡むと使えないので、適用範囲を見極める判断が要ります。
詳しい使い方とアップデート戦略は、第21章のアップグレード周りと一緒に扱います。
ビルドエラーへの向き合い方
クラウドビルドが失敗するのはよくあります。落ち込まずにログを読みましょう。
EASのダッシュボードに各フェーズのログが時系列で並びます。よく見るエラーパターンを挙げておきます。
- ネイティブモジュールの不整合: SDKバージョンとライブラリバージョンが噛み合っていない。
npx expo install --checkで確認 - iOS署名エラー: 証明書/プロビジョニングプロファイルが古い。
eas credentialsで再生成 - Android署名エラー: アップロード鍵とPlay App Signing鍵の食い違い。Play Consoleで鍵を確認
ログが長すぎて読み切れない場合は、エラーメッセージでgrepするか、関連する行をAIエージェントに渡して整理させると早いことが多いです。第20章で扱うCoding Agent活用の出番です。
ここまでで、自分のTODOアプリをストアに出すところまでが一通りなぞれました。途中で実機で動かす感覚や、ビルドが通ったときの安堵感は、文章で読むのと実際にやるのとで体験がだいぶ違います。サンプルアプリを使い回してでも、一度この一連の流れを通しておくのを強くおすすめします。
次章からは、ここまでとは趣向を変えて、Expo標準モジュールではカバーできない機能を扱う「Expo Modulesでネイティブ機能を自作する」話に入ります。