Androidアプリ向けにビルドしたFlutterモジュールをGoogle Cloud Storageにデプロイする

f:id:zigenin:20201023114828p:plain

こんにちは。 ANDPADでモバイルアプリの開発を担当しているzigeninです。

ANDPADでは、既存のKotlinで書かれたAndroidアプリに、Flutterモジュールを組み込む取り組みをしています。 最近、Flutterモジュールを自動でデプロイできるようにしました。

Flutterの公式サイトだと、デプロイする方法までは記述されていなかったので、少し悩みました。 どなたかの参考になれば、幸いです。

構成

  • Flutterモジュールをaarとしてビルドする
  • GCS上にmavenリポジトリを構築する
    • リポジトリは特定のアカウントのみアクセスできるようにします*1
  • Flutterモジュールのリポジトリにタグをつけた時、aarのビルドとデプロイをする
    • GitHub Actionsを使います

Flutterモジュールをaarとしてビルドする理由

公式のAndroidアプリ向けFlutterモジュールのビルド方法
Integrate a Flutter module into your Android project - Flutter

Flutterのビルド方法は2つあります。

  • A:aarとしてビルドする
  • B:FlutterモジュールのソースコードをAndroidアプリと一緒にビルドする

aarを使う方法では、Androidアプリビルド時にFlutter SDKとFlutterモジュールのソースコードは不要です。 つまり、Flutterの開発をしていないメンバーがFlutterのことを意識しなくて済みます。 また、個人の環境ごとに、Flutter SDKのバージョンが異なる状況を回避できます。

以上より、aarを使う方法を採用しました。

GCSを使う理由

mavenリポジトリの構築方法はいくつかあります。

  1. GCS, S3などのストレージサービスを使う
  2. FlutterモジュールのGitリポジトリをmavenリポジトリとして扱う
    • git-repoプラグインを使う
  3. Sonatype Nexus無償版などを使い、自前のmavenリポジトリサーバを用意する
  4. Sonatype有償版などの用意されているmavenリポジトリサーバを使う
  5. Google Artifact Registryを使う

2の方法は、ビルド生成物をリポジトリに含めるのがいまいちなのと、 git-repoプラグインがメンテされていないので、不採用にしました。

3の方法は、アプリエンジニアでサーバを管理しないといけなくなるのが面倒なので不採用にしました。

4の方法は、1の方法に比べて料金が高いので不採用にしました。 それほど高機能なものを求めていないというのもあります。

5の方法が一番良さそうですが、Google Artifact Registryがα版なので採用は見送りました*2。 GCSと使い勝手に差がなさそうに思えたというのもあります。

ということで、消去法で1のストレージサービスを使う方法に決めました。 ストレージサービスにはGCSを採用しました。ANDPADでは、Firebaseを利用しているためです。 Firebase と GCP は、アカウントなどの設定を共有しているので、AWS S3を採用する場合よりも設定の手間が減ります。

GitHub Actionsを使う理由

深い理由はないです。 ANDPADでは、AndroidアプリはCircle CI、iOSアプリはBitriseを主に使っています。 それ以外のCIツールを使ってみたかったというのが大きいです。

環境構築手順

前提

  • Firebaseをすでに使っており、アプリエンジニアをFirebaseプロジェクトのメンバーに登録している
  • GCPの基本操作は把握している
  • GCPの課金を有効にしている(GCSのバケットを作るのに必須)
  • コマンドラインからGCPを扱うツールも導入済み(gcloud)
  • GCPのサービスアカウントを作成済み
  • GitHub Actionsが使える環境

1. GCSにバケットを作る(これがMavenリポジトリになる)

  1. Firebaseプロジェクトに対応するGCPプロジェクトを開く
  2. GCPのStorageにアクセス
  3. バケットを作成
  4. 一意なバケット名を適当に入力(ここでは「flutter-maven-repo」とします)
  5. リージョンとロケールを設定
    • どれを設定しても使えますが「単一リージョン」「asia-northeast1 (東京)」を設定しました
    • 可用性は低いもののレイテンシが小さく、価格が少しだけ安いからです($0.020/GB/月)
  6. デフォルトのストレージクラスに「Standard」を選択
  7. アクセス制御に「均一」を設定
    • 一部のアプリエンジニアのアクセスを禁止するモチベーションがないので
  8. 詳細設定はデフォルトのまま
  9. バケットの権限 -> メンバー -> 作成したサービスアカウントに「Storage オブジェクト管理者」のロールを与えて登録

2. mavenでGCSのバケットを扱うためのプラグインを導入する

Flutterモジュールのリポジトリのトップに「.mvn/extensions.xml」を作成します。 記述は以下です。

<extensions>
  <extension>
    <groupId>com.gkatzioura.maven.cloud</groupId>
    <artifactId>google-storage-wagon</artifactId>
    <version>1.7</version>
  </extension>
</extensions>

このファイルにより、Flutterモジュールをデプロイするときにプラグインを自動で入手してくれます。 このプラグインでは、1.7より新しいバージョンがありますが、それらを使うとGCSの認証に失敗します。

3. PCからFlutterモジュールをデプロイしてみる

CIツールからデプロイする前に、PCからデプロイしてみます。 ここで動作を見ておいた方が、設定の問題を解決しやすいです。

mavenをインストールします(初回のみ)。

brew install maven

gcloudにサービスアカウントを使ってログインします。

export GOOGLE_APPLICATION_CREDENTIALS=<サービスアカウントの認証情報JSONファイルへのパス>

# gcpのプロジェクト設定(不要かも)
gcloud config set project <your-project-id>

Flutterモジュールをビルド&デプロイします。

# Flutterモジュールをaarとしてビルド
flutter build aar --build-number=0.1.0

# ビルドしたモジュールをGCSにデプロイする(sqliteなどの依存モジュールもデプロイしている)
find . -name "*.pom" -type f -print0 | xargs -I{} -0 bash -c 'mvn deploy:deploy-file -Durl="gs://flutter-maven-repo/" -DpomFile="$0" -Dfile="${0%.pom}.aar"' '{}' \;

WebのGCSのBrowser上のflutter-maven-repoを見て、aarがアップロードされていればOKです。

4. AndroidアプリからFlutterモジュールを使う

flutter build aarを実行したときに、Androidアプリ側のbuild.gradleにどのような記述をすれば良いのか出力されています。 大体その通りに記述すればOKですが、少しだけGCSを独自の部分もあるので、記述を示します。

AndroidアプリのappモジュールからFlutterモジュールを使うとします。 このとき、app/build.gradleに必要な記述は以下です。

android {
    buildTypes {
        // Flutterモジュールのprofileビルドを使う場合。profileビルド不要なら、この記述は不要。
        profile {
            initWith debug

            // マルチモジュール構成にしているとき、他のモジュールのBuildTypeに'profile'が存在しないとビルドエラー。
            // 他のモジュールのBuildTypeに'profile'がない場合は、BuildTypeに'debug'を使うようにする。
            // 他のモジュールのBuildTypeに'profile'をいちいち追加しなくても良くなる。
            matchingFallbacks = ['debug']
        }
    }
}

repositories {
    maven {
        url 'https://storage.googleapis.com/download.flutter.io'
    }

    // Mavenリポジトリ用に作成したGCSのバケットのURLを記述。schemeはgsではなくgcsなので注意。
    maven {
        url 'gcs://flutter-maven-repositories'
    }
}

dependencies {
    debugImplementation 'jp.andpad.andpad_flutter:flutter_debug:0.2.0'
    profileImplementation 'jp.andpad.andpad_flutter:flutter_profile:0.2.0'
    releaseImplementation 'jp.andpad.andpad_flutter:flutter_release:0.2.0
}

GCSのバケットへアクセスする時にサービスアカウントが必要になります。 ここでは、デフォルトサービスアカウントを使うように設定します。

以下のコマンドを実行します。

# gcpのプロジェクト設定(不要かも)
gcloud config set project <your-project-id>

# Webブラウザでログイン画面が表示されるので、エンジニア個人のアカウントでログインします。
gcloud auth application-default login

# CIツール上でビルドする場合は、1つ上のコマンドではなく、以下のコマンドで認証情報を設定します。
# https://cloud.google.com/docs/authentication/production?hl=ja#passing_variable
# export GOOGLE_APPLICATION_CREDENTIALS=<サービスアカウントの認証情報の.json>

後は、Androidアプリをビルドするだけです*3

5. Flutterモジュールのリポジトリにタグを付けた時、aarのビルドとデプロイをする

Flutterモジュールのリポジトリに、セマンティックバージョン形式のタグ(例:v1.0.0)をつけたときに、aarをビルドしてデプロイするようにします。

GitHub Actions向けに、Flutterモジュールのリポジトリに「.github/workflows/publish-aar.yml」というファイルを作成します。 記述を以下に示します。デプロイのコマンドは、PC上でデプロイしたときと同じです。

name: publish-aar

on:
  push:
    tags:
      - "v[0-9].[0-9]+.[0-9]+"

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-java@v1
      with:
        java-version: '12.x'
    - uses: subosito/flutter-action@v1
      with:
        flutter-version: '1.22.0'
    - name: build aar
      run: |
        version=${GITHUB_REF#refs/tags/}
        flutter build aar --build-number=${version:1} 
    - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
      with:
        project_id: develop-andpad-40
        service_account_key: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
        export_default_credentials: true
    - name: mvn deploy
      run: |
        find . -name "*.pom" -type f -print0 | xargs -I{} -0 bash -c 'mvn deploy:deploy-file -Durl="gs://flutter-maven-repositories/" -DpomFile="$0" -Dfile="${0%.pom}.aar"' '{}' \;

後は、GitHubのSecretsの"GOOGLE_APPLICATION_CREDENTIALS"に、サービスアカウントの認証用JSONファイルの中身をコピペしておきます。

以上で、設定は全て完了です。 適当なタグをプッシュすれば、aarがビルドされてGCSにデプロイされます。

最後に

ANDPADでは、一緒に働く仲間を募集中です! engineer.andpad.co.jp

補足

GCSのmavenリポジトリを誰でも読み込めるようにする場合のAndroidアプリのビルドについて

mavenリポジトリの読み込み時に認証不要にした方が、運用が楽です。 Androidアプリをビルドするだけであれば、gcloudの設定は不要になります。

やり方を紹介しておきます。

まず、GCS上のmavenリポジトリのバケットにて、「allUsers」に「Storage閲覧者」の権限を付与します(WebのGCPのConsoleなどで実施)。

次に、app/build.gradleを以下のように記述します。 「4. AndroidアプリからFlutterモジュールを使う」の記述との違いは、mavenリポジトリのURLの記述のみです。

android {
    buildTypes {
        // Flutterモジュールのprofileビルドを使う場合。profileビルド不要なら、この記述は不要。
        profile {
            initWith debug
            matchingFallbacks = ['debug']
        }
    }
}

repositories {
    maven {
        url 'https://storage.googleapis.com/download.flutter.io'
    }

    // gcsでなくhttpsであることがポイント。gcsだと一般公開されているバケットでも認証失敗でビルドエラー
    maven {
        url 'https://storage.googleapis.com/flutter-maven-repositories'
    }
}

dependencies {
    debugImplementation 'jp.andpad.andpad_flutter:flutter_debug:0.2.0'
    profileImplementation 'jp.andpad.andpad_flutter:flutter_profile:0.2.0'
    releaseImplementation 'jp.andpad.andpad_flutter:flutter_release:0.2.0
}

後は、Androidアプリをビルドするだけです。gcloudの設定は不要です。

雑だが楽なデプロイ方法

ここまで、find + mvn deployコマンドでデプロイしましたが、gsutilコマンドでデプロイする方法もあります。

gsutil cp <flutter build aarで生成されるローカルmavenリポジトリのパス> gs://flutter-maven-repositories/

この方法だと、「.mvn/extensions.xml」は不要になりますし、デプロイ時のコマンドが簡単になります。 ただし、mavenリポジトリ内の「maven-metadata.xml」が不正になります。

maven-metadata.xmlには、これまでのモジュールのバージョンのリストが記載されます。 gsutil cpを使うと、最新のバージョンの情報しか記載されません。

といっても、この不正な状態でも、Androidアプリから過去バージョンのモジュールを取得できます。 なので、細かいことが気にならない方は、gsutilを使えば良さそうです。

FlutterモジュールをGCP Artifact Registryにデプロイする方法

https://medium.com/google-cloud/flutter-aar-deploy-to-maven-and-gcp-artifact-registry-d107adebec4d

ひとまず、GCP Artifact Registryの採用は見送りましたが、mvn deployの使い方は、このページが非常に参考になりました。

*1:Androidだとリバースエンジニアリング簡単なので、読み込みには制限かけない方が良い気もしています。最後の補足で、その場合の設定とAndroidアプリのビルド方法も記述しています。

*2:β版になった時点で採用する予定

*3:筆者の環境では、上記の認証情報の設定の後、PCを再起動しないと、Android Studioからのビルド時に認証が成功しませんでした。Terminalからgradlewでビルドする分には、PCの再起動は不要でした。