クラッシュ修正版をビルドしようとした、その最後の最後。EASが「今月の無料iOSビルド枠を使い切った(リセットは21日後)」と返してきた。
課金もせず、3週間も待たず、eas build --local で自分のMacからビルドして、Appleの2FAなしでTestFlightまで提出した。その一部始終を、実際のコマンドと実物確認つきで残す。同じところで詰まった人が、そのまま手を動かせるように書いた。
環境はExpo SDK 56 / React Native 0.85 / TypeScript。検証はmacOS + Xcode 26.3で行っている。
TL;DR(先に結論)
- EAS Buildの無料枠は「月次・アカウント単位」。複数アプリを並行で反復ビルドしていると、意外と早く尽きる。
-
eas build --localなら自分のMacでビルドでき、EASのビルドクレジットを消費しない(=無料の抜け道)。 -
同じ
eas.json設定のまま使える。署名はEASのリモート資格情報(配布証明書・プロビジョニングプロファイル)をそのまま利用できる。 -
ASC API Keyを
eas.jsonの submit に入れれば、Appleの2FAなしでTestFlight / App Store Connect への提出まで自動化できる。 - この記事のコマンドと数値は、実際に動かしたものをそのまま載せている。
最短ルートはこれだけ。
# 1. UTF-8ロケールを効かせる(必須)
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
# 2. ローカルビルド(EASのビルド回数枠を消費しない)
eas build --platform ios --profile production --local --non-interactive --output /tmp/app.ipa
# 3. ASC API Keyで2FAなし提出
eas submit --platform ios --profile production --path /tmp/app.ipa --non-interactive
以下、ハマりどころを順に潰していく。
背景:AIエージェントのチームでiOSアプリを複数並行開発している
少し変わった運用環境なので先に共有しておく。
kosei-labs は、人間のオーナーが構築した複数のAIエージェントのチームでiOSアプリを開発・運営する実験プロジェクトだ。CEO「イーロン」(オーケストレーター兼プランナー)の下に、実装・レビュー・執筆・マーケティングを担う役割特化のエージェントが並ぶ(Anthropic の Planner–Generator–Evaluator 型がベース)。現在運営しているアプリは次の3本。
- お願い!ご飯 — カップル向けの料理注文アプリ
- 脳内メモ帳 — 脳内リソースを可視化するアプリ
- 寝坊知らず — グループアラームアプリ
技術スタックは共通で、概ね以下のとおり。
- Expo SDK 56
- React 19 / React Native 0.85
- TypeScript
- react-native-svg / react-native-reanimated ほか(ネイティブ依存を含む)
この体制で複数アプリを並行開発し、反復的にビルドを回していた。さらにマルチエージェントのワークフローでクラッシュ監査まで実施していたため、EAS Buildをかなり多用していた。
ポイントは「1アプリだけを淡々と作っていたわけではない」こと。複数アプリ × 反復ビルドという使い方が、後述の無料枠の壁に直結する。
ぶつかった壁:ビルド最後でEASに弾かれた
あるアプリのクラッシュ修正版を出すべく、いつものコマンドを叩いた。
eas build --platform ios --profile production
認証も、資格情報の解決も、プロジェクトのアップロードも、全部すんなり通った。そして最後の最後、EASがこう返してきた。
This account has used its iOS builds from the Free plan this month, which will reset in 21 days (on Wed Jul 01 2026).
Error: build command failed.
原因はシンプルだ。EAS Buildの無料プランには「月あたりのiOSビルド回数の上限」がある。1アプリで build 1〜6 と反復し、そこに別アプリのビルドも重なって、アカウント単位の月次枠を使い切っていた。
ここはハマりやすいので強調しておく。枠切れの判定は「アップロードが全部終わった後」に出る。それまではいつも通りに進んでしまうので、「あれ、毎回やってる手順なのに」「え、今の全部ムダだったの?」となって原因に気づきにくい。途中でやさしく止めてくれるわけではない。
【重要】Expo Go と EAS Build を混同しない
ここで一度立ち止まりたい。この制限を正しく理解するには、用語を正確に切り分ける必要がある。
| 何か | 料金 | |
|---|---|---|
| Expo Go | 開発用のサンドボックスアプリ(手元でJSを読み込んで動かす) | 無料・無制限 |
| EAS Build | クラウド上でネイティブをビルドするサービス | 無料プランは回数枠あり |
回数枠があるのは EAS Build の方だ。Expo Goは別物で、ここに回数制限はない。
「Expoって無料だよね?」という認識のままだと、この制限に説明がつかず混乱する。実際、検索しても「Expo Goの制限」と「EAS Buildの制限」を取り違えた情報が混ざりがちで、「Expo Goは無制限なのに、なぜビルドが止まる?」と取り違えると、原因の切り分けがまるごと迷子になる。「日常の開発に使うExpo Go」と「ストア提出用のネイティブビルドを焼くEAS Build」は別サービス、と頭の中で分けておくこと。これが対処の第一歩になる。
選択肢の比較:課金 / 待つ / ローカルビルド
枠が尽きた状態からの選択肢は、現実的に3つあった。
| 方法 | コスト | 速度 | |
|---|---|---|---|
| (A) | EASを有料プランへ(月額制 もしくはオンデマンド課金) | 発生する | 最速・確実 |
| (B) | 枠リセット(翌月1日)まで約3週間待つ | 無料 | 遅い |
| (C) | ローカルビルド(eas build --local) |
無料・クレジット消費なし | 中(ビルド時間はかかる) |
(A) は確実だがコストが出る。(B) は「クラッシュ修正版を3週間放置」になるので論外に近い。
今回選んだのは (C) ローカルビルド。eas build --local は自分のMac上でビルドし、EASのビルドクレジットを消費しない。課金するほどの頻度ではないが、3週間も待てない——「あと1本だけ出したい」というこの状況に、いちばん効く落としどころだった。Macは目の前にある。
料金プランの具体額は変動するので、最新の条件はEAS Pricingで確認してほしい。本記事では「ローカルビルドなら無料枠を消費せず出せる」という点だけを扱う。
ローカルビルドの前提:ツールチェーンの事前点検
ローカルビルドは「自分のMacでネイティブをコンパイルする」ということ。つまり、クラウドが代わりにやってくれていた作業を手元でやるので、ツールチェーンが揃っていないと普通に止まる。実行前に以下を確認した。
| ツール | バージョン(確認した実物) |
|---|---|
| Xcode | 26.3 |
| CocoaPods | 1.16.2 |
| fastlane | (インストール済み) |
ios ディレクトリ |
prebuild 済み |
| 空き容量 | 十分(Podsのコンパイルで結構使う) |
確認は普通にバージョンを叩くだけでいい。
xcodebuild -version
pod --version
fastlane --version
特に重要なのが ios ディレクトリが prebuild 済みであること。eas build --local はこのネイティブプロジェクトをそのままコンパイルするので、ここが整っていないと話が始まらない。Expoの管理プロジェクトで ios/ が無い場合は、先にprebuildして生成しておく。
npx expo prebuild --platform ios
ハマりどころ:UTF-8ロケールを必ず設定する
これは地味だが踏むと厄介なので、先に潰しておく。
CocoaPodsもfastlaneも、「ロケールがUTF-8でない」と警告する。未設定のままだとビルドが不安定になったり、失敗したりしうる。しかもエラーメッセージが本質を指していないことが多く、原因究明に時間を溶かす類の失敗になる。
なので、ビルドを実行する前に、必ずこれを効かせておく。
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
ここを飛ばすと「なぜか分からないけど落ちる」を踏みやすい。シェルを開き直すたびに設定するのが面倒なら、ビルド用スクリプトの先頭に入れておくとよい。とにかく最初にやっておくこと。
ローカルビルドの実行と署名
下準備が済んだら、本体を実行する。
eas build --platform ios --profile production --local --non-interactive --output /tmp/app.ipa
-
--local:クラウドではなく手元のMacでビルドする。ここがEASクレジットを消費しないポイント。 -
--non-interactive:対話プロンプトを出さない(自動化向き)。 -
--output:出力先の.ipaパス。
挙動はこうだ。
- fastlane / Xcode が Pods をコンパイルする(
react-native-svg、react-native-reanimatedなどネイティブ依存も含む) - 所要時間は 20〜40分(クラウドではなく自分のMacの性能に依存する)
- 完了すると、署名済みの
.ipaが/tmp/app.ipaに出力される - そして重要なのは、EASのビルド回数枠は消費しないこと
署名はどうなるのか
「ローカルでビルドするなら、署名証明書も手元で用意しないといけないのでは?」と身構えるところだが、ここがうまくできている。
ローカルビルドでも eas-cli は、配布証明書とプロビジョニングプロファイルを EAS サーバ(リモート資格情報)から取得する。つまり、過去にEASでビルドして署名情報をEAS側に持たせていれば、ローカルビルドでもその署名がそのまま通る。手元で証明書を作り直したり、Apple Developerポータルを触り直したりする必要はない。
「ビルドの計算だけ手元に持ってきて、署名まわりはこれまでどおりEASに任せる」——この合わせ技が、移行コストをほぼゼロにしてくれた。ここが「同じ eas.json 設定のまま使える」と言える理由であり、eas build --local の効きどころだった。
版番号の落とし穴:appVersionSource と実物確認
ここも公開中アプリの更新では避けて通れない。
App Store は、公開中アプリの更新に対して「より大きい version/一意の buildNumber」を要求する。ローカルビルドに切り替えたときに怖いのは、「version や buildNumber が .ipa に正しく焼き込まれているのか?」が見えにくいことだ。ここを外すと、せっかくビルドしても提出時に弾かれて手戻りになる。
これは eas.json の設定で担保できる。cli.appVersionSource を "local" にしておくと、app.json の version と ios.buildNumber が、ローカルビルドの .ipa に正しく焼き込まれる。
{
"cli": {
"version": ">= 19.0.0",
"appVersionSource": "local"
}
}
{
"expo": {
"version": "1.1.0",
"ios": {
"buildNumber": "3"
}
}
}
焼き込みは「実物」で確認する
設定したからといって、信じて提出するのは危ない。版番号のミスは提出後に弾かれて手戻りになる。なので、出来上がった .ipa を開いて実物を確認した。
.ipa は実体がZIPなので、unzipして Payload/*.app/Info.plist の中身を plutil で見る。
cd /tmp
unzip -o app.ipa -d app_unzipped
plutil -p app_unzipped/Payload/*.app/Info.plist | grep -E 'CFBundleShortVersionString|CFBundleVersion'
このとき確認できた実例は次のとおり。
"CFBundleShortVersionString" => "1.1.0"
"CFBundleVersion" => "3"
CFBundleShortVersionString(=ユーザー向けの version)が 1.1.0、CFBundleVersion(= buildNumber)が 3。app.json に書いた値がちゃんと焼き込まれている。憶測せず、実物で確認するのがいちばん安全だ。ビルド直後の数十秒が、提出後の手戻りを防いでくれる。
2FAなしで提出:ASC API Key を使う
.ipa ができたので、あとはTestFlight / App Store Connect(ASC)へ提出するだけ。ここで効くのが App Store Connect API Key だ。
Apple IDとパスワードで提出しようとすると2FA(二要素認証)のコード入力が挟まり、自動化が止まる。ASC API Keyを使えば、この2FAを回避して提出できる。CIや自動化と相性がいい。
App Store ConnectでAPI Key(.p8)を発行したら、eas.json の submit プロファイルに次を設定する。
{
"submit": {
"production": {
"ios": {
"ascApiKeyPath": "/Users/you/.appstoreconnect/private_keys/AuthKey_XXXXXXXXXX.p8",
"ascApiKeyId": "XXXXXXXXXX",
"ascApiKeyIssuerId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
}
}
}
-
ascApiKeyPath:発行した.p8の置き場所 -
ascApiKeyId:Key ID -
ascApiKeyIssuerId:Issuer ID
これで、先ほど出力した .ipa をそのまま提出できる。
eas submit --platform ios --profile production --path /tmp/app.ipa --non-interactive
--non-interactive でも2FAで止まらない。これがASC API Keyの効きどころだ。アップロード後は Apple側の処理が5〜10分 ほど走り、その後TestFlightに反映される。ビルド(ローカル)→ 提出(API Key)まで、Appleの2FAプロンプトに一度も触らず完走できた。
補足:上の3つの値は秘匿情報なので、この記事ではプレースホルダに置き換えてある。実運用では発行した実値を入れること。
.p8と Issuer ID / Key ID は他人に渡らないよう、リポジトリにコミットしない(.gitignore管理 or 環境変数経由)。
並行処理の小技:ビルド中に別作業を回す
ローカルビルドの唯一の弱点は 時間がかかる(20〜40分) こと。ただ、これは待ち方を工夫すれば取り返せる。
今回は 2本のアプリをローカルビルドで順に処理した。ポイントは、ビルドをバックグラウンドで走らせて、その裏で別の作業を並行させること。
- ビルドが走っている裏で、版上げ・コミット・別アプリの作業を進める
-
eas submitはネットワーク主体でローカルCPUをほぼ食わないため、別アプリのビルドと並走させられた
「片方のアプリをビルド(CPUを使う)しながら、もう片方を submit(ネットワークを使う)する」と、リソースの食い合いが起きにくい。ローカルビルドは確かに時間を食うが、「ビルドを待つ」のではなく「ビルドを走らせながら次を進める」設計にすれば、複数アプリ運用でも詰まらない。
まとめ:同じ詰まり方をした人へ
最後に要点を畳んでおく。
- EAS Buildの無料枠は月次・アカウント単位。複数アプリ並行+反復ビルドだと、意外と早く尽きる。しかも枠切れはアップロード後に出るので気づきにくい。
-
eas build --localが無料の抜け道。同じeas.json設定のまま自分のMacでビルドでき、クレジット消費はゼロ。「あと1本だけ出したい」に最も効く。 - 事前に Xcode / CocoaPods / fastlane を用意し、UTF-8ロケール(
LANG/LC_ALL)を設定する。署名はEASのリモート資格情報をそのまま使えるので、過去にEASでビルドしていればそのまま通る。 -
appVersionSource: "local"+ Info.plistで版焼き込みを実物確認(CFBundleShortVersionString/CFBundleVersion、実例:1.1.0/3)。 -
ASC API Key を
eas.jsonの submit に入れれば、2FAなしで完全自動の提出が可能。 - (メタ)人間オーナー+AIエージェントチームという体制で、クラッシュログ解析 → 修正 → ローカルビルド → 提出まで一気通貫でやり切れた。
EAS Buildの無料枠は便利だが、反復開発ではあっさり溶ける。だが枠が尽きても、iOSアプリは出せる。eas build --local を知っているかどうかで、「ビルド最後で弾かれて課金か3週間待ちか」の二択を回避できる——「3週間待ち」か「今日中に提出」かが分かれる。同じ画面の前で固まっている人の助けになれば。