【GCP】Cloud BuildでCI/CDパイプラインを構築する。

## はじめに

[Cloud Build](https://cloud.google.com/cloud-build/docs?hl=ja)はGCP上でCI/CDを行うためのサービスです。
特にコンテナを扱ったソリューションと相性がよく、
テストやビルドはGCP上にプロビジョニングされたコンテナ内で行われ、
Cloud FunctionsやCloud Runといった様々なコンピューティングサービス上にデプロイすることができます。

本記事では、

- ソースコードの管理：[Cloud Source Repositories](https://cloud.google.com/source-repositories/docs?hl=ja)
- テスト及びビルド：[Cloud Build](https://cloud.google.com/cloud-build/docs?hl=ja)
- コンテナレジストリ：[Container Registry](https://cloud.google.com/container-registry/docs?hl=ja)
- デプロイ：[Cloud Run](https://cloud.google.com/run/docs?hl=ja)

![スクリーンショット 2020-09-04 18.08.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94505/cf152fb1-2a56-131b-30dc-837bd4c14358.png)

の全てGCP内で完結するリソースを使ってコンテナアプリケーションにおけるCI/CD環境を構築したいと思います！
本記事で使用したソースコードは、[こちら](https://github.com/kyhei/cloudbuild-sample)にあります。


## 開発環境

- macOS Catalina 10.15.6
- Docker Desktop stable 2.3.0.4
- Google Cloud SDK 308.0

## 登場人物たち

#### Cloud Source Repositories
プライベートなGitリポジトリをホスティングできるサービスです。
https://cloud.google.com/source-repositories/docs?hl=ja

#### Container Registry
プライベートなDockerイメージをホスティングできるサービスです。
https://cloud.google.com/container-registry/docs?hl=ja

#### Cloud Run
マネージドなコンテナ実行プラットフォームです。
コンテナを手軽にサービスとしてデプロイ、公開可能です。
https://cloud.google.com/run/docs?hl=ja

#### Cloud Build
本記事の主役です。
冒頭にもあるように、マネージドなCI/CD環境を構築できます。
詳しくは後の項で説明しますが、ビルドコンテナ内でgcloudコマンド叩けたりと柔軟なシナリオが作成可能です。
https://cloud.google.com/cloud-build/docs?hl=ja

## 作ってみよう！

### ローカルにGitリポジトリを作成

適当な空のディレクトリを作成し、`git init`を行います。

```sh
$ mkdir cloudbuild-sample && cd cloudbuild-sample
$ git init
```

### サンプルアプリケーションの作成
Cloud Runにデプロイするアプリケーションを作成します。
AとBの和を返す簡単なアプリを考えます。

```sh
$ mkdir workspace
```

```go:cloudbuild-sample/workspace/go.mod
module mymod

go 1.15
```

```go:cloudbuild-sample/workspace/main.go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"mymod/utils"
	"net/http"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		j := json.NewDecoder(r.Body)
		var req struct {
			A int `json:"a"`
			B int `json:"b"`
		}

		j.Decode(&req)
		fmt.Fprintf(w, "%d\n", utils.Add(req.A, req.B))
	})

	log.Println("start server...")
	http.ListenAndServe(":8080", mux)
}
```

足し算の関数を定義するサブパッケージを作成します。

```sh
$ mkdir workspace/utils
```


```go:cloudbuild-sample/workspace/utils/add.go
package utils

// Add return a + b
func Add(a int, b int) int {
	return a + b
}
```

```go:cloudbuild-sample/workspace/utils/add_test.go
package utils

import "testing"

func TestSuccessAdd(t *testing.T) {
	expect := 3
	result := Add(2, 1)

	if expect != result {
		t.Fatalf("result is expected %d, got %d ", expect, result)
	}
}

func TestFailureAdd(t *testing.T) {
	t.Fatal("FAILURE!!")
}
```

CI実行時にテストの失敗を考慮されるかを確認するため、意図的に失敗するテストコードを作成しました。

さらに、公開用のDockerイメージファイルも作成します。

```dockerfile:cloudbuild-sample/Dockerfile
FROM golang:1.15-alpine as builder

WORKDIR /opt/build

COPY go.* ./
RUN go mod download

COPY . ./

RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -v -o app


FROM alpine:3.12
ENV PORT 8080

COPY --from=builder /opt/build/app /opt/app

WORKDIR /opt
CMD ["./app"]
```

お疲れ様です！
上記のアプリは、

```shell
$ curl -X POST -H "Content-Type: application/json" -d '{"a":41, "b":10}' http://localhost:8080
51
```

のようなリクエストに対して、aとbの和を返してくれます。

### gcloudコマンドの下準備

前提として、既にGCPプロジェクトが作成済みでgcloudコマンドが使用できることを確認してください。
ここではパイプライン環境構築のためのGCPプロジェクト側での設定を行います。

まずは利用するサービスのAPIを有効化しましょう！

```sh
$ gcloud services enable sourcerepo.googleapis.com \
run.googleapis.com \
cloudbuild.googleapis.com \
containerregistry.googleapis.com
```

次にSource Repositoriesにリポジトリを作成し、リモートにセットします。

```sh
$ gcloud source repos create cloudbuild-sample
$ git remote add google https://source.developers.google.com/p/{YOUR_PROJECT_ID}/r/cloudbuild-sample
```

さらにリポジトリに対し認証情報を付与します。

```sh
$ git config --local credential.https://source.developers.google.com.helper gcloud.sh
```

ここまでのコードをpushできるか確認してましょう。

```sh
$ git add .
$ git commit -m 'first commit'
$ git push google master
```

次にCloud Buildのサービスアカウントに対しCloud Runへデプロイするための役割を付与します。

```sh
$ gcloud projects add-iam-policy-binding {YOUR_PROJECT_ID} \
--member serviceAccount:{YOUR_PROJECT_NUMBER}@cloudbuild.gserviceaccount.com \
--role roles/run.admin

$ gcloud projects add-iam-policy-binding {YOUR_PROJECT_ID} \ 
--member serviceAccount:{YOUR_PROJECT_NUMBER}@cloudbuild.gserviceaccount.com \
--role roles/iam.serviceAccountUser
```

お疲れ様です！
これで下準備は完了です。

### Cloud Buildの構成ファイルを作成する。

Cloud Buildではテストやビルドの手順をYAMLファイルとして定義します。
簡単な例として、前項で作成したサンプルアプリケーションのテストを実行し、
成功したらデプロイ用のイメージをビルドする手順書を作成してみます。

```yaml:cloudbuild-sample/cloudbuild.yaml
steps: # steps下にテスト〜デプロイまでの各手順を定義します。
  - name: 'golang:1.15-alpine' # コマンドを実行するコンテナイメージ
    id: 'do testing' # 各ステップを表す識別子
    entrypoint: '/bin/sh' # コンテナが実行される際のエントリーポイント
    env: # コンテナが実行される際に渡される環境変数
    - 'CGO_ENABLED=0'
    dir: 'workspace' # エントリーポイントが実行されるディレクトリ
    args: # エントリーポイントに対する引数
    - '-c'
    - |
        go mod download \
        && go test mymod/...
  - name: 'gcr.io/cloud-builders/docker' # 注１
    id: 'do building'
    args: # エントリーポイントは docker です。
    - 'build'
    - '-t'
    - 'gcr.io/$PROJECT_ID/sample_app:$SHORT_SHA' # 注２
    - '-f'
    - './Dockerfile'
    - './workspace'
```

設定しているオプションをよく見てみると、
普段の`docker run`コマンドを構成するオプションと似ていることが確認できるかと思います。

**注１**
nameオプションに設定できるコンテナイメージですが、
**クラウドビルダー**と呼ばれるGCP側で用意された特別なコンテナイメージも利用可能です。
クラウドビルダーコンテナにはIAM等が適切に割り当てられた環境で`gcloud`コマンド等が実行できるため、
柔軟なパイプラインを作成することが可能になります。
利用可能なクラウドビルダーについては
https://cloud.google.com/cloud-build/docs/cloud-builders?hl=ja
を確認してみてください。

**注２**
`$PROJECT_ID`や`$SHORT_SHA`といった、いくつかの変数はCloud Build側で適切な値に自動で置換されます。
今回の例で使用した
`$PROJECT_ID`は、GCPプロジェクトのID
`$SHORT_SHA`は、ビルドをトリガーしたリポジトリへのコミットID
です。
変数値の置換については、
https://cloud.google.com/cloud-build/docs/configuring-builds/substitute-variable-values?hl=ja
を確認してみてください。

それでは完全なCloud Buildの構成ファイルを作成してみましょう！
下記の構成ファイルでは、

1. `go test`を実行しテストが全てパスされるかを確認
2. デプロイ用のコンテナイメージをビルド
3. ビルドされたコンテナイメージをGCRへpush
4. GCRへプッシュされたイメージを使い、Cloud Runへサービスをデプロイ

を行います。

```yaml:cloudbuild-sample/cloudbuild.yaml
steps:
  - name: 'golang:1.15-alpine'
    id: 'do testing'
    entrypoint: '/bin/sh'
    env:
    - 'CGO_ENABLED=0'
    dir: 'workspace'
    args:
    - '-c'
    - |
        go mod download \
        && go test mymod/...
  - name: 'gcr.io/cloud-builders/docker'
    id: 'do building'
    args:
    - 'build'
    - '-t'
    - 'gcr.io/$PROJECT_ID/sample_app:$SHORT_SHA'
    - '-f'
    - './Dockerfile'
    - './workspace'
  - name: 'gcr.io/cloud-builders/docker'
    id: 'do pushing image'
    args:
      - 'push'
      - 'gcr.io/$PROJECT_ID/sample_app:$SHORT_SHA'
  - name: 'gcr.io/cloud-builders/gcloud'
    id: 'do deploying'
    args:
    - 'run'
    - 'deploy'
    - 'sample-app'
    - '--image=gcr.io/$PROJECT_ID/sample_app:$SHORT_SHA'
    - '--region=asia-northeast1'
    - '--platform=managed'
    - '--allow-unauthenticated'
```

お疲れ様です！
パプリックなコンテナイメージとクラウドビルダーを使い分け様々なシナリオに対応できることが実感できたかと思います。
構成ファイルに関する正確な情報はこちらをご確認ください。
https://cloud.google.com/cloud-build/docs/build-config?hl=ja

### Source Repositoriesへのpushでビルドをトリガーする。

最後に、Source Repositoriesへコードがプッシュされたときに自動的にCloud Buildが走るようトリガーを設定します。

```sh
$ gcloud beta builds triggers create cloud-source-repositories \
--repo cloudbuild-sample \
--branch-pattern="master" \
--build-config cloudbuild.yaml
```

cloudbuild-sampleリポジトリのmasterブランチにコードがpushされた際に、
cloudbuild.yamlに定義された内容のパイプラインが実行されます。

お疲れ様です！
作業は以上となります。
それではビルドの様子を確認していきたいと思います。

## ビルドを実行してみよう！

これまでの内容をpushし、Cloud Buildが実行されるか確認してみます。

```sh
$ git add .
$ git commit -m 'second commit'
$ git push google master
```

下記のURLからビルドの履歴を確認できます。
https://console.cloud.google.com/cloud-build/builds?hl=ja

今回のビルドでは`go test`に失敗し、ビルドが途中で中断されていることが確認できるかと思います。

テストコードを修正し、もう一度ビルドを実行しましょう。

```go:cloudbuild-sample/workspace/utils/add_test.go
package utils

import "testing"

func TestSuccessAdd(t *testing.T) {
	expect := 3
	result := Add(2, 1)

	if expect != result {
		t.Fatalf("result is expected %d, got %d ", expect, result)
	}
}

func TestFailureAdd(t *testing.T) {
	// t.Fatal("FAILURE!!") # コメントアウトしました。
}
```

再びpushします。

```sh
$ git add .
$ git commit -m 'fix test code'
$ git push google master
```

ビルドが正常に完了したことを確認します。
https://console.cloud.google.com/cloud-build/builds?hl=ja

さらに、正常にCloud Runへデプロイされたかを確認しましょう！
https://console.cloud.google.com/run?hl=ja

サービスのURLを確認し、curlを叩いてみます。

```sh
$ curl -X POST -H "Content-Type: application/json" -d '{"a":41, "b":10}' YOUR_CLOUD_RUN_URL
51
```

お疲れ様でした！

## 終わりに

本記事ではCloud Buildを使ってGo言語アプリケーションのCI/CDパイプラインを構築しました。
テストやビルドには様々なコンテナイメージを使えるためGo言語に限らず、柔軟なパイプラインを構築できるかと思います！
ありがとうございました！