Goの開発で Bazel を使っているので簡単にまとめてみました。
サンプル用のプロジェクトを作ったのでこれをもとに進めていこうと思います。 GitHub - righ/bazel-sample-projectContribute to righ/bazel-sample-project development by creating an account on GitHub.https://github.com/righ/bazel-sample-project
利用する Bazel のバージョンは以下です。
- info
今回使う開発環境は Mac Catalina 64bit です。
Bazel
Bazelは Google が開発したOSSのビルドツールです。
キャッシュが効くため高速にビルドできたり、バイナリを作るだけでなくコンテナに乗せるところもやってくれるので 私達は開発に専念することができます。
Rule
Bazel はルールと呼ばれる拡張機能を用いることでさまざまな言語のビルドに対応できます。
今回は以下のルールを使います。
GitHub - bazelbuild/rules_go: Go rules for BazelGo rules for Bazel. Contribute to bazelbuild/rules_go development by creating an account on GitHub.https://github.com/bazelbuild/rules_go GitHub - bazelbuild/rules_docker: Rules for building and handling Docker images with BazelRules for building and handling Docker images with Bazel - GitHub - bazelbuild/rules_docker: Rules for building and handling Docker images with Bazelhttps://github.com/bazelbuild/rules_docker
設定ファイル
Bazel で重要なのは WORKSPACE
、 BUILD.bazel
(BUILD
) という2種類のテキストファイルです。
フォーマットは Python に似た Starlark と呼ばれる 言語で記述します。
Python の構文がすべて使えるわけではないので注意してください。例えば import
文は使えません。
WORKSPACE
WORKSPACE が配置されたディレクトリ配下は Bazel ではワークスペースとして認識されます。
このファイルにはプロジェクト全体に関する設定を書いていきますが、必要がなければ空でも構いません。
WORKSPACEでは主に以下のことを行います。
load
と呼ばれる関数を使って必要なルールを読み込み- Python でいうなら
from import
のようなものと考えるとわかりやすい
- Python でいうなら
- 使用するルールを外部から読み込む
http_archive
,http_file
の実行とそれらの設定を有効にする命令の実行
Bazel で指定するターゲットは @ルール//ワークスペース直下からBUILD.bazelまでのパス:名前
のようなフォーマットになっています。
先頭が //
から始まる場合はローカルを指します。load に指定する場合、 名前
の部分はマクロファイル名になります。
WORKSPACE に書くことはほかにもありますが、詳しくは gazelle セクションで話します.
BUILD.bazel
BUILD.bazel (あるいは BUILD
) はビルド対象のソースコードが配置されたディレクトリごとに配置し、
このファイルが置かれているディレクトリを Bazel では パッケージ
と呼びます。
BUILD.bazel と BUILD はいずれもパッケージを定義するための設定ファイルで同じ意味を持つので片方だけあれば問題ありません。
どちらにすべきというのは公式には書かれていないようですが、両方置いた場合には
BUILD.bazel
が優先されるっぽいです(未検証)
資料はこのあたり
BUILD.bazel にはビルドに必要なソースコードや依存パッケージの指定などを行います。 以下のようなものがあります。
- go_library
- パッケージ内のソースをビルドして GoLibrary を作成します。
- これは直接実行することはできず、後述する go_binary などから参照される形で利用されます。
- オプションは以下のURLを参照してもらうとして、重要なのは依存パッケージを指定する
deps
です。 - 必要なパッケージがここに指定されていないとビルドが通らないので注意です。
- go_binary
- mainパッケージに属するファイル群をビルドして実行ファイルを作ります。
- 実行するには
bazel run
かbazel build
を実行します。
- go_test
$ bazel test
とすることでテストをビルドします。- ワークスペース内のすべてのテストを行う場合は
bazel test --test_output=errors //...
とします。 - これは
go test ./...
と同じです。
https://github.com/bazelbuild/rules_go/blob/master/go/core.rst
gazelle
ビルドするのにルールなんていちいち覚えてられないし、 何よりファイルを作るたびにビルドの設定ファイルを手動で書き換えるのはめんどくさいですね。
この設定を自動生成するためのツールが gazelle です。
以下のようにセットアップします。
WORKSPACE で
bazel_gazelle
を取得- 詳しくは WORKSPACEセクション に戻るか ドキュメント を参照
BUILD.bazel で gazelle を load して登録する
これで bazel run //:gazelle
のように呼び出せるようになります。
$ bazel run //:gazelle INFO: Analyzed target //:gazelle (3 packages loaded, 139 targets configured). INFO: Found 1 target... Target //:gazelle up-to-date: bazel-bin/gazelle-runner.bash bazel-bin/gazelle INFO: Elapsed time: 4.819s, Critical Path: 0.01s INFO: 0 processes. INFO: Build completed successfully, 1 total action INFO: Build completed successfully, 1 total action
このコマンドは ワークスペース配下の
goのソースコードが含まれるすべてのディレクトリに
BUILD.bazel
を配置します。
単純な上書きではないので、もともと書いてある内容を勝手に消し去ったりすることはありません。
実際に作成された BUILD.bazel は 後のセクションで見ていくことにします。
- info
# gazelle:prefix github.com/example/project
のようなコメントを BUILD.bazel に追加するか-go_prefix
のようなオプションを渡してあげないと以下のようなエラーが発生します。[gazelle: /bazel-sample-project/echo: go prefix is not set, so importpath can't be determined for rules. Set a prefix with a '\# gazelle:prefix' comment or with -go_prefix on the command line
- 今回は bazel-sample-project/BUILD.bazel にコメントを記述しました。
update-repos
このままでは 外部パッケージのバージョンが固定されていないためビルドに失敗することがあります。
そこで、 update-repos
というサブコマンドを使います。 --
の後に指定した引数が gazelle に渡ります。(ターゲットに渡す引数は --
以降に記述します)
$ bazel run //:gazelle -- update-repos -from_file ./go.mod INFO: Analyzed target //:gazelle (59 packages loaded, 6979 targets configured). INFO: Found 1 target... Target //:gazelle up-to-date: bazel-bin/gazelle-runner.bash bazel-bin/gazelle INFO: Elapsed time: 5.798s, Critical Path: 0.01s INFO: 0 processes. INFO: Build completed successfully, 1 total action INFO: Build completed successfully, 1 total action
このコマンドは go.mod
や Gopkg.lock
のようなパッケージのバージョンを管理するファイルを指定することで
依存パッケージのリポジトリ情報を WORKSPACE に書き出してくれます。
先程の WORKSPACE の下の方に書かれていた大量の
go_repository
はこれによるものだったのですね。
GitHub - bazelbuild/bazel-gazelle: Gazelle is a Bazel build file generator for Bazel projects. It natively supports Go and protobuf, and it may be extended to support new languages and custom rule sets.Gazelle is a Bazel build file generator for Bazel projects. It natively supports Go and protobuf, and it may be extended to support new languages and custom rule sets. - GitHub - bazelbuild/bazel-g...https://github.com/bazelbuild/bazel-gazelle https://github.com/bazelbuild/bazel-gazelle/blob/master/repository.rst#go_repository
実際に使ってみる
これまでの概要だけでは何が嬉しいのかイマイチ分かりづらいと思うので、 以降のセクションではサンプルプロジェクトを動かして解説していきます。 最終的に k8s で動かすのが目標です。
gateway サーバと echo サーバがあって、 gateway にきたリクエストを echo に gRPC で受け流してリクエストをそのままgatewayに返却するという簡単な構成です。
sequenceDiagram participant client participant gateway participant echo client->>gateway: HTTP Request gateway->>echo: gRPC Request echo-->>gateway: gRPC Response gateway-->>client: HTTP Response
それぞれのプログラム自体は簡易ですが 別々のサーバなのでビルドはそれぞれで行わなければなりません。
bazel を使わずにビルドして実行してみます。
go run gateway/main.go
$ go run echo/main.go
localhost:8000 にアクセスしてから、以下のように /echo
に
aaa
を渡します。
aaa
が返ってきたので期待通りです。
bazel run
続いて bazel を使って動かしてみます。
bazel でビルドしたものをそのまま動かすには bazel run ターゲット
のようにします。 ターゲットの名前部分(:
の右側)を省略するとパッケージ名で補完されます。
以下は 実行対象の BUILD.bazel と登録された go_binary を呼び出す例です。
$ bazel run //gateway --platforms=@io_bazel_rules_go//go/toolchain:darwin_amd64 INFO: Build option --platforms has changed, discarding analysis cache. INFO: Analyzed target //gateway:gateway (1 packages loaded, 7590 targets configured). INFO: Found 1 target... Target //gateway:gateway up-to-date: bazel-bin/gateway/darwin_amd64_pure_stripped/gateway INFO: Elapsed time: 20.093s, Critical Path: 19.34s INFO: 82 processes: 82 darwin-sandbox. INFO: Build completed successfully, 85 total actions INFO: Build completed successfully, 85 total actions
$ go run echo/main.go $ bazel run //echo --platforms=@io_bazel_rules_go//go/toolchain:darwin_amd64 INFO: Build option --platforms has changed, discarding analysis cache. INFO: Analyzed target //echo:echo (0 packages loaded, 7590 targets configured). INFO: Found 1 target... Target //echo:echo up-to-date: bazel-bin/echo/darwin_amd64_pure_stripped/echo INFO: Elapsed time: 2.091s, Critical Path: 1.56s INFO: 2 processes: 2 darwin-sandbox. INFO: Build completed successfully, 5 total actions INFO: Build completed successfully, 5 total actions
(service_image
というのは次のセクションで説明しますが、今回の実行には関係ありません)
先ほどと同じように localhost:8000 にアクセスしてから、以下のように
/echo
に aaa
を渡します。
期待通りですがなんか普通のビルドよりも複雑になっている気がします。
--platforms=@io_bazel_rules_go//go/toolchain:darwin_amd64
ってなんなの?って思いましたよね。
実は最終的に
Dockerのコンテナとして動かすことを想定しているため、Linuxのバイナリとして出力されるように
.bazelrc
を設定しているのです。
ビルドだけなら動作しますがそのバイナリをMacの環境で動かそうとすると当然動かないのです。 (cannot execute binary file って言われる)
そこで私が使っている環境で動くように \--platforms
オプションでMac用バイナリを吐くように制御しています。
通常(Dockerで動かさない場合)は bazel run //xxxx
だけでうまく動くはずです。
Container で動かす
コンテナを作るには (container_imageと) go_image を使います。
が、今回は作成した BUILD.bazel には直接これらを書かず、service.bzl
というマクロファイルに独自定義した service_image
を介して利用しています。
わざわざ関数化した理由は単に複数回利用するからです。今回の例ではサービスは2つですが多くなると同じような処理を何回も書きたくありません。
マクロ関数は以下のようになっています。
このマクロ関数は サービス名を受け取り、そのサービス名を元にビルドしたバイナリが指定されます。
実はただコンテナを作るだけであれば go_image
だけでよいのですが、コンテナを tar 形式のファイルとして出力するために
container_image
で作ったイメージをベースに go_image
を呼び出しています。 そして
container_image
の tars
引数に pkg_tar
の tar ファイルを指定すれば tar
にコンテナイメージを埋め込めるようです。
これはドキュメントにも書かれている custom base
と呼ばれる手法だそうです。
一番最後に書いた container_push はその名の通り作成したコンテナをレジストリに登録するためのものです。 Push のセクションで解説しますが、ここでは利用しません。
前置きが長くなりましたが実際に実行してみます。
$ bazel run //gateway:image INFO: Analyzed target //gateway:image (0 packages loaded, 0 targets configured). INFO: Found 1 target... Target //gateway:image up-to-date: bazel-bin/gateway/image-layer.tar INFO: Elapsed time: 0.556s, Critical Path: 0.19s INFO: 1 process: 1 darwin-sandbox. INFO: Build completed successfully, 5 total actions INFO: Build completed successfully, 5 total actions 1410da508793: Loading layer [==================================================>] 10.17MB/10.17MB Loaded image ID: sha256:6d5a86f263dbe7c9d67bed2f929227b47e0575622103f2249951b301b9d19d26 Tagging 6d5a86f263dbe7c9d67bed2f929227b47e0575622103f2249951b301b9d19d26 as bazel/gateway:image
$ bazel run //echo:image INFO: Analyzed target //echo:image (0 packages loaded, 0 targets configured). INFO: Found 1 target... Target //echo:image up-to-date: bazel-bin/echo/image-layer.tar INFO: Elapsed time: 0.393s, Critical Path: 0.15s INFO: 1 process: 1 darwin-sandbox. INFO: Build completed successfully, 5 total actions INFO: Build completed successfully, 5 total actions ae82e6e875bc: Loading layer [==================================================>] 9.943MB/9.943MB Loaded image ID: sha256:64abfa06b48b087828fa48e05f2e4d1906f92689f8f1c3895b530d608094c5a3 Tagging 64abfa06b48b087828fa48e05f2e4d1906f92689f8f1c3895b530d608094c5a3 as bazel/echo:image
一見動いているように見えますが、 http://localhost:8000/
にアクセスすることができません。 理由は gateway が echo サーバ
(localhost:8001
) にアクセスしてもDocker的に別ホストのため
localhost で接続できず処理がブロックされHTTPサーバが起動しないためです
(ブロックしないようにもできるけどechoサーバに接続できないのは同じこと)
このために gateway のプログラムは echo のホストを環境変数(ECHO_HOST)で変えられるようにしています。
つまり ECHO_HOST
に実際の echo コンテナのIPアドレスを指定すればうまくいきそうです。
ただ、今回は以下の理由で bazel run
で通信させることはできませんでした。
- コンテナ起動時に特定のDockerネットワークに所属させることができなかった
- BUILD.bazel でシェルからわたした環境変数を読み出すことができなかった
- container_image には
env
引数で辞書形式で受け取れるが全環境で固定になってしまうのでホストから渡した環境変数を読みたい
- container_image には
というわけで、ビルドしたコンテナを docker コマンドで呼び出すようにしてみます。
docker network create test-network
で test-network
を作り、以下を実行します。
# gateway $ docker run --rm --name="gateway" --net=test-network --env ECHO_HOST=echo -p 8000:8000 07908b17146
# echo $ docker run --rm --name="echo" --net=test-network 82ccbd677fb
今度は http://localhost:8000/
にアクセスできました。
なんとか動いたようです。
bazel から直接呼び出せているわけではないのでなんか気持ち悪いですが、今回のゴールはここじゃないので目を瞑ってください。
- info
go_image
のname
,base
,deps
,binary
,layers
以外のキーワード引数はすべてgo_binary
関数に引き継がれます。- https://github.com/bazelbuild/rules_docker/blob/master/go/image.bzl
Push する
Bazelを使って任意のコンテナレジストリに Push することができます。
先程 container_push
を書いたのはこれが理由です。
- info
- container_push には
go_image
の名前を指定します。 container_image
の名前を指定して Push しても イメージの CMDがなくNo command specified
と言われます。
- container_push には
今回のアップロード先は Docker Hub です。 予めログインを済ませて、リポジトリを作成しておく必要があります。
ここはレジストリによって操作が異なるのであんまり詳しくやりませんが、 私の場合は以下のようなリポジトリがある状態です。
今回 DockerHub に Push するための、 push_to_dockerhub
と
push_to_gcr
の2つを用意しました。
以下は DockerHub に Push する例です。
$ bazel run //gateway:push_to_dockerhub INFO: Analyzed target //gateway:push_to_dockerhub (0 packages loaded, 1 target configured). INFO: Found 1 target... Target //gateway:push_to_dockerhub up-to-date: bazel-bin/gateway/push_to_dockerhub.digest bazel-bin/gateway/push_to_dockerhub INFO: Elapsed time: 0.277s, Critical Path: 0.04s INFO: 1 process: 1 darwin-sandbox. INFO: Build completed successfully, 5 total actions INFO: Build completed successfully, 5 total actions 2020/05/03 23:12:28 Destination docker.io/righm9/gateway:{BUILD_TIMESTAMP} was resolved to docker.io/righm9/gateway:1588515147 after stamping. 2020/05/03 23:12:38 Successfully pushed Docker image to docker.io/righm9/gateway:1588515147
bazel run //echo:push_to_dockerhub INFO: Analyzed target //echo:push_to_dockerhub (0 packages loaded, 1 target configured). INFO: Found 1 target... Target //echo:push_to_dockerhub up-to-date: bazel-bin/echo/push_to_dockerhub.digest bazel-bin/echo/push_to_dockerhub INFO: Elapsed time: 0.267s, Critical Path: 0.03s INFO: 1 process: 1 darwin-sandbox. INFO: Build completed successfully, 5 total actions INFO: Build completed successfully, 5 total actions 2020/05/03 23:13:06 Destination docker.io/righm9/echo:{BUILD_TIMESTAMP} was resolved to docker.io/righm9/echo:1588515186 after stamping. 2020/05/03 23:13:13 Successfully pushed Docker image to docker.io/righm9/echo:1588515186
GCR へ Push するのは GKE に kustomize を使ってデプロイしてみる を御覧ください。
Skaffold で動かす
ローカルで Kubernetes を動かすために Skaffold というツールを使います。
Bazel と同様に Google が開発したツールで、 Bazel でビルドしたコンテナを指定することができます。
以下のような設定ファイルを用意しました。
artifacts の bazel.target
にコンテナを指定できますが、
tar
形式である必要があります。 先程のマクロ関数で
pkg_tar
を使っていたのはこれが理由だったのです。
Kubernetes の解説になってしまいますが、 deploy.kustomize
には Kustomization の定義を指定できるので、 kube/skaffold
を指定しています。 これにより kube/skaffold/kustomization.yaml
が読み込まれます。
skaffold/kustomization.yaml では echo と gateway のローカル用設定を 読み込み、 ローカル設定はそれぞれの base 設定を読み込んでいるという具合です。 (この記事では production は使いません)
構造化しているのでファイルは多いですがやっていることは割と単純です。
また、 gateway の ECHO_HOST
環境変数は
echo.default.svc.cluster.local
にしています。
これは Kubernetes 内で Pod のアドレスを解決するための FQDN です。
(詳しくは こちら)
Skaffold でサーバを起動するために skaffold dev
を実行します。
これはファイルの変更検知をして Bazel
のリビルドが実行されるというすぐれものです。
(Realize とか使わなくていいですね
$ skaffold dev Listing files to watch... - righm9/echo - righm9/gateway Generating tags... - righm9/echo -> righm9/echo:0907753 - righm9/gateway -> righm9/gateway:0907753 Checking cache... - righm9/echo: Found Locally - righm9/gateway: Found Locally Tags used in deployment: - righm9/echo -> righm9/echo:82ccbd677fb859dd731e61c4d4e2cf5f80a50636eb56e85f8263b320263cf080 - righm9/gateway -> righm9/gateway:07908b17146c811733912239c242080108fcf917580cbfe0cd97e2d613cd10a2 local images can't be referenced by digest. They are tagged and referenced by a unique ID instead Starting deploy... - configmap/nginx-conf created - service/echo created - service/gateway created - deployment.apps/echo created - deployment.apps/gateway created Waiting for deployments to stabilize... - deployment/echo: waiting for rollout to finish: 0 of 1 updated replicas are available... - deployment/gateway: waiting for rollout to finish: 0 of 1 updated replicas are available... - deployment/echo is ready. [1/2 deployment(s) still pending] - deployment/gateway is ready. Deployments stabilized in 2.752441541s Watching for changes...
今回はせっかくの k8s なので gateway の前に Nginx を噛ませてみました。 gateway と同じ Pod で稼働させています。
sequenceDiagram participant client participant nginx participant gateway participant echo client->>nginx: HTTP Request nginx->>gateway: HTTP Request gateway->>echo: gRPC Request echo-->>gateway: gRPC Response gateway-->>nginx: HTTP Response nginx-->client: HTTP Response
Nginxが待ち受けている ポート 80番 にアクセスしてみます。
うまく動いているようなので完了ですね
Errors
bazel run
でいくつかエラーがでてハマったので対応方法を残しておきます。
no such package '@zlib//': The repository '@zlib' could not be resolved and referenced by '@com_google_protobuf//:protobuf'
WORKSPACEで以下を実行する。
load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") protobuf_deps()
com.google.devtools.build.lib.packages.BuildFileContainsErrorsException: error loading package '@io_bazel_rules_docker//toolchains/docker': Unable to load file '@bazel_skylib//:bzl_library.bzl': file doesn't exist
WORKSPACEで以下を実行する。
load("@io_bazel_rules_go//go:deps.bzl", "go_rules_dependencies") go_rules_dependencies()
no such package '@org_golang_google_grpc//': no such package '@bazel_gazelle_go_repository_cache//': gazelle could not find a Go SDK. Specify which one to use with gazelle_dependencies(go_sdk = "go_sdk").
WORKSPACEで以下を実行する。
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains") go_register_toolchains()
Failed to load Starlark extension '@rules_python//python:pip.bzl'. Cycle in the workspace file detected. This indicates that a repository is used prior to being defined.
WORKSPACEで以下を実行する。
load( "@io_bazel_rules_docker//repositories:repositories.bzl", container_repositories = "repositories", ) container_repositories()
no such package '@go_image_base//image': The repository '@go_image_base' could not be resolved
WORKSPACEで以下を実行する。
load( "@io_bazel_rules_docker//go:image.bzl", go_image_repos = "repositories", ) go_image_repos()
終わりに
デバッグのやり方まで書きたかったんですが skaffold を通していい感じに設定する方法がわからなかったので今回はここまでにしときます。
bazel でデバッグするだけなら -c dbg
でビルドしたものを delve
で解析する感じかな?
- Missing external sources when debugging with dlv · Issue #1708 · bazelbuild/rules_go
- dlv problem · Issue #993 · bazelbuild/rules_go
- Breakpoint on absolute path not working · Issue #1730 · go-delve/delve
- Expose test cases in test XML · Issue #236 · bazelbuild/rules_go
(資料だけおいて撤退
わかったら別の記事に上げます。もし知ってる方がいたら教えてもらえるとうれしいです。
その他の参考資料