Android プロジェクトでの CI/CD 環境を比較 (GitHub Actions vs. GitLab CI/CD)

皆様お久しぶりです、医療ユニット開発の山口です。実に半年ぶりの投稿になります。

つい1週間前にコロナのワクチンを打って、3日くらい腕が痛くて上げられなかったです。
(寝ている間に寝ぼけたちびっ子のヒップアタックが腕にダイレクトヒットしたときは本当に痛かった・・・)

今回は GitHub と GitLab における Android プロジェクトでの CI/CD 環境を比較してみました。

f:id:optim-tech:20210720180208p:plain

各々のざっくりとした概要

GitHub Actions

GitHub といえば開発者の皆さんおなじみ、開発プロジェクトの管理ツールでもっとも知られたものの一つです。
GitHub の CI/CD 機能は GitHub Actions *1 と呼ばれます。
2019 年 11 月に正式に公開されたもので、比較的最近できた機能です。
パブリックなリポジトリであれば無料で利用可能です。(Freeアカウントでプライベートリポジトリであっても 1 カ月あたり 2,000分 以内であれば無料です。)

ビルドマシンは、GitHub そのものが提供しているホストランナー(仮想マシン)で動かします。
もしくは自前でマシンにランナーをホストすることも可能です。*2

GitLab CI/CD

弊社ではプロジェクト管理ツールとしてGitLab を利用しています。
GitLab の CI/CD 機能は GitLab CI/CD *3 *4 と呼ばれます。(お名前そのままです。)
こちらもビルドマシンは GitLab.com 上の Shared Runner もしくは GitLab Runner をインストールした自前のマシン上で実行されます。*5
Freeアカウントであれば、1 カ月あたり 2,000 分まで Shared Runner での CI を無料で実行可能です。

ちなみに医療ユニットでは Mac をビルドマシンとして連携させています。
(Runner の設定方法は以前 伊藤さんの記事 にて触れられていたのでここでは省略します。)

比較するにあたっての前提条件

今回は前提条件として、以下のような Android プロジェクトが存在することを想定します。ごく一般的なものですね。

  • リポジトリ名: android_sample
  • モジュール名: app
    • buildTypes: release と debug
    • productFlavors: 特に指定なし
    • signingConfigs: release のみ以下の設定を適用する。ただし keystore.jks はリポジトリ上にプッシュしない
      • storeFile rootProject.file('keystore.jks')
      • storePassword System.getenv('STORE_PASSWORD')
      • keyAlias System.getenv('KEY_ALIAS')
      • keyPassword System.getenv('KEY_PASSWORD')

下の条件を満たすような CI/CD を作成します。

  • GitHub や GitLab で提供されている仮想マシンを利用する(ビルドマシンを別途用意しない)
  • master ブランチにプッシュされたことをトリガーに CI イベントを行う
  • Android のリリースビルドをすることで、リリース版の APK ファイルを生成する
  • 成果物は GitHub, GitLab 上からダウンロード可能な状態にする(ただし3日後には自動的に削除される)

CI/CD スクリプト全体

GitHub

~/android_sample/.github/workflows/master_build.yml

name: master build

on:
  push:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Check out
        uses: actions/checkout@v2

      - name: Set up JDK 8
        uses: actions/setup-java@v2
        with:
          java-version: '8'
          distribution: 'adopt'

      - name: Restore cache for Gradle
        uses: actions/cache@v2
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Restore keystore and export password
        run: |
          echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > ./keystore.jks
          export STORE_PASSWORD="${{ secrets.STORE_PASSWORD }}"
          export KEY_ALIAS="${{ secrets.KEY_ALIAS }}"
          export KEY_PASSWORD="${{ secrets.KEY_PASSWORD }}"

      - name: Build project with Gradle
        run: |
          chmod +x gradlew
          ./gradlew assembleRelease --no-daemon

      - name: Archive build artifacts
        uses: actions/upload-artifact@v2
        with:
          name: build-artifacts-${{ github.run_number }}
          path: ./app/build/outputs/apk/release/*.apk
          retention-days: 3

GitLab

~/android_sample/.gitlab-ci.yml

image: openjdk:8-jdk

variables:
  ANDROID_COMPILE_SDK: "30"
  ANDROID_BUILD_TOOLS: "31.0.0"
  ANDROID_SDK_TOOLS: "7302050"

before_script:
  - apt-get --quiet update --yes
  - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1
  - wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS}_latest.zip
  - unzip -d android-sdk-linux android-sdk.zip
  - echo y | android-sdk-linux/cmdline-tools/bin/sdkmanager --sdk_root=. "platforms;android-${ANDROID_COMPILE_SDK}" >/dev/null
  - echo y | android-sdk-linux/cmdline-tools/bin/sdkmanager --sdk_root=. "platform-tools" >/dev/null
  - echo y | android-sdk-linux/cmdline-tools/bin/sdkmanager --sdk_root=. "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null
  - export ANDROID_SDK_ROOT=$PWD
  - export PATH=$PATH:$PWD/platform-tools/
  - chmod +x ./gradlew
  # temporarily disable checking for EPIPE error and use yes to accept all licenses
  - set +o pipefail
  - yes | android-sdk-linux/cmdline-tools/bin/sdkmanager --sdk_root=. --licenses
  - set -o pipefail

stages:
  - build

master build:
  stage: build
  script:
    # Restore keystore and export password
    - echo $KEYSTORE_BASE64 | base64 -d > ./keystore.jks
    - export STORE_PASSWORD=$STORE_PASSWORD
    - export KEY_ALIAS=$KEY_ALIAS
    - export KEY_PASSWORD=$KEY_PASSWORD
    # Build project with Gradle
    - ./gradlew assembleRelease --no-daemon
  artifacts:
    name: build-artifacts-$CI_PIPELINE_IID
    paths:
      - ./app/build/outputs/apk/release/*.apk
    expire_in: 3 days
  only:
    - master

具体的にどう違うのか?

YMLファイルの設置場所

GitHub Actions では [Root Repository]/.github/workflows ディレクトリに YML ファイルを設置します。*6
別の CI イベントを追加したい場合は、このディレクトリ配下に YML ファイルを新しく追加すればよいです。
(Ex. サンプルでは master_build.yml ファイルのみですが、これ以外に別途 master_deploy.yml という定義を追加することが可能。)
各 CI イベントが分かれている関係上、上から順にジョブを行っていくので、慣れてしまえば見やすいですね。
ただし同じような CI のスクリプトだった場合に同じような中身の YML ファイルが増えていくのは仕様的に仕方ないでしょう。

GitLab CI/CD ではルートディレクトリ直下に .gitlab-ci.yml を設置します。
別の CI イベントを追加したい場合は、この YML ファイルに定義を追加していきます。
(Ex. サンプルでは master build という定義のみですが、この下に master deploy という定義を追加することが可能。)
どんどん追加すれば追加するほど YML ファイルが膨大になっていきます。
Anchors で共通化させたり、ファイル分割して include: で読み込んだりして、スクリプトを見やすくしましょう。
工夫次第では共通化でスッキリまとめることが可能です。

CI/CD イベント発生条件

GitHub Actions では on: push: で指定しています。
ブランチ名やタグ名などをしているすることが可能で、一定のブランチに対して CI を除外したりマージリクエストをターゲットにすることも可能です。
サンプルの場合、「ブランチ master にプッシュがオンされたときにCI イベントを発生させる」といった見た目通りのスクリプトですね。

GitLab CI/CD では only: で指定しています。
ブランチ名やタグ名をしているすることが可能で、除外する場合は except: が使えます。
サンプルの場合、「CI イベント発生条件は master オンリーである」と見れば、こちらも見た目通りではないでしょうか。

ビルド環境設定

GitHub Actions では、actions/checkout *7 でリポジトリのプロジェクトをチェックアウト、 actions/setup-java *8 で使用する JDK を指定しています。
actions/cache *9 を適用することで Gradle のキャッシュをリストアしてビルド時間の短縮になります。
actions/xxxx の使い方を理解するまではちょっと戸惑いますが、一度慣れてしまうとさらっと記述できるようになりますよ。

GitLab CI/CD では、まず images: で JDK8 の Docker イメージを取得して *10before_script: で Android のビルドツールを設定しています。
GitLab 公式の情報にテンプレ *11 がありますが、 Android SDK Tools sdk-tools-linux-*.zip は既にサポートが終了しています。*12
よって、こちら *13 に記述されている通り、 最新の Android Command Line Tools commandlinetools-linux-*_latest.zip *14 から取得してくる方法を適用しました。
ちなみに自前のビルドマシンを用意した場合は、ビルドマシン側に Android Studio の環境構築をすればよいのでここら辺の設定は不要です。(医療のビルドマシンではそうしてます。)
その代わりに tags: にてビルドマシンを連携させましょう。

なお、Android Gradle plugin v7.0 以降から Java の要求バージョンが 8 から 11 に変更になります。*15
Android プロジェクトの設定に応じてバージョンは適宜変更しましょう。

機密情報の取得

前提条件にも記述しましたが、アプリ署名の keystore (.jksファイル) やそれに付随する storePassword, keyAlias, keyPassword は機密情報であるためプロジェクトソースコードには公開しないようにしています。
よって、プロジェクトビルド前にこれらの情報をリストアしたり環境変数として読み込んだりする必要があります。
GitHub Actions にも GitLab CI/CD にも Web 上から設定が可能です。

GitHub Actions では GitHub 上の画面右側の Settings → Secrets → New repository secret で設定可能です。 *16
スクリプト上では ${{ secrets.STORE_PASSWORD }} と呼び出します。

GiLab CI/CD では GitLab 上の左側のメニューから Settings → CI/CD → Variables → Add variable で設定可能です。 *17
機密情報なので Mask variable にチェックを入れ忘れないようにしましょう。
スクリプト上では $STORE_PASSWORD と呼び出します。

.jksファイルは Base64 で予めエンコードした文字列を登録して、ビルドスクリプト上にて Base64 デコードすることで復元しています。

プロジェクトのビルド

Android プロジェクトのビルドコマンドは共通です。特記事項は特にありません。
強いて言うならば、実行時にデーモンを使わないように --no-daemon を付与してる、というくらいです。

成果物の保存、サーバーへアップロード

GitHub Actions では actions/upload-artifact *18 を使用します。
デフォルトで定期されている CI 専用の環境変数 github.run_number *19 を用いることで、成果物の zip ファイル名をユニークに定めています。
それ以外は成果物のパスと保存期間を定めています。

GitLab CI/CD では artifacts: によって指定します。
こちらも同じように CI 専用の環境変数 *20CI_PIPELINE_IID を用いています。
それ以外は成果物のパスと保存期間を定めています。こちらも意味的には変わりません。

まとめ

GitHub Actions は上から順にジョブが流れていく仕様だったり、actions/xxxx などのフォーマットが一定であったりすることから、 他者が記述したスクリプトであっても解読しやすいです。
ただし、各 YML ファイルがトリガーとなっていることから、各スクリプトごとに YML ファイルが増えていってしまいます。

GitLab CI/CD は定義の順番が特に定められていなかったり(script: attifacts: の順番を逆にしても動く)、 .gitlab-ci.yml ファイルがトリガーでもあることからファイルサイズが膨大になってしまったりします。
しかし、工夫次第で共通化を図ることで自分独自の YML ファイルを作ることが可能です。


比較して感じた個人的なイメージは、

「作成者に関わらずほぼ一定のスクリプトに仕上がる GitHub Actions」
「作成者によって千差万別で自由度の高いスクリプトになる GitLab CI/CD」

でした。
どちらのほうが好みかは、人それぞれになりそうですね。




オンライン診療ポケットドクターはお手持ちのスマホの AppStore もしくは GooglePlay から無料でダウンロードすることが可能です。

オプティムでは共に開発を行うエンジニアを募集しています。