2019年のGoモジュール

Go Modules in 2019 by Russ Cox

激動の1年

2018年はGoのエコシステムにとって素晴らしい年でした。パッケージ管理が私たちの主要な焦点の一つだったからです。 2月には、パッケージ管理をGoのツールチェインに直接統合する方法についてコミュニティ全体での議論を始め、 8月にはその機能の最初の粗い実装であるGoモジュールをGo 1.11でリリースしました。 Goモジュールへの移行は、Go 1以来Goのエコシステムにとって最も影響範囲の広い変更になるでしょう。 コード、ユーザー、ツールなど、エコシステム全体をGOPATHからモジュールへと変換するには、 さまざまな領域での作業が必要になります。 このモジュールシステムは、ひいてはGoのエコシステムにより優れた認証とビルド速度をもたらす助けとなるでしょう。

この記事では、2019年にモジュールに関してGoチームが計画していることを紹介します。

リリース

2018年8月にリリースされたGo 1.11では、モジュールの予備的なサポートが導入されました。 現時点では、モジュールのサポートは従来のGOPATHベースの仕組みと並行して維持されています。 go コマンドは、GOPATH/srcの外側にあり、ルートに go.mod ファイルが置かれているディレクトリツリー内で実行された場合、 デフォルトでモジュールモードになります。 この設定は、移行期間用の環境変数 $GO111MODULEon または off に設定することで上書きできます。 デフォルトの動作は auto モードです。 私たちはすでにGoコミュニティ全体でモジュールの著しい採用が進んでいるのを目にしていますし、 モジュールを改善する助けとなる有益な提案やバグ報告も数多く寄せられています。

2019年2月に予定されているGo 1.12は、モジュールのサポートを洗練させますが、 デフォルトでは引き続き auto モードのままです。 数多くのバグ修正やその他の細かな改善に加えて、Go 1.12における最も重要な変更はおそらく、 go run x.gogo get rsc.io/2fa@v1.1.0 のようなコマンドが、 明示的な go.mod ファイルなしでも GO111MODULE=on モードで動作するようになったことでしょう。

私たちの目標は、2019年8月に予定されているGo 1.13で、モジュールモードをデフォルトで有効にすること(つまり、 デフォルトを auto から on に変更すること)、そしてGOPATHモードを非推奨にすることです。 そのためには、オープンソースのモジュールエコシステムに対するより優れたサポートとあわせて、 より優れたツールサポートに取り組む必要があります。

ツールとIDEの統合

GOPATHを使ってきた8年間で、Goのソースコードがgopath内に格納されていることを前提とした 膨大な量のツールが作られてきました。 モジュールへの移行には、その前提に基づいているすべてのコードを更新する必要があります。 私たちは、与えられた対象に関するGoのソースコードを見つけて読み込むという操作を抽象化した新しいパッケージ、 golang.org/x/tools/go/packagesを設計しました。 この新しいパッケージはGOPATHモードとモジュールモードの両方に自動的に対応し、 Bazelが使っているようなツール固有のコードレイアウトにも拡張できます。 私たちはGoコミュニティ全体のツール作者と協力し、彼らがそれぞれのツールで golang.org/x/tools/go/packagesを採用できるよう支援してきました。

この取り組みの一環として、gocode、godef、go-outlineといった、 さまざまなソースコード検索ツールを、コマンドラインから使え、かつ最近のIDEで使われている language server protocolにも対応する、単一のツールへと統合する作業も進めています。

モジュールへの移行とパッケージの読み込み方法の変更は、Goのプログラム解析にも大きな変更をもたらしました。 モジュールをサポートするために go vet を作り直す作業の一環として、 1つのパッケージごとに解析器を呼び出すという形で、Goプログラムの逐次的な解析を行うための汎用フレームワークを導入しました。 このフレームワークでは、あるパッケージの解析結果を、そのパッケージをインポートしている他のパッケージの解析で 利用できる形の事実として書き出せます。 たとえば、logパッケージに対する go vet の解析では、 log.Printffmt.Printf のラッパーであるという事実を特定して記録します。 そして go vet は、 log.Printf を呼び出している他のパッケージ内のprintf形式の書式指定文字列を検査できます。 このフレームワークによって、開発者がバグをより早く見つけ、コードをより深く理解できるようにするための、 洗練された新しいプログラム解析ツールが数多く生まれるようになるはずです。

モジュールインデックス

go get のもともとの設計における最も重要な点の一つは、それが 分散化 されていたことでした。 私たちは当時から、そして今も、PerlのCPAN、JavaのMaven、NodeのNPMのような中央集権的なレジストリとは対照的に、 誰もが自分のコードを任意のサーバー上に公開できるべきだと考えています。 go get のインポート空間の先頭にドメイン名を置くことは、既存の分散システムを再利用することであり、 誰がどの名前を使えるかを決めるという問題をあらためて解決する必要がなくなりました。 またこれにより、企業は公開サーバー上のコードと並んで、社内の非公開サーバー上のコードをインポートできるようになりました。 Goモジュールへの移行に際して、この分散性を維持することが極めて重要です。

Goの依存関係の分散化には多くの利点がありましたが、いくつかの重大な欠点ももたらしました。 一つ目は、公開されているGoパッケージすべてを見つけ出すのが難しすぎることです。 パッケージに関する情報を提供したいサイトはどこも、独自にクロールを行うか、 あるいはユーザーが特定のパッケージについて尋ねてくるまで、そのパッケージの取得を待つかのどちらかを しなければなりません。

私たちは、Goのエコシステムに登録されるパッケージの一覧を公開ログとして提供する、 Go Module Indexという新しいサービスに取り組んでいます。 godoc.orgやgoreportcard.comのようなサイトは、新しいパッケージを見つけるための独自のコードを それぞれ実装する代わりに、このログを監視して新しいエントリを検知できるようになります。 また、まだローカルシステムにダウンロードされていないパッケージのインポートを goimports が追加できるように、 簡単なクエリを使ってパッケージを検索できる機能もこのサービスに持たせたいと考えています。

モジュールの認証

現在、 go get は正しいサーバーと通信してコードをダウンロードしていることを確認するために、 接続レベルの認証(HTTPSまたはSSH)に依存しています。 コード自体に対する追加の検証は行われておらず、HTTPSやSSHの仕組みが何らかの形で危殆化した場合には 中間者攻撃の可能性が残されたままになっています。 分散化されているということは、ビルドに使うコードが多数の異なるサーバーから取得されるということであり、 つまりビルドが正しいコードを提供する多数のシステムに依存しているということです。

Goモジュールの設計は、各モジュールに go.sum ファイルを保存することでコードの認証を改善します。 このファイルには、そのモジュールが依存する各モジュールについて、期待されるファイルツリーの暗号学的ハッシュ値が 記載されています。 モジュールを使う場合、 go コマンドはビルドで使用する前に、依存パッケージが期待されるバージョンと ビット単位で一致していることを検証するために go.sum を使います。 しかし go.sum ファイルには、そのモジュールが使用している特定の依存関係のハッシュ値しか記載されていません。 新しい依存関係を追加する場合や、 go get -u で依存関係を更新する場合には、 go.sum 内に対応するエントリが存在しないため、ダウンロードしたバイト列を直接認証する手段がありません。

公開されているモジュールについて、私たちは notary(公証人) と呼ぶサービスを稼働させる予定です。 このサービスはモジュールインデックスのログを追いかけて新しいモジュールをダウンロードし、 「モジュールMのバージョンVはファイルツリーのハッシュ値Hを持つ」という形の文に暗号学的な署名を行います。 notaryサービスは、これらの公証済みのハッシュ値をすべて、誰もがnotaryが正しく振る舞っていることを検証できるように、 問い合わせ可能なCertificate Transparency方式の 改ざん耐性のあるログとして公開します。 このログは、依存関係を追加あるいは更新する際に go get がモジュールを認証するために使える、 公開されたグローバルな go.sum ファイルとして機能します。

私たちは、Go 1.13から、まだ go.sum に記載されていない公開モジュールについて go コマンドが 公証済みのハッシュ値を検証するようにすることを目指しています。

モジュールミラー

分散化された go get は複数のオリジンサーバーからコードを取得するため、 コードの取得速度は最も遅く、最も信頼性の低いサーバーによって決まってしまいます。 モジュール以前に利用できた唯一の防御策は、依存関係を自分のリポジトリにベンダリングすることでした。 ベンダリングは今後もサポートされ続けますが、私たちとしては、すでに使っているモジュールだけでなく すべてのモジュールに対して機能し、依存関係をそれを使うリポジトリすべてに複製する必要のない解決策を望んでいます。

Goモジュールの設計では、 go コマンドがオリジンサーバーの代わりにモジュールを問い合わせる サーバーである、モジュールプロキシという考え方を導入しています。 プロキシの重要な一種が モジュールミラー です。これはオリジンサーバーからモジュールを取得して キャッシュし、以降のリクエストに応答するプロキシです。 きちんと運用されているミラーは、一部のオリジンサーバーがダウンしているときでも高速かつ信頼性が高いはずです。 私たちは2019年に、公開モジュール向けのミラーサービスを立ち上げる予定です。 GoCenterやAthensのような他のプロジェクトも、それぞれミラーサービスを計画しています。 (企業は自社内部向けのミラーを運用する選択肢も複数持てるようになると見込んでいますが、 この記事では公開ミラーに焦点を当てています。)

ミラーに関して潜在的な問題の一つは、ミラーがまさに中間者となるサーバーであり、 攻撃の格好の標的になってしまうことです。 Go開発者は、ミラーがオリジンサーバーが提供するのと同じバイト列を提供していることを 何らかの形で保証してもらう必要があります。 前のセクションで説明したnotaryの仕組みは、まさにこの懸念に対応するものであり、 オリジンサーバーを使ったダウンロードだけでなく、ミラーを使ったダウンロードにも適用されます。 ミラー自体を信頼する必要はなくなります。

私たちは、Go 1.13から、Googleが運用するモジュールミラーが go コマンドでデフォルトで 使えるようにすることを目指しています。 別のミラーを使うことも、まったくミラーを使わないようにすることも、簡単に設定できるようにします。

モジュールの発見

最後に、モジュールインデックスによってgodoc.orgのようなサイトを構築しやすくなると先に触れました。 2019年の私たちの取り組みの一部は、利用可能なモジュールを発見し、 あるモジュールに依存するかどうかを判断する必要がある開発者にとって、godoc.orgがより役立つものになるよう、 大幅に刷新することです。

全体像

次の図は、この記事で説明した設計の中を、モジュールのソースコードがどのように移動するかを示しています。

modules2019-code

以前は、 go コマンドやgodoc.orgのようなあらゆるサイトなど、Goのソースコードのすべての消費者が 各コードホストから直接コードを取得していました。 これからは、ダウンロードしたビット列が正しいことを認証しつつ、高速で信頼性の高いミラーから キャッシュされたコードを取得できるようになります。 そしてインデックスサービスによって、ミラーやgodoc.org、その他同様のサイトが、 日々Goのエコシステムに追加される素晴らしいコードすべてに簡単に追従できるようになります。

2019年のGoモジュールの未来に、私たちはワクワクしています。皆さんもそうであってほしいと思います。よいお年を!

By Russ Cox