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.2Go なので 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
fivendor/ ディレクトリがなかった場合のみ、 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_failCircleCI 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/artifactspersist_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:
- testcontext は横断的に環境変数を設定できるもののようなので、 deploy や test に必要な認証情報などなどを渡したりしています。workflow に対して複数設定できるようになって欲しい。
以上
同一 key に対して上書き更新をかけられないとのことだったので最初は混乱しましたが、前方一致での検索と把握して納得しました。
build cache を効かせるととにかく爆速になるのでおすすめです。
キャッシュの有無による挙動変更はゴリゴリ書くしかなかったので、 step 間に条件分岐など仕込めるようになったらうれしいですね。