GCP Cloud Build の基礎からの説明 & CI/CD 実践

  • このエントリーをはてなブックマークに追加

最終更新

Cloud Build は GCP のサービスのひとつで、継続的インテグレーション (CI)・継続的デプロイ (CD) を楽に行うためのものです。使い方を基礎から説明し、CI/CD のどう実現するかを説明します。

目次



Cloud Build とは何か

Cloud Build は GCP のサービスのひとつで、継続的インテグレーション (CI)・継続的デプロイ (CD) を楽に行うためのものです。

本質的には Cloud Build は、指定されたジョブ (ビルドステップ) を順番にこなしていき、失敗したらそこで終了する、というだけのサービスです。CI/CD のために使うのが普通ですが、全く関係ないことに使っても問題はありません。

シェルスクリプトとどう違うのか

テスト・デプロイはシェルスクリプトなどで実現することもできます。ではシェルスクリプトと Cloud Build で何が違うのか。

本質的には同じではありますが、Cloud Build にはいろいろな便利機能が用意されていて、ゴリゴリ書かなくてよいのが利点です。具体的な Cloud Build の利点は下記です。

  • GitHub などのソース管理システムと連携が可能。リポジトリに push → 自動的にビルドができる。
  • ビルドステップは Docker コンテナ内で実行されるため、ステップ間の環境差異が問題になりづらい。例えばアプリ自体は Python2 で書かれているのでユニットテストは Python3 だが、静的解析ツール Python3 で書かれている場合、ユニットテストを Python2 コンテナ、静的解析ツールを Python3 入りコンテナで動かせばよい。
  • タスクの並行実行が可能 (時間短縮につながる)。
  • Docker コンテナは、Docker Hub や Google Container Registry にあるものに加え、ユーザーグループが作成した便利なコンテナ、さらに自前で準備したコンテナも利用可能。
  • 自前コンテナについては、Cloud Build 内でイメージ作成することも可能。
  • 完了後 Cloud Pub/Sub にメッセージ送信することができる。Cloud Functions と連携させれば、Slack への通知などができる。
  • ビルドステップは、コマンドの終了ステータスが 0 なら OK、0 以外ならエラーと統一されており、エラーチェック漏れが起きにくい。
  • ビルド時の Docker コンテナ環境は Google 側で準備するため、GCE インスタンスなどを用意する必要がない。
  • Cloud Build により、過去のビルドログが全て残る。
  • GCP の認証として IAM が使えるため、認証が楽

繰り返しですが、上記は自前シェルスクリプトなどで実現可能です。しかしながらこれを設計し、コーディングし、テストする工数を考えると、結構な時間がかかるでしょう。Cloud Build にてその時間を節約できます。

Cloud Build がやってくれないこと

Cloud Build がやってくれないことは下記です。テストケースを書いたり、どうデプロイするのかを指示するのは結局は人間の仕事です。

  • 作成したプログラムを、よい感じに自動的にテストしてくれる
  • 作成したうログラムを、 よい感じに自動的にデプロイしてくれる

Cloud Build の料金・コスト

Cloud Build の料金は下記のとおり、マシンタイプごとに1分あたりの料金が決まっています。

デフォルトの n1-standard-1 の場合、1日あたり 120分は無料ですのでお得です。 なお、10分かかるビルドを 3回、40分かかるビルドを1回動かした場合、合計は 10×3 + 40 =「70分」です。

マシンタイプCPU料金・費用毎日4時間実行
での1ヶ月料金
(20営業日・
$1=110円・
税抜)
n1-standard-11$0.003/分。ただし1日あたり
120分は無料。デフォルトはこれ。
1,188円
n1-highcpu-8
8$0.016/分8,448円
n1-highcpu-32
32$0.064/分 33,792円

試してみよう

まずは Cloud Shell から試すのが一番よいと思います。

Cloud Shell でログイン後、作業用ディレクトリを作成し、cloudbuild.yaml というファイルを作成してください。

mkdir work
cd work
cat <<EOS > cloudbuild.yaml 
steps:
 - name: 'ubuntu'
   args: [ 'ls', '-l']
EOS

作業用ディレクトリの中で、下記コマンドを実行します。

gcloud builds submit .

すると下記のようになります。

% gcloud builds submit .
Creating temporary tarball archive of 1 file(s) totalling 49 bytes before compression.
Uploading tarball of [.] to [gs://myproject_cloudbuild/source/1559904202.89-c95823ea63ee459b8af755e4c94b0ce8.tgz]
Created [https://cloudbuild.googleapis.com/v1/projects/myproject/builds/eac39d3c-eba0-4274-b0fd-88664c6c91d4].
Logs are available at [https://console.cloud.google.com/gcr/builds/eac39d3c-eba0-4274-b0fd-88664c6c91d4?project=123456].
------------------------------------------------------------------------- REMOTE BUILD OUTPUT -------------------------------------------------------------------------
starting build "eac39d3c-eba0-4274-b0fd-88664c6c91d4"

FETCHSOURCE
Fetching storage object: gs://myproject_cloudbuild/source/1559904202.89-c05823ea63ee459b8af755e4c94b0ce8.tgz#1559904204270175
Copying gs://myproject_cloudbuild/source/1559904202.89-c05823ea63ee459b8af755e4c94b0ce8.tgz#1559904204270175...
/ [1 files][  190.0 B/  190.0 B]
Operation completed over 1 objects/190.0 B.
BUILD
Pulling image: ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
Digest: sha256:b36667c98cf8f68d4b7f1fb8e01f742c2ed26b5f0c965a788e98dfe589a4b3e4
Status: Downloaded newer image for ubuntu:latest
total 4
-rw-r--r-- 1 1000 1000 49 Jun  7 10:41 cloudbuild.yaml
PUSH
DONE
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

ID                                    CREATE_TIME                DURATION  SOURCE                                                                                 IMAGES  STATUS
eac39d3c-eba0-4274-b0fd-88664c6c91d4  2019-06-07T10:43:25+00:00  8S        gs://myproject_cloudbuild/source/1559904202.89-c05823ea63ee459b8af755e4c94b0ce8.tgz  -       SUCCESS

ここで Cloud Build のコンソールの履歴を見てみましょう。下記のようにビルド実行の記録が残ります。該当行をクリックすると、実行時のログも残っています。

Cloud Build が何を行ったかというと、こういうことです。

  • Cloud Build の「ビルド実行」が開始されると、指定ディレクトリ (ここではカレントディレクトリ) にあるファイル群をまとめて Cloud Build が準備したインスタンスのディスクに展開する。
  • Cloud Build は、cloudbuild.yaml に書かれているとおりに step を実行する。
  • name: ‘ubuntu’ とあるので、Docker Hub より Ubuntu のイメージを取得し、コンテナを起動する。さらに引数として “ls -l” を渡す。
  • Ubuntu のコンテナは指定された通り ls -l を実行する。

次に、下記を cloudbuild2.yaml として保存します。ここではビルドステップを3つ定義しています。

steps:
 - name: 'ubuntu'
   args: [ 'ls', '-l' ]
 - name: 'ubuntu'
   args: [ 'sh', '-c', 'echo hoge > xxx' ]
 - name: 'centos'
   args: [ 'ls', '-l' ]

下記で実行します。デフォルトのビルド構成ファイル名は cloudbuild.yaml ですが、それ以外のファイル名を使いたい場合は –config オプションで指定可能です。

gcloud builds submit --config=cloudbuild2.yaml

この例では、echo hoge > xxx にて新しいファイルを生成しています。また、前半は ubuntu、後半は centos の Docker イメージで実行しています。

それぞれのビルドステップ 1つずつは、別コンテナとして動きます。よって、OS も別、プロセスも別、環境変数も別です。

ただし、作業領域である /workspace は各ビルドステップ間で共有されます。上記例だと、2つ目で生成した xxx というファイルが、3つ目のステップの ls -l でファイルが存在することが確認できます。

ビルドステップの成功失敗判定

各ビルドステップの成功・失敗判定について。コマンドの終了ステータスが 0 なら成功、1 以上なら失敗、という単純なものです。

下記は date コマンド、ls コマンド、date コマンドを順に実行するものですが、ls コマンドは存在しないファイル名を指定しているため、終了ステータスが 1 とエラーになります。そして 3つ目の date コマンドは実行されません。

cat <<EOS > cloudbuild3.yaml
steps:
 - name: 'ubuntu'
   args: [ 'date']
 - name: 'ubuntu'
   args: [ 'ls', 'noexistfile']
 - name: 'ubuntu'
   args: [ 'date']
EOS

gcloud builds submit --config=cloudbild3.yaml

ビルドステップが失敗するとどうなるか。

ビルド履歴にはビルド実行結果は失敗として記録されます。



ビルド詳細では、ls で失敗し、後続ビルドステップが実行されなかったことがわかります。

いろいろなビルドステップの書き方

いろいろなビルドステップの書き方を説明します。

下記は Docker Hub より Ubuntu イメージを取得し、その中で ls -l を実行します。具体的には https://hub.docker.com/_/ubuntu のイメージが使われます。他にも CentOS や Python など様々なコンテナイメージが公開されています。

steps:
  - name: ubuntu
    args: ['ls -l']

下記は Google が Cloud Build のために準備した「クラウドビルダー」と呼ばれる便利なコンテナから、gsutilを使っています。

steps:
  - name: gcr.io/cloud-builders/gsutil
    args: ['cp', './file.txt', 'gs://foobar/']

gsutil のイメージには gsutil だけが入っているというわけではありません。下記は gsutil のイメージに含まれている bash を起動し、bash -c ‘コマンド’ にて、カレントディレクトリ変更やコマンド実行などの前処理を経て、gsutil を実行する例です。

steps:
  - name: gcr.io/cloud-builders/gsutil
    entrypoint: 'bash'
    args: ['-c', 'cd foo/bar/ && ./hoge.sh && gsutil cp * gs://foobar/'] 

成果物 (アーティファクト)

ビルド定義を実行したあと、ファイルを残したいケースがあります。例えば以下のような例です。

  • ビルドとデプロイを分離しており、ビルドした後に jar や zip などのファイルを残したい。
  • ユニットテスト等で、テスト結果を HTML として出力するツールがある。

Docker コンテナ内で実行され、/workspace もビルドが終了すればなくなってしまいますので、残したいファイルは GCS 等に配置しておく必要があります。

自前で GCS などにコピーしてもよいのですが、アーティファクトと呼ばれる成果物として登録しておけば、GCS 保存を Cloud Build が手伝ってくれるので少し楽になります。

CI/CD の実践

ここまでで Cloud Build は何をしてくれるのかを説明してきました。しかし本当にやりたいことは、CI/CD ですので、例をあげていくことにします

Python 編

まず、Python で足し算を行うスクリプトがあるとします。標準入力から 2つの値を入力させ、足した結果を表示する、というだけのものです。

#!/usr/bin/env python3

def myadd(a, b):
    return int(a) + int(b)

def mainwork():
    while True:
        print("数字を入力してください")
        a = input()
        print("数字を入力してください")
        b = input()
        result = myadd(a, b)
        print("{} + {} = {}".format(a, b, result))

if __name__ == "__main__":
    mainwork()

では上記の上記の myadd メソッドのユニットテストを書いてみましょう。Python においては標準モジュールである unittest が使われることが多いようなので、それにならってみます。

下記がテストコードです。3+5 が 8 になることと、(-3)+(-5) が -8 になることを確認するものです。

#!/usr/bin/env python3

import unittest
import mycalc

class TestMyCalc(unittest.TestCase):
    def test_my_add_plus(self):
        value1 = 3
        value2 = 5
        result = mycalc.myadd(value1, value2)
        self.assertEqual(8, result)
    def test_my_add_minus(self):
        value1 = -3
        value2 = -5
        result = mycalc.myadd(value1, value2)
        self.assertEqual(-8, result)

if __name__ == "__main__":
    unittest.main()

上記テストは下記で実行できます。

% python3 test_myadd.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

もうちょっと親切に出力させたい場合は、-v オプションをつけることで個々のテストケース名が表示されます。

% python3 test_mycalc.py -v
test_my_add_minus (__main__.TestMyCalc) ... ok
test_my_add_plus (__main__.TestMyCalc) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.004s

OK

ではこれを Cloud Build にて実行してみましょう。Cloud Build においては、Docker イメージを選ぶ必要があります。python3 が入っている Docker イメージはどれか。よくわかっていないのですが、おそらく Docker Hub にある python イメージなら大丈夫でしょう。

というわけで下記の cloudbuild.yaml を作成します。

steps:
  - name: 'python'
    args: ['python3', 'test_mycalc.py', '-v']

カレントディレクトリに cloudbuild.yaml、mycalc.py、test_mycalc.py がある状態で、Cloud Build を実行します。

% gcloud builds submit .
(略)
test_my_add_minus (__main__.TestMyCalc) ... ok
test_my_add_plus (__main__.TestMyCalc) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK
(略)

うまくいっているようですね。コマンドラインから python3 test_mycalc.py -v で実行した場合、テストは Cloud Shell 内で実行されます。Cloud Build で実行した場合は、テストは Cloud Build が用意した python コンテナ上で実行されます。

Python + モジュール編

上記は Python プロジェクトが提供する標準の Docker イメージでテストが実現できました。しかしながら標準 Docker イメージで解決できないケースは多々あります。例として、nose というモジュールを使ってみましょう。

Python のテストにおいて標準モジュールである unitetest はよく使用されますが、あくまで簡単なテストを行うのみで、テストフレームワークというほどの位置づけではありません。Python 界隈では nose というモジュールが使われることが多いようです。

nose は unittest の存在を前提とし、複数のテスト・カバレッジ (網羅性) 計測なども行うことができるツールです。nose は標準モジュールではないので、インストールする必要があります。

pip3 コマンドで nose をインストールします。この際、sudo で root 権限を一時的に取得します。

% sudo pip3 install nose

% nosetest -v . --with-coverage --cover-html
test_my_add_minus (test_mycalc.TestMyCalc) ... ok
test_my_add_plus (test_mycalc.TestMyCalc) ... ok
test_my_add_plus_value_error_NG (test_mycalc2.TestMyCalc2) ... ok


Name        Stmts   Miss  Cover
-------------------------------
mycalc.py      12      8    33%
----------------------------------------------------------------------
Ran 3 tests in 0.022s

OK

cloudbuild-nose.yaml を下記のようにします。args にて、nose と coverage をインストールしているのがポイントです。

steps:
  - name: 'python'
    args: ['bash', '-c', 'pip3 install nose coverage && nosetests -v --with-coverage --cover-html .']

Cloud Build にて実行します。

gcloud builds submit . --config=cloudbuild-nose.yaml

テストが通り、カバレッジ (網羅率) が 33% であることがわかります。

(略)
Installing collected packages: nose, coverage
Successfully installed coverage-4.5.3 nose-1.3.7
test_my_add_minus (test_mycalc.TestMyCalc) ... ok
test_my_add_plus (test_mycalc.TestMyCalc) ... ok
test_my_add_plus_value_error_NG (test_mycalc2.TestMyCalc2) ... ok

Name        Stmts   Miss  Cover
-------------------------------
mycalc.py      12      8    33%
----------------------------------------------------------------------
Ran 3 tests in 0.011s

OK
PUSH
DONE
(略)

Cloud Build HowTo

並列実行するには

ビルド実行自体は、10個まで並列実行可能です。それを超えた分は待ちになりますが、順番に処理されていきます。

ビルドステップはデフォルトでは上から下に順番に実行します。並行実行したい場合、各 step に waitFor を記述することで、どのステップの実行を待つかを指定できます。

例として、下記の依存関係を持つ step があるとします。

これを実現するビルド構成ファイルは下記です。

steps:
 - name: 'ubuntu'
   id: step1
 - name: 'ubuntu'
   id: step2
   waitFor: ['-']
 - name: 'ubuntu'
   id: step3
   waitFor: ['step1']
 - name: 'ubuntu'
   id: step4
   waitFor: ['step1', 'step2']
 - name: 'ubuntu'
   id: step5

ポイントは下記のとおり。

  • 待つべきステップを waitFor で指定する。参照される側のステップには id を指定しておくこと。
  • waitFor には複数のステップを記述可能なので配列 [~] になっている。
  • ビルド実行直後に並列実行したい場合は waitFor: [‘-‘] と書く。
  • waitFor を記述しない場合、先行するすべてのステップの完了を待ってから実行が開始される (デフォルト)。上記例の step5 は waitFor を記述していないが、あえて書くならば下記と同じである。
 - name: 'ubuntu'
   id: step5
   waitFor: ['step1', 'step2', 'step3', 'step4']

マシンタイプを変更するには

デフォルトでは n1-standard-1 が使われます。他のマシンタイプもありますが、GCE のようにたくさんマシンタイプがあるわけではなく、使えるのは n1-standard-1・n1-highcpu-8・n1-highcpu-32 の3つのみです。

下記のようにビルド構成ファイルに記載します。

steps:
  ....
options:
  machineType: 'N1_HIGHCPU_8'

一時的な変更であれば下記のように gcloud builds submit のオプションとして指定する方法もあります。

gcloud builds submit --machine-type=n1-highcpu-8 

なお、毎日 120分の無料枠があるのは n1-standard-1 のみであり、その他のマシンタイプの場合無料枠はないことに注意してください。

タイムアウト設定をするには

デフォルトでは、ビルド全体のタイムアウト設定は 10分 (600秒) です。これを 900秒に延長する例は下記です。

steps:
- name: 'ubuntu'
  args: ['sleep', '600']
timeout: 900s

タイムアウトは上記のようにビルド全体に設定することもできますが、各ステップに設定することも可能です。

下記は、ステップ1 で 200秒、ステップ2 で 300秒、ビルド全体で 1000秒のタイムアウト設定を行っています。

steps:
- name: 'ubuntu'
  args: ['sleep', '100']
  timeout: 200s
- name: 'ubuntu'
  args: ['sleep', '100']
  timeout: 300s
timeout: 1000s

環境変数

ビルド構成ファイルにて、ファイルを include するには

共通処理を別ファイルに切り出しておいて、個別のビルド構成ファイル cloudbuild.yaml から共通処理を include したい、というケースがあるかもしれません。

が、そのようなことは 2019/6 現在できないようです。なんらかのテキスト処理ツールを使い、ひな形から最終的な cloudbuild.yaml を自動生成するしかないと思われます。

ビルドトリガをコマンドラインから実行するには

gcloud alpha builds trigeers run を使います。

gcloud alpha builds triggers run [トリガID] \
   --branch=BRANCH

上記の「トリガID」は gcliud alpha builds triggers list で確認することができます。2019/07 時点では GCP コンソール (管理画面) 上ではトリガIDは確認できないように見えます

また、ブランチ名は指定必須です (master, develop, feature/foobar など)。

ビルド構成ファイルから、他のビルドトリガを実行するには

steps:
  - name: gcr.io/cloud-builders/gcloud
    args: ['alpha', 'builds', 'triggers', 'run', '[トリガID]', '--branch=xxx']

ビルドトリガ設定

ビルドトリガとは、GitHub 等のソース管理システムと、Cloud Build を紐づけるものです。これにより、git に push した際に自動的にビルドやリリース作業を行うなどの自動化が可能になります。

ブランチ名を指定することができます。

ビルドをスキップするには

ビルドトリガが設定済のとき、コメント修正などの軽微な修正の場合はビルドさせたくない場合があります。Git のコミットメッセージに “[skip ci]” または “[ci skip]” を含めることで、トリガが起動しなくなります。

ビルドトリガの無効化

ビルドトリガは無効化することが可能です。無効化とは「GitHub等での更新を契機としてビルド実行する自動連携機能を止める」ということなので、 無効化した場合でも、手動でのビルド実行は可能です。

Cloud Functions による slack 通知

Cloud Build の起動・終了などのタイミングで、Cloud Pub/Sub にメッセージを発行することができます。例として、Cloud Build → Pub/Sub → Cloud Functions → Slack 通知する例をあげておきます。

Cloud Funtions を作成する際に Pub/Sub トピック名を指定することで、Pub/Sub サブスクリプションが自動的に作成されます。

参考:https://cloud.google.com/cloud-build/docs/configure-third-party-notifications?hl=ja

トピック・サブスクリプション

Cloud Build が送信する Pub/Sub トピックは、1つだけで、トピック名は “cloud-builds” 固定です。Cloud Build API 有効化の際、トピックが自動的に生成されます。

トピックには好きなサブスクリプションをぶらさげることができますので、下記のような構成にすることも可能です

  • トピック cloud-builds
    • サブスクリプション ビルド結果 Slack 通知用
    • サブスクリプション 静的解析結果 Slack 通知用
    • サブスクリプション メール送信用
    • サブスクリプション 日次ビルドサマリ通知用

ただし、Cloud Build の結果はすべてのサブスクリプションに同じものが送信されますので、各サブスクリプションでの取捨選択が必要になります。

ビルド定義ごとに任意の Pub/Sub トピックを指定できるようになるといいんですが。

output への出力

Pub/Sub メッセージ内には各stepの成功・失敗は記録されているのですが、標準出力・標準エラー出力は含まれていません。そのような場合、ビルドステップ内で $BUILDER_OUTPUT/output に出力することで、Pub/Sub メッセージにその出力が載りますので、Slack 通知などに使うことができます。

例えば下記のようなビルド構成ファイルを使うと、

steps:
  - name: ubuntu
    args: ['bash', '-c', 'ls -l > $BUILDER_OUTPUT/output']
  - name: ubuntu
    args: ['ls -l']
  - name: ubuntu
    args: ['bash', '-c', 'date > $BUILDER_OUTPUT/output']

下記のような Pub/Sub メッセージが出力されます。step 1つめと 3つめの $BUILDER_OUTPUT/output の内容が builStepOutputs 内に入ります。出力されなかった 2つめは null になっています。base64 エンコードされているので、base64 デコードしてから処理しましょう。

results":{
 "buildStepOutputs":[
    "a29tbW(略)", // step1 の出力 (ls -l)
    null,         // step2 の出力
    "xxx(略)",    // step3 の出力 (date)
 ],
  • このエントリーをはてなブックマークに追加

SNSでもご購読できます。

Leave a Reply

*

CAPTCHA