Fastlaneでモバイルアプリの配信を自動化 - ツール編
はじめに
前回はGitHub Actionsの基本について学びました。今回は、さらに高度な自動化ツールFastlaneについて詳しく見ていきます!
Fastlaneを使えば、ストアへのアップロードまで完全自動化できます!
Fastlaneとは何か、なぜ使うのか
Fastlaneの基本概念
Fastlaneは、モバイルアプリの配信プロセスを自動化するためのツールです。
従来の手動配信:
# 1. ビルド
$ ./gradlew assembleRelease # Android
$ xcodebuild archive # iOS
# 2. 署名
$ jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 -keystore my-release-key.keystore app-release-unsigned.apk alias_name
# 3. ストアアップロード
$ # Google Play Consoleに手動ログイン
$ # ファイルをドラッグ&ドロップ
$ # メタデータを手動入力
$ # App Store Connectに手動ログイン
$ # ファイルをドラッグ&ドロップ
$ # スクリーンショットを手動アップロード
Fastlaneを使った自動配信:
# 1コマンドで全て完了
$ fastlane android production
$ fastlane ios production
なぜFastlaneが必要なのか?
React Native開発における配信の複雑さ:
- 複数プラットフォーム: iOSとAndroid、それぞれ異なる手順
- 複雑な署名: 証明書、プロビジョニングプロファイル、keystore
- ストア固有の手順: App Store、Google Play、それぞれ異なるAPI
- メタデータ管理: 説明文、スクリーンショット、リリースノート
- バージョン管理: 複数ファイルの同期
Fastlaneの解決策:
- 統一されたインターフェース: 1つのコマンドで両プラットフォーム対応
- 自動署名管理: 証明書とプロビジョニングプロファイルの自動管理
- ストアAPI統合: App Store Connect、Google Play Console API
- メタデータ自動化: スクリーンショット、説明文の自動生成
- バージョン同期: 複数ファイルの自動同期
Android用Fastfileの詳細解説
基本的なFastfile構造
# android/fastlane/Fastfile
default_platform(:android)
platform :android do
# 開発用ビルド
desc "Build development APK"
lane :dev do
gradle(
task: "assembleDebug",
project_dir: ".."
)
end
# 本番用ビルド
desc "Build release APK"
lane :release do
gradle(
task: "assembleRelease",
project_dir: ".."
)
end
# Google Play Store配信
desc "Deploy to Google Play Store"
lane :production do
gradle(
task: "bundleRelease",
project_dir: ".."
)
upload_to_play_store(
json_key: ENV["GOOGLE_PLAY_JSON_KEY_PATH"],
track: "production",
aab: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH]
)
end
end
fastlane android devでデバッグAPK、releaseで署名APK、productionでPlayアップロードまで一括実行でき、ローカル/CI共通の1コマンドに揃えられます。project_dirはFastfileがあるandroid/fastlaneから1つ上のandroidを指す..に固定しておくと、実行場所に左右されずgradlewを確実に呼び出せます。json_keyはCIシークレットや.envに置いて権限エラーを防ぐ運用が定番です。主要要素はdefault_platformとplatformで対象宣言、lane+descで処理単位を整理、gradleでタスク実行、upload_to_play_storeで鍵・トラック・AABを指定する構成。
実際のmy-appのFastfile解説
my-appプロジェクトの実際のFastfileを見てみましょう:
※ここで使用するensure_keystore_existsは後述の「キーストア管理の自動化」で定義します。
※GradleタスクのbundleDevRelease/bundleProdReleaseは、dev/prodフレーバーが存在する想定です。フレーバーがない場合はbundleReleaseに読み替えてください。
# android/fastlane/Fastfile
default_platform(:android)
platform :android do
# 主要なレーン(lane)の解説
# 1. 開発用ビルド
lane :dev do
gradle(
task: "assembleDebug",
project_dir: ".."
)
end
# 2. リリース用APKビルド
lane :apk do
ensure_keystore_exists
gradle(
task: "assembleRelease",
project_dir: ".."
)
# APKをbuildsディレクトリにコピー
sh("mkdir -p builds")
sh("cp app/build/outputs/apk/release/app-release.apk builds/")
end
# 3. Google Play Store用AABビルド
lane :bundle do
ensure_keystore_exists
gradle(
task: "bundleRelease",
project_dir: ".."
)
# AABをbuildsディレクトリにコピー
sh("mkdir -p builds")
sh("cp app/build/outputs/bundle/release/app-release.aab builds/")
end
# 4. 内部テスト配信
lane :internal do
ensure_keystore_exists
validate_play_console_access
gradle(
task: "bundleDevRelease",
project_dir: ".."
)
upload_to_play_store(
json_key: ENV["GOOGLE_PLAY_JSON_KEY_PATH"],
track: "internal",
aab: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH]
)
end
# 5. 本番配信
lane :production do
ensure_keystore_exists
validate_play_console_access
sync_version_code_with_play_console(track: "production")
gradle(
task: "bundleProdRelease",
project_dir: ".."
)
upload_to_play_store(
json_key: ENV["GOOGLE_PLAY_JSON_KEY_PATH"],
track: "production",
aab: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH]
)
end
# Google Play Console APIアクセス確認
private_lane :validate_play_console_access do
unless ENV["GOOGLE_PLAY_JSON_KEY_PATH"]
raise "GOOGLE_PLAY_JSON_KEY_PATH environment variable is required"
end
unless File.exist?(ENV["GOOGLE_PLAY_JSON_KEY_PATH"])
raise "Google Play JSON key file not found: #{ENV["GOOGLE_PLAY_JSON_KEY_PATH"]}"
end
end
end
レーンの役割例:
-
dev: ローカル確認/PR用の軽いAPK -
apk: 署名済みAPKを配布物として取得 -
bundle: Play配信用AABを生成 -
internal: developなどをトリガーに内部テストへ自動配信 -
production: タグやmainマージで本番配信
ensure_keystore_existsとvalidate_play_console_accessを前段に置くと、鍵不足や権限エラーでビルド後に落ちる事故を防止でき、内部テスト→本番の二段階配信も組みやすくなります。validate_play_console_accessは鍵存在チェック専用のprivate_laneとして切り出し、共通前提を一箇所にまとめると再利用しやすい。
キーストア管理の自動化
# android/fastlane/Fastfile
require 'base64'
platform :android do
# キーストアの存在確認と復元
private_lane :ensure_keystore_exists do
# Fastfileの位置を基準にパスを固定しておくと、実行ディレクトリに左右されない
keystore_path = File.expand_path("../app/my-app-keystore.jks", __dir__)
unless File.exist?(keystore_path)
# Base64エンコードされたキーストアをデコード
decode_keystore_from_base64
end
end
# Base64からキーストアを復元
private_lane :decode_keystore_from_base64 do
keystore_path = File.expand_path("../app/my-app-keystore.jks", __dir__)
base64_content = ENV["KEYSTORE_BASE64"]
if base64_content.nil? || base64_content.empty?
raise "KEYSTORE_BASE64 environment variable is required"
end
File.write(keystore_path, Base64.decode64(base64_content))
end
end
注意: KEYSTORE_BASE64の改行や余計なスペースでデコードが失敗しやすく、keystore_pathの誤りは既存ファイルを上書きするので慎重に。CIログへBase64文字列を出力しない運用にしておきましょう。
Base64化例:
$ base64 -i android/app/my-app-keystore.jks | pbcopy # クリップボードにコピー
Base64化はバイナリをシークレットに載せるための手段で、鍵はリポジトリに含めずシークレットで保管し、ジョブ開始時にデコードして復元するのが定番。ログ出力やechoで文字列を漏らさないようにすること。
バージョン管理の自動化
# android/fastlane/Fastfile
platform :android do
# バージョンコードの自動インクリメント
private_lane :sync_version_code_with_play_console do |options|
track = options[:track]
json_key_path = ENV["GOOGLE_PLAY_JSON_KEY_PATH"]
unless json_key_path
raise "GOOGLE_PLAY_JSON_KEY_PATH environment variable is required"
end
# Play Consoleから最新のバージョンコードを取得
all_version_codes = []
["internal", "alpha", "beta", "production"].each do |track_name|
track_codes = google_play_track_version_codes(
package_name: "com.example.myapp",
track: track_name,
json_key: json_key_path
)
all_version_codes.concat(track_codes) if track_codes
end
# 最高のバージョンコードを取得して+1
max_version_code = all_version_codes.max || 0
new_version_code = max_version_code + 1
# バージョンコードを更新
increment_version_code(
app_project_dir: "app",
version_code: new_version_code
)
end
end
全トラックのバージョンコードを集め最大値+1を採用するので、内部テストと本番を並行しても番号衝突を防げます。app_project_dirで対象を明示し、package_name誤りやAPI一時失敗に備えてリトライ/対象絞り込みを検討。Play側の「既存より小さいバージョンコード」エラーを避ける定番パターン。
iOS用Fastfileの詳細解説
基本的なFastfile構造
# ios/fastlane/Fastfile
default_platform(:ios)
platform :ios do
# 開発用ビルド
desc "Build development version"
lane :dev do
setup_ci if ENV['CI']
setup_app_store_connect_api
match(
type: "development",
readonly: true,
app_identifier: "com.example.myapp",
git_url: ENV["MATCH_GIT_URL"],
git_basic_authorization: ENV["MATCH_GIT_BASIC_AUTHORIZATION"]
)
build_app(
workspace: "MyApp.xcworkspace",
scheme: "MyApp",
configuration: "Debug",
export_method: "development"
)
end
# TestFlight配信
desc "Build and upload to TestFlight"
lane :beta do
setup_ci if ENV['CI']
setup_app_store_connect_api
match(
type: "appstore",
readonly: true,
app_identifier: "com.example.myapp",
git_url: ENV["MATCH_GIT_URL"],
git_basic_authorization: ENV["MATCH_GIT_BASIC_AUTHORIZATION"]
)
build_app(
workspace: "MyApp.xcworkspace",
scheme: "MyApp",
configuration: "Release",
export_method: "app-store"
)
upload_to_testflight(
skip_waiting_for_build_processing: true
)
end
# App Store配信
desc "Build and upload to App Store"
lane :release do
setup_ci if ENV['CI']
setup_app_store_connect_api
match(
type: "appstore",
readonly: true,
app_identifier: "com.example.myapp",
git_url: ENV["MATCH_GIT_URL"],
git_basic_authorization: ENV["MATCH_GIT_BASIC_AUTHORIZATION"]
)
build_app(
workspace: "MyApp.xcworkspace",
scheme: "MyApp",
configuration: "Release",
export_method: "app-store"
)
upload_to_app_store(
skip_metadata: true,
skip_screenshots: true
)
end
end
iOSは署名設定依存が大きいため、matchのapp_identifierとスキームのバンドルIDを揃え、CIではxcode-selectでXcodeバージョンを固定しておくと安全。setup_ciでCI向けキャッシュ/キーチェーンを整え、ビルド前にmatchで証明書・プロビジョニングプロファイルを復元するのが必須。ビルドは.xcworkspace、バージョン操作は.xcodeprojを対象にする住み分けを意識するとPods環境でも安定する。
App Store Connect API設定
# App Store Connect APIの設定
def setup_app_store_connect_api
key_id = ENV["APP_STORE_CONNECT_KEY_ID"] || raise("APP_STORE_CONNECT_KEY_ID is required")
app_store_connect_api_key(
key_id: key_id,
issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
key_filepath: "./fastlane/AuthKey_#{key_id}.p8",
duration: 1200,
in_house: false
)
end
APIキーはApp Store Connectの「ユーザーとアクセス」→「キー」で発行し、Key IDとIssuer IDを控え、.p8はfastlane/AuthKey_*.p8に置いてGitから外す。.p8は一度しかダウンロードできないためCIシークレットに保管し、ログ出力や過度なduration設定を避け、不要なキーは無効化する。key_id/issuer_id/key_filepathでJWTを生成し、Fastlane経由で署名付きリクエストを送る仕組み。
証明書管理(Match)の仕組み
# 証明書とプロビジョニングプロファイルの管理
lane :certificates do
setup_ci if ENV['CI']
setup_app_store_connect_api
# 開発用証明書
match(
type: "development",
readonly: false,
app_identifier: "com.example.myapp",
git_url: ENV["MATCH_GIT_URL"],
git_basic_authorization: ENV["MATCH_GIT_BASIC_AUTHORIZATION"]
)
# 配信用証明書
match(
type: "appstore",
readonly: false,
app_identifier: "com.example.myapp",
git_url: ENV["MATCH_GIT_URL"],
git_basic_authorization: ENV["MATCH_GIT_BASIC_AUTHORIZATION"]
)
end
更新は期限前だけreadonly: falseで実行し、通常はtrueに戻してCIでは取得専用に固定するのが安全。MATCH_PASSWORDやMATCH_GIT_BASIC_AUTHORIZATIONの管理者を分け、PRや定常運用ではreadonlyを強制して誤更新を防ぐ。
バージョン管理の自動化
先ほどのbetaレーンに以下のバージョン管理処理を追加します(ビルド前に挿入する想定です)。
# TestFlightから最新のビルド番号を取得してインクリメント
current_version = get_version_number(xcodeproj: "MyApp.xcodeproj")
begin
latest_build = latest_testflight_build_number(
app_identifier: "com.example.myapp",
version: current_version,
initial_build_number: 0
)
increment_build_number(
xcodeproj: "MyApp.xcodeproj",
build_number: latest_build + 1
)
rescue => e
# エラーの場合はローカルでインクリメント
increment_build_number(xcodeproj: "MyApp.xcodeproj")
end
挿入位置はbetaレーンのbuild_app直前が定番で、ビルド前に最新番号を+1して被りを防ぐ。releaseにも入れればベータ/本番の整合が保て、rescueでAPI失敗時もローカルインクリメントに切り替えてパイプライン停止を回避。latest_testflight_build_numberが返す最新値+1を使うことで並列CIでも衝突しにくい。
証明書管理の仕組み
Matchによる証明書管理
従来の証明書管理の問題:
- 証明書の期限切れ
- チーム間での証明書共有の困難
- 証明書の紛失リスク
- 手動での証明書更新
Matchの解決策:
- Gitベースの証明書管理: 証明書をGitリポジトリで管理
- 自動更新: 期限切れ前に自動で更新
- チーム共有: チーム全員が同じ証明書を使用
- 暗号化: 証明書は暗号化されて保存
Matchの設定手順
# 1. Matchの初期化
$ fastlane match init
# 2. 証明書の生成
$ fastlane match development
$ fastlane match appstore
# 3. 証明書の復元
$ fastlane match development --readonly
$ fastlane match appstore --readonly
環境変数の設定
Matchでは証明書を暗号化してGitに保存するため、復号用のパスワード(MATCH_PASSWORD)を必ず設定します。
# Match用の環境変数
export MATCH_GIT_URL="https://github.com/your-org/certificates"
export MATCH_GIT_BASIC_AUTHORIZATION="your-git-token"
export MATCH_PASSWORD="your-match-encryption-password" # 証明書暗号化の復号パスワード(必須)
# App Store Connect API
export APP_STORE_CONNECT_KEY_ID="your-key-id"
export APP_STORE_CONNECT_ISSUER_ID="your-issuer-id"
# Google Play Console API
export GOOGLE_PLAY_JSON_KEY_PATH="path/to/service-account.json"
CI環境でのシークレット管理
GitHub ActionsなどのCIでは、上記の環境変数(MATCH_PASSWORD、MATCH_GIT_BASIC_AUTHORIZATION、APP_STORE_CONNECT_*、GOOGLE_PLAY_JSON_KEY_PATHなど)をリポジトリの「Secrets」に保存し、ワークフロー内でenvとして渡します。キーストアやp8鍵のようなファイルはBase64化してシークレットに登録し、ジョブ開始時にデコードしてファイルとして配置する運用が安全です。
ストア配信の自動化
Google Play Store配信
前述のinternal/productionレーンでは、末尾のアップロード部分を以下のように設定します(抜粋)。
# internalレーンの配信部分
upload_to_play_store(
json_key: ENV["GOOGLE_PLAY_JSON_KEY_PATH"],
track: "internal",
aab: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],
skip_upload_metadata: true,
skip_upload_changelogs: true,
release_status: "completed"
)
# productionレーンの配信部分
upload_to_play_store(
json_key: ENV["GOOGLE_PLAY_JSON_KEY_PATH"],
track: "production",
aab: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],
skip_upload_metadata: true,
skip_upload_changelogs: true,
release_status: "draft" # 手動でレビュー提出
)
主なオプション: テキスト類を別フローで管理したいときはskip_upload_metadata/skip_upload_changelogsを使い、release_statusで自動リリースかドラフト停止かを切り替える。内部テストはcompletedで即配信、本番はdraftにして人の確認を挟むと安全。段階的ロールアウトをしたい場合は、本番レーンを以下のように設定します。
upload_to_play_store(
json_key: ENV["GOOGLE_PLAY_JSON_KEY_PATH"],
track: "production",
aab: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],
release_status: "inProgress",
user_fraction: 0.1 # 10%ロールアウトから開始
)
user_fractionを徐々に増やしていけば指標を見ながら安全に全量展開できる。Play側の処理待ちはスキップできないため、CIのタイムアウトを長めにするか、アップロードとリリース判定を別ジョブに分けておくと安定。
App Store配信
beta/releaseレーンの配信部分も抜粋で示します。ビルド処理の後段に配置してください。
# betaレーンの配信部分
upload_to_testflight(
skip_waiting_for_build_processing: true,
changelog: "Beta build from Fastlane - my-app v#{get_version_number(xcodeproj: "MyApp.xcodeproj")}",
distribute_external: false,
groups: ["App Store Connect Users"]
)
# releaseレーンの配信部分
upload_to_app_store(
skip_metadata: true,
skip_screenshots: true,
force: true,
submit_for_review: false, # 手動でレビュー提出
precheck_include_in_app_purchases: false
)
TestFlightは審査が軽く配布が速いので、まずbetaで社内/クローズド配布→問題なければreleaseで本番提出の二段階が安全。提出前はビルド番号とバージョン整合、プライバシー設定、スクショ/説明文、税務・銀行情報、テストアカウント記載などを確認して差し戻しを減らす。
よくある問題とトラブルシューティング
1. 証明書関連のエラー
問題: 証明書が見つからない
Error: Could not find a matching code signing identity
解決策:
# Matchで証明書を復元
lane :fix_certificates do
match(
type: "development",
readonly: true,
app_identifier: "com.example.myapp"
)
end
2. Google Play Console APIエラー
問題: API認証エラー
Error: The caller does not have permission
解決策:
- サービスアカウントの権限確認
- Google Play Developer APIの有効化
- JSONキーファイルのパス確認
3. App Store Connect APIエラー
問題: APIキーが無効
Error: Invalid API key
解決策:
- APIキーの有効期限確認
- キーIDとIssuer IDの確認
- キーファイル(.p8)の存在確認
まとめ
今回はFastlaneを使ったモバイルアプリの配信自動化について学びました:
- Fastlaneの基本概念とメリット
- Android用Fastfileの詳細解説
- iOS用Fastfileの詳細解説
- Matchによる証明書管理
- ストア配信の自動化
- よくある問題とトラブルシューティング
Fastlane導入の効果:
- 配信時間の短縮: 2時間 → 10分(※環境やプロジェクト規模により変動)
- 人的エラーの削減: 手動操作の自動化
- チーム開発の効率化: 統一された配信プロセス
- 品質の向上: 一貫した配信プロセス
次回は、マルチプラットフォーム対応のCI/CD戦略について詳しく見ていきます!
複数の環境(ステージング/本番)をどう管理するか、チーム開発での運用のコツについて解説します。
それでは、次回もお楽しみに!
次回予告: 「マルチプラットフォーム対応のCI/CD戦略」- 戦略編
- 環境分離(ステージング/本番)の設計
- 証明書とキーストアの管理方法
- バージョン管理の自動化戦略
- ビルド成果物の管理と保持期間
- エラーハンドリングとロールバック戦略
- セキュリティ考慮事項
お疲れ様でした!
Discussion