blog.syfm

徒然なるままに考えていることなどを書いていくブログ

Go 1.13 に向けて知っておきたい Go Modules とそれを取り巻くエコシステム

はじめに

今年の 8 月にリリースが予定されている Go 1.13 では、Go 1.11 で導入された Go modules に加え、Go module proxy といった新しいエコシステムが登場します。
そこで、そもそも Go modules は何を行っているのかや、何ができるのか、どういった要素で構成されているのかを紹介します。 また、古い Go バージョンから Go 1.13 へアップデートする場合や、 depGlide といったベンダリングツールから Go modules へ移行する際の懸念点も併せて紹介します。

先日発表した "Go Modules and Proxy Walkthrough" はこのポストがベースになっています。
TL;DR な人はスライドを見るのがおすすめです。

speakerdeck.com

Go Modules

Go modules という仕組みは Russ Cox の Go & Versioning というポストと vgo という参照実装の中で登場しました。
Go 1.11 からは vgo ではなく、Go 本体に実装され、実際に Go modules を使うことができるようになりました。

Go modules は大きく以下のような特徴を持っています。

  • リポジトリのモジュール化
  • セマンティックバージョニングによるモジュールのバージョニング
  • go.mod による依存関係の管理
  • go.sum による依存モジュールのチェックサムの管理

リポジトリに含まれる複数のパッケージをまとめてモジュール化し、モジュール単位でセマンティックバージョニングを使ってバージョニングをします。
モジュールパスや、go.mod を作成した Go のバージョン、そのモジュールが依存するモジュール群などは go.mod に記述されます。

go.sum には、依存しているモジュールのファイルツリーのハッシュ値が記録され、ビルド時に verify されます。

go.modgo.sum ともに手動で書き換えることはなく、 go getgo mod tidygo mod edit などでプログラム的に書き換えられます。

Go modules の思想や、Semantic Import Versioning、Minimal Version Selection などについては Go & Versioning に書いてあることがすべてなのでそちらを参照してください。

$GO111MODULE のデフォルト値

Go 1.11 で導入された $GO111MODULE は、Go modules を使うかどうかを制御するための環境変数です。

  • on にされていれば Go modules を使う (module-aware mode)
  • off にされていれば従来どおり $GOPATH を使う (GOPATH mode)
  • auto の場合 $GOPATH/src の外に対象のリポジトリがあり、 go.mod が存在する場合は module-aware mode、そうでない場合は GOPATH mode

Go 1.11 時点では、 $GO111MODULE=auto がデフォルトとして設定されていたため、従来どおり $GOPATH 内にリポジトリを配置していた場合は明示的に on にしない限り Go modules が有効化されることはありませんでした。

以前 *1 は Go 1.13 から Go modules がデフォルト ($GO111MODULE=on) になるとアナウンスされていましたが、Issue#31857 にて auto をデフォルトのままとする決定が行われました。

ただ、Issue にもある通り、 auto の定義が若干変わっており、 $GOPATH/src 内であっても go.mod が存在する場合は module-aware mode と認識されるようになります。

module-aware go get

module-aware mode の場合、 go get が GOPATH mode とは異なる振る舞いをします。 GOPATH mode の場合、取得されたパッケージは $GOPATH/src 以下に配置されますが、 module-aware mode の場合、 $GOPATH/pkg/mod 以下にバージョンごとに配置され、モジュール内の go.modgo.sum が更新されるようになります。

また、以下のようにバージョンやコミットハッシュ、ブランチ名などを指定しての取得ができるようになります。
ツールもバージョンを指定してインストールすることができます。

$ export GO111MODULE=on

$ go get go.mercari.io/go-emv-code # v0.1.2 (latest)
$ go get go.mercari.io/go-emv-code@latest # v0.1.2 (latest)

$ go get go.mercari.io/go-emv-code@670be75
$ go get go.mercari.io/go-emv-code@v0.1.0

$ go get -u=patch go.mercari.io/go-emv-code # v0.1.2

Go 1.12 では go.mod があるリポジトリの外でも module-aware go get ができるようになっています。*2
ただ、go.mod があるリポジトリ内で実行してしまうと意図せず go.mod が書き換わってしまうため、グローバルにツールをインストールするためのコマンドを追加すべきかが Issue#30515 で議論されています。

プロジェクトで使うツールの依存管理

ローカル環境や CI 等で再現性のあるテストを行うためには、プロジェクトで使用するツールのバージョンも適切に管理されているべきです。 Go Modules Wiki にもあるとおり、 tools.go というツール管理用のファイルを作るのがベストプラクティスです。 ツールとプロジェクトのコードの依存を分けたい場合は、ツール用にサブの go.mod を作成します。

Go Module Ecosystem

Go 1.12 までは Go modules のサポートは限定的でしたが、Go 1.13 ではさらに以下の点で強化が行われます。

  • Module Index
  • Module Authentication
  • Module Mirrors
  • Module Proxy

Module Index

GoDoc のような、利用可能なモジュールの情報を集約し、クエリによってモジュールのフィードを取得できるサービスです。 現在は Google によって管理されている https://index.golang.org のみが存在します。 厳密な仕様はまだ定義されてなく、また index.golang.org の実装も公開されていないようです。 index.golang.org では limitsince がサポートされており、以下のようにモジュールを取得できます。
limit は 2000 件がデフォルトかつ最大値で、それ以上を指定すると Bad Request になります。

$ curl 'https://index.golang.org/index' # 最も古いものから 2000 件取得
$ curl 'https://index.golang.org/index?since=2019-08-01T00:00:00.000000-09:00'
$ curl 'https://index.golang.org/index?limit=100'

Module Index は GoDoc や外部サービスから利用されることを想定しており、GoDoc が Module Index を利用するように改良する予定のようです。

Module Authentication

GOPATH mode での go get は、取得したパッケージ自体の真正性を確認する方法がなかったため、Man-in-the-middle attack (中間者攻撃) に対して脆弱でした。
Go modules では go.sum に記録されたチェックサムによって取得したモジュールが go.sum を生成したときと同じ内容であるかを検証できるようになりました。 dep などのベンダリングツールでも同様のことはできましたが、 go コマンドに組み込まれるため、ベンダリングの有無に関わらず常に検証が行われるというのが大きな違いかつメリットです。

ただし、新しくモジュールを使用する際には go.sum に記録がないため、その正しさがわかりません。
そこで、あらゆるモジュールのチェックサムを集約するデータベースとして sumdb というサービスが登場します。世界中のモジュールに依存する go.sum とも考えられます。 初期は notary という名前でしたが、Issue#30825 にて Go checksum database (sumdb) という名前に変更されました。

しかし、sumdb が存在していても、世界中で初めてあるモジュールを使用する場合はその真正性をチェックできないという問題はあります。

Module Index と同様に Google が管理している https://sum.golang.org のみしか動いているサービスはありませんが、proposal に仕様が定義されています。
また、 Go 1.13 のリポジトリには sumweb という、サーバ実装が入ったパッケージがありますが、まだ開発中であるため internal パッケージ以下に配置されています。

また、 sumdb のバックエンドとして tkv パッケージが internal で定義されています。
CL161664 を見る感じだと Spanner による実装が入るようです。

Module Mirrors

GOPATH mode での go get では、GitHub や Bitbucket などのそれぞれのオリジンサーバへ直接アクセスしコードを取得しています。そのため、それぞれのサーバの可用性やレイテンシの影響を直接受けてしまっていました。
また、GitHub 上からリポジトリが削除されたり、なりすましが行われたりした場合に対処することができませんでした。 (e.g. go-bindata)

Go 1.13 からはモジュールをホストするための mirror がサポートされます。
Mirror は後述する proxy が必要とするモジュールのコードやチェックサムのキャッシュを行う役割を持ちます。

ホスティングサービスのリポジトリ削除の影響を防ぐため、基本的にミラーにキャッシュされたコードは削除されないという点には注意する必要があります。

Module Proxy

Go 1.13 からデフォルトで go コマンドはオリジンサーバへ直接モジュールを取得しに行くのではなく、proxy へ取得しに行くようになります。

proxy は HTTP サーバとして実装され、備えるべき API は "Module proxy protocol" として以下のように定義されています。

  • GET $GOPROXY/<module>/@v/list
    • そのモジュールのすべてのバージョンを返す
  • GET $GOPROXY/<module>/@v/<version>.info
  • GET $GOPROXY/<module>/@v/<version>.mod
    • そのモジュールバージョンの go.mod を返す
  • GET $GOPROXY/<module>/@v/<version>.zip
    • そのモジュールバージョンのコードを zip で返す

公式の proxy は他と同様に Google が管理しており、https://proxy.golang.org で公開されています。
また、proxy の実装は以下の URL のセクションにあるようにいくつか存在します。

Modules · golang/go Wiki · GitHub

Go 1.13 では $GOPROXY という環境変数が使えるようになり、go コマンドはこの値を見てどの proxy へアクセスするかを決定します。 *3 デフォルト値は https://proxy.golang.org,direct で、まず proxy.golang.org へ取得しに行き、取得できなかった場合は proxy を介さずに直接取得します。

proxy.golang.org を含むほとんどの proxy 実装は mirror も内包しているため、 proxy ≒ mirror と言えます。

各サービスの実装

それぞれの実装は Go のリポジトリ内 や、 golang.org/x/exp 以下で開発されています。 それらの実装が十分に成熟した段階で golang.org/x/mod 以下へエクスポートする予定になっています。
公開予定のパッケージは Issue#31761 で挙げられています。

すでに公開されているものとしては、 go.mod で使われている path と version やセマンティックバージョニングに関するロジック、sumdb の一部の実装があります。

Issue のマイルストーンが Go 1.14 となっているため、すべてのパッケージがエクスポートされるのはまだまだ先のようです。

Private Modules

社内リポジトリ等のパブリックに取得できないモジュールの場合、 $GOPRIVATE を設定する必要があります。

Proposal によると、セキュリティの観点 *4 から sumdb からチェックサムを取得できなかった場合は go コマンドの実行を中止し、他の proxy へプライベートなモジュールのパスを流出させないようになっています。
そのため、環境変数をデフォルトのままでプライベートリポジトリを扱おうとするとエラーが発生します。

これを回避するために以下のようにプライベートリポジトリの取得に proxy を使用しないように指定する必要があります。

例えば、 github.com/ktr0731/priv-mod というプライベートリポジトリがあるとします。 このリポジトリのモジュールを使用したい場合は、

$ export GOPRIVATE="github.com/ktr0731/priv-mod"

とします。 $GOPRIVATE にマッチするパスの場合、proxy も sumdb も使用されません。この挙動をオーバライドしたい場合は $GONOPROXY$GONOSUMDB を設定します。
それらが指定されている場合は $GOPRIVATE ではなく、その設定値が使用されます。

$GOPRIVATE$GOSUMDB には glob も使用できます。

Private Proxy

上記のような理由から、プライベートモジュールに関しては proxy.golang.org を使用することができません。
そのため、プライベートモジュールでも proxy を使用したい場合、サードパーティの認証・認可付きのサービスを使うか、セルフホスティングをする必要があります。

ただ、前述の通り公式の proxy 実装はまだ存在しないことだったり、認証の方法が現状 .netrc による Basic 認証しかないことを鑑みるとセルフホスティングをするのは難しいのではないかと思っています。

終わりに

Go & Versioning の序文で Russ Cox が以下のようなことを言っています。

Although we must add versioning, we also must not remove the best parts of the current go command: its simplicity, speed, and understandability. Today, many programmers mostly don't pay attention to versioning, and everything mostly works fine. If we get the model and the defaults right, we should be able to add versioning in such a way that programmers still mostly don't pay attention to versioning, and everything just works better and is easier to understand. Existing workflows should change as little as possible. Releasing new versions should be very easy. In general, version management work must fade to the background, not be a day-to-day concern.

実際、既存のベンダリングツールからの移行は非常に簡単に行なえますし *5$GOPRIVATE のような一部の点さえ気をつけていればあとはほとんど考えることはありません。

まだ問題は残っているとはいえ、Go modules がもたらすメリットは非常に多いため、ぜひ体験してみてください。

参考

*1:https://blog.golang.org/modules2019

*2:https://golang.org/doc/go1.12#modules

*3:正確には Go 1.11 で使えるようになっていますが、デフォルト値が与えられるのは Go 1.13 からです

*4:https://go.googlesource.com/proposal/+/master/design/25530-sumdb.md#private-module-paths

*5:https://godoc.org/golang.org/x/vgo/vendor/cmd/go/internal/modconv には既存のベンダリングツールから移行するための実装が入っています