🚗

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開発における配信の複雑さ:

  1. 複数プラットフォーム: iOSとAndroid、それぞれ異なる手順
  2. 複雑な署名: 証明書、プロビジョニングプロファイル、keystore
  3. ストア固有の手順: App Store、Google Play、それぞれ異なるAPI
  4. メタデータ管理: 説明文、スクリーンショット、リリースノート
  5. バージョン管理: 複数ファイルの同期

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_platformplatformで対象宣言、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_existsvalidate_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は署名設定依存が大きいため、matchapp_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 IDIssuer IDを控え、.p8fastlane/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_PASSWORDMATCH_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_PASSWORDMATCH_GIT_BASIC_AUTHORIZATIONAPP_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

解決策:

  1. サービスアカウントの権限確認
  2. Google Play Developer APIの有効化
  3. JSONキーファイルのパス確認

3. App Store Connect APIエラー

問題: APIキーが無効

Error: Invalid API key

解決策:

  1. APIキーの有効期限確認
  2. キーIDとIssuer IDの確認
  3. キーファイル(.p8)の存在確認

まとめ

今回はFastlaneを使ったモバイルアプリの配信自動化について学びました:

  • Fastlaneの基本概念とメリット
  • Android用Fastfileの詳細解説
  • iOS用Fastfileの詳細解説
  • Matchによる証明書管理
  • ストア配信の自動化
  • よくある問題とトラブルシューティング

Fastlane導入の効果:

  • 配信時間の短縮: 2時間 → 10分(※環境やプロジェクト規模により変動)
  • 人的エラーの削減: 手動操作の自動化
  • チーム開発の効率化: 統一された配信プロセス
  • 品質の向上: 一貫した配信プロセス

次回は、マルチプラットフォーム対応のCI/CD戦略について詳しく見ていきます!

複数の環境(ステージング/本番)をどう管理するか、チーム開発での運用のコツについて解説します。

それでは、次回もお楽しみに!


次回予告: 「マルチプラットフォーム対応のCI/CD戦略」- 戦略編

  • 環境分離(ステージング/本番)の設計
  • 証明書とキーストアの管理方法
  • バージョン管理の自動化戦略
  • ビルド成果物の管理と保持期間
  • エラーハンドリングとロールバック戦略
  • セキュリティ考慮事項

お疲れ様でした!

NonEntropy Tech Blog

Discussion