Cloud BuildでAndroidプロジェクトをビルドする

[LIFULL Advent Calendar 2018](https://qiita.com/advent-calendar/2018/lifull2) 2日目

# GCPでのCI/CD実現
2018年に開催された[Cloud Next '18](https://cloud.withgoogle.com/next18/sf/)では、様々なGCPの新プロダクトが発表された。その中で盛り上がっていたのはML・AI関連プロダクトと、CI/CDに関するものだった。
WebならCloud BuildでCIを実施、Spinnakerでカナリアリリース含めた自動デプロイがすべてGCP上で実現できる。
それらの中で今回は第一歩として、[Cloud Build](https://cloud.google.com/cloud-build/?hl=ja)についていろいろと触って試してみた。

# やること
Cloud Build + Cloud Source Repository + Container Registryを利用し、試しにAndroidのプロジェクトをビルドしてみる。

## 手順
手順はシンプルで以下の通り。1番の準備が少し厄介だが、そこさえクリアすれば特段ハマることなく出来た。
1. Androidビルド用のDockerイメージを作成し、Container Registryにpushする
2. 任意のAndroidプロジェクトを作成する（今回はHello Worldレベルの簡単なもので準備）
3. Cloud Source Repositoryにリポジトリを作成し、Cloud Buildと連携させる
4. ソースコードをpushするとビルドが走るようにする

## 下準備 Cloud Storageにバケットを作成
いくつかのStorageバケットを利用するため、以下のバケットを任意の名前で作成する（後述）。

* Android SDK License 用バケット
* Build Cache用バケット
* Debug Buildバケット

Source RepositoryやContainer RegistryでもStorageを利用するが、勝手に作成されるため割愛。

# 順を追って自動ビルドを実現する
## 1. Androidビルド用のDockerコンテナイメージを作成し、Container Registryにpushする
作成方法はgithubにサンプルがあるため、それを参考にする。
https://github.com/GoogleCloudPlatform/cloud-builders-community/tree/master/android

#### Android SDKパッケージの記述リストを作る
githubサンプルにあるスクリプトからpackages.txtというファイルを生成する。
今回生成したのは以下の通り。

```bash:packages.txt
extras;m2repository;com;android;support;constraint;constraint-layout;1.0.2
extras;google;google_play_services
platforms;android-28
extras;android;m2repository
platform-tools
extras;android;gapid;3
patcher;v4
build-tools;28.0.2
```

#### 各種Storageバケットの作成
cacheやビルドファイル、ライセンス用の保存領域としてCloud Storageを使用するため、必要なバケットを作成する。今回作成したのは以下の通り。

|Bucket name  |用途  |
|---|---|
|android_build_caches  |ビルドキャッシュ用  |
|debug_build_etet  |デバッグビルド(apk)保存用  |
|licenses_bucket  |Android SDKライセンスディレクトリ用  |


#### Android SDKライセンスをGCP Storageに上げる
以下のコマンドで、SDKライセンスをGCP Storageにアップロードする。

```
$ gsutil rsync -d [ANDROID_SDK_HOME]/licenses/ gs://[_LICENSES_BUCKET]
```


※ もしCloud Build上でビルドしたときに、「You have not accepted the license agreements of the following SDK components」と出てしまった場合は以下のコマンドでライセンス同意をして再度licensesディレクトリをrsyncさせる

```
$ [path_to_sdk]/tools/bin/sdkmanager --licenses
```

#### コンテナビルドをsubmitする

```
$ gcloud builds submit --config cloudbuild.yaml . --substitutions=_ANDROID_SDK_LICENSE=$ANDROID_SDK_LICENSE
```

CloudBuildを見てみると、ビルドプロセスが走っていることがわかる。
<img width="1399" alt="Screen Shot 2018-11-18 at 16.55.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/29379/cd0e9f90-a10a-11d1-bfba-7cf81a80f75e.png">

buildがパスすると、Container Registryにイメージがpushされていることが確認できる。
"android-builder"という名前でイメージが作成出来ている。
<img width="971" alt="Screen Shot 2018-11-18 at 16.58.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/29379/471ee435-23b7-1cba-1c91-f25a1b29032f.png">


これでSource Repositoryにpushしたコードを作成したDockerイメージでビルドすることができるようになる。

Dockerfileは以下の通り。

```bash:Dockerfile
FROM openjdk:8-slim
ENV DEBIAN_FRONTEND noninteractive
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8'

ARG ANDROID_SDK_TOOLS_URL="https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip"
ARG ANDROID_SDK_LICENSE
ARG ANDROID_BUILD_CACHE
ARG GRADLE_DISTRIBUTION="https://services.gradle.org/distributions/gradle-4.6-bin.zip"

ENV GRADLE_BUILD_CACHE=${ANDROID_BUILD_CACHE}
ENV DOCKER_ANDROID_DISPLAY_NAME mobileci-docker
ENV GRADLE_TARGETS="assembleDebug"
ENV GRADLE_HOME=/usr/local/gradle-4.6

ENV ANDROID_HOME /android-sdk
ENV PATH ${ANDROID_HOME}/tools:$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools/bin:/bin:$PATH
#Install underlying development packages and languages
ADD packages.txt .
COPY gradle-build /bin
COPY licenses licenses

    # Prepare to install packages
RUN apt-get update && \
    apt-get install -y locales && \
    sed -i 's/^# *\(en_US.UTF-8\)/\1/' /etc/locale.gen && \
    locale-gen && \
    dpkg-reconfigure locales && \
    apt-get install -y apt-utils && \
    dpkg --add-architecture i386 && \
    # install packages
    apt-get install -qq -y \
    software-properties-common \
    curl \
    wget \
    build-essential \
    zip \
    unzip \
    --no-install-recommends && \
    # Install gradle
    wget --quiet $GRADLE_DISTRIBUTION && \
    unzip -qq  gradle-4.6-bin.zip -d /usr/local/ && \
    ln -s /usr/local/gradle-4.6/bin/gradle /usr/local/bin/gradle && \
    # Install android sdk
    mkdir -p /root/.android && touch /root/.android/repositories.cfg && \
    wget --quiet --output-document=android-sdk.zip ${ANDROID_SDK_TOOLS_URL} && \
    unzip -qq android-sdk.zip -d $ANDROID_HOME && \
    cp -r licenses /$ANDROID_HOME/licenses && \
    echo ${ANDROID_SDK_LICENSE} >> $ANDROID_HOME/licenses/android-sdk-license && \
    echo "sdk.dir=${ANDROID_HOME}" > android_local.properties && \
    sdkmanager --package_file=packages.txt && \
    chmod -R +x bin && \
    rm $ANDROID_HOME/licenses/* && \
    keytool -genkey -v -keystore /root/.android/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US" && \
    keytool -list -v -keystore /root/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
CMD ["sh", "-c", "build-debug", "${GRADLE_TARGETS}"]
```

## 2. 任意のAndroidプロジェクトを作成する
今回は、簡単なHello Worldを表示するだけのプロジェクトを作成するだけ。

## 3. Cloud Source Repositoryにリポジトリを作成し、Cloud Buildと連携させる

#### cloudbuild.yamlファイルの作成
Androidプロジェクト直下にcloudbuild.yamlという名前でビルド用のファイルを作成する。
build cacheなどは、前述で作成したStorageバケット名を指定する。
(注) githubのサンプルでは、gs://が指定されていないため、ビルド成功してもapkやキャッシュがStorgaeに保存されなかった。

```yaml:cloudbuild.yaml
steps:
#Copy the build cache locally, if it exists.
- name: 'gcr.io/cloud-builders/gsutil'
  args: ['rsync', 'gs://${_ANDROID_BUILD_CACHE}/', '/build_cache']
  id: copy_build_cache_local
  volumes:
    - name: 'build_cache'
      path: '/build_cache'
#Android build and distribute with gradle
- name: 'gcr.io/$PROJECT_ID/android-builder'
  args: ['gradle-build','${_ANDROID_SDK_LICENSE}','assembleDebug']
  id: gradle_build
  waitFor:
    - copy_build_cache_local
  volumes:
    - name: 'build_cache'
      path: '/build_cache'
#Copy debug apk to cloud storage bucket
- name: 'gcr.io/cloud-builders/gsutil'
  args: ['-q','cp', 'app/build/outputs/apk/debug/app-debug.apk', 'gs://${_DEBUG_BUILD_BUCKET}/$PROJECT_ID-$BRANCH_NAME-$SHORT_SHA-debug.apk']
  waitFor:
    - gradle_build
#Repopulate the build-cache
- name: 'gcr.io/cloud-builders/gsutil'
  args: ['-q','cp', '/build_cache/dot_gradle.zip', 'gs://${_ANDROID_BUILD_CACHE}']
  waitFor:
    - gradle_build
  volumes:
    - name: 'build_cache'
      path: '/build_cache'
timeout: 1200s
```

#### Cloud BuildでTriggerの設定する
GCPコンソール → トリガー → トリガーを追加　でBuildのトリガーを追加する。
今回のサンプルでは、masterブランチへのpushでBuildされるように設定。
以下が、設定した内容。
![Screen Shot 2018-11-24 at 19.57.49.png](https://qiita-image-store.s3.amazonaws.com/0/29379/05c27a8a-e082-f3a7-c2d2-028fd7a69933.png)


## 4. ソースコードをpush
ソースコードがmasterにpushされると、Cloud BuildのTriggerにより自動的にビルドが実行される。
数分待つと、ビルドが完了しビルドステップが全て成功している状態となっている。
![Screen Shot 2018-11-18 at 18.39.24.png](https://qiita-image-store.s3.amazonaws.com/0/29379/a538b9bc-46f3-49ed-265f-ab0b46785a51.png)

### ビルドで何をやってるか確認してみる
cloudbuild.ymlのstepに従ってビルドが実行されているのがわかる。
#### 1.キャッシュ読み込み

```
Building synchronization state...
Starting synchronization...
Copying gs://android_build_caches/dot_gradle.zip...
 [0 files][ 0.0 B/108.2 MiB] 
 [0 files][ 64.2 MiB/108.2 MiB] 
 [1 files][108.2 MiB/108.2 MiB] 
Operation completed over 1 objects/108.2 MiB.
```
#### 2.gradleのビルド

```
~ 略 ~
:app:transformDexArchiveWithExternalLibsDexMergerForDebug
:app:transformDexArchiveWithDexMergerForDebug
:app:mergeDebugJniLibFolders
:app:transformNativeLibsWithMergeJniLibsForDebug
:app:checkDebugLibraries
:app:processDebugJavaRes NO-SOURCE
:app:transformResourcesWithMergeJavaResForDebug
:app:validateSigningDebug
:app:packageDebug
:app:assembleDebug

BUILD SUCCESSFUL in 1m 25s
28 actionable tasks: 28 executed
Exit status is: 0
```
#### 3.出来上がったapkをStorageに保存
.gradle/ディレクトリがzipファイルとして上がっていることが確認できる。
![Screen Shot 2018-11-24 at 21.34.02.png](https://qiita-image-store.s3.amazonaws.com/0/29379/27a7f4ba-652e-9df4-9393-defef09d5105.png)

#### 4.ビルドキャッシュをStorageに保存
apkファイルが上がっていることが確認できる。
![Screen Shot 2018-11-24 at 21.34.45.png](https://qiita-image-store.s3.amazonaws.com/0/29379/76cbc1db-bec4-f892-133d-0d36ead813b3.png)

### gradleキャッシュがない場合とある場合のビルド速度の違い
dot_gradle.zipファイルがある場合は、余計な依存関係のダウンロードが必要なくなるため、ビルド時間が短くなる。
実際に試してみると、1分ほどビルド時間が短くなっているのがわかる。

|キャッシュなし  |キャッシュあり  |
|---|---|
|![Screen Shot 2018-11-24 at 21.40.40.png](https://qiita-image-store.s3.amazonaws.com/0/29379/436b204c-eff0-004d-f413-37b40579b4db.png) |![Screen Shot 2018-11-24 at 21.40.50.png](https://qiita-image-store.s3.amazonaws.com/0/29379/cfb16155-33e8-f20c-2e02-78a689410343.png) |

# 次にやること
ビルドしてapkのアップロードまで出来たので、次はテストコードを書いてUT実行 → Fabricベータ公開までできそうなので試してみたい。