[Android] 배포 파이프라인 개선(1)

이상진·2025년 6월 1일

CI/CD 개선

목록 보기
3/4
post-thumbnail

개요

기존의 배포 파이프라인은 다음과 같다.

▲ 그림 1. 기존의 CI/CD 파이프라인

Flavor 를 이용하여 운영(prod) 과 개발(dev) 환경을 운영하였고, Android 에서는 개발 환경으로 된 앱을 내부 테스트에 업로드가 불가한 문제로 Firebase App Distribution 을 도입했고,이 과정에서 4가지의 환경을 구성하였다.

development: Flavor - dev && Build Type: Debug
hotfix: Flavor - prod && Build Type: Debug
sandbox: Flavor - dev && Build Type: Release
production: Flavor - prod && Build Type: Release

이러한 네이밍을 지은 이유는 'Firebase App Distribution' 게시글에서 확인할 수 있다.

따라서 본 게시글과 다음 포스트할 예정인 iOS 편에서 목표는 기존 배포 파이프라인에서 공통적으로 Firebase App Distribution 을 적용하고, iOS 의 경우 Build Scheme 을 위 4가지 빌드 환경으로 구성하는 방법론에 대해 알아볼 예정이다.


Fastlane 설정

우선 다음 명령어를 통해 fastlane 에 firebase_app_distribution 플러그인을 추가해줘야 한다.

fastlane add_plugin firebase_app_distribution

이전 게시글에서 이미 GCP 에서 fastlane 전용 서비스 계정을 이미 만들었으니 이에 대한 내용은 생략하겠다.

Firebase App Distribution이나 Google API를 사용할 때는 인증이 필요한데, 이 때 Google은 기본 인증 방식인 ADC(Application Default Credentials)를 제공한다. ADC는 GOOGLE_APPLICATION_CREDENTIALS 환경 변수에 서비스 계정 키(JSON 파일)의 경로를 설정하면, 해당 파일을 자동으로 참조해 인증 토큰을 생성하고 CLI나 Fastlane 같은 도구들이 이를 통해 안전하게 API 요청을 수행할 수 있도록 해준다. 따라서 인증을 자동화하고 수동 입력 없이 배포나 빌드를 진행하려면 이 환경 변수 설정이 필수적이다.

 export GOOGLE_APPLICATION_CREDENTIALS=/absolute/path/to/credentials/file.json

Fastfile 수정

- build_app(flavor)

기존의 build_app(flavor) 는 다음과 같다.

  • 기존
platform :android do
  def build_app(flavor)
    if flavor == "dev"
      sh "flutter build appbundle --flavor dev --release --dart-define=FLAVOR=dev"
      return "../build/app/outputs/bundle/devRelease/app-dev-release.aab"
      
    elsif flavor == "prod"
      sh "flutter build appbundle --flavor prod --release --dart-define=FLAVOR=prod"
      return "../build/app/outputs/bundle/prodRelease/app-prod-release.aab"

    else
      UI.user_error!("Unknown flavor: #{flavor}")
    end
  end

2개의 환경에서 4개로 변경되었기 때문에 네이밍 변경뿐 만 아니라 2개의 환경도 추가해줘야 한다.

  • 변경
def build_app(flavor)
    if flavor == "sandbox"
      sh "flutter build apk --flavor sandbox --release --dart-define=FLAVOR=dev"
      return "../build/app/outputs/flutter-apk/app-sandbox-release.apk"

    elsif flavor == "production-firebase"
        sh "flutter build apk --flavor production --release --dart-define=FLAVOR=prod"
        return "../build/app/outputs/flutter-apk/app-production-release.apk"

    elsif flavor == "production"
      sh "flutter build appbundle --flavor production --release --dart-define=FLAVOR=prod"
      return "../build/app/outputs/bundle/productionRelease/app-production-release.aab"

    else
      UI.user_error!("Unknown flavor: #{flavor}")
    end
  end

우선 flavor == "production-firebase"flavor == "production" 이 분기 처리된 이유 먼저 봐야하는데, 'production' 환경은 운영 환경에서의 실제 제품이기 때문에 이는 개발자가 만든 업로드용 키와 Play Console 에서 부여한 인증키 2개로 구성되어 있다. 이를 가지고 .apk 가 아닌 .aab 앱 번들 파일을 만들어 업로드가 가능한 것이다.

Firebase App Distribution 에서는 apk 와 aab 업로드 둘 다 지원하고 있었고, 제품 출시 때 앱 번들을 사용하니 Firebase 에도 aab 를 업로드 하려고 했으나,

▲ 그림 2. Link 에러

테스트 과정에서 계속 연결 에러가 발생했고, 이러한 원인으로는

▲ 그림 3. Firebase 설정 오류

Firebase 콘솔에서 앱을 찾을 수 없다는 에러가 발생해서 연결 자체가 되지 않아 발생하였다. 이는 Firebase Console > 프로젝트 설정 > 통합 에서 확인할 수 있다. 해결 방법으로는 Firebase 프로젝트 설정에 Play Console 에서 부여한 서명 키의 SHA-1, SHA-256 을 넣어주면 된다곤 하지만, 이미 이전부터 추가가 된 상태였었고 연결 오류에 대한 추가적인 원인을 찾기 어려워 Firebase 에는 apk 를 업로드 하는 방식으로 우회하였다.(만약에 해당 문제가 발생하지 않은 경우라면 운영 환경의 앱일 경우 Firebase 에 aab 파일을 업로드 하는 것을 권장한다)

따라서 분기처리의 의미로는 다음과 같다.

production-firebase: Firebase -> APK 업로드
production: Console 의 내부테스트, 프로덕션 -> AAB 업로드

lane 추가

lane 의 구성은 총 4개로,

Lane 1: 알파 테스트 -> Firebase 에 sandbox APK 업로드
Lane 2: 알파 테스트 -> Firebase 에 production APK 업로드
Lane 3: 베타 테스트 -> Play Console 의 내부테스트에 production aab 업로드
Lane 4: 제품 출시 및 버전 업데이트 > Play Console 의 프로덕션에 production aab 업로드

추가가 된 함수는 get_app_version 인데, pubspec.yaml 을 읽어 버전 정보를 가져오는 함수이다. 이는 단순히 Firebase App Distribution 에서 출시 노트에 해당 버전을 기재하기 위한 용도이다.

Lane 3,4는 기존의 Fastfile 에 있었던 내용과 동일하고, Lane 1,2가 추가 되었다.

  def get_app_version
      pubspec = File.read("../../pubspec.yaml")
      version_line = pubspec.lines.find { |line| line.start_with?("version:") }
      version, build_number = version_line.split(":").last.strip.split("+")
      return version, build_number
    end
    
  ... (build_app 생략)
  
# Lane 1: [sandbox] 환경 - 알파 테스트용 apk -> Firebase 배포
  desc "Deploy sandbox .apk to Firebase App Distribution"
  lane :deploy_sandbox_to_firebase do
    apk_path = build_app("sandbox")
    version, build_number = get_app_version()

    firebase_app_distribution(
      app: ENV['FIREBASE_APP_ID_SANDBOX'],
      apk_path: apk_path,
      groups: "qa",
      release_notes: "[sandbox] Version: #{version}+#{build_number} 내부 테스트)"
    )
  end

  # Lane 2: [production] 환경 - 알파 테스트용 aab -> Firebase 배포
  desc "Deploy production .aab to Firebase App Distribution"
  lane :deploy_prod_to_firebase do
    apk_path = build_app("production-firebase")
    version, build_number = get_app_version()

    firebase_app_distribution(
      app: ENV['FIREBASE_APP_ID_PRODUCTION'],
      apk_path: apk_path,
      groups: "qa",
      release_notes: "[production] Version: #{version}+#{build_number} 배포 전 내부 테스트)",
    )
  end

# [Lane 2] aab 업로드 로직
#   desc "Deploy production .aab to Firebase App Distribution"
#     lane :deploy_prod_to_firebase do
#       aab_path = build_app("production")
#       version, build_number = get_app_version()
#
#       firebase_app_distribution(
#         app: ENV['FIREBASE_APP_ID_PRODUCTION'],
#         android_artifact_path: aab_path,
#         android_artifact_type: "AAB",
#         groups: "qa",
#         release_notes: "[production] Version: #{version}+#{build_number} 배포 전 내부 테스트)",
#         service_credentials_file: "../gachiga-serviceAccount.json",
#       )
#     end

  # Lane 3: [production] 환경 - 베타 테스트용 aab -> Play Console 내부 테스트 배포
  desc "Deploy production .aab to Play Console Internal Test"
  lane :deploy_prod_to_internal_test do
    aab_path = build_app("production")

    upload_to_play_store(
      track: "internal",
      aab: aab_path
    )
  end

  # Lane 4: [production] 환경 - 프로덕션 배포
  desc "Deploy production .aab to Play Console Production"
  lane :deploy_prod_to_production do
    aab_path = build_app("production")

    upload_to_play_store(
      track: "production",
      aab: aab_path,
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true,
      skip_upload_changelogs: true,
    )
  end

Github Actions Workflow 수정

- Github Actions Secrets 추가

sandbox 환경이 추가되었기 때문에 로컬과 동일한 환경 구성을 위해 sandbox 용 앱 서명 키에 대한 keystore 를 secrets 에 추가해줘야 한다.

▲ 그림 4. 'sandbox' 용 앱 서명 키

마찬가지로 sandbox 환경의 Firebase 프로젝트 연동을 위해서 App Id 도 추가해주었다.

▲ 그림 5. 'sandbox' 용 Firebase 연동 관련 App Id

- CI 단계에서 'sandbox' 환경 고려

기존의 deploy_android 작업에서 production 환경만 고려했던 것을 sandbox 도 추가하여 google-services.json, key.properties 등을 추가하였다.

  deploy_android:
    name: Android Deployment
    runs-on: self-hosted
    needs: setup_environment
    steps:
      - name: Google-services.json 생성
        working-directory: android
        run: |
          mkdir -p app/src/production
          mkdir -p app/src/sandbox
          
          cat <<EOF > app/src/sandbox/google-services.json
          ${{ secrets.ANDROID_GOOGLE_SERVICE_DEV_JSON }}
          EOF
          
          cat <<EOF > app/src/production/google-services.json
          ${{ secrets.ANDROID_GOOGLE_SERVICE_PROD_JSON }}
          EOF

      - name: local.properties 생성
        working-directory: android
        run: |
          cat <<EOF > local.properties
          ${{ secrets.ANDROID_LOCAL_PROPERTIES }}
          EOF

      - name: Signing Key 복호화 및 key.properties 생성
        working-directory: android
        run: |          
          # keystore 전용 디렉토리 생성
          mkdir -p keystore/release/dev
          mkdir -p keystore/release/prod
          
          # .jks 파일 복호화
          echo "${{ secrets.ANDROID_PRODUCTION_KEY_BASE_64 }}" | base64 -d > keystore/release/prod/production-key.jks
          echo "${{ secrets.ANDROID_SANDBOX_KEY_BASE_64 }}" | base64 -d > keystore/release/dev/sandbox-key.jks

          # 권한 설정
          chmod 600 keystore/release/prod/production-key.jks
          chmod 600 keystore/release/dev/sandbox-key.jks
          
          # production-key.properties 생성
          cat <<EOF > keystore/release/prod/production-key.properties
          ${{ secrets.ANDROID_PRODUCTION_KEY_PROPERTIES }}
          EOF
          
          # sandbox-key.properties 생성
          cat <<EOF > keystore/release/dev/sandbox-key.properties
          ${{ secrets.ANDROID_SANDBOX_KEY_PROPERTIES }}
          EOF

이후에 커밋 메시지의 옵션에 따라 배포를 실행하기 위해서 4가지 옵션으로 구성하였다.

Option 1: [sandbox] 환경의 알파테스트 진행 -> Firebase 업로드
Option 2: [production] 환경의 알파테스트 진행 -> Firebase 업로드
Option 3: [production] 환경의 베타테스트 진행 -> Play Console 내부 테스트 업로드
Option 4: [production] 환경의 제품 출시 및 버전 업데이트 -> Play Console 프로덕션 배포

      - name: 옵션에 따른 배포 실행
        working-directory: android
        env:
          FIREBASE_APP_ID_SANDBOX: ${{ secrets.FIREBASE_APP_ID_SANDBOX }}
          FIREBASE_APP_ID_PRODUCTION: ${{ secrets.FIREBASE_APP_ID_PRODUCTION }}
        run: |
          COMMIT_MSG=$(git log -1 --pretty=%B)
          if [[ "$COMMIT_MSG" =~ deploy:[1-9] ]]; then
            DEPLOY_OPTION=$(echo "$COMMIT_MSG" | grep -o 'deploy:[1-9]' | cut -d':' -f2)
          else
            echo "배포 옵션이 지정되지 않았습니다. 기본 옵션(1) 사용"
            DEPLOY_OPTION="1"
          fi
          echo "선택된 배포 옵션: $DEPLOY_OPTION"

          case "$DEPLOY_OPTION" in
            "1")
              echo "[sandbox] Firebase App Distribution 배포 시작"
              fastlane deploy_sandbox_to_firebase
              ;;
            "2")
              echo "[production] Firebase App Distribution 배포 시작"
              fastlane deploy_prod_to_firebase
              ;;
            "3")
              echo "[production] Play Console Internal Test 배포 시작"
              fastlane deploy_prod_to_internal_test
              ;;
            "4")
              echo "[production] Play Console Production 배포 시작"
              fastlane deploy_prod_to_production
              ;;
            *)
              echo "Invalid deployment option selected: $DEPLOY_OPTION"
              exit 1
              ;;
          esac

결과

Option 1: Sandbox 환경 Firebase 배포

▲ 그림 6. [Option 1] 실행 결과

▲ 그림 7. 'sandbox' 용 Workflow 실행 결과

▲ 그림 8. 'sandbox' 용 Firebase 업로드 결과

Option 2: Production 환경 Firebase 배포

▲ 그림 9. [Option 2] 실행 결과

▲ 그림 10. 'production' 용 Firebase 업로드 결과

Option 3: Production 환경 내부테스트 배포

▲ 그림 11. [Option 3] 실행 결과

Option 4: Production 환경 프로덕션 배포

▲ 그림 13. [Option 4] 실행 결과

▲ 그림 14. 'production' 용 프로덕션 배포


Reference

https://firebase.google.com/docs/app-distribution/android/distribute-fastlane?hl=ko

profile
모바일 개발에 관하여 이것, 저것 다 합니다.

0개의 댓글