DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

Google Cloud Buildとは一体何者なのか

f:id:swet-blog:20180814114425p:plain

こんにちは。SWETの加瀬(@Kesin11)です。
Google Cloud Next ’18でGoogleのCI/CDサービスとしてGoogle Cloud Build(以後GCBと略します)というものが発表されました。
https://cloud.google.com/cloud-build/
https://www.youtube.com/watch?v=iyGHW4UQ_Ts

CI/CDサービスを追っているものとして、これは要チェック!
ということで、GCBを軽く使ってみて分かった特徴をCircleCIと比較してまとめてみました。

最初にまとめを書いてしまうと、GCBはCI/CDとして見るとCircleCIと比べてまだまだ扱いづらいところがあります。一方で、従量課金制のフルマネージドなビルドサービスというCircleCIとは違った使い方もできるところが特徴と言えます。

GCBの特徴

1ステップ1コンテナ

一般的なCI/CDサービスでは、以下のようなステップをGUIや専用のファイルに自分で記述していきます。

  • git checkout
  • 依存ライブラリのインストール
  • ビルド
  • テスト実行
  • デプロイ

CircleCIを例にすると、それぞれのステップは1つの仮想マシン、あるいは最近では1つのdockerコンテナ上で実行されるのが一般的です。

.circleci/config.yml

version: 2
jobs:
  build:
    docker:
      - image: circleci/node:9
    working_directory: ~/repo

    steps:
      - checkout
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "package.json" }}
          - v1-dependencies-
      - run: npm install
      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}
      - run:
          name: lint
          command: npm run lint
      - run:
          name: test
          command: npm run test

上の.circleci/config.ymlの場合、circleci/node:9のコンテナ上で全てのステップが実行されることになります。Workflowsを活用した場合には複数のコンテナを使用してビルドフローを構築することも可能ですが、基本的には1コンテナ上で複数のステップを実行するという考え方です。

GCBではここから根本的に異なっており、1ステップ1コンテナで実行されます。

cloudbuild.yaml

steps:
# build node script
- name: 'gcr.io/cloud-builders/npm'
  args: ['install']
- name: 'gcr.io/cloud-builders/npm'
  args: ['run', 'lint']
- name: 'gcr.io/cloud-builders/npm'
  args: ['run', 'test']
- name: 'gcr.io/cloud-builders/docker'
- args: ['build', '-t', 'gcr.io/$PROJECT_ID/sampleproject:latest', '.']

各ステップに必ずdockerイメージ名が記載されており、実際にこのコンテナ上で各ステップが実行されます。 そのため、例えばnpmを使いたいときはnpmが使えるコンテナ、docker buildしたいときはdockerが使えるコンテナを指定します。

Cloud Buildではよく使われるコマンドを実行できるdoockerイメージをGitHub上で公式コミュニティがメンテしています。 例えばgcloudkubectlfirebaseなどが使えるイメージが存在します。

もちろん、Cloud Buildによってメンテされているdockerイメージ以外も使用可能です。例えば先ほどのcloud-buildersのnpmのディレクトリにあるREADMEを見ると、以下のようにentrypointを指定することで公式のnodeイメージを使用することも可能であると書かれています。

steps:
- name: node:10.8.0
  entrypoint: npm
  args: ['install']

より詳しく知りたい場合、GCBのCreating Custom Build Stepsのドキュメントを参照するとよいでしょう。
https://cloud.google.com/cloud-build/docs/create-custom-build-steps?hl=en

GitHubのChecks APIが使える

2018/05にGitHubからChecks APIというものが公開されました。 これはPull Requestの画面からCI結果の詳細が見られるようになるという新機能なのですが、まだ対応されているサービスは多くありません。 この記事が公開された2018/08の時点で対応しているサービスはTravis CI、Visual Studio App Center、そしてGCBの3つしかないようです。 https://developer.github.com/v3/checks/

CircleCIは5月の発表時点では名を連ねていましたが、まだ使えるようになっていません。

docker imageのpushが簡単

GCBの特筆すべき特徴の1つとして、Google Cloud Platform(GCP)に含まれるGoogle Container Registry(GCR)との連携が挙げられます。 GCRはプライベートなコンテナレジストリを提供してくれるサービスで、特にGCPのコンテナを扱うサービス(GKEなど)を利用する上ではとても便利です。

GCBでは以下のようにdocker buildしたイメージをimagesで指定するだけで、自動的にGCRにpushしてくれます。 認証のための鍵などを考える必要はなく、非常にお手軽です。

cloudbuild.yaml

steps:
# docker build
- name: 'gcr.io/cloud-builders/docker'
  args: ['build', '-t', 'gcr.io/$PROJECT_ID/sampleproject:latest', '.']

# push built images to Container Registry
images: ['gcr.io/$PROJECT_ID/sampleproject:latest']

ローカルからコマンド実行するだけで手軽にビルドできる

CircleCIなどのCIサービスは基本的にGitHubにpushされたトリガーによってジョブが走るため、最初にビルドフローをトライ&エラーで構築するときに設定ファイルを少し変えてはgit pushをひたすら繰り返すことになります。

GCBの場合はgcloud builds submit --config=cloudbuild.yaml .このコマンド一発だけでローカルのソースコードを全てtarで固めて、cloudbuild.yamlに従ってクラウド上ジョブが実行されるので非常に簡単です。
https://cloud.google.com/cloud-build/docs/running-builds/start-build-manually?hl=en

設定ファイルを修正するたびにgit pushして動作確認するのもそれほど手間ではありませんが、トライ&エラーの時期にはコマンド一発で動作確認ができるのは意外と便利です。また、CI/CDという文脈を外せばリモートでdocker buildなどを実行できる便利なサービスとして使うこともできるでしょう。

ちなみにGCBでも手動で実行するだけではなく主要なCIサービスと同様に、GitHubにpushしたタイミングで自動的にジョブが動くように設定することは可能です。
https://cloud.google.com/cloud-build/docs/run-builds-on-github?hl=en

8コア、32コアマシン

GCBではビルドを実行するマシンのスペックを簡単に選択できます。どれだけ簡単かというと、先程のコマンドに--machine-typeオプションを追加するだけです。

gcloud builds submit --config=cloudbuild.yaml --machine-type=n1-highcpu-8 .

これだけでビルドを実行するマシンは8コアのスペックになります。オプション1つでマシンスペックを上げることが可能なので、非常に時間がかかるビルドフローを実行する場合だけハイスペックマシンにするという使い方が可能です。
もちろんcloudbuild.yamlに直接マシンスペックを記載することで、常に8コアマシンを使用してビルドするように設定することも可能です。
https://cloud.google.com/cloud-build/docs/api/reference/rest/v1/projects.builds#machinetype

従量課金制

CircleCIは月額課金制で、並列数やコンテナ数に応じて料金が変動する料金体系になっています。
https://circleci.com/pricing/

GCBでは料金体系が一般的なCIサービスとは異なっており、月額課金ではなくて従量課金制です。1コアマシンなら毎日120分は無料で、それを超えた先は分単位で課金されます。
ちなみに無料枠は1コアマシンだけのようで、先程紹介した8コア、32コアマシンの場合は無料枠がありませんので注意してください。
https://cloud.google.com/cloud-build/pricing?hl=en

CircleCIとは料金体系が異なるのでどちらが良いとは一概に言えませんが、少なくともGCBの場合はハイスペックマシンが必要な瞬間や、ビルドフローによってマシンスペックを変更することが可能という柔軟性があります。

GCBのイマイチな点

ここまでGCBの特徴として基本的に良い点を取り上げてきましたが、使ってみてイマイチだと感じた点もあるのでこちらについても触れておきます。

Slack連携がない。Cloud Functionでやるしかない

これが痛すぎるマイナスポイント。なんとビルドの成功/失敗を通知するという基本的な機能ですらデフォルトでSlackやメールに対応していません。

ビルドの結果はGoogle Cloud Pub/Subに流されるので、それをトリガーにしたCloud Functionを自前で用意してSlackなりMailgunのようなメール送信サービスのAPIを叩けということのようです。

https://cloud.google.com/cloud-build/docs/send-build-notifications https://cloud.google.com/cloud-build/docs/configure-third-party-notifications

CircleCIのような主要なCI/CDサービスではSlackなどに簡単に接続できるので、この点はだいぶ方針が異なると思いました。

キャッシュの記法が無い

CircleCIでは、ビルド時間を短くするために有効な方法として依存ライブラリのキャッシュがあります。CircleCIではキャッシュの読み取り/保存のためにrestore_cachesave_cacheという特別なステップが用意されています。

GCBの場合には、そのようなキャッシュのためのステップは用意されていないようです。自分がドキュメントを探した限りでは代替手段の解説も見当たりませんでした。
どうしてもキャッシュを使用したい場合は、Google Cloud Storage(GCS)をキャッシュの代わりに使用することで実現できるかも…?

並列ビルドフローの書き方が分かりにくい、フローのGUIが無い

CircleCIでは2.0からWorkflowを使うことで比較的簡単に並列のビルドフローを組み立てることができ、さらにビルドフローの図をGUIで表示してくれます。
https://circleci.com/docs/2.0/workflows/

GCBでも並列のビルドフローを組み立てることは可能ですが、設定ファイルの記述方法がCircleCIと比較してとても把握しやすいとは言えません。

例として、1つのリポジトリの中にツール用のTypeScriptと、Firebase Hostingにデプロイする用のディレクトリが存在する場合に並列で処理させる場合の書き方を比較します。

ディレクトリの構成はこのような感じです。

.
├── firebase.json
├── node_modules
├── package-lock.json
├── package.json
├── ts # ツール用のTypeScript
├── js # build後に生成されるjs
├── tsconfig.json
├── tslint.json
└── web # Firebase HostingにデプロイするNuxt.jsによるウェブサイト
    ├── assets
    ├── components
    ├── dist
    ├── layouts
    ├── middleware
    ├── node_modules
    ├── nuxt.config.js
    ├── package-lock.json
    ├── package.json
    ├── pages
    ├── plugins
    ├── static
    └── store

まずはCircleCI。一連のステップをbuild, build-web, deployのジョブに分割し、最後のworkflowsで順序や依存関係を記述するスタイルです。

以下の設定ではこのような流れになります(キャッシュ周りなど、今回の説明に不要な一部のステップは省略しています)。

f:id:swet-blog:20180814114416p:plain

.circleci/config.yml

version: 2
jobs:
  build:
    docker:
      - image: circleci/node:9
    working_directory: ~/repo

    steps:
      - checkout
      - run: npm install
      - run: npm run lint
      - run: npm run build

  build-web:
    docker:
      - image: circleci/node:9
    working_directory: ~/web

    steps:
      - checkout
      - run:
          name: npm install
          command: npm install
          working_directory: web
      - run:
          name: lint
          command: npm run lint
          working_directory: web

  deploy:
    docker:
      - image: circleci/node:9
    working_directory: ~/repo

    steps:
      - checkout
      - run:
          name: firebase deploy
          command: npm run deploy -- --token "$FIREBASE_TOKEN"

workflows:
  version: 2
  build_and_deploy:
    jobs:
      - build
      - build-web
      - deploy:
          requires:
            - build
            - build-web

次がGCBです。ステップごとにidを設定しておき、waitForで依存するステップを記述するスタイルです。
waitForが無いステップでは、自動的に1つの上のステップに依存するという挙動になります。 ただしwaitForで'-'を指定した場合は特別で、どのステップにも依存しないという意味になるので並列化する部分の起点になります。

以下の設定ではbuild webの最初のステップでwaitFor: ['-']を指定することで、build TypeScriptbuild webの一連のステップが並列化され、両方が完了してからfirebase deployが実行されます。

cloudbuild.yaml

secrets:
- kmsKeyName: ****
  secretEnv:
    FIREBASE_TOKEN: ****

steps:
# build TypeScript
- name: 'gcr.io/cloud-builders/npm'
  args: ['install']
- name: 'gcr.io/cloud-builders/npm'
  args: ['run', 'lint']
- name: 'gcr.io/cloud-builders/npm'
  args: ['run', 'build']
  id: 'script:build'

# build web
- name: 'gcr.io/cloud-builders/npm'
  args: ['install']
  dir: 'web'
  waitFor: ['-']
- name: 'gcr.io/cloud-builders/npm'
  args: ['run', 'lint']
  id: 'web:lint'

# firebase deploy
- name: 'gcr.io/cloud-builders/npm'
  args: ['run', 'deploy']
  id: 'firebase:deploy'
  waitFor: ['script:build', 'web:lint']
  secretEnv: ['FIREBASE_TOKEN']

個人的には、どの部分が並列に実行されるかがwaitForの挙動を把握していたとしても分かりづらいと感じました。さらに、CircleCIのworkflowと比べてyaml上で全体的なフローが把握しにくいだけではなく、GUIでフロー図を出してくれる機能もないのがマイナスポイントです。

とてもお手軽にマシンスペックを変更可能な点がGCBの長所なのですが、この並列ビルドフローの書きにくさがその長所を活かしにくくしてしまっています。

https://cloud.google.com/cloud-build/docs/configuring-builds/configure-build-step-order?hl=en

設定ファイル中に分岐処理を記述できない

CircleCIのconfig.ymlにはif-elseや、ブランチの指定などによって例えばmasterブランチの場合にだけデプロイのステップを実行する、といった制御を行うことができます。
先ほどのconfig.ymlも最後のworkflowsの部分にfiltersを追加することで、最後のdeployのジョブはmasterブランチの場合のみ実行されるといった制御が可能です。

workflows:
  version: 2
  build_and_deploy:
    jobs:
      - build
      - build-web
      - deploy:
          requires:
            - build
            - build-web
          filters:
            branches:
              only:
                - master

一方で、GCBには1つの設定ファイルの中で条件分岐を行うための記法が存在しません。分岐させたい処理だけシェルスクリプトに記述して実行させるという方法でおそらく実現は可能だと思いますが、全体の見通しは悪くなってしまうでしょう。

代わりにGCBはビルドトリガーを設定する際に、ブランチ名を正規表現で指定してビルドに使用する設定ファイルをcloudbuild.yaml以外にも指定できます。例えば以下のようにブランチ毎に使用する設定ファイルを分けることでブランチ毎のフロー制御が可能です。

  • developブランチ
    • cloudbuild.yaml(lintやテストを行う)
  • masterブランチ
    • cloudbuild_deploy.yaml(デプロイを行う)

f:id:swet-blog:20180815165338p:plain

ただし、CircleCIと異なり設定ファイルを分割する必要があるので、保守性の観点から個人的にはCircleCIの方法の方が好みです。

シークレットキーの扱いが面倒

デプロイを実行するフローを構築する場合に、秘密にしておく必要があるトークンをどうやってCIサービス中に読み取るかという問題があります。トークンが記されたファイルをリポジトリに含めてしまうのが最も簡単ですが、セキュリティ的な観点からは望ましくありません。
CircleCIではconfig.yamlとは別にCircleCIのGUIから環境変数の登録が可能なため、秘密にしておきたいトークンなどは環境変数にセットしてしまうのが一般的な方法です。

GCBの場合でも環境変数を使うというアプローチは可能なのですが、GCB単体にはその機能は存在せず、Cloud Key Management Service(Cloud KMS)というまた別のサービスと連携させる必要があります。
先程のcloudbuild.yamlでは、secrets:の箇所で暗号化されたトークンを環境変数としてセットしています。
https://cloud.google.com/cloud-build/docs/securing-builds/use-encrypted-secrets-credentials?hl=en#encrypting_an_environment_variable_using_the_cryptokey

詳しい使い方は上記のドキュメントを参照してください。正直、環境変数を登録したいだけなのにCircleCIと比較すると手順が多いという印象です。

まとめ

他のCI/CDサービス(今回は代表としてCircleCI)と比較したときのGoogle Cloud Buildの良い点、イマイチな点を紹介しました。最後にまとめとして、自分の主観ですがざっくりとした比較表を用意してみました。

CircleCI Google Cloud Build
ビルドフローの書きやすさ
GitHub連携
dockerコンテナのデプロイ(GCPとの連携)
チャットツール連携
マシンパワー
料金 月額課金 従量課金

GCBをCircleCIと比較してみて、個人的にはCI/CDとしてはまだ発展途上かなという印象でした。GCPとの連携は強みですが、ビルドフローが書きにくかったり、Slack通知がデフォルトでは無いあたりが現時点では少々扱いづらい点です。

一方で、コマンド一発でソースコードをアップロードしてクラウドでビルドができたり、マシンスペックを簡単に変更できるところが特徴的で、Cloud Buildというその名の通りフルマネージドな従量課金ビルドサービスとして非常に便利だと思います。

ブランチ戦略によってビルドやテスト、デプロイを複雑に制御するCI/CD用途よりもdocker buildを並列に行ってコンテナレジストリに登録したり、ソースコードのコンパイルといった、シンプルにコア数の多いハイスペックマシンを用意できると時間が短縮できるタスクを行う方が向いているという印象です。

近い将来、もはやビルド用のハイスペックマシン実機を手元に置いておく必要はなくなるかもしれません。
コーディング環境はコード補完が不自由ない程度のスペックのPCを使い、ビルドしたいときだけGCBにソースコードを送りつけてビルドしてもらう。そんな未来が来るかもしれないですね。