Golang

GolangのVendoringをCIでキャッシュしたときの話

By なんぞー 03 December 2019
GolangのVendoringをCIでキャッシュしたときの話

こんにちは、なんぞーです!

この記事は Go5 Advent Calendar 2019 の 3 日目の記事です!

日頃、業務や趣味で Golang を書いている皆様方の中には、CircleCI や Github Actions などの CI で自動化をなさっている方も多いと思います! 僕もその一人なのですが、Go modules の vendoring を CI でキャッシュした際にハマってしまったことがあったので、記事にしたいと思います。

何をしようとしたか

utility 系のライブラリ(中にサブパッケージがいくつかある)をプロジェクトや会社単位で作って、様々なプロジェクトで使い回すといったことは多々あると思います! その utility 系のライブラリを使用した開発を行っている際に、vendoring のキャッシュを CI 上で行い、CI の高速化を測ろうとしました

実際どんな感じで開発していたか

今回は開発プロジェクトを「project1」、utility ライブラリを「go-utils」とすることにします。

  • go-utils@v1.0.0
    • /auth(package) (認証用)
    • /hash(package) (ハッシュ関数など)
    • /id(package) (UUID とか ULID の生成)
    • /assert(package) (テストの assert 関連)
    • /error(package) (global な error 達)

このような構成の utility ライブラリだとします。

開発初期、project1@v1.0 では authid を使っていました。

その時の go.mod と go.sum は

1
2
3
4
5
6
7
module github.com/nandehu0323/project1
go 1.12
require (
github.com/nandehu0323/go-utils v1.0.0
)
1
2
github.com/nandehu0323/go-utils v1.0.0 h1:[hash]
github.com/nandehu0323/go-utils v1.0.0/go.mod h1:[hash]

このようになっていました。

また CI の設定ファイルでは、Tag を付けるとgo mod vendorを行い、依存パッケージをキャッシュしテストに通過後、キャッシュされた依存パッケージを用いてビルドを行うという処理を設定していました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
version: 2
reference:
work_dir: &work_dir /go/src/tmp
golang_container_config: &golang_container_config
docker:
- image: circleci/golang:1.12
environment:
GO111MODULE: "on"
GOENV: test
working_directory: *work_dir
test_go: &test_go
run:
name: Testing Go Sources
command: |
go vet ./...
go get -u golang.org/x/lint/golint
golint -set_exit_status $(go list ./... | grep -v vendor)
go test -race ./...
build_docker_image: &build_docker_image
run:
name: Build Docker Image
command: |
[ビルド処理]
jobs:
test:
<<: *golang_container_config
steps:
- checkout
- run: go mod vendor
- save_cache:
name: Saving Go Vendor Cache
key: go-vendor-cache-{{ checksum "go.sum" }}
paths:
- /go/src/tmp/vendor
- *test_go
build:
<<: *cloud-sdk_container_config
steps:
- checkout
- restore_vendor_cache:
name: Restoring Go Vendor Cache
keys:
- go-vendor-cache-{{ checksum "go.sum" }}
- go-vendor-cache-
- *build_docker_image
workflows:
version: 2
test_-->_build:
jobs:
- test:
filters:
branches:
only: /.*/
tags:
only: /^v.*/
- build:
requires:
- test
filters:
branches:
ignore: /.*/
tags:
only: /^v.*/

次に開発が進み project1@v1.1 になり、 hashasserterror のパッケージを使う必要が出てきました。 その他の依存関係の変更はなく、go-utils の使用パッケージが増えました。

そこで、いつも通りタグを付けると CI が Build の時点で落ちてしまうようになりました。

1
build github.com/nandehu0323/project1/cmd/app: cannot load github.com/nandehu0323/go-utils/hash: open /app/vendor/github.com/nandehu0323/go-utils/hash: no such file or directory

go.modgo.sum にはちゃんと go-utils@v1.0.0 を使用するように記述されているし、 go mod tidy を行っても差分は見つかりません。

原因

原因としては、

  • go mod vendor は、サブパッケージが含まれている依存パッケージでは必要なサブパッケージのみを vendor ディレクトリに保存する
    • project1@v1.0で必要な authid しか cache に含まれていない。(初歩的なミスで Go 使いの方からは笑われそうです笑)
  • 依存パッケージの追加および削除は行われていないので、go.mod および go.sum の差分はない
    • よって、 {{ checksum "go.sum" }} が変化しないので cache の更新が行われない

このような utility系のパッケージを使用していてそれ以外の依存関係のアップデートが無くutility系のパッケージ内の新しいサブパッケージをimportする 環境下では CI の設定に気をつけないとこのようなことが起こるみたいです。

ここで、僕は CI(CircleCI)の Cache を削除しようとしましたが、version2 ではRerun without cacheができないらしく、困っていました。

そこで、プロジェクトキャッシュのクリアを参考に設定ファイルを変更することにしました。

変更後

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
jobs:
test:
<<: *golang_container_config
steps:
- checkout
- run: go mod vendor
- save_cache:
name: Saving Go Vendor Cache
key: go-vendor-cache-{{ .Environment.CACHE_VERSION }}-{{ checksum "go.sum" }}
paths:
- /go/src/tmp/vendor
- *test_go
build:
<<: *cloud-sdk_container_config
steps:
- checkout
- restore_vendor_cache:
name: Restoring Go Vendor Cache
keys:
- go-vendor-cache-{{ .Environment.CACHE_VERSION }}-{{ checksum "go.sum" }}
- go-vendor-cache-{{ .Environment.CACHE_VERSION }}-
- *build_docker_image

以降、同じような環境で再現した場合はCACHE_VERSIONを変えてキャッシュクリアをするように変更しました。

まとめ

今回は、かなり特殊な環境下で起こる現象だと思いますが、go mod vendorの動作などをちゃんと理解していなかったために起こったものでした。 なかなかこのような事象に遭遇することはないし、Gopher の皆さんなら余裕で解決できると思いますが、もしいつか困っている人がこの記事をみて解決の糸口にしてもらえたらなと思いこの記事を書き残します 👍

最後まで読んで頂きましてありがとうございました!

Twitter()もやっておりますのでぜひご感想やアドバイスお待ちしております!