CircleCI 2.0 で Go 1.10 の build cache をいい感じに効かせる
今更ながら CircleCI 2.0 を本格的に触りました。 CircleCI 2.0 になりキャッシュの柔軟性が向上したようなので、 Go 1.10 から導入された build cache と併せて最適化してみました。
前提知識
ドキュメントなど
- Language Guide: Go - CircleCI
- Caching Dependencies - CircleCI
- go - hdr-Build_and_test_caching - The Go Programming Language
戦略
- 依存管理は dep を使う
- vendor は Gopkg.lock が一緒なら同じものを用いる
- build cache は基本的にはブランチごとに管理
- ただし、新規ブランチは master からキャッシュを拾いたい
お急ぎの人のための結論
version: 2
jobs:
build:
working_directory: /go/src/github.com/hori-ryota/hoge # 対象のリポジトリに合わせて変更
docker:
- image: circleci/golang:1.10.2
environment:
- GOCACHE: "/tmp/go/cache"
steps:
- checkout
- run: git submodule sync
- run: git submodule update --init
- restore_cache:
keys:
- vendor-{{ checksum "Gopkg.lock" }}
- dep
- run:
name: ensure
command: |
if [ ! -d vendor ]; then
if ! type dep >/dev/null 2>&1; then
go get github.com/golang/dep/cmd/dep
fi
dep ensure
fi
- save_cache:
key: vendor-{{ checksum "Gopkg.lock" }}
paths:
- vendor
- save_cache:
key: dep
paths:
- /go/bin/dep
- restore_cache:
keys:
- build-cache-{{ .Branch }}--
- build-cache-master--
- build-cache-
- run:
name: build
command: make build
- save_cache:
key: build-cache-{{ .Branch }}--{{ .Revision }}
paths:
- /tmp/go/cache
when: on_fail
- run:
name: prepare cache dir if not exists
command: mkdir -p $GOCACHE
- persist_to_workspace:
root: /
paths:
- tmp/go/cache
- go/src/github.com/hori-ryota/hoge/artifacts
test:
working_directory: /go/src/github.com/hori-ryota/hoge
docker:
- image: circleci/golang:1.10.2
environment:
- GOCACHE: "/tmp/go/cache"
steps:
- checkout
- restore_cache:
keys:
- vendor-{{ checksum "Gopkg.lock" }}
- attach_workspace:
at: /
- run:
name: test
command: make test
- save_cache:
key: build-cache-{{ .Branch }}--{{ .Revision }}
paths:
- /tmp/go/cache
when: always
- persist_to_workspace:
root: .
paths:
- artifacts
deploy:
working_directory: /go/src/github.com/hori-ryota/hoge
docker:
- image: circleci/golang:1.10.2
steps:
- checkout
- attach_workspace:
at: .
- run:
name: deploy
command: make deploy
workflows:
version: 2
build:
jobs:
- build
- test:
context: test # if needed
requires:
- build
- deploy:
context: deploy # if needed
requires:
- test
解説
キャッシュの挙動とともに上から解説していきます。
version: 2
jobs:
build:
working_directory: /go/src/github.com/hori-ryota/hoge # 対象のリポジトリに合わせて変更
docker:
- image: circleci/golang:1.10.2
Go なので woking_directory
を GOPATH 内に設定しています。
environment:
- GOCACHE: "/tmp/go/cache"
build cache の保存先は $GOCACHE
で指定できます。扱いやすいように明示的に設定。
steps:
- checkout
- run: git submodule sync
- run: git submodule update --init
本題と関係ないですが submodule を取得するならここ。
- restore_cache:
keys:
- vendor-{{ checksum "Gopkg.lock" }}
- dep
- run:
name: ensure
command: |
if [ ! -d vendor ]; then
if ! type dep >/dev/null 2>&1; then
go get github.com/golang/dep/cmd/dep
fi
dep ensure
fi
- save_cache:
key: vendor-{{ checksum "Gopkg.lock" }}
paths:
- vendor
- save_cache:
key: dep
paths:
- /go/bin/dep
ここは vendor
のキャッシュです。まず restore_cache
ですが、 restore_cache
は keys
で指定したリストを順番にチェックし、初めに見つけた最初のキャッシュのみ取得します。
- restore_cache:
keys:
- vendor-{{ checksum "Gopkg.lock" }}
- dep
よってこの場合だと vendor-{{ checksum "Gopkg.lock" }}
があれば使用し、なければ dep
を取得します。更になければ無視。vendor
が存在していたら dep
は入れる必要がないので便利ですね。
次に ensure
部ですが、 vendor/
と dep
の存在によって処理を分岐させます。
- run:
name: ensure
command: |
if [ ! -d vendor ]; then
if ! type dep >/dev/null 2>&1; then
go get github.com/golang/dep/cmd/dep
fi
dep ensure
fi
vendor/
ディレクトリがなかった場合のみ、 dep
の存在チェックをして、なければインストール。その後 dep
を使って ensure
しています。
最後はキャッシュに保存。 CircleCI 2.0 のキャッシュは key がすでに存在すれば上書きせずにスルーします。今回は vendor
は Gopkg.lock
に対して一意とし、 dep
の更新性も考慮しないので決め打ちです。
- save_cache:
key: vendor-{{ checksum "Gopkg.lock" }}
paths:
- vendor
- save_cache:
key: dep
paths:
- /go/bin/dep
もし dep
の更新を行いたい場合は key にリビジョンや日付などの suffix をつけても良いかもです。
次に build cache です。方針のおさらいをしておきます。
- build cache は基本的にはブランチごとに管理
- ただし、新規ブランチは master からキャッシュを拾いたい
これを実現するための restore_cache
がこちら。
- restore_cache:
keys:
- build-cache-{{ .Branch }}--
- build-cache-master--
- build-cache-
上からチェックされるので意図通りの取得ができるかと思います。3 つ目の build-cache-
のみのものは使われることはないと思いますが、一応つけてみました。
save_cache
側がこちら。
- run:
name: build
command: make build
- save_cache:
key: build-cache-{{ .Branch }}--{{ .Revision }}
paths:
- /tmp/go/cache
when: on_fail
CircleCI 2.0 のキャッシュは、 key が前方一致する中で最新のものを取得する仕組みになっています。
なので更新性のあるデータについては {{ .Revision }}
のような suffix をつけて保存しておき、取得側では suffix を取り除いた key を用いるとうまくいきます。同じ Revision でも更新性をもたせたい場合は {{ epoch }}
などを併用すると良いかと思います。
変数の一覧はこちら。
ここで when: on_fail
を使っている理由はこちら。
- build が成功した場合は test 後のキャッシュを保存したい
- build が失敗した場合でも何らかのキャッシュが作成される場合、保存しておきたい
まぁ、一つ前の revision のキャッシュが残っているのであれば頑張って保存するほどじゃないかなーとは思っています。前方一致で取れるのを知らずに CIRCLE_PREVIOUS_BUILD_NUM
らへんで頑張ろうとしていたときの名残り。
そして build の workflow のラスト。
- run:
name: prepare cache dir if not exists
command: mkdir -p $GOCACHE
- persist_to_workspace:
root: /
paths:
- tmp/go/cache
- go/src/github.com/hori-ryota/hoge/artifacts
persist_to_workspace
で次の workflow にデータを渡します。ディレクトリがないと失敗するので mkdir -p
して保証。 artifacts
ディレクトリは build の成果物などなどの想定です。
次に test の workflow ですが、ここは vendor
の restore_cache
をして test して build cache を save_cache
するだけです。 vendor
は attach_workspace
するより restore_cache
のほうが速かった。
test:
working_directory: /go/src/github.com/hori-ryota/hoge
docker:
- image: circleci/golang:1.10.2
environment:
- GOCACHE: "/tmp/go/cache"
steps:
- checkout
- restore_cache:
keys:
- vendor-{{ checksum "Gopkg.lock" }}
- attach_workspace:
at: /
- run:
name: test
command: make test
- save_cache:
key: build-cache-{{ .Branch }}--{{ .Revision }}
paths:
- /tmp/go/cache
when: always
ただし、 test が失敗してもキャッシュは保存したいので when: always
を使用しています。
あとは artifacts
らへんの必要なものを deploy などの後続 workflow に渡してあげれば完了です。
- persist_to_workspace:
root: .
paths:
- artifacts
deploy:
working_directory: /go/src/github.com/hori-ryota/hoge
docker:
- image: circleci/golang:1.10.2
steps:
- checkout
- attach_workspace:
at: .
- run:
name: deploy
command: make deploy
workflows:
version: 2
build:
jobs:
- build
- test:
context: test # if needed
requires:
- build
- deploy:
context: deploy # if needed
requires:
- test
context は横断的に環境変数を設定できるもののようなので、 deploy や test に必要な認証情報などなどを渡したりしています。workflow に対して複数設定できるようになって欲しい。
以上
同一 key に対して上書き更新をかけられないとのことだったので最初は混乱しましたが、前方一致での検索と把握して納得しました。
build cache を効かせるととにかく爆速になるのでおすすめです。
キャッシュの有無による挙動変更はゴリゴリ書くしかなかったので、 step 間に条件分岐など仕込めるようになったらうれしいですね。