Google Cloud Build で Go のビルドをする

# はじめに

* Google Cloud Build は GCP のビルドサービスでリポジトリへの push などをきっかけにコードのビルドやデプロイなどを自動で行うことができる。
* GCP のサービスだけあって Go のビルド環境は標準で用意されているが、若干クセがあったのでビルド方法をまとめる。

# ビルドと Docker イメージの作成

* `go install .` だけでビルド可能なソースコードを対象とする
* Go バイナリの作成を行い、作成したバイナリを実行するだけの Docker イメージをビルドし、イメージを Google Container Registry へアップロードするところまでを行う

## ビルド対象のソースコード

```main.go
package main

import "fmt"

func main() {
	fmt.Println("hello world")
}
```

## Cloud Build 設定ファイルの作成

* Cloud Build によるビルド処理内容の指定は `cloudbuild.yaml` で行う。
* `steps` には実行する処理を順に記載する。
* `steps` の個々の処理は Docker コンテナ上にて実行される。
  * ステップごとに指定した Docker コンテナが立ち上がり、ソースコードをマウントして、指定した引数で処理を実行する。
  * git, docker, kubectl, gcloud, gsutils, go などよく使われるコマンドを実行するためのコンテナは標準で用意されている。
  * 自分でビルドして GCR に配置すれば任意のコマンドを実行するコンテナも利用することができる。
* `images` に指定された名前の Docker イメージが steps の中でビルドされると、ビルド処理終了時に GCR へ自動で push される

```cloudbuild.yaml
steps:
# 標準の go コンテナでビルドを行う. 詳細は後述する.
- name: 'gcr.io/cloud-builders/go:alpine'
  args: ['install', '.']
  env: ['PROJECT_ROOT=myapp']
# 標準の docker コンテナで Docker イメージのビルドを行う. Docker ファイルは後述する.
- name: gcr.io/cloud-builders/docker
  args: ['build', '-t', 'gcr.io/$PROJECT_ID/myapp', '.']
# ビルドした Docker イメージを終了時に GCR に push する
images: ['gcr.io/$PROJECT_ID/myapp']
```

* Docker イメージのビルド時に利用される Dockerfile はこちら

```Dockerfile
FROM alpine:latest

WORKDIR /usr/local/bin/

# ビルドされたバイナリが gopath/bin/myapp に配置されているのでコピーする
COPY gopath/bin/myapp .

ENTRYPOINT ["/usr/local/bin/myapp"]
```

## go コンテナの解説

* 標準の go コンテナ `gcr.io/cloud-builders/go` は中で何をやっているのかを理解していないと使いこなすのが難しいので解説していく

### GOPATH 配下のソースコードをビルドしたい場合

* ソースコードが $GOPATH/src/github.com/foo/bar など GOPATH 配下に置いてある場合、環境変数 PROJECT_ROOT を使ってビルドを行う

```cloudbuild.yaml
- name: 'gcr.io/cloud-builders/go:alpine'
  args: ['install', '.']
  env: ['PROJECT_ROOT=myapp']
```

* 上記の設定で `gcr.io/cloud-builders/go` が実行されると下記のような処理が走る

```
1. ソースコードを /workspace へマウント
2. /workspace のシンボリックリンクとして /workspace/gopath/src/$PROJECT_ROOT を作成
3. GOPATH を /workspace/gopath に設定
4. cd /workspace/gopath/src/$PROJECT_ROOT
```

* 結果的に実行時のコンテナ内のファイル構成は以下のようになる

```
/
  workspace       <- ソースコードが自動でマウントされるディレクトリ
    main.go       <- ソースコードの main.go
	gopath        <- 初期化時にここが GOPATH に指定される
	  src
	    myapp     <- 初期化時に /workspace のシンボリックリンクとして gopath/src/$PROJECT_ROOT が作成される
		  main.go <- /workspace/main.go と同じファイル
```

* このようにコンテナの初期化処理でソースコードのコピーと GOPATH の指定をすることで、各種 go tool でソースコードを操作しやすいようにお膳立てしてくれる
* 初期化処理後のワーキングディレクトリは `/workspace/gopath/src/myapp` となるので `go install .` でビルドが実行され、GOPATH で指定された `/workspace/gopath/bin/myapp` にビルドしたバイナリが配置される
* 実際の初期化処理は[コンテナのコード](https://github.com/GoogleCloudPlatform/cloud-builders/tree/master/go)から確認できる

### GOPATH を含むソースコードをビルドしたい場合

* リポジトリの内部に GOPATH を内包している場合は下記のように設定する

```cloudbuild.yaml
- name: 'gcr.io/cloud-builders/go:alpine'
  args: ['install', '.']
  env: ['GOPATH=path/to/gopath']
```

* この場合は PROJECT_ROOT 指定時のような各種初期化処理は実行されず、そのまま GOPATH が指定され、ワーキングディレクトリは `/workspace` のままである。

# 内部のパッケージを参照しているコードのビルド

* 同一リポジトリ内部のパッケージを参照しているコードをビルドする場合は PROJECT_ROOT でパッケージが解決できるように指定する必要がある

## ビルド対象のソースコード

```main.go
package main

import (
	"fmt"
	"github.com/nirasan/myapp/hello"
)

func main() {
	fmt.Println(hello.Hello())
}
```

```hello/hello.go
package hello

func Hello() string {
	return "hello world from subdir"
}
```

## Cloud Build 設定

* ソースコードは `/workspace/gopath/src/$PROJECT_ROOT` に配置され、`GOPATH=/workspace/gopath` になるので `import` が解決できるように PROJECT_ROOT を指定する

```cloudbuild.yaml
steps:
- name: 'gcr.io/cloud-builders/go:alpine'
  args: ['install', '.']
  env: ['PROJECT_ROOT=github.com/nirasan/myapp']
- name: gcr.io/cloud-builders/docker
  args: ['build', '-t', 'gcr.io/$PROJECT_ID/myapp', '.']
images: ['gcr.io/$PROJECT_ID/myapp']
```

# dep で依存関係を管理しているコードをビルドする

* dep コマンド用のコンテナは標準で用意されていないためちょっと工夫が必要
* cloudbuild.yaml の設定では entrypoint オプションを指定することで任意のコマンドを実行することができる
* これを利用して dep のインストールと dep ensure の実行を行う
* entrypoint を変更して実行した場合、通常の entrypoint 使用時には自動で実行される処理（設定ファイルの prepare_workspace の部分）を手動で実行する必要があるので注意が必要

```cloudbuild.yaml
steps:
- name: 'gcr.io/cloud-builders/go:alpine'
  entrypoint: sh
  args:
    - '-c'
    - |
      # /workspace/gopath/src/$PROJECT_ROOT の作成と GOPATH の指定をするコマンドを直接実行する
      . /builder/prepare_workspace.inc
      prepare_workspace
	  # dep のインストールと実行
      go get github.com/golang/dep/cmd/dep
      ./gopath/bin/dep ensure
	  # インストールの実行
      go install .
  env: ['PROJECT_ROOT=myapp']
- name: gcr.io/cloud-builders/docker
  args: ['build', '-t', 'gcr.io/$PROJECT_ID/myapp', '.']
images: ['gcr.io/$PROJECT_ID/myapp']
```

## 参考URL

* https://cloud.google.com/cloud-build/docs/api/custom-build-steps?hl=ja#entrypoint
* https://www.slideshare.net/lestrrat/google-container-builder-87244724

# dep で github.com のプライベートリポジトリに依存したコードをビルドする

* dep で github.com のプライベートリポジトリを参照する際には [dep 公式 FAQ](https://github.com/golang/dep/blob/master/docs/FAQ.md#how-do-i-get-dep-to-consume-private-git-repos-using-a-github-token) の通りに github.com のアクセストークンを発行して .netrc を用意する
* アクセストークンの発行手順は[こちら](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/)

```cloudbuild.yaml
steps:
- name: 'gcr.io/cloud-builders/go:alpine'
  entrypoint: sh
  args:
    - '-c'
    - |
      echo === CREATE GOPATH ===
      . /builder/prepare_workspace.inc
      prepare_workspace
      echo === DEP ===
      cat <<EOF >~/.netrc
      machine github.com
          login $_GITHUB_USERNAME
          password $_GITHUB_ACCESS_TOKEN
      EOF
      go get github.com/golang/dep/cmd/dep
      ./gopath/bin/dep ensure
      echo === INSTALL ===
      go install .
  env: ['PROJECT_ROOT=myapp']
- name: gcr.io/cloud-builders/docker
  args: ['build', '-t', 'gcr.io/$PROJECT_ID/myapp', '.']
images: ['gcr.io/$PROJECT_ID/myapp']
```

* `$_GITHUB_USERNAME` と `$_GITHUB_ACCESS_TOKEN` はトリガーの変数で指定したり gcloud コマンドのオプションで置換する
* 変数置換を含めた gcloud コマンドはこちら

```
gcloud builds submit --config=cloudbuild.yaml \
    --substitutions=_GITHUB_USERNAME="username",_GITHUB_ACCESS_TOKEN="token" .
```

# dep 用の独自コンテナを作って実行する

* entrypoint オプションを駆使するのが独自若干複雑なのでコンテナを作成して dep を実行する
* github.com のプライベートリポジトリにも対応する

```cloudbuild.yaml
steps:
# dep コンテナをビルドする. Dockerfile は後述.
- name: gcr.io/cloud-builders/docker
  args: ['build', '-f', 'Dockerfile.dep', '-t', 'gcr.io/$PROJECT_ID/dep', '.']
# ビルドした dep コンテナを使って dep ensure を実行する
# ビルドしたイメージは Cloud Build の Docker デーモン中に保存されているので、前のステップで作成したコンテナがそのまま利用できる
# もちろん先に dep コンテナをビルドして GCR に push しておき、このステップから実行することも可能
- name: gcr.io/$PROJECT_ID/dep
  args: ['ensure']
  env:
  - "PROJECT_ROOT=myapp"
  - "_GITHUB_USERNAME=$_GITHUB_USERNAME"
  - "_GITHUB_ACCESS_TOKEN=$_GITHUB_ACCESS_TOKEN"
# go のビルド
- name: gcr.io/cloud-builders/go
  args: ['install', '.']
  env: ['PROJECT_ROOT=myapp']
# Docker イメージのビルド
- name: gcr.io/cloud-builders/docker
  args: ['build', '-t', 'gcr.io/$PROJECT_ID/myapp', '.']
# Docker イメージの push
images:
  - 'gcr.io/$PROJECT_ID/myapp'
```

* dep コンテナ用の Dockerfile はこちら
* エントリーポイントには単純に dep コマンドを指定するのではなく、go コンテナと同様の初期化処理を実行するスクリプトにする

```Docker.dep
FROM gcr.io/cloud-builders/go:alpine

RUN go get github.com/golang/dep/cmd/dep

COPY dep.ash /builder/bin
RUN chmod +x /builder/bin/dep.ash

ENTRYPOINT ["/builder/bin/dep.ash"]
```

* go コンテナと同様の初期化処理を行うエントリーポイントスクリプトがこちら
* github.com のプライベートリポジトリ用に .netrc の用意もやっている

```dep.ash
#!/bin/ash
. /builder/prepare_workspace.inc
prepare_workspace || exit

if [[ "$_GITHUB_USERNAME" != "" ]]; then
    cat <<EOF >~/.netrc
machine github.com
    login $_GITHUB_USERNAME
    password $_GITHUB_ACCESS_TOKEN
EOF
fi

/root/go/bin/dep "$@"
```


# おわりに

* `gcr.io/cloud-builders/go` はクセがあって慣れるまでは面倒
* だが、自分で汎用的な go のビルドコンテナを作ろうとすると結局同じような感じにはなりそうな気がするので結果的に標準で用意してくれてありがとうという感じになってきた