Skip to main content
mobile2026年2月8日5分で読めます

Google Play ストアへのAndroidデプロイを自動化する

Gradleの設定からCI/CDを介したPlayストアへの公開まで、Androidのビルドとデプロイを自動化する方法を解説します。

androidgradlecicd
Google Play ストアへのAndroidデプロイを自動化する

AndroidアプリをPlayストアにリリースするのは、APKをビルドしてアップロードするだけで簡単そうに聞こえます。しかし実際には、署名設定、ビルドバリアント、バンドル形式、Playストアのトラック、段階的ロールアウト、そしてキャリア選択を疑うほど多くのGradle設定が関わってきます。iOSとAndroidの両方のパイプラインが必要なFlutterアプリを含む、いくつかのプロジェクトでAndroidデプロイを自動化した後、コミットから本番ロールアウトまで手動介入なしで全てを処理するセットアップができました。

Androidビルドシステム

何かを自動化する前に、Androidビルドがどのように機能するかをしっかり理解する必要があります。XcodeがGUIの背後でほとんどの複雑さを処理するiOSとは異なり、Androidのビルドシステムは完全にGradleベースであり、コードを通じて設定されます。これは実際には自動化にとって有利です。すべてが明示的でバージョン管理されています。

リリースビルドのためのGradle設定

android/app/build.gradle(Kotlin DSLに移行している場合はbuild.gradle.kts)が中心となる設定ファイルです。リリースビルドの場合、署名、ミニファイ、最適化を設定する必要があります。

android {
    compileSdkVersion 35

    defaultConfig {
        applicationId "com.yourcompany.yourapp"
        minSdkVersion 24
        targetSdkVersion 35
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

    signingConfigs {
        release {
            keyAlias System.getenv("KEY_ALIAS") ?: properties["keyAlias"]
            keyPassword System.getenv("KEY_PASSWORD") ?: properties["keyPassword"]
            storeFile file(System.getenv("KEYSTORE_PATH") ?: properties["keystorePath"] ?: "keystore.jks")
            storePassword System.getenv("STORE_PASSWORD") ?: properties["storePassword"]
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

署名設定は、まず環境変数から読み込み、次にローカルのプロパティファイルにフォールバックします。このパターンにより、開発者はkey.propertiesファイルを使用してローカルでビルドに署名でき、CIは環境変数を使用できます。同じGradleファイルが変更なしで両方のコンテキストで機能します。

ローカルプロパティファイル

ローカル開発のために、android/ディレクトリにkey.propertiesファイルを作成し(そしてすぐに.gitignoreに追加します):

storePassword=your_store_password
keyPassword=your_key_password
keyAlias=your_key_alias
keystorePath=../keys/release-keystore.jks

次に、build.gradleでそれを参照します:

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

署名設定

Androidアプリの署名はiOSのコード署名よりもシンプルですが、永続的なリスクを伴います。アップロードキーを紛失すると、アプリを更新する能力を失います。Googleはこれを軽減するためにPlay App Signingを導入しており、私はその使用を強く推奨します。

Play App Signing

Play App Signingを有効にすると、Googleが実際の配布キーを保持します。アップロードキーでアップロードに署名し、Googleがユーザーに配信する前に配布キーで再署名します。利点は次のとおりです。

  • キー紛失からの回復: アップロードキーを紛失した場合、Googleがリセットできます。Play App Signingがない場合、キーを紛失すると新しいアプリリストを作成する必要があります。
  • より小さなAPK: Googleは配布キーを使用して、各デバイス構成に合わせてアプリを最適化できます。
  • キーローテーション: インストール済みのユーザーに影響を与えることなく、アップロードキーをローテーションできます。

Play Consoleの「設定」>「アプリの署名」で有効にします。新しいアプリの場合、デフォルトで有効になっています。

キーストアの生成

新しいキーストアを作成する必要がある場合:

keytool -genkey -v \
  -keystore release-keystore.jks \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000 \
  -alias your_key_alias

このキーストアファイルを安全に保管してください。CIの場合、私はそれをbase64エンコードし、GitHub Actionsのシークレットとして保存します:

base64 -i release-keystore.jks | pbcopy

次に、ビルド前にCIワークフローでデコードします:

- name: Decode keystore
  run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks

AAB vs APK

Googleは現在、Playストアのすべての新しいアプリにAndroid App Bundles (AAB) を要求しています。この違いはビルドパイプラインにとって重要です。

APK (Android Package) は、すべてのデバイス構成に必要なすべてを含む単一のファイルです。サイズは大きいですが、普遍的です。どのAndroidデバイスでもどのAPKでもインストールできます。APKは、内部配布、物理デバイスでのテスト、Playストア外での配布には依然として有用です。

AAB (Android App Bundle) は、すべてのコードとリソースを含みますが、Google Playが各デバイスに最適化されたAPKを生成できるようにします。Pixelフォンを持つユーザーは、すべての画面密度とCPUアーキテクチャのアセットではなく、必要なリソースのみをダウンロードします。バンドルは通常、ユニバーサルAPKよりも15〜30%小さくなります。

CIパイプラインでは、両方をビルドします:

# For Play Store
flutter build appbundle --release

# For internal testing / direct installation
flutter build apk --release --split-per-abi

--split-per-abiフラグは、各CPUアーキテクチャ(arm64-v8a、armeabi-v7a、x86_64)ごとに個別のAPKを生成し、直接配布のファイルサイズを削減します。

PlayストアのためのFastlane Supply

Fastlaneのsupplyアクションは、Playストアへのアップロードとメタデータ管理を処理し、iOSのdeliverがすることと同様の役割を果たします。

Supplyのセットアップ

まず、適切な権限を持つGoogle Playサービスアカウントが必要です。

  1. Google Cloud Consoleにアクセスし、サービスアカウントを作成します。
  2. JSONキーファイルをダウンロードします。
  3. Play Consoleで、「設定」>「APIアクセス」に移動し、サービスアカウントをリンクします。
  4. アプリに対して「リリース管理者」権限を付与します。
# android/fastlane/Appfile
json_key_file(ENV["PLAY_STORE_JSON_KEY"] || "path/to/service-account.json")
package_name("com.yourcompany.yourapp")

基本的なアップロード

default_platform(:android)

platform :android do
  desc "Deploy to Play Store Internal Testing"
  lane :internal do
    gradle(
      task: "bundle",
      build_type: "Release",
      project_dir: "./"
    )

    supply(
      track: "internal",
      aab: "../build/app/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end
end

Flutterプロジェクトの場合、Fastlane経由でGradleタスクを使用するよりも、Flutterビルドコマンドを直接実行する方が好きです。なぜなら、Flutterのビルドプロセスには、Gradleだけでは正しく処理できないDartコンパイルステップが含まれているからです。

lane :internal do
  Dir.chdir("..") do
    sh("flutter build appbundle --release")
  end

  supply(
    track: "internal",
    aab: "../build/app/outputs/bundle/release/app-release.aab",
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

Playストアのトラック

Playストアには4つのトラックがあり、それぞれの使用時期を理解することはリリース戦略にとって重要です。

内部テスト

  • テスターは最大100人。
  • レビューは不要 — ビルドはほぼ即座に利用可能。
  • 最適な用途: 開発チームのテスト、QAサイクル、プロモーション前のビルドチェック。

クローズドテスト (アルファ)

  • テスターは無制限ですが、リストは自分で管理します。
  • Googleによる簡単なレビューが必要(通常数時間)。
  • 最適な用途: 招待されたユーザーとのベータプログラム、クライアントプレビュー。

オープンテスト (ベータ)

  • Playストアのリストから誰でも参加できます。
  • レビューが必要。
  • 最適な用途: 公開ベータプログラム、広範なリリース前のフィードバック収集。

プロダクション

  • すべてのユーザーが利用可能。
  • 完全なレビューが必要。
  • 段階的ロールアウトをサポート。

トラック間のプロモーション

Fastlaneはトラックのプロモーションを簡単に行えます:

lane :promote_to_beta do
  supply(
    track: "internal",
    track_promote_to: "beta",
    skip_upload_changelogs: false,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

lane :promote_to_production do
  supply(
    track: "beta",
    track_promote_to: "production",
    rollout: "0.1",  # 10% staged rollout
    skip_upload_changelogs: false,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

段階的ロールアウト

段階的ロールアウトは、AndroidがiOSに比べて持つ最大の利点の1つです。ユーザーの割合にリリースし、徐々に増やしながら、各段階でクラッシュ率とユーザーフィードバックを監視できます。

lane :staged_rollout do |options|
  percentage = options[:percentage] || "0.1"

  supply(
    track: "production",
    rollout: percentage,
    aab: "../build/app/outputs/bundle/release/app-release.aab"
  )
end

lane :increase_rollout do |options|
  percentage = options[:percentage] || "0.5"

  supply(
    track: "production",
    rollout: percentage,
    skip_upload_aab: true,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

lane :complete_rollout do
  supply(
    track: "production",
    rollout: "1.0",
    skip_upload_aab: true,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

典型的なロールアウトスケジュール:

  1. 1日目: 10% — Firebase Crashlyticsでクラッシュ率を監視。
  2. 2〜3日目: 25% — ユーザーフィードバックとサポートチケットを確認。
  3. 4〜5日目: 50% — 大規模でのパフォーマンス低下がないことを確認。
  4. 6〜7日目: 100% — 完全リリース。

いずれかの段階で問題が発生した場合、すべてのユーザーに影響を与えることなく、ロールアウトを停止して修正をプッシュできます。

GitHub Actionsワークフロー

Android用の完全なCI/CDワークフローを以下に示します:

name: Deploy to Play Store

on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      track:
        description: 'Play Store track'
        required: true
        default: 'internal'
        type: choice
        options:
          - internal
          - alpha
          - beta
          - production

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'
          cache: 'gradle'

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.27.x'
          channel: stable
          cache: true

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.2'
          bundler-cache: true
          working-directory: android

      - name: Flutter dependencies
        run: flutter pub get

      - name: Decode keystore
        run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks

      - name: Create service account file
        run: echo '${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}' > android/play-store-key.json

      - name: Build and deploy
        working-directory: android
        env:
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
          STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
          KEYSTORE_PATH: app/keystore.jks
          PLAY_STORE_JSON_KEY: play-store-key.json
        run: bundle exec fastlane ${{ github.event.inputs.track || 'internal' }}

      - name: Cleanup secrets
        if: always()
        run: |
          rm -f android/app/keystore.jks
          rm -f android/play-store-key.json

このワークフローに関するいくつかの注意点:

UbuntuランナーはAndroidで問題なく動作します。 macOSが必要なiOSとは異なり、AndroidビルドはLinuxで実行されます。UbuntuランナーはGitHub Actionsでより安価で起動が速いです。

現在のバージョンのAndroid Gradle PluginにはJava 17が必要です。プロジェクトが古いAGPバージョンを使用している場合、Java 11が必要になるかもしれませんが、これはますます稀になっています。

クリーンアップステップは、ビルドが失敗した場合でもデコードされたシークレットを削除します。これは多層防御の措置です。GitHub Actionsランナーは一時的ですが、良い習慣です。

バージョン管理

Androidは2つのバージョン識別子を使用します:versionCode(アップロードごとに増加する必要がある整数)とversionName(人間が読めるバージョン文字列)。

Flutterプロジェクトの場合、両方ともpubspec.yamlから取得されます:

version: 1.2.3+45
# 1.2.3 = versionName
# 45 = versionCode

私はCIの実行番号を使用してバージョンコードのインクリメントを自動化しています:

lane :set_version_code do
  build_number = ENV["GITHUB_RUN_NUMBER"] || "1"

  Dir.chdir("..") do
    current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
    sh("sed -i 's/^version: .*/version: #{current_version}+#{build_number}/' pubspec.yaml")
  end
end

これにより、ブランチをまたいでもバージョンコードが常に増加することが保証されます。より詳細な制御が必要な場合は、Playストアから最新のバージョンコードを取得できます:

lane :smart_version_code do
  current = google_play_track_version_codes(track: "internal").max || 0
  new_code = current + 1

  Dir.chdir("..") do
    current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
    sh("sed -i 's/^version: .*/version: #{current_version}+#{new_code}/' pubspec.yaml")
  end
end

Playストアのリスティング管理

Supplyは、バージョン管理されたファイルからPlayストアのリスティング全体を管理できます:

android/fastlane/metadata/android/
├── en-US/
│   ├── title.txt
│   ├── short_description.txt
│   ├── full_description.txt
│   ├── changelogs/
│   │   ├── default.txt
│   │   └── 45.txt          # Changelog for versionCode 45
│   └── images/
│       ├── phoneScreenshots/
│       │   ├── 1_home.png
│       │   └── 2_detail.png
│       ├── featureGraphic.png
│       └── icon.png
└── ru-RU/
    ├── title.txt
    ├── short_description.txt
    └── full_description.txt

現在のリスティングをダウンロードして開始点とするには:

fastlane supply init

次にファイルを更新し、変更をプッシュします:

lane :update_listing do
  supply(
    skip_upload_aab: true,
    skip_upload_apk: true
  )
end

ProGuardとR8の設定

minifyEnabledがtrueの場合、R8(ProGuardの後継)はコードを縮小、難読化、最適化します。これは本番ビルドに不可欠であり、APKサイズを削減し、リバースエンジニアリングを困難にします。しかし、適切に設定されていないと問題を引き起こす可能性もあります。

一般的な問題とそのProGuardルール:

# Keep Flutter's classes
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }

# Keep Firebase classes
-keep class com.google.firebase.** { *; }

# Keep models used with JSON serialization
-keep class com.yourcompany.yourapp.models.** { *; }

# Keep classes referenced via reflection
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes InnerClasses

# Common crash fix: OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**

リリースビルドを徹底的にテストしてください。R8の問題は、デバッグビルドでは現れないランタイムクラッシュとして現れることがよくあります。Flutterの--obfuscateおよび--split-debug-infoフラグは、さらなる最適化を可能にします:

flutter build appbundle --release --obfuscate --split-debug-info=build/symbols

build/symbolsディレクトリは保持してください。クラッシュレポートからのスタックトレースをシンボリケートするために必要です。

完全なFastfile

default_platform(:android)

platform :android do
  desc "Deploy to internal testing"
  lane :internal do
    set_version_code
    build_release
    supply(
      track: "internal",
      aab: "../build/app/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end

  desc "Deploy to closed beta"
  lane :beta do
    set_version_code
    build_release
    supply(
      track: "beta",
      aab: "../build/app/outputs/bundle/release/app-release.aab",
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end

  desc "Deploy to production with staged rollout"
  lane :production do
    set_version_code
    build_release
    supply(
      track: "production",
      rollout: "0.1",
      aab: "../build/app/outputs/bundle/release/app-release.aab"
    )
  end

  desc "Promote internal to beta"
  lane :promote_to_beta do
    supply(
      track: "internal",
      track_promote_to: "beta"
    )
  end

  desc "Increase production rollout"
  lane :increase_rollout do |options|
    supply(
      track: "production",
      rollout: options[:percentage] || "0.5",
      skip_upload_aab: true
    )
  end

  private_lane :build_release do
    Dir.chdir("..") do
      sh("flutter build appbundle --release --obfuscate --split-debug-info=build/symbols")
    end
  end

  private_lane :set_version_code do
    build_number = ENV["GITHUB_RUN_NUMBER"] || Time.now.strftime("%Y%m%d%H%M")
    Dir.chdir("..") do
      current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
      sh("sed -i '' 's/^version: .*/version: #{current_version}+#{build_number}/' pubspec.yaml")
    end
  end
end

iOS自動化との違い

両方のプラットフォームを自動化してきましたが、いくつかの重要な違いがあります。

Androidの署名はよりシンプルです。 iOSの証明書とプロビジョニングプロファイルの複雑な手順とは異なり、単一のキーストアファイルで済みます。matchやその他の証明書管理ツールは不要です。

macOSの必須要件はありません。 AndroidビルドはLinuxで実行されるため、CIランナーはより安価で高速です。

レビュー時間が速い。 GoogleのレビュープロセスはAppleよりも通常速く、内部トラックへのアップロードはレビューがまったく不要です。

段階的ロールアウトはネイティブです。 iOSでは段階的リリースが可能ですが、同じ粒度で割合を制御したり、ロールアウト中に停止したりすることはできません。

PlayストアAPIはより寛容です。 ストアリスティングの実験や価格設定など、ほとんどすべてをプログラムで管理できます。AppleのApp Store Connect APIにはより多くの制限があります。

主な欠点はGradleのビルド時間です。R8最適化を伴うクリーンなAndroidビルドは、同等のiOSビルドよりも大幅に時間がかかることがあります。Gradleの組み込みキャッシュと.gradleディレクトリのCIレベルのキャッシュの両方が役立ちます。

成熟したパイプラインの姿

すべてのセットアップが完了した後、日々の体験は目に見えないものになるはずです。mainにプッシュするとビルドが実行され、数分後にはテスターが内部トラックで新しいバージョンを利用できるようになります。リリースにタグを付けると、段階的ロールアウトで本番ビルドが進行します。

このパイプラインは、「私のマシンでは動作する」という問題のカテゴリ全体を排除し、すべてのビルドが再現可能であることを保証し、チームにリリースが特別なイベントではないという自信を与えます。最後の部分こそが本当の目標です。デプロイを誰も考えないほど日常的なものにすることです。

DU

Danil Ulmashev

Full Stack Developer

一緒にお仕事しませんか?