手元にDockerいらず GCP上でコンテナビルドして、そのままGKEにデプロイする Google Cloud Container Builder


最近GCP・GKEにハマっております。とても楽しいです。AWSをさわり始めたときと同じようなワクワク感があります。今回はGCPのコンテナビルドサービスGoogle Cloud Container Builderを使ってGCP上でDockerコンテナをビルドして、そのままGKEにデプロイする手順をまとめてみました。

## Google Cloud Container Builder

Google Cloud Container Builder（以下GCB）は、Cloud Storage上にあるソースコードをDockerビルドしてContainer Registryに登録してくれるサービスです。つまり、手元のパソコンにDockerがなくてもビルドしてくれるのです（実際コード書いて開発するとなると手元にDockerはないと面倒くさいけど…）。

## Railsアプリをビルドしてデプロイする

今回はRailsアプリをGKEにデプロイしてみます。デプロイまでの流れは以下のような感じ行こうと思います。

- 手元でRailsアプリの開発をする
	- 秘匿情報は `.env` ファイルをKMSで暗号化する
- コミットしてGitHubにpushする
- GCBのトリガーが発動してGitHubからソースコードをfetch
- 定義ファイルに基づいてビルドを開始
- 秘匿情報をKMSで復号化
- Dockerコンテナをビルド
- ビルド後、minitestを実行
	- テストが失敗したらビルドは終了
- テストを通過したらContainer Registryにイメージをpush
- kubectl（k8s）でGKEでデプロイ
- 完了後、slackに通知

予め許可しておくAPIは以下の4つです。

- Google Cloud Key Management Service (KMS) API
- Google Cloud Container Builder API
- Cloud Source Repositories API
- Google Cloud Function API

### GCBの設定ファイル

GCBの設定ファイル名はなんでも良いんですが、公式ドキュメントだと `cloudbuild.yaml` とよく出てくるのでこのまま進めます。いきなり全部入りの設定内容を書くのでうわっと思いますが、よく見ると単純です。少し解説します。

```yaml
steps:

# 秘匿情報を復号化
- name: gcr.io/cloud-builders/gcloud
  args:
  - kms
  - decrypt
  - --ciphertext-file=.env.production.enc
  - --plaintext-file=.env.production
  - --location=global
  - --keyring=<KEYRING_NAME>
  - --key=<KEY_NAME>

# Railsコンテナをビルド
- name: 'gcr.io/cloud-builders/docker'
  args: [ 'build', '-t', 'gcr.io/$PROJECT_ID/rails-docker-sample:$BRANCH_NAME-$REVISION_ID', '.' ]

# テスト用のマイグレーション実行
- name: 'gcr.io/$PROJECT_ID/rails-docker-sample:$BRANCH_NAME-$REVISION_ID'
  args: [ 'bundle', 'exec', 'rails', 'db', 'migrate', 'RAILS_ENV=test' ]

# minitestを実行
- name: 'gcr.io/$PROJECT_ID/rails-docker-sample:$BRANCH_NAME-$REVISION_ID'
  args: [ 'bundle', 'exec', 'rails', 'test' ]
  
# Dockerイメージをpush
- name: 'gcr.io/cloud-builders/docker'
  args: ["push", "gcr.io/$PROJECT_ID/rails-docker-sample:$BRANCH_NAME-$REVISION_ID"]

# GKEにデプロイ
- name: 'gcr.io/cloud-builders/gcloud'
  entrypoint: 'bash'
  args:
  - '-c'
  - |
    gcloud container clusters get-credentials <CLUSTER_NAME> --zone <ZONE_NAME>
    kubectl set image deployment/rails rails=gcr.io/$PROJECT_ID/rails-docker-sample:$BRANCH_NAME-$REVISION_ID
```

GCBは基本的にステップごとにビルドを進めていきます。上から順に終わったら次のタスクという感じです。設定するのは `name` と `args` で、それぞれDockerイメージ名とコマンドです。Dockerをさわったことがあれば、いつものやつと分かると思います。イメージは、Googleが元から準備してくれている各種イメージがあるのでそれを利用すると楽です。

GitHubにイメージがあるので、それを見るとどんなのがあるか分かります。よく使うのは `docker` `gcloud` `gsutil` あたりですね。

- https://github.com/GoogleCloudPlatform/cloud-builders

#### 変数

設定ファイル内では自動で展開される変数があります。

- $PROJECT_ID
- $REPO_NAME
- $BRANCH_NAME
- $TAG_NAME
- $REVISION_ID

これらを使ってイメージにユニークなタグをつけたりします。ただし、 `$REPO_NAME` `$BRANCH_NAME` `$TAG_NAME` `$REVISION_ID` は後述するビルドトリガー実行時じゃないと使えないです。手元でGCBに手動でビルドリクエストもできますが、その場合は空になってしまいます。


#### 並列ビルド

また、各ステップを並列で実行することも可能です。状況によっては並列にしたほうが効率良く出来ることもあると思います。 `waitFor` で何を待つかを指定できます。 `waitFor: ['-']` の場合だと待たずに並列になります。下記の例だと、1つ目と2つ目のステップが並列処理されて、3つ目は2つが終わった後に実行されます。

```
steps:
# Download the binary and the data in parallel.
- name: 'gcr.io/cloud-builders/wget'
  args: ['https://example.com/binary']
- name: 'gcr.io/cloud-builders/gsutil'
  args: ['cp', 'gs://$PROJECT_ID-data/rawdata.tgz', '.']
  waitFor: ['-']

# Run the binary on the data, once both are downloaded.
- name: 'ubuntu'
  args: ['./binary', 'rawdata.tgz']
```

詳しくは公式ドキュメントを参照してください。

- https://cloud.google.com/container-builder/docs/config?hl=ja

### KMSで秘匿情報を暗号・復号化

Railsに限らず秘匿情報をどうやって管理するかが悩みのタネだったりしますが、GCBやGKEを使ってやるならKMSで暗号・復号化するのが楽かもしれないと思っています。今回はdotenv-railsのライブラリを利用していますが、 `secrets.yml` に直接書いてもいいし他の方法でも良いと思います。

`gcloud` コマンドでKMSのキーリングを作成します。

```
$ gcloud kms keyrings create builder-keyring --location=global
$ gcloud kms keys create builder-key \
  --location=global \
  --keyring=builder-keyring \
  --purpose=encryption
```

そして、暗号化したいファイル `.env.production` を暗号化します。

```
$ gcloud kms encrypt \
  --plaintext-file=.env.production \
  --ciphertext-file=.env.production.enc \
  --location=global \
  --keyring=builder-keyring \
  --key=builder-key
```

あとは `.gitignore` に `.env.production` を追加して、暗号化された `.env.production.enc` をgitで管理するようにします。`.env.production` をバージョン管理していては意味ないので注意してください。

最後にGCBでKMS復号化が出来るように、cloudbuildサービスアカウントにKMS権限を付与します。cloudbuildサービスアカウントはGCBのAPIを有効にすると自動で作られます。名前はおそらく `プロジェクト番号@cloudbuild.gserviceaccount.com` という感じです。

- Encryption keys -> 作成したキーリング選択 -> 権限
- 「プロジェクト番号@cloudbuild.gserviceaccount.com」に「KMSの復号化」を付与

### トリガーを作成

次にGitHubと連携してmasterブランチにpushされたらビルドを開始するように設定します。GitHub以外にもBitBucketの連携にも対応していました。連携は簡単でOAuth認証して連携したいリポジトリを選択し、設定ファイルの場所や発火ルールを入れるだけです。

- Container Registry -> トリガーを作成 -> GitHub認証
- 連携したいリポジトリを選択 -> ブランチやビルドファイルなどの設定

### GKEデプロイ設定

GCBでビルド後に自動でGKEにデプロイできたら良いなーという人間なら誰しも思う要望に答えてくれてます。普通だったら面倒なkubectlのクレデンシャル設定を一発でやってくれるようになりました。そのためには前述したcloudbuildサービスアカウントにGKEの権限を付与します。

- IAMと管理 -> IAM -> cloudbuildアカウント選択
- 役割で「ContainerEngine管理者」を追加

これで下記のようにするとクレデンシャル設定を取得することができます。簡単便利ですね。

```
- name: 'gcr.io/cloud-builders/gcloud'
  entrypoint: 'bash'
  args:
  - '-c'
  - |
    gcloud container clusters get-credentials <CLUSTER_NAME> --zone <ZONE_NAME>
    kubectl set image deployment/rails rails=gcr.io/$PROJECT_ID/rails-docker-sample:$BRANCH_NAME-$REVISION_ID
```

GKEのクラスタやサービスなどの設定は今回は説明しません。公式チュートリアルや、解説記事は山のようにあると思うのでそれを参照してください。

### slackへ通知

ビルドが完了したら知りたいというのも人の性。slackに通知しましょう。slackの設定からIncoming Webhookを作成してHookURLを取得します。通知にはCloud Functionsを利用します。AWS Lambdaみたいなものですね。

スクリプトを設定するバケットを作成します。

```
$ gsutil mb gs://<BUCKET_NAME>
```

スクリプトを設置するディレクトリを作成します。

```
$ mkdir gcb_slack
$ cd gcb_slack
```

#### index.js

ソースコードは公式ドキュメントにあったものを拝借して少し修正を加えています。

```js
const IncomingWebhook = require('@slack/client').IncomingWebhook;
const SLACK_WEBHOOK_URL = "<WEBHOOK_URL>"

const webhook = new IncomingWebhook(SLACK_WEBHOOK_URL);


module.exports.subscribe = (event, callback) => {
  const build = eventToBuild(event.data.data);

  const status = ['SUCCESS', 'FAILURE', 'INTERNAL_ERROR', 'TIMEOUT'];
  if (status.indexOf(build.status) === -1) {
    return callback();
  }


  const message = createSlackMessage(build);
  webhook.send(message, (err, res) => {
    if (err) console.log('Error:', err);
    callback(err);
  });
};


const eventToBuild = (data) => {
  return JSON.parse(new Buffer(data, 'base64').toString());
}

const createSlackMessage = (build) => {
  let message = {
   text: "Build `" + build.id + "`",
    mrkdwn: true,
    attachments: [
      {
        title: 'Build logs',
        title_link: build.logUrl,
        fields: [{
          title: 'Status',
          value: build.status
        }]
      }
    ]
  };
  return message
}
```

#### package.json

パッケージ情報も作っておきます。

```
{
  "name": "google-container-slack",
  "version": "0.0.1",
  "description": "Slack integration for Google Cloud Container Builder, using Google Cloud Functions",
  "main": "index.js",
  "dependencies": {
    "@slack/client": "3.9.0"
  }
}
```

最後にCloud Functionsにデプロイします。

```
$ gcloud beta functions deploy subscribe --stage-bucket <BUCKET_NAME> --trigger-topic cloud-builds
```

これだけでビルド実行後にslackに通知が来ると思います。来ない場合はロギングで動作ログが確認できるので見てみてください。

## まとめ

- GCBがシンプルなCIっぽい感じで使える
- DockerビルドをDocker環境意識しなくていいので気が楽（CircleCI1.0はつらかった）
- 料金は1日120分は無料なので、かなり安く使えそう
- GCPにどっぷり浸かるならクレデンシャルまわりも楽
- トリガーでブランチに応じてビルドステップを使い分けられるので良い

と良いところばかりを上げています。複雑なことをしないならGCBはけっこういい選択肢になるんじゃないかなと思っています。是非使ってみてください。