บทความนี้จะอธิบายวิธีอัปโหลด Android App Bundle (.aab) ไปที่ Play Store beta track โดยอัตโนมัติ ใช้ Android Studio และ AWS เป็นโครงสร้างพื้นฐานคลาวด์
หลังอัปโหลด app bundle แล้ว จะทริกเกอร์การแจ้งเตือน Slack
สิ่งนี้คุ้มค่าเพราะสร้าง observability และจัดลำดับความสำคัญของกระบวนการ
เทคโนโลยีที่ใช้
ทรัพยากรที่ใช้ในบทช่วยสอนนี้:
- Android Studio
- AWS CodeBuild
- AWS Lambda
- S3
- Slack
ภาพรวมโครงการระดับสูง

ภาพแสดงโครงสร้างโดยรวม
จำเป็นต้องมี Code Pipeline บน AWS สำหรับ Android repository โดยมี Code Build เป็นหนึ่งใน stages
Push ไปที่ master branch จะทริกเกอร์ Code Build Code Build project จะเซ็น Android app จาก command line และอัปโหลด artifact ไป S3 bucket
อัปโหลด bundle ไป S3 จะทริกเกอร์ Lambda ซึ่งจะดาวน์โหลด bundle และอัปโหลดไป Play Store ผ่าน Google Publishing API เมื่อได้ response 200 Lambda จะทริกเกอร์ Slack notification
วิธีได้ Google Play Service Account Key
เพื่อใช้ Google Play Publisher API ต้องมี Google Play Service Account key
Service account คือบัญชีที่ทำงานแทนคุณเมื่อ server ติดต่อกัน อ่านเพิ่มเติมเกี่ยว OAuth2.0 สำหรับ server-to-server communication ได้ที่นี่
ดูวิธีสร้าง service account และให้สิทธิ์เข้าถึง Google Play Publisher API ที่นี่
หลังสร้าง service account และให้สิทธิ์แล้ว ดาวน์โหลด service account key และเก็บไว้ปลอดภัย จะอัปโหลดไป S3 bucket ไป
วิธีเซ็น Android Bundle
สำคัญคือเซ็น Android App Bundle อย่างไร Google มีเอกสารที่ดีอยู่ที่นี่
สรุปลิงก์ด้านล่าง
สร้าง private key ด้วย keytool:
keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias ตั้งชื่อ key อะไรก็ได้ ตัวอย่างใช้ my-release-key.jks alias ก็เหมือนกัน ใช้ชื่อและ alias ที่ถูกต้องตลอดบทช่วยสอน
เปิด build.gradle ใน app directory ใน Android Studio เพิ่มโค้ด:
android { ... defaultConfig { ... } signingConfigs { release { // ระบุ absolute path หรือใส่ keystore file ในไดเรกทอรีเดียวกับ build.gradle storeFile file("my-release-key.jks") storePassword "password" keyAlias "my-alias" keyPassword "password" } } buildTypes { release { signingConfig signingConfigs.release ... } } } หากเปลี่ยนชื่อ release key ให้ระบุชื่อใหม่ ตัว alias ด้วย
Store password คือรหัสผ่านตอนอัปโหลดแอปครั้งแรกไป Play Store
ตอนรัน ./gradlew :app:bundleRelease จาก command line ใน Android Studio จะได้ signed App Bundle
ลบข้อมูลเซ็นออกจากโค้ด
Commit โค้ดพร้อมข้อมูลเซ็นเป็น plain text ใน build.gradle เป็นความเสี่ยงด้านความปลอดภัย
Google มีเอกสารเรื่องนี้ที่นี่
สร้าง keystore.properties ใน root ของโปรเจกต์
เนื้อหาไฟล์:
storePassword=myStorePassword keyPassword=myKeyPassword keyAlias=myKeyAlias storeFile=myStoreFileLocation Store password และ key password คือรหัสผ่านตอนอัปโหลด app bundle ครั้งแรก
keyAlias และ storeFile คือ alias ตอนสร้าง private key และ location ของ private key
โหลดไฟล์นี้เข้า build.gradle Gradle ทำงานเป็น DSL ทำให้เขียน config ง่ายขึ้น
// Load properties from keystore.properties def keystorePropertiesFile = rootProject.file("keystore.properties") // Creating a new Properties() object def keystoreProperties = new Properties() // If keystorePropertiesFile exists, read from that, else set from build environment if (keystorePropertiesFile.exists()) { // Loading the keystoreProperties file keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } else { // Read all environment variables from the build environment 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 นั้นสำหรับ Code Build ต่อไป
เปลี่ยน signingConfigs ใน build.gradle:
signingConfigs { release { storeFile file(keystoreProperties['storeFile']) keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storePassword keystoreProperties['storePassword'] } } ตั้งค่า AWS Code Pipeline
ไม่รายละเอียดมาก เพราะตรงไปตรงมา
สร้าง AWS Code Pipeline 3 stages:
- Source stage เชื่อมกับ master branch ของ GitHub repository
- Build stage เชื่อมกับ AWS Code Build
- Deploy stage deploy ไป S3 bucket
เอกสารเพิ่มเติมเกี่ยว Code Pipeline ที่นี่
ตั้งค่า AWS S3
ต้องมี Code Pipeline พร้อม Code Build เป็น stage แล้ว สร้าง S3 buckets 2 ตัว:
- Bucket เก็บ release key เรียกว่า
release-key.jks - Bucket เก็บ Google Play Service Account private key (ที่ดาวน์โหลดตอนสร้าง service account)
ต้องอนุญาตให้ Code Build service role เข้าถึง buckets ไปที่ IAM console หา Code Build service role เอา ARN
ไปที่ Permissions tab ของ bucket release-key.jks เพิ่ม policy:
{ "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/*" } ] } Policy นี้อนุญาตให้เข้าถึง S3 bucket จากเครื่องที่ CodeBuild รัน
แทนที่ ARN ด้วย ARN ของบัญชีคุณ ระบุ ARN ของ Code Build service role ให้ถูกต้อง
Bucket ที่สองไม่ต้องเปลี่ยน policy จะเพิ่ม permission ให้ Lambda role แทน
ตั้งค่า AWS CodeBuild
สร้าง buildspec.yml ใน root ของโปรเจกต์
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 ไฟล์นี้ดึง release key จาก bucket บันทึกลงไฟล์ local บน Code Build server export variables สำหรับ build.gradle รันคำสั่ง Gradle release
ก่อนรันใน Code Build ต้องเพิ่ม variables ใน Code Build environment ไปที่ AWS Code Build console เลือก build project ของ Android app
เลือก Edit > Environment

เลือก Additional Configuration dropdown จะมีตัวเลือกเพิ่ม environment variables ผ่าน key-value pairs
ตอนนี้ Code Build จะ export variables ที่ระบุได้
Pipeline รัน Code Build จะดาวน์โหลด private key เซ็นและ build Android app อัปโหลด signed bundle ไป S3 bucket
ตั้งค่า Slack App
Observability เป็นหัวใจของอัตโนมัติ ต้องรู้ว่า automation รันเมื่อไหร่ สำเร็จหรือล้มเหลว และสาเหตุ
AWS มักใช้ CloudWatch แต่วิธี Slack integration ก็ดีพอ
วิธีง่ายสุดคือสร้าง Slack app ส่ง notification ไปที่ app จาก automation workflow
เรียนรู้สร้าง Slack app ที่นี่ กระบวนการง่าย ใช้ได้ในไม่กี่นาที
หลังสร้าง app จะได้ WebHook URL ใช้เรียก app โพสต์ใน channel ที่เกี่ยวข้อง เก็บ WebHook URL ไว้ จะใช้กับ AWS Lambda function
ตั้งค่า AWS Lambda
ตอนนี้มี Android App Bundle ถูกเซ็น build และอัปโหลดไป S3 ต่อไปต้องอัปโหลด bundle ไป beta track บน Play Store
วิธีคือตั้งค่า AWS Lambda ทริกเกอร์เมื่อ bundle อัปโหลดไป S3 bucket Lambda จะรัน ดาวน์โหลด bundle ดึง service account key อัปโหลด bundle ไป Play Store beta track
หลังสร้าง Lambda และเพิ่มทริกเกอร์เมื่อไฟล์อัปโหลดไป bucket ดูโค้ด:
"""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 ใช้ library googleapiclient และ discovery module สร้าง URL สำหรับ Google Play Publishing API
Lambda ดาวน์โหลด service account key จาก bucket ที่ตั้งค่าไว้ ต้องระบุ bucket names ให้ถูกต้อง
ขึ้นอยู่กับว่าอัปโหลดสำเร็จหรือล้มเหลว ต้องการส่งข้อความ Slack เพิ่ม Slack WebHook URL จากส่วนก่อนหน้าเข้า environment variables ของ Lambda ฟังก์ชันใช้ Python os module เข้าถึง environment variable โพสต์ข้อความไป Slack
หาก Lambda ล้มเหลว อาจเพราะ Lambda ไม่มีสิทธิ์เข้าถึง S3 bucket ที่เก็บ key ของ Google Play service account จะเห็น error message
แก้โดยเพิ่ม permission ให้ Lambda role
Policy ที่ต้องเพิ่ม:
{ "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 ของ bucket ด้วย ARN ที่เกี่ยวข้องกับบัญชีคุณ
สรุป
เสร็จแล้ว แม้จะไม่ง่ายและมีส่วนประกอบเยอะ แต่ automation นี้จะประหยัดเวลาและความพยายามมาก
หากทีมของคุณเผยแพร่ app update บ่อย ไม่อยากถูกขวางขวากหน้าจากการขาดคนที่รับผิดชิด release
การสร้าง automation แบบนี้ทำให้ CI/CD workflow ราบรื่นและแข็งแกร่งขึ้น
สนใจบล็อกแบบนี้ อ่านเพิ่มที่ https://redixhumayun.github.io หรือติดตาม Twitter