Android
 Computer >> コンピューター >  >> システム >> Android

Android App Bundle(.aab)をPlayストアへ自動アップロードする方法 ― Android Studio × AWS × Slack連携

本記事では、Android App Bundle(.aabファイル)をPlayストアのベータトラックへ自動的にアップロードする仕組みの構築方法を解説します。開発環境にはAndroid Studioを使用し、クラウドインフラにはAWSを採用します。

さらに、App Bundleのアップロードが完了したら、Slackへ通知を送る仕組みも実装します。

このような自動化は、リリースプロセスの可観測性(オブザーバビリティ)を高め、作業の優先順位付けを効率化するうえで大きな価値があります。

使用する技術スタック

このチュートリアルで使用するツールとサービスは以下のとおりです。

  1. Android Studio
  2. AWS CodeBuild
  3. AWS Lambda
  4. S3
  5. Slack

プロジェクト全体のアーキテクチャ概要

Android App Bundle(.aab)をPlayストアへ自動アップロードする方法 ― Android Studio × AWS × Slack連携

上記の画像は、今回構築するシステム全体の構成イメージです。

大まかな流れとしては、まずAndroidアプリのリポジトリに対してAWS Code Pipelineを設定します。このCode Pipelineの中で、CodeBuildがステージの一つとして動作します。

AndroidアプリのリポジトリのmasterブランチへのプッシュをトリガーとしてCodeBuildが起動し、コマンドラインからAndroidアプリに署名を行い、成果物(アーティファクト)をS3バケットへアップロードします。

S3へのバンドルのアップロードを検知してLambdaが起動し、バンドルをダウンロードしたうえでGoogle Publishing APIを使ってPlayストアへアップロードします。APIから200レスポンスを受け取ると、LambdaはSlackへの通知を実行します。

Google Playサービスアカウントキーの取得方法

Google Play Publisher APIを利用するには、Google Playのサービスアカウントキーが必要です。

サービスアカウントとは、サーバー間通信において自分の代理として動作できるアカウントのことです。Googleがサーバー間通信にOAuth2.0をどのように活用しているかについては、公式ドキュメントを参照してください。

サービスアカウントの作成手順や、Google Play Publisher APIへのアクセス権限の付与方法についても、公式ドキュメントに詳しい説明があります。

サービスアカウントを作成し、適切な権限を付与したら、必ずサービスアカウントキーをダウンロードして安全に保管してください。このキーは後ほどS3バケットへアップロードすることになります。

Androidバンドルへの署名方法

最初に押さえるべきポイントは、Android App Bundleへの署名方法です。Googleが比較的充実した公式ドキュメントを公開しているので、そちらも併せて参照するとよいでしょう。

以下に要点をまとめます。

まずkeytoolを使って秘密鍵を生成します。コマンド例は次のとおりです。

keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias

キーのファイル名やエイリアスは自由に決められます。ここではmy-release-key.jksという名前を使用しています。以降の手順では、ご自身で設定した正しい名前とエイリアスを使用するようにしてください。

次に、Android Studioでappディレクトリ内のbuild.gradleを開き、以下のコードブロックを追加します。

android {
    ...
    defaultConfig { ... }
    signingConfigs {
        release {
            // keystoreファイルへの絶対パスを指定するか、
            // build.gradleと同じディレクトリにkeystoreファイルを配置する必要があります。
            storeFile file("my-release-key.jks")
            storePassword "password"
            keyAlias "my-alias"
            keyPassword "password"
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
            ...
        }
    }
}

リリースキーの名前をデフォルトから変更している場合は、新しい名前を必ず指定してください。エイリアスも同様です。

ストアパスワードには、初めてアプリをPlayストアにアップロードした際に生成したパスワードを設定します。

これで、Android Studioのコマンドラインから./gradlew :app:bundleReleaseを実行すると、署名済みのApp Bundleが生成されるようになります。

署名情報をソースコードから取り除く方法

署名情報が平文のままbuild.gradleファイルにコミットされているのはセキュリティリスクであり、攻撃の糸口になりかねません。

この点についてもGoogleが公式ドキュメントを提供していますので、参考にしてください。

まず、プロジェクトのルートディレクトリにkeystore.propertiesファイルを作成します。

ファイルの内容は以下のようにします。

storePassword=myStorePassword
keyPassword=myKeyPassword
keyAlias=myKeyAlias
storeFile=myStoreFileLocation

ストアパスワードとキーパスワードには、初めてApp Bundleをストアへアップロードしたときに使用したパスワードを指定します。

keyAliasstoreFileには、それぞれ秘密鍵の作成時に設定したエイリアスと、作成した秘密鍵の保存場所を指定します。

続いて、このファイルをbuild.gradleに読み込ませます。GradleはDSL(ドメイン固有言語)として動作するため、設定の記述が非常に柔軟に行えるのが便利なところです。

//  keystore.propertiesからプロパティを読み込む
def keystorePropertiesFile = rootProject.file("keystore.properties")

//  新しいProperties()オブジェクトを生成
def keystoreProperties = new Properties()

//  ファイルが存在すればそこから読み込み、なければビルド環境の環境変数から取得
if (keystorePropertiesFile.exists()) {
    //  keystorePropertiesファイルを読み込む
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
} else {
    //  ビルド環境の環境変数を読み込む
    keystoreProperties.setProperty("storeFile", "${System.getenv('STORE_FILE')}")
    keystoreProperties.setProperty("keyAlias", "${System.getenv('KEY_ALIAS')}")
    keystoreProperties.setProperty("keyPassword", "${System.getenv('KEY_PASSWORD')}")
    keystoreProperties.setProperty("storePassword", "${System.getenv('STORE_PASSWORD')}")
}

if条件があることに気づいたかもしれませんが、現時点では気にしなくて大丈夫です。これは後述のCodeBuildのための処理です。

そのうえで、build.gradle内のsigningConfigsセクションを以下のように書き換えます。

signingConfigs {
        release {
            storeFile file(keystoreProperties['storeFile'])
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
            storePassword keystoreProperties['storePassword']
        }
    }

AWS Code Pipelineのセットアップ方法

この部分は比較的シンプルなので、詳細には踏み込みません。

以下の3つのステージを持つAWS Code Pipelineを構成します。

  1. GitHubリポジトリのmasterブランチに接続されたSource(ソース)ステージ
  2. AWS CodeBuildに接続されたBuild(ビルド)ステージ
  3. S3バケットへデプロイするDeploy(デプロイ)ステージ

Code Pipelineのセットアップに関する詳細は、AWSの公式ドキュメントを参照してください。

AWS S3のセットアップ方法

まず、CodeBuildをステージの一つとして含むCode Pipelineが構成済みであることを確認してください。次に、以下の2つのS3バケットを作成します。

  1. リリースキーを格納するバケット。ここではrelease-key.jksという名前にします。
  2. Google Playサービスアカウントの秘密鍵を格納するバケット(サービスアカウント作成時にダウンロードしたキーです)。

これらのバケットには、CodeBuildのサービスロールからのアクセスを許可する必要があります。CodeBuildのサービスロールは、Code Pipelineのセットアップ時に作成されているはずです。

IAMコンソールにアクセスし、CodeBuildのサービスロールを見つけてARNを控えておきましょう。

次に、コンソールからrelease-key.jksバケットの「アクセス許可」タブを開き、以下のポリシーを追加します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "arn:aws:iam::123456789:role/service-role/codebuild-service-role-dummy",
                ]
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::release-key-bucket/*"
        }
    ]
}

このポリシーにより、CodeBuildプロジェクトが実行される環境からS3バケットへのアクセスが可能になります。

ポリシー内のARNは、ご自身のアカウントのものに置き換えてください。ポリシー更新時に、CodeBuildサービスロールの正しいARNを指定するよう注意しましょう。

2つ目のバケットについては、アクセス許可ポリシーの変更は不要です。必要な権限は後ほどAWS Lambdaのロールに追加します。

AWS CodeBuildのセットアップ方法

次に、プロジェクトのルートフォルダにbuildspec.ymlファイルを作成します。

version: 0.2

phases:
  build:
    commands:
      - aws s3api get-object --bucket release-key.jks --key release-key.jks ./releaseKey.jks
      - cp ./releaseKey.jks ${CODEBUILD_SRC_DIR}/app/releaseKey.jks
      - export STORE_FILE=releaseKey.jks
      - export KEY_ALIAS=$keyAlias
      - export KEY_PASSWORD=$keyPassword
      - export STORE_PASSWORD=$storePassword
      - ./gradlew :app:bundleRelease

artifacts:
  files:
    - app/build/outputs/bundle/release/app-release.aab

このファイルの内容はシンプルです。指定したバケットからリリースキーを取得し、CodeBuildサーバー上の指定した場所にローカルファイルとして保存します。

続いて、build.gradleの設定が正しく機能するために必要な変数をすべてエクスポートします。最後に、コマンドラインからGradleのリリースコマンドを実行します。

このスクリプトをCodeBuildで実行する前に、必要な変数をCodeBuildの環境に登録しておく必要があります。まずAWS CodeBuildコンソールにアクセスし、Androidアプリ用のビルドプロジェクトを選択してください。

次に、下のスクリーンショットのように「編集」→「環境」を選択します。

Android App Bundle(.aab)をPlayストアへ自動アップロードする方法 ― Android Studio × AWS × Slack連携

表示される画面で「追加設定」のドロップダウンを開くと、キーバリュー形式で環境変数を追加できる項目があります。

これで、CodeBuildがbuildspec.ymlを実行する際に、指定した変数をエクスポートできるようになります。

ここまでの状態で、パイプラインを実行すると、CodeBuildが秘密鍵をダウンロードしてAndroidアプリに署名・ビルドを行い、署名済みのバンドルをS3バケットへアップロードできるようになっています。

Slackアプリのセットアップ方法

可観測性(オブザーバビリティ)は自動化の重要な要素です。自動化がいつ実行されたか、成功したのか失敗したのか、そして失敗した場合はその理由まで把握したいはずです。

AWSでは通常CloudWatchを使って可観測性を確保しますが、Slackとの統合でも十分に目的を果たせます。

Slackを自動化ワークフローに組み込む最も簡単な方法は、Slackアプリを作成し、自動化ワークフローからそのアプリへ通知を送信することです。

Slackアプリのセットアップ方法については公式ドキュメントを参照してください。手順はとても簡単で、数分でアプリを稼働させられるはずです。

アプリを作成すると、該当チャンネルへ投稿するためのWebHook URLが発行されます。このURLはAWS Lambda関数で使用するので、忘れずに控えておいてください。

AWS Lambdaのセットアップ方法

ここまでで、Android App Bundleの署名・ビルド・S3へのアップロードが実現できました。次は、そのバンドルをPlayストアのベータトラックへアップロードする仕組みが必要です。

そのためには、S3バケットへのバンドルのアップロードをトリガーとして起動するAWS Lambdaを構成します。トリガーが発火すると、Lambdaが実行され、バンドルをダウンロードし、サービスアカウントキーを取得して、バンドルをPlayストアのベータトラックへアップロードします。

Lambdaを作成し、「バケットにファイルがアップロードされたら実行する」トリガーを追加したら、以下のコードを確認してください。

"""This Python3 script is used to upload a new .aab bundle to the play store. The execution of this Python script
    occurs through an AWS Lambda which is invoked when a new file is uploaded to the relevant S3 buckets"""

import json
import boto3
import os
from urllib import request, parse
from google.oauth2 import service_account
import googleapiclient.discovery

#   Defining the scope of the authorization request
SCOPES = ['https://www.googleapis.com/auth/androidpublisher']

#   Package name for app
package_name = 'com.app.name'

#   Define the slack webhook url
slack_webhook_url = os.environ['SLACK_WEBHOOK_URL']

def send_slack_message(message):
    data = json.dumps({ 'text': message })
    post_data = data.encode('utf-8')
    req = request.Request(slack_webhook_url, data=post_data, headers={ 'Content-Type': 'application/json' })
    request.urlopen(req)

#   This is the main handler function
def lambda_handler(event, context):
    #   Create a new client S3 client and download the correct file from the bucket
    s3 = boto3.client('s3')
    s3.download_file('service-account-bucket-key', 'service-account-bucket-key.json', '/tmp/service-account-key.json')
    SERVICE_ACCOUNT_FILE = '/tmp/service-account-key.json'

    #   Download the app-release.aab file that triggered the Lambda
    bucket_name = event['Records'][0]['s3']['bucket']['name']
    file_key = event['Records'][0]['s3']['object']['key']
    s3.download_file(bucket_name, file_key, '/tmp/app-release.aab')
    APP_BUNDLE = '/tmp/app-release.aab'

    print(f"A bundle uploaded to {bucket_name} has triggered the Lambda")

    #   Create a credentials object and create a service object using the credentials object
    credentials = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_FILE, scopes=SCOPES
    )
    service = googleapiclient.discovery.build('androidpublisher', 'v3', credentials=credentials, cache_discovery=False)
    
    #   Create an edit request using the service object and get the editId
    edit_request = service.edits().insert(body={}, packageName=package_name)
    result = edit_request.execute()
    edit_id = result['id']

    #   Create a request to upload the app bundle
    try:
        bundle_response = service.edits().bundles().upload(
            editId=edit_id,
            packageName=package_name,
            media_body=APP_BUNDLE,
            media_mime_type="application/octet-stream"
        ).execute()
    except Exception as err:
        message = f"There was an error while uploading a new version of {package_name}"
        send_slack_message(message)
        raise err

    print(f"Version code {bundle_response['versionCode']} has been uploaded")

    #   Create a track request to upload the bundle to the beta track
    track_response = service.edits().tracks().update(
        editId=edit_id,
        track='beta',
        packageName=package_name,
        body={u'releases': [{
            u'versionCodes': [str(bundle_response['versionCode'])],
            u'status': u'completed',
        }]}
    ).execute()

    print("The bundle has been committed to the beta track")

    #   Create a commit request to commit the edit to BETA track
    commit_request = service.edits().commit(
        editId=edit_id,
        packageName=package_name
    ).execute()

    print(f"Edit {commit_request['id']} has been committed")

    message = f"Version code {bundle_response['versionCode']} has been uploaded from the bucket {bucket_name}.\nEdit {commit_request['id']} has been committed"
    send_slack_message(message)
    
    return {
        'statusCode': 200,
        'body': json.dumps('Successfully executed the app bundle release to beta')
    }

上記のLambdaは、googleapiclientライブラリとそのdiscoveryモジュールを使って、Google Play Publishing APIのURLを構築します。

続いて、先ほど作成したバケットからサービスアカウントキーをダウンロードします。バケット名は必ず正しく指定してください。

アップロードの成否にかかわらず、Slackへメッセージを送りたいところです。前のセクションで取得したSlack WebHook URLを、Lambdaの環境変数に追加してください。上記の関数ではPythonのosモジュールを使って環境変数にアクセスし、メッセージをSlackへ投稿します。

Lambdaが失敗する場合、多くはGoogle Playサービスアカウントのキーが保存されているS3バケットへのアクセス権限がLambdaにないことが原因です。その場合は、それを示すエラーメッセージが表示されます。

対処法は簡単で、Lambdaのロールに必要な権限を追加するだけです。

追加すべきポリシーは以下のとおりです。

{
    "Version": "2012-10-07",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObjectVersion",
                "s3:GetBucketVersioning",
                "s3:GetBucketAcl",
                "s3:GetObject",
                "s3:GetBucketTagging",
                "s3:GetBucketLocation",
                "s3:GetObjectVersionAcl"
            ],
            "Resource": [
                "arn:aws:s3:::arn:aws:s3:::your-bucket-name-with-service-account-key"
            ]
        }
    ]
}

バケットのARNをご自身のアカウントのものに置き換えれば、準備完了です。

まとめ

以上で完成です。決して簡単な構築ではありませんし、連携する要素も多いですが、この自動化は多くの時間と労力を節約してくれるはずです。

頻繁にアプリのアップデートをリリースするチームに所属しているなら、リリース担当者が一人しかいない状態に悩まされたくはないでしょう。

こうした自動化を整備することで、CI/CDワークフローは格段にスムーズで堅牢なものになります。

このような記事をもっと読みたい方は、https://redixhumayun.github.ioをご覧いただくか、Twitterでフォローしてください。

  1. 2019年のAndroidアプリ開発の実際:最新Android技術スタックへの完全移行

    Bitotsav 19 アプリがどのように現実になったか背景:Pantheon 17 ふり返り約2年前の2017年9月、友人のAshank Anshumanに誘われて、私たちの大学の技術祭向けアプリ開発を始めました。2週間ほど昼夜を問わず作業し、祭りの開幕に間に合うようにリリースすることができました。疲れ切ってはいましたが、自分たちが作ったものが実際に人々に使われるというのは、何とも言えない素晴らしい感覚でした。このアプリは主催者が参加者へ情報を伝える目的を完璧に果たしました。Pantheon 17 - Google Play若者が技術力を発揮できる全国的なプラットフォームを提供することを目指

  2. Windows 11でAndroidアプリを動かす方法|Amazon Appstore導入から使い方まで徹底解説

    Windows 11の最も待ち望まれていた機能がついに登場しました。Amazon Appstoreと「Windows Subsystem for Android(WSA)」を利用することで、Windows 11上で直接Androidアプリを実行できるようになっています。仕組みが気になる方も多いはず。この記事では、導入手順をステップごとにわかりやすく解説し、実際に試した使用感のレビューもお届けします。 ステップ1:必要条件を確認する 作業を始める前に、まずMicrosoftが定めるWindows 11でのAndroidアプリ実行の要件を確認しましょう。条件はそれほど複雑ではありません。現時点