Google Cloud Container Builderを使う


本稿はGoogle Cloud Container Builderの可能性を探る試みだ。[Google Cloud Platform(1) Advent Calendar 2016](http://qiita.com/advent-calendar/2016/gcp)の23日目の記事として書かれるはずのものだった（ごめんなさい）。

# Google Cloud Container Builderとは

[Google Cloud Container Builder](https://cloud.google.com/container-builder/docs/overview)とは，Google Cloud StorageまたはGoogle Cloud Source RepositoryにあるソースコードからDockerイメージを作成するためのサービスだ。GitHubのリポジトリ連携できないならもういいや，などと考えて読むのを止めようとする方がおられるだろうが，少し待ってほしい。Google Cloud Source Repositoryは[GitHubまたはBitbucketのソースコードを同期する機能](https://cloud.google.com/source-repositories/docs/connecting-hosted-repositories)があるので，これらのホスティングサービスとの連携も可能なのだ。具体的にリポジトリを同期する方法はここでは説明しない。

# Google Cloud Container Builderに出来る事

Google Cloud Container Builder（以下GCCBと呼ぶ）に出来る事は前述の通りで，指定されたソースコードからDockerイメージを作成する事だ。しかし，同じくリポジトリのソースコードからDockerイメージを作成する[Quay](https://quay.io)や[Docker Hub](https://hub.docker.com/)などのサービスとは異なり，ビルドに関するメタ情報を持つ[Build]()オブジェクトが柔軟なビルド定義を行えることだ。具体的には，[Build Steps](#build-steps)を使って，複雑なDockerイメージのビルドプロセスを構築できる。詳細に見ていこう。

# Build Steps

Build StepsはBuild Stepのシーケンスで構成される。一つの「Build Step」はDockerコンテナであり，スクリプトを構成する一つのコマンドの様なものであると説明されている。コンパイル環境とランタイム環境が異なる場合，これらのDockerイメージを分けてビルドを行うことは広く知られたプラクティスだと思うが，それを実現するにはちょっと泥臭い手間が必要になる（少なくとも僕にとってはそうだ）。例えば，Kubernetes上で動いているCIサービスの[Wercker](http://www.wercker.com/)は素晴らしいサービスだが，Kubernetes上で動いているが為に，逆に自由にDockerイメージを作るのが難しい。[Codeship](https://codeship.com)はこのような問題を認識しているようで，少し前から「DockerネイティブなCI」を標榜してプラットフォームの刷新を行い，[Codeship Pro](https://codeship.com/features/pro)としてリブランドして提供している。

少し話がそれた。つまりBuild Stepsを使えば，Dockerイメージを函数合成の様に組み合わせて，求める最適なDockerイメージを得ることができるということだ。複数のBuild Stepを`Build`オブジェクトの`steps`フィールドに指定した場合，前のBuild Stepの成果物は`/workspace`に渡される。この連鎖で，単純ながら求める殆どの事が可能になるはずだ。

Build Stepには任意のDockerイメージを利用できるが，Googleは基本的なツール群をイメージとして公開している。リポジトリは[ここ](https://github.com/GoogleCloudPlatform/cloud-builders)だ。このリポジトリの中をのぞくことで，またイメージを膨らませる事ができると思う。

# 実例

実際にいくつかのBuild Stepを組み合わせた例を作ってみよう。以下の様なビルドプロセスをGCCBを使って定義してみる。

1. [Finch](https://github.com/finagle/finch)を使ったScalaのソースコードをリポジトリから取得し，そこから単一の実行可能Jarを生成する
2. Jarをカレントディレクトリにもってくる
2. 生成したJarを含む，実行用Dockerコンテナを作成する

ごくごく単純な例だが，これを実際にGCCBでやってみる。`gcloud`コマンドを使う方法と，REST APIを直接使う方法がQuickstartでは提示されているが，`gcloud`コマンドでCloud Source Repositoriesを使う方法が無いようなので，`curl`を使ってやってみよう。[`Resource:Build`のAPIドキュメント](https://cloud.google.com/container-builder/docs/api/reference/rest/v1/projects.builds#top_of_page)に沿ってペイロードのJSONを書いていくだけなので，直観的に進めることができると思う。

## ソースコードを取得する

`Build`オブジェクトの`source`プロパティには，[`Source`オブジェクト](https://cloud.google.com/container-builder/docs/api/reference/rest/v1/projects.builds#source)を設定する。`Source`オブジェクトは`storageSource`または`repoSource`のいずれかになる`source`プロパティがあり，それぞれ[`StorageSource`オブジェクト](https://cloud.google.com/container-builder/docs/api/reference/rest/v1/projects.builds#Build.StorageSource)と[`RepoSource`オブジェクト](https://cloud.google.com/container-builder/docs/api/reference/rest/v1/RepoSource)を受け入れる。`StorageSource`はその名の通り，Cloud Storageのアドレスを表現するオブジェクトで，`RepoSource`はCloud Source Repositoriesを表現するものだ。今回はCloud Source Repositoriesにあるソースコードを使いたいので，`RepoSource`オブジェクトを設定する。

```json
"source": {
  "repoSource": {
    "projectId": "yananananana",
    "repoName": "finch-hello",
    "branchName": "master"
  }
}
```

この例ではこのようなオブジェクトを書いてやる。これでCloud Source Repositoriesから全ての起点となるソースコードが`/workspace`に配置される。

## 成果物を作成する

この例では，Javaのランタイムと，その上で実行するJarを最終的に生成するDockerイメージに配置したい。今回は全ての依存関係を一つのfat JARにまとめて成果物とする。ビルドツールとして[sbt](http://www.scala-sbt.org/)を用いて，JARを作成する。sbtが入ったDockerイメージ上で`sbt assembly`とコマンドを叩くだけだ。Build stepはこうした。

```json
{
  "name": "hseeberger/scala-sbt",
  "args": ["sbt", "assembly"],
  "id": "assemble"
}
```

とてもわかりやすい。Build Stepに組み込むDockerイメージは，この例の様にDocker image as a command的なイメージを使うとシンプルに記述でき，理解しやすいのでいいと思う。

これで`/workspace`をカレントディレクトリとして，`./target/scala-2.11/hello.jar`というJARができるはず。ディレクトリの中身を確認したいところだが，それもアドホックにBuild Stepに組み込んでしまえばいい。

```json
{
  "name": "gcr.io/cloud-builders/docker",
  "args": ["run", "--volume", "/workspace:/workspace/", "busybox", "ls", "-lR", "/workspace/target"],
  "id": "probe-intermediary"
}
```

Dockerを使っているから，こんなことは簡単にできる。`/workspace`をマウントしてやって`ls`コマンドを叩いているだけだが，これもまたGCCBのBuild Stepの使いやすさを示す一つの証左となるだろう。実際，途中に挟むBuild Stepはこれを発展させる形で書いてやればいい。

## Jarをカレントディレクトリにもってくる

最終的なイメージからファイルを除外するのは`.dockerignore`を使えばできるので，`hello.jar`をカレントディレクトリに移しておこう。これはDockerファイルの`COPY`ディレクティブを使えば不要な処理だが，汎用的なBuilderを書いてみたかったのでわざわざ追加してみた。

```json
{
  "name": "quay.io/yanana/cloud-builder-extractor",
  "args": ["target/scala-2.11/hello.jar", "."],
  "id": "extract"
}
```

`extract`と云うBuild Stepを定義した。ここでは，Builderをどうやって作ればいいのかを調べるのも兼ねて，ごくごく簡単なものを作ってみた。[cloud-builder-extractor](https://github.com/yanana/cloud-builder-extractor)は渡した引数のファイルを移動するもので，カレントディレクトリを`/workspace`として処理を行う。この程度なら`busybox`イメージで処理を行えばいいと思うが，非常に簡単にワンオフのBuilderを作ることがリポジトリを見ればわかると思う（そして作らなくても大抵の事ができる事も）。

## 最終形のイメージを作成する

Google提供の`docker`コマンドを実行できるBuilderを使う。`args`には`docker`コマンドの引数を渡して実行するというものなので，Dockerイメージを作成する以外の目的でも，もちろん使える。イメージを作成する場合はこんな感じになるが，よく考えたらこの際は`.dockerignore`ファイルが有効なので，前節の内容は全くの無駄だった。まあでもせっかくなのでそのまま残しておく。

```json
  "name": "gcr.io/cloud-builders/docker",
  "args": ["build", "--tag=asia.gcr.io/$PROJECT_ID/finch-hello", "-f", "Dockerfile.run", "."],
  "id": "imagize"
```

これで`asia.gcr.io/yananananana/finch-hello:latest`というイメージが出来た。GCCBのビルド成否は，`Build`オブジェクトの[`images`プロパティ](https://cloud.google.com/container-builder/docs/api/reference/rest/v1/projects.builds#Build.FIELDS.images)に指定したDockerイメージが作成されたかどうかで決定される。今回の場合は最後に作成したイメージだけなので，こうなる。

```json
"images": [
  "asia.gcr.io/$PROJECT_ID/finch-hello"
]
```

## `Build`オブジェクトの全容

これまで説明したことを全て一つのJSONにするとこうなる（アドホックなデバッグコードは除いてある）。

```json
{
  "source": {
    "repoSource": {
      "projectId": "yananananana",
      "repoName": "finch-hello",
      "branchName": "master"
    }
  },
  "steps": [
    {
      "name": "hseeberger/scala-sbt",
      "args": ["sbt", "assembly"],
      "id": "assemble"
    }, {
      "name": "quay.io/yanana/cloud-builder-extractor",
      "args": ["target/scala-2.11/hello.jar", "."],
      "id": "extract"
    }, {
      "name": "gcr.io/cloud-builders/docker",
      "args": ["build", "--tag=asia.gcr.io/$PROJECT_ID/finch-hello", "."],
      "id": "imagize"
    }
  ],
  "images": [
    "asia.gcr.io/$PROJECT_ID/finch-hello"
  ]
}
```

# まとめ

GCCBでできることとして，ありそうなユースケースを追いかけて使ってみた。感想としては，Werckerでこれができたらよかったのになあ，という事をまず感じた。使って感じたProsとConsを以下にまとめてみたので，参考までに見てほしい。

- **Pros**
  - 宣言的にビルドパイプラインを記述できることによる明快さ
  - できることの自由度の高さ
- **Cons**
  - gcloudコマンドがAPIと完全に整合が取れていないこと
  - 直接GitHubやBitbucketと連携できないこと
  - Google Container Registry以外のDockerレジストリとの連携の弱さ
  - クレデンシャルなどの見せたくない情報を外部から注入できないこと（KMSがAPIとして公開されれば解決する？）
  - ビルド結果を同期で待つAPIが無い（と思われる）こと

Consで挙げた点は，今後改善されていくのを期待したい。僕は自分のプロジェクトで，すぐに使ってみたいと感じた。今回触れていない要素として，[ビルドトリガ](https://cloud.google.com/container-builder/docs/api/reference/rest/v1/projects.triggers)があるが，トリガでビルドされた結果の完了を知る方法がわかればこれも是非使いたい。CIサーバで同一コミットに対するビルドを同定するには，`Build`オブジェクトのリストをとってコミットハッシュを見ていくしか今のところなさそうなので，ちょっと不便そう。

みんなでGCCBをどんどん使って機能改善してもらおう！