FlutterのCI/CD - Android編

⏱️約10分
シェア:

アプリケーション開発を効率的に進めるうえで、CI/CD pipeline の構築は欠かすことができません。本記事では、Flutter による Android アプリ開発の CI/CD pipeline 構築時に必要な情報について記載します。

本記事では pipeline 構築には github actions を使用します。また、fastlane は使用していません。

1. ローカルでビルドする

pipeline を作成する前にビルドに必要な設定を行ってローカルで動くことを確認しましょう。 公式サイトのBuild and release an Android appに詳細な情報が書いていますので、ここでは簡単に記載します。また、Java や Flutter のインストールは完了している前提です。flutter doctorを実行してエラー等がある場合は、まずそれを解消してください。

1-1. key.jks と key.properties

Android アプリ開発時にはアプリに署名するための鍵が必要です。その鍵が key.jks ファイル、その鍵のパスワードなどが記載されたファイルが key.properties ファイルです。

key.jks は下記のコマンドで生成されます。

Windows

bash
1keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key

Mac/Linux

bash
1keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key

このコマンドで鍵を生成するときに key password と store password が必要になります。任意のパスワードを設定し、その情報は key.properties に記載しておきます。

properties
1storePassword=<your password>
2keyPassword=<your password>
3keyAlias=key
4storeFile=C:/Users/USER_NAME/key.jks // depends on your environment

これらのファイルを用意したら次にビルド時にこれらのファイルを使って署名できるように Android 側のファイルを修正する必要があります。

1-2. app/bundle.gradle

bundle.gradle ファイルを下記のように修正します。bundle.gradle ファイルは 2 つあるので、app/bundle.gradle の方を修正します。

gradle
1def keystoreProperties = new Properties()
2def keystorePropertiesFile = rootProject.file('key.properties')
3if (keystorePropertiesFile.exists()) {
4 keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
5}
6
7android {
8 signingConfigs {
9 release {
10 keyAlias keystoreProperties['keyAlias'] ? keystoreProperties['keyAlias'] : "key"
11 keyPassword keystoreProperties['keyPassword'] ? keystoreProperties['keyPassword'] : "$System.env.KEY_PASSWORD"
12 storeFile file("../key.jks")
13 storePassword keystoreProperties['storePassword'] ? keystoreProperties['storePassword'] : "$System.env.STORE_PASSWORD"
14 }
15 }
16
17 buildTypes {
18 release {
19 signingConfig signingConfigs.release
20 }
21 }
22}

最初のブロックは先ほど作った key.properties ファイルを読み込むための記述です。rootProject.file('key.properties')の部分で、プロジェクトのルートに存在するkey.propertiesを読み込んでいます。ファイル名やファイルの置き場所を変えたければここのコードで読み出すファイル名等を変更しましょう。

次に android ブロックの中に二つのブロックを記載しています。

signingConfigs は文字通り、アプリに署名するための configuration が記載されていて、ここで、先ほど読み込んだ key.properties や key.jks の情報を取り出しています。 ここで設定するのは keyAlias, keyPassword, storeFile, storePassword の 4 つです。先ほど読み込んだ key.properties ファイルから読み込む場合はkeystoreProperties['keyPassword']という形式で読み出せます。上記の例はあえて色んな書き方をしています。例えば、keyAlias は properties ファイルから読み出して、null だった場合は固定値でkeyという値を与えています。また、keyPassword は properties から読み出して、null だったら環境変数のKEY_PASSWORDを使うように指定しています。storeFile はfile("../key.jks")で直接パスを指定してファイルを読み込んでいますね。 このように、いろんな方法で値を指定できるので、任意の方法を使ってください。まあ、key.properties を作っているなら普通は全部そこから読み出すのがいいと思いますが。

buildTypes は build タイプを指定するブロックで、ここで、先ほど設定した signingConfig を読み込んでいます。

最後にビルドをしてみましょう。

bash
1flutter build apk --release

成功すると、apk が生成されます。 ちなみにこのコマンドでビルドすると、appbundle を使用するか、apk を split しろ、という警告が出るかもしれませんが、今は無視してかまいません。後で少し解説します。

2. github に secrets を登録する

ローカルでビルドできるようになったら github actions でビルドするための下準備として、必要な secret を github に登録しておきます。今回、build で必要になる最低限の secret は下記です。github への secret の登録方法等の詳細は公式サイトのEncrypted Secretsを参照してください。

  • key password
  • store password
  • key.jks

key password と store password は文字列ですので、そのまま登録すればいいです。key.jks はバイナリなのでそのまま登録はできません。このような場合、base64 エンコードを行い文字列化して、登録してください。参照する際は base64 デコードを忘れずに。

GitHub の secret を使うのではなく、他の secret 管理サービスと連携するということも考えられます。例えば Azure の KeyVault や GCP の SecretManager で管理して、pipeline からアクセスするケースなどです。

いずれにしても、key.jks などはソースコードとは別にして、厳重に管理し、ビルド時のみ参照するような構成にしましょう。

3. GitHub にワークフローを追加する

最後に、github actions を使って pipeline を構築していきます。 github actions では pipeline のワークフローを yaml ファイルで定義できます。 .github/workflows/xxx.yamlにワークフローを記載していきます。 github actions の詳細な説明は公式サイトに譲るとして、今回の目的である flutter で Android アプリを開発するときに最低限必要な部分を説明します。

yaml
1name: CICD
2
3on:
4 pull_request:
5 branches: [master]
6 workflow_dispatch:
7
8jobs:
9 dev:
10 runs-on: ubuntu-latest
11
12 steps:
13 # checkout source code
14 - uses: actions/checkout@v2
15 # setup java
16 - name: set up JDK 1.8
17 uses: actions/setup-java@v1
18 with:
19 java-version: 1.8
20 # setup flutter
21 - name: Setup flutter
22 uses: subosito/flutter-action@v1
23 with:
24 flutter-version: "2.0.1"
25 # pub get
26 - run: flutter pub get
27 # analyze
28 - run: flutter analyze
29 # test
30 - run: flutter test
31 # build
32 - name: build dev
33 env:
34 KEY_JKS: ${{ secrets.KEY_JKS }}
35 KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
36 STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
37 GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }}
38 run: echo $KEY_JKS | base64 --decode --ignore-garbage > android/key.jks && flutter build apk --release

上から見ていくと、コードをチェックアウトしてきて、java をセットアップし、flutter をセットアップし、analyze や test を行ったうえで build をしていきます。build 以外の操作は、特に文字通りのことをやっているだけなので説明を割愛します。build のところは複雑なので少し詳細を見ていきます。

build のコマンドは&&でいくつかのコマンドをつなぎ合わせているので、1 つずつ見ていきます。

bash
1echo $KEY_JKS | base64 --decode --ignore-garbage > android/key.jks

このコマンドは、key.jks を secret から取り出して、ファイル化しています。key.jks を github の secret に登録する場合、バイナリをそのまま登録することはできないので、base64 encode で文字列に変換して secret に登録します。ここでは、secret の文字列を base64 decode で元に戻してから android/key.jks ファイルにしています。先ほどの bundle.gradle の説明の中でfile("../key.jks")でファイルを読み込むように記載していたので、指定された場所に key.jks ファイルを置くための処理になります。

気づいた方もいるかもしれませんが、この例ではkey.propertiesファイルの生成はやっていません。先ほどのbundle.gradleファイルの記述の中で、key.propertiesファイルがない時は、key passwordとstore passwordは環境変数から読み込むように記述していました。下記のenvの設定で環境変数にこれらのパスワードが設定されたので、key.propertiesがなくても環境変数から読み込むことができています。もちろんkey.propertiesファイルを生成しても構いません。bundle.gradleの記述と合わせて任意の方法を適用すればいいのです。

yaml
1- name: build dev
2 env:
3 KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
4 STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}

次に build のコマンドです。

bash
1flutter build apk --release

これは先ほどローカルでビルドしたときと同じです。このままだと、apk を split するか、appbundle 形式を使え、という警告が出ると思います。

公式サイトのBuilding the app for releaseに書かれていますが、android では app bundle 形式でビルドするのが推奨されています。なので、基本的に app bundle でビルドすればいいと思いますが、実機でテストしたり、Firebase の App Distribution を使う場合は apk が必要です。ここは用途に合わせてどちらをビルドするのか検討しましょう。

例えば、普段の CI/CD では apk をビルドして App Distribution への登録までをする。リリース用の pipeline では app bundle をビルドして Google Play Store に登録する。などの使い分けが考えられます。下に一例を載せときます。

App Distribution に登録

yaml
1name: CICD
2
3on:
4 pull_request:
5 branches: [master]
6 workflow_dispatch:
7
8jobs:
9 dev:
10 runs-on: ubuntu-latest
11
12 steps:
13 # checkout source code
14 - uses: actions/checkout@v2
15 # setup java
16 - name: set up JDK 1.8
17 uses: actions/setup-java@v1
18 with:
19 java-version: 1.8
20 # setup flutter
21 - name: Setup flutter
22 uses: subosito/flutter-action@v1
23 with:
24 flutter-version: "2.0.1"
25 # pub get
26 - run: flutter pub get
27 # analyze
28 - run: flutter analyze
29 # test
30 - run: flutter test
31 # build
32 - name: build
33 env:
34 KEY_JKS: ${{ secrets.KEY_JKS }}
35 KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
36 STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
37 GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }}
38 run: echo $KEY_JKS | base64 --decode --ignore-garbage > android/key.jks && echo $GOOGLE_SERVICES > android/app/google-services.json && flutter build apk --release
39 # app distribution
40 - name: Firebase App Distribution
41 uses: wzieba/Firebase-Distribution-Github-Action@v1.2.2
42 with:
43 appId: ${{secrets.FIREBASE_APP_ID}}
44 token: ${{secrets.FIREBASE_APP_TOKEN}}
45 file: build/app/outputs/apk/release/app-release.apk
46 groups: Tester

google Play Store へ登録

yaml
1name: Release
2
3on:
4 workflow_dispatch:
5
6jobs:
7 dev:
8 runs-on: ubuntu-latest
9
10 steps:
11 # checkout source code
12 - uses: actions/checkout@v2
13 # setup java
14 - name: set up JDK 1.8
15 uses: actions/setup-java@v1
16 with:
17 java-version: 1.8
18 # setup flutter
19 - name: Setup flutter
20 uses: subosito/flutter-action@v1
21 with:
22 flutter-version: "2.0.1"
23 # pub get
24 - run: flutter pub get
25 # analyze
26 - run: flutter analyze
27 # test
28 - run: flutter test
29 # build
30 - name: build
31 env:
32 KEY_JKS: ${{ secrets.KEY_JKS }}
33 KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
34 STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
35 GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }}
36 run: echo $KEY_JKS | base64 --decode --ignore-garbage > android/key.jks && flutter build appbundle --obfuscate --split-debug-info=build/app/outputs/symbols --release
37 # google play
38 - name: Create service account json
39 env:
40 SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON}}
41 run: echo $SERVICE_ACCOUNT_JSON > android/app/service-account.json
42 - uses: r0adkll/upload-google-play@v1
43 with:
44 serviceAccountJson: android/app/service-account.json
45 packageName: your app id
46 releaseFiles: build/app/outputs/bundle/release/app-release.aab
47 track: alpha
48 whatsNewDirectory: release-notes
49 mappingFile: build/app/outputs/mapping/release/mapping.txt

4. その他のオプション

4-1. Firebase の google-services.json

bash
1echo $GOOGLE_SERVICES > android/app/google-services.json

Firebase に接続する場合は google-services.json が必要です。その場合は上記のコマンドを追加して、secret に登録した google-services.json の中身をファイル化し、ビルド時に参照するようにします。それ以外にも bundle.gradle 等に設定の追加が必要ですが、その説明についてはスコープ外とします。FlutterFire - android installationあたりが参考になると思います。

4-2. App Distribution

Firebase が提供する機能の 1 つで、開発中のアプリを登録して、テスターに配布することができます。これを使う場合は build 後に下記のタスクを pipeline に追加しましょう。

yaml
1- name: Firebase App Distribution
2 uses: wzieba/Firebase-Distribution-Github-Action@v1.2.2
3 with:
4 appId: your app id
5 token: ${{secrets.FIREBASE_APP_TOKEN}}
6 file: build/app/outputs/apk/release/app-release.apk
7 groups: Tester

appId は対象のアプリケーション ID です。 token は、firebase にアクセスするための token です。Firebase CLIを参考に、事前に token を発行して GitHub の secrets に登録しておきましょう。 file は対象の apk です。app bundle は登録できないので注意してください。 groups はテスターのグループです。事前に Firebase 上で作成しておきましょう。 その他のオプションについてはFirebase Distribution Github Actionを確認してください。

4-3. プロダクトフレーバー

プロダクトフレーバーを使用する場合は、ビルド時に--flavor <flavor name>を指定します。プロダクトフレーバーについてはbuild variantsを参考にしてください。

bash
1flutter build appbundle --obfuscate --split-debug-info=build/app/outputs/symbols --release --flavor jp

プロダクトフレーバーを使うと、ソースコードは共通で、アプリケーション ID をフレーバーごとに変えて別アプリとしてビルドできたりします。

gradle
1android {
2
3 defaultConfig {
4 applicationId "xxx.yyy.zzz"
5 ...
6 }
7
8 flavorDimensions "targetArea"
9 productFlavors {
10 jp {
11 applicationIdSuffix ".jp"
12 dimension "targetArea"
13 }
14 eu {
15 applicationIdSuffix ".eu"
16 dimension "targetArea"
17 }
18 }

上記の例は、エリアごとにアプリケーションを変えています。このように app/bundle.gradle に記載しておくと、アプリケーション ID が、defaultConfig で指定した値に各フレーバーのapplicationIdSuffixを組み合わせた値になります。例えば、ビルド時に--flavor jpと指定すると、アプリケーション ID は、xxx.yyy.zzz.jpとなります。

さらに、manifest もフレーバーごとに変更できます。用途としては、例えば、google AdMob で広告を表示する場合、ApplicationId ごとに manifest に id を記載する必要があります。今回の例のようにフレーバーごとにアプリ ID を変えたい場合は manifest もフレーバーごとに変える必要があります。 下記のようにフォルダを構成し、それぞれのフレーバーに対応する manifest ファイルを置いておけばそれが可能となります。

txt
1android/app/src
2 |- main/AndroidManifest.xml
3 |- jp/AndroidManifest.xml
4 |- eu/AndroidManifest.xml

android/app/src/mainの下にベースとなる AndroidManifest.xml を置き、各フレーバー名の下にそのフレーバー用の AndroidManifest.xml を置きます。 そうすれば、各フレーバーを指定してビルドしたアプリの manifest は main の manifest と対象のフレーバーの manifest をマージしたものになります。

xml
1<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2 package="xxx.yyy.zzz">
3 <uses-permission android:name="android.permission.INTERNET" />
4 <application>
5 <meta-data
6 android:name="com.google.android.gms.ads.APPLICATION_ID"
7 android:value="your admob app id"/>
8 </application>
9</manifest>

上記のように、フレーバーごとの manifest に admob のアプリ ID を記載することができます。

4-4. --dart-define

このオプションを使うと、build 時に環境変数を指定できます。例えば、ソースコードは共通化し、アプリの仕向けに応じて接続先のバックエンドの region を変える、などできます。

bash
1flutter build appbundle --obfuscate --split-debug-info=build/app/outputs/symbols --release --flavor jp --dart-define=REGION=asia-northeast1

これは先ほどのプロダクトフレーバーと組み合わせていますが、jp 向けのアプリケーションをビルドして、その接続先リージョンを REGION という環境変数に設定しています。 こうすると、アプリケーションからは以下のコードで読み出せます。

dart
1String region = String.fromEnvironment('REGION'); // get 'asia-northeast1'

これらをうまく使うことで、共通のソースコードで複数のアプリケーションをビルドできます。

4-5. Google Play Store に登録する

pipeline から Google Play Store に登録するときは、下記のタスクを build 後に追加します。

yaml
1- name: Create service account json
2 env:
3 SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON}}
4 run: echo $SERVICE_ACCOUNT_JSON > android/app/service-account.json
5- uses: r0adkll/upload-google-play@v1
6 with:
7 serviceAccountJson: android/app/service-account.json
8 packageName: your app id
9 releaseFiles: build/app/outputs/bundle/release/app-release.aab
10 track: alpha
11 whatsNewDirectory: release-notes
12 mappingFile: build/app/outputs/mapping/release/mapping.txt

2 つのタスクから構成されています。1 つ目のタスクは、Google Play Store にアクセスするための Service Account の資格情報(service-account.json)を secrets から取得してきてファイル化しています。なので、事前に service-account.json の内容を GitHub の secrets に登録しておく必要があります。

2 つ目のタスクは、Google Play Store に app bundle を登録しています。各オプションの説明は、upload google playをご覧ください。リリースノートの登録等もできるので大変便利です。

参考文献

Build and release an Android app

Encrypted Secrets

Building the app for release

FlutterFire - android installation

Firebase CLI

Firebase Distribution Github Action

build variants

upload google play

シェア:

関連記事

FlutterのLocalization
Guides

FlutterのLocalization

FlutterでARBファイルを使ったLocalization対応方法を解説します。Flutter 2.0.1で動作確認しています。

mark241