오늘은 자동 배포를 위한 CI/CD 파이프라인 구축에 대해 이야기를 해보겠습니다. 저의 많은 애정이 담겨 있는 작업이니 잘 읽어주세요~~
배포하는 임무를 인수인계 받고 배포를 진행하려고 하니 너무 귀찮아졌습니다. 그래서 바로 자동 배포에 대해 알아보았습니다. (사실 iOS 배포되었다고 문자오는게 좀 부러웠습니다...ㅠ). 재미있는 경험이 될 거 같아 파이프라인을 구축하기로 했습니다.
아래는 수동 배포의 과정입니다.
- 배포할 내역들을 main 브렌치에 머지합니다.
- 릴리즈 모드로 .aab or .apk 버전을 생성합니다.
- 구글 플레이 스토어 콘솔로 들어갑니다.
- 배포할 앱을 선택합니다.
- 파일 업로드 및 변경사항을 적습니다.
- 검수 요청을 합니다.
CI/CD 파이프라인 구축하는 환경을 알아본 결과 Github Action으로 하는게 가장 편할 거 같아 사용하게 되었습니다. Gradle Play Publisher이라는 앱 배포를 도와주는 라이브러리를 사용했습니다. Google Cloud Platform
에서 프로젝트를 생성하여 계정을 만든 다음 Goole Play Console
과 연결해서 배포되도록 하였습니다.
android {
signingConfigs {
create("release") {
storeFile = file("./keystore/jobis_v2_key.jks")
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
}
signingConfigs
파일을 생성하여 배포 파일을 만들 수 있게 세팅합니다. 그리고 release로 빌드 할 때 signingConfig를 등록하여 앱 서명을 진행하게 합니다. release 빌드는 Github Action
에서 자동으로 되게끔 구축했습니다.
play {
serviceAccountCredentials.set(file("src/main/play/google-cloud-platform.json"))
defaultToAppBundles.set(true)
releaseStatus.set(ReleaseStatus.IN_PROGRESS)
track.set("production")
}
tasks.register("release") {
dependsOn(tasks["clean"])
dependsOn(tasks["bundleRelease"])
mustRunAfter(tasks["clean"])
}
Google Play Publisher
플러그인을 사용하여 Google Play Store
에 배포할 때 사용하는 설정입니다.
google-cloud-platform.json
에 있는 서비스 계정 인증 정보를 사용합니다.
defaultToAppBundles.set(true)
: App Bundle 형식으로 배포하게 됩니다.
App Bundle: Google Play에서 권장하는 배포 방식으로, APK보다 더 작은 크기의 설치 파일을 제공합니다.
releaseStatus.set(ReleaseStatus.IN_PROGRESS)
: 점진적으로 릴리즈하여 일정 비율로 배포됩니다.
track.set("production")
: 실제 사용자에게 배포됩니다.
tasks.register("release")
:./gradlew release
를 실행하면 태스크가 호출 됩니다.
dependsOn(tasks["clean"])
: 기존 빌드 파일들을 삭제 시켜줍니다.
dependsOn(tasks["bundleRelease"])
: 앱 번들을 빌드합니다.
mustRunAfter(tasks["clean"])
: clean 태스크가 먼저 실행된 후에 release 태스크가 실행됩니다.
- name: Create google-services.json
env:
DATA: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: echo $DATA > /home/runner/work/JOBIS-ANDROID-V2/JOBIS-ANDROID-V2/app/google-services.json
- name: Create google-cloud-platform.json
env:
DATA: ${{ secrets.GOOGLE_CLOUD_PLATFORM }}
run: echo $DATA | base64 --decode > /home/runner/work/JOBIS-ANDROID-V2/JOBIS-ANDROID-V2/app/src/main/play/google-cloud-platform.json
파이어베이스에서 사용되는 google-services.json
파일과 배포할 때 사용되는 google-cloud-platform.json
파일을 가져오는 내용입니다. google-cloud-platform.json
은 Google Cloud Platform
에서 사용되는 api 정보를 담고 있고 base64
로 디코딩하여 데이터를 가져옵니다.
- name: Create local.properties
run: |
echo "BASE_URL_PROD=\"${{ secrets.BASE_URL_PROD }}\"" >> ${{ github.workspace }}/local.properties
local.properties
파일에 BASE URL
을 넣기 위해 깃허브 시크릿 파일에서 데이터를 가져오는 내용입니다.
- name: Create keystore directory
run: mkdir -p ${{ github.workspace }}/app/keystore
- name: Decode Keystore
run: |
echo "$KEYSTORE" > ${{ github.workspace }}/app/keystore/keystore.b64
base64 -d -i ${{ github.workspace }}/app/keystore/keystore.b64 > ${{ github.workspace }}/app/keystore/jobis_v2_key.jks
env:
KEYSTORE: ${{ secrets.APP_RELEASE_KEY_STORE }}
Github Action 가상 환경에서 keystore를 저장 할 수 있게 폴더를 만들어줍니다. base64로 디코딩한 keystore을 가져와 jobis_v2_key.jks
파일을 생성합니다. 이렇게 되면 배포 세팅이 끝나게 됩니다.
- name: Build Release And Publish AAB
run: ./gradlew publishReleaseBundle
env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
AAB형식으로 빌드를 한 후, Google Play Store
에 자동으로 업로드하게 됩니다.
keystore에서 필요한 정보를 환경 변수로 설정된 GitHub Secrets에서 가져옵니다.
- name: Get version
id: get_version
run: |
echo "::set-output name=code::$(grep VERSION_CODE buildSrc/src/main/kotlin/ProjectProperties.kt | awk '{print $5}')"
echo "::set-output name=name::$(grep VERSION_NAME buildSrc/src/main/kotlin/ProjectProperties.kt | awk '{print $5}' | tr -d '"' )"
- name: Get tag name
id: get_tag
run: echo "::set-output name=name::v${{ steps.get_version.outputs.name }}"
- name: Generate Release
uses: actions/create-release@latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
release_name: "🚀 :: ${{ steps.get_tag.outputs.name }}"
tag_name: ${{ steps.get_tag.outputs.name }}
draft: false
prerelease: false
깃허브에서 릴리즈 태그를 자동으로 만들기 위해 추가하였습니다. 출시되는 앱 버전을 가져와 태그를 만들어 생성합니다.
- name: Read and format release notes
id: read_release_note
run: |
RELEASE_NOTE=$(cat ./app/src/main/play/release-notes/ko-KR/default.txt | sed ':a;N;$!ba;s/\n/\\n/g' | sed 's/"/\\"/g')
echo "RELEASE_NOTE=$RELEASE_NOTE" >> $GITHUB_ENV
- name: Notify Slack on Success
if: ${{ success() }}
id: slack-success
uses: slackapi/slack-github-action@v1.24.0
with:
payload: |
{
"channel": "${{ secrets.SLACK_DEPLOY_CHANNEL_ID }}",
"attachments":
[
{
"color": "#36a64f",
"title": "${{ github.repository }}",
"title_link": "https://github.com/${{github.repository}}",
"text": "🚀 앱이 배포되었습니다.",
"fields":
[
{
"title": "Repository",
"value": "${{ github.repository }}",
"short": true
},
{
"title": "Tag",
"value": "${{ github.ref_name }}",
"short": true
},
{
"title": "Version",
"value": "${{ steps.get_tag.outputs.name }}",
"short": true
},
{
"title": "Release Note",
"value": "${{ env.RELEASE_NOTE }}",
"short": false
}
]
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
CD가 성공적으로 실행되었을 슬랙으로 알림이 가는 작업을 추가하였습니디. slack api를 사용하여 문자를 커스텀하여 보낼 수 있습니다. 배포에 어떤 내용이 추가되었는지 한눈에 알기 위해 릴리즈 노트도 같이 보내게 만들었습니다.
- name: Notify Slack on Failure
if: ${{ failure() }}
id: slack-failure
uses: slackapi/slack-github-action@v1.24.0
with:
payload: |
{
"channel": "${{ secrets.SLACK_DEPLOY_CHANNEL_ID }}",
"attachments":
[
{
"color": "#ff0000",
"title": "${{ github.repository }}",
"title_link": "https://github.com/${{github.repository}}",
"text": "💣 앱 배포에 실패했어요 ㅠㅠ",
"fields":
[
{
"title": "Repository",
"value": "${{ github.repository }}",
"short": true
},
{
"title": "Tag",
"value": "${{ github.ref_name }}",
"short": true
},
{
"title": "Version",
"value": "${{ steps.get_tag.outputs.name }}",
"short": true
}
]
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
CD가 실패하였을 때 슬랙으로 알림이 가게 됩니다.
아래는 깃 시크릿에 저장된 값들입니다.
이 작업을 하면서 기도를 정말 많이 했던거 같습니다. 일주일 동안 CI/CD만 돌리면서 돌아가는거 보고 많이 쫄렸었는데 결국 해낼 수 있어서 정말 좋았습니다. 이상한 오류도 많이 만나고 답답함도 있었지만 이런걸 해쳐나가니 더 성장할 수 있다는걸 느낄 수 있었습니다.
위 사진은 CD 고치고 rebase 받아서 실행하는거 반복하니 9780커밋이라는 살면서 처음보는 커밋수를 찍게 되었습니다. 커밋수가 너무 많아 pr은 결국 닫고 새로하였습니다...ㅠㅠ
자비스 서비스가 커지면 내부에서 테스트를 돌릴 수 있도록 파이어베이스를 사용하여 내부 테스트 배포 자동화를 만들어보려고 합니다. 저의 로망이긴 하지만..ㅎㅎ
나는 결코 성공에 대해 꿈꾸지 않았다, 나는 꿈을 위해 행동했다. -에스티 로더