이 글에서는 Android App Bundle(.aab 파일)을 Play 스토어의 베타 트랙에 자동으로 업로드하는 방법을 단계별로 소개합니다. 개발 환경은 Android Studio를 사용하고, 클라우드 인프라는 AWS를 활용합니다.
앱 번들 업로드가 완료되면 Slack 알림까지 자동으로 발송되도록 구성할 것입니다.
이런 자동화 파이프라인은 관측 가능성(observability) 확보와 배포 프로세스 우선순위 정리 등 여러 면에서 시간 투자 가치가 충분합니다.
사용할 기술 스택
이 튜토리얼에서 사용하는 도구와 서비스는 다음과 같습니다.
- Android Studio
- AWS CodeBuild
- AWS Lambda
- AWS S3
- Slack
프로젝트 전체 구조 한눈에 보기

위 이미지는 전체 시스템이 어떻게 구성되는지 보여주는 개요도입니다.
핵심 구조를 정리하면 다음과 같습니다. 먼저 Android 저장소를 대상으로 하는 Code Pipeline을 AWS에 구축해야 하며, 이 파이프라인의 단계 중 하나로 CodeBuild가 포함됩니다.
Android 앱 저장소의 master 브랜치에 푸시하면 CodeBuild가 트리거됩니다. CodeBuild 프로젝트는 명령줄에서 Android 앱에 서명하고, 생성된 아티팩트를 S3 버킷에 업로드합니다.
S3에 번들이 업로드되면 Lambda가 트리거되어 해당 번들을 다운로드한 뒤, Google Publishing API를 통해 Play 스토어에 업로드합니다. 응답 코드 200을 받으면 Lambda는 Slack 알림을 발송합니다.
Google Play 서비스 계정 키 발급 방법
Google Play Publisher API를 사용하려면 먼저 Google Play 서비스 계정 키가 필요합니다.
서비스 계정(service account)은 서버 간 통신에서 사용자를 대신해 작업을 수행할 수 있는 계정입니다. Google이 서버 간 통신에 OAuth 2.0을 활용하는 방식에 대한 내용은 공식 문서에서 확인할 수 있습니다.
서비스 계정 생성 및 Google Play Publisher API 권한 부여 절차 역시 Google 공식 문서를 참고하면 됩니다.
서비스 계정을 만들고 적절한 권한을 부여했다면, 반드시 서비스 계정 키를 다운로드해 안전하게 보관하세요. 이 키는 곧 S3 버킷에 업로드하게 됩니다.
Android 번들 서명 방법
가장 중요한 부분은 Android App Bundle에 서명하는 것입니다. Google에서 제공하는 공식 문서가 꽤 잘 정리되어 있으니 참고하시고, 여기서는 핵심만 요약하겠습니다.
먼저 keytool을 사용해 다음과 같이 비공개 키(private key)를 생성합니다.
keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias
키 이름은 자유롭게 지정할 수 있습니다. 위 예제에서는 my-release-key.jks라고 했으며, 별칭(alias) 역시 원하는 값으로 설정하면 됩니다. 튜토리얼 전반에서 본인의 실제 키 이름과 별칭으로 바꿔 사용하세요.
Android Studio에서 app 디렉터리 안의 build.gradle 파일을 열고 다음 코드 블록을 추가합니다.
android {
...
defaultConfig { ... }
signingConfigs {
release {
// 절대 경로를 지정하거나 build.gradle과 같은 디렉터리에
// keystore 파일을 포함시켜야 합니다.
storeFile file("my-release-key.jks")
storePassword "password"
keyAlias "my-alias"
keyPassword "password"
}
}
buildTypes {
release {
signingConfig signingConfigs.release
...
}
}
}
릴리즈 키 이름을 기본값이 아닌 다른 이름으로 변경했다면 새 이름을 정확히 지정해야 합니다. 별칭도 마찬가지입니다.
store password는 앱을 처음 Play 스토어에 업로드할 때 설정했던 비밀번호를 사용합니다.
이제 Android Studio의 명령줄에서 ./gradlew :app:bundleRelease 명령을 실행하면 서명된 App Bundle이 생성되는 것을 확인할 수 있습니다.
서명 정보 숨기기 (보안 처리)
서명 정보가 build.gradle 파일에 평문으로 커밋되어 있다면 심각한 보안 취약점이자 공격 경로가 될 수 있습니다.
Google에서도 이에 대한 공식 가이드를 제공하고 있으니 참고하세요.
먼저 프로젝트 루트 디렉터리에 keystore.properties 파일을 생성하고 아래 내용을 입력합니다.
storePassword=myStorePassword
keyPassword=myKeyPassword
keyAlias=myKeyAlias
storeFile=myStoreFileLocation
store password와 key password는 앱 번들을 처음 스토어에 업로드할 때 사용한 비밀번호입니다. keyAlias와 storeFile은 각각 비공개 키 생성 시 지정한 별칭과 키 파일 위치입니다.
다음으로 이 파일을 build.gradle에 로드해야 합니다. Gradle은 사실상 DSL(Domain Specific Language)처럼 동작하기 때문에 설정 코드를 깔끔하게 작성할 수 있습니다.
// keystore.properties에서 속성 불러오기
def keystorePropertiesFile = rootProject.file("keystore.properties")
// 새 Properties() 객체 생성
def keystoreProperties = new Properties()
// keystorePropertiesFile이 존재하면 해당 파일에서 읽고,
// 없으면 빌드 환경 변수에서 가져옵니다.
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 CodePipeline 설정 방법
이 부분은 비교적 간단하므로 세부 사항보다는 흐름 위주로 설명하겠습니다.
AWS CodePipeline을 다음 세 단계로 구성하세요.
- GitHub 저장소의
master브랜치와 연결된 Source 단계 - AWS CodeBuild와 연결된 Build 단계
- S3 버킷으로 배포하는 Deploy 단계
CodePipeline 구성에 대한 자세한 내용은 AWS 공식 문서를 참고하시면 됩니다.
AWS S3 설정 방법
먼저 CodeBuild가 단계 중 하나로 포함된 CodePipeline이 준비되어 있는지 확인합니다. 그다음 두 개의 S3 버킷을 생성합니다.
- 릴리즈 키(.jks)를 보관할 버킷 — 이 글에서는
release-key.jks라는 이름을 사용합니다. - Google Play 서비스 계정 비공개 키를 보관할 버킷 (서비스 계정 생성 시 다운로드한 키 파일)
CodeBuild 서비스 역할(service role)에서 이 버킷들에 접근할 수 있도록 권한을 허용해야 합니다. CodeBuild 서비스 역할은 CodePipeline을 설정할 때 함께 생성되었을 것입니다.
IAM 콘솔로 이동해 CodeBuild 서비스 역할을 찾고 ARN을 복사합니다.
그다음 release-key.jks 버킷의 권한(Permissions) 탭으로 이동해 아래 정책을 추가합니다.
{
"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은 반드시 본인 계정의 실제 ARN으로 교체해야 합니다. 특히 CodeBuild 서비스 역할의 올바른 ARN을 정확히 지정했는지 확인하세요.
두 번째 버킷은 권한 정책을 수정할 필요가 없습니다. 관련 권한은 나중에 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 설정이 정상적으로 동작하는 데 필요한 모든 변수를 export하고, 마지막으로 Gradle 릴리즈 명령을 실행합니다.
이 스크립트를 CodeBuild에서 실행하기 전에, 해당 변수들을 CodeBuild 환경에 등록해야 합니다. AWS CodeBuild 콘솔로 이동해 Android 앱의 빌드 프로젝트를 선택하세요.
그다음 아래 스크린샷처럼 Edit > Environment를 선택합니다.

열리는 화면에서 Additional Configuration 드롭다운을 펼치면, 키-값(key-value) 쌍 형태로 환경 변수를 추가할 수 있는 옵션이 있습니다.
이제 CodeBuild가 buildspec.yml을 실행할 때 지정된 변수들을 export할 수 있게 됩니다.
여기까지 완료하면 파이프라인이 실행될 때 CodeBuild가 비공개 키를 다운로드해 Android 앱에 서명·빌드하고, 서명된 번들을 S3 버킷에 업로드할 수 있습니다.
Slack 앱 설정 방법
관측 가능성(observability)은 자동화의 핵심 요소입니다. 자동화가 언제 실행되었는지, 성공했는지 실패했는지, 실패했다면 그 원인이 무엇인지 알 수 있어야 합니다.
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
# 인증 요청의 scope 정의
SCOPES = ['https://www.googleapis.com/auth/androidpublisher']
# 앱 패키지 이름
package_name = 'com.app.name'
# 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)
# 메인 핸들러 함수
def lambda_handler(event, context):
# S3 클라이언트를 생성하고 버킷에서 올바른 파일을 다운로드
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'
# Lambda를 트리거한 app-release.aab 파일 다운로드
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")
# credentials 객체를 생성하고 이를 기반으로 service 객체 생성
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
service = googleapiclient.discovery.build('androidpublisher', 'v3', credentials=credentials, cache_discovery=False)
# service 객체로 edit 요청을 생성하고 editId 획득
edit_request = service.edits().insert(body={}, packageName=package_name)
result = edit_request.execute()
edit_id = result['id']
# 앱 번들 업로드 요청 생성
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")
# 번들을 베타 트랙에 업로드하기 위한 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")
# edit를 BETA 트랙에 커밋하기 위한 commit 요청 생성
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을 구성합니다.
다음으로 Lambda는 앞서 설정한 버킷에서 서비스 계정 키를 다운로드합니다. 이때 버킷 이름을 정확히 지정했는지 반드시 확인하세요.
업로드 성공 여부와 관계없이 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을 본인 계정에 맞는 값으로 교체하면 바로 사용할 수 있습니다.
마무리
지금까지 Android App Bundle을 Play 스토어에 자동 업로드하는 파이프라인을 구축해 보았습니다. 결코 쉽지 않은 과정이고 구성 요소도 많지만, 한 번 만들어두면 상당한 시간과 노력을 절약해 주는 자동화입니다.
특히 앱 업데이트를 자주 배포하는 팀이라면, 배포 담당자 한 사람의 부재 때문에 릴리즈가 막히는 상황은 피하고 싶을 것입니다.
이런 자동화를 구축하면 CI/CD 워크플로가 훨씬 매끄럽고 견고해집니다.
이런 주제의 글이 더 궁금하다면 https://redixhumayun.github.io 에서 추가 콘텐츠를 확인하거나 Twitter를 팔로우해 보세요.