Bazel で Go のソースコードをビルドするぞ

2020-04-26

Goの開発で Bazel を使っているので簡単にまとめてみました。

サンプル用のプロジェクトを作ったのでこれをもとに進めていこうと思います。

righ/bazel-sample-project - Github

利用する Bazel のバージョンは以下です。

bazel-sample-project/.bazelversion
3.0.0

備考

Bazelisk というツールを使うとプロジェクトで利用する Bazel のバージョンを固定できます。

今回使う開発環境は Mac Catalina 64bit です。

目次

Bazel

Bazelは Google が開発したOSSのビルドツールです。

キャッシュが効くため高速にビルドできたり、バイナリを作るだけでなくコンテナに乗せるところもやってくれるので 私達は開発に専念することができます。

Rule

Bazel はルールと呼ばれる拡張機能を用いることでさまざまな言語のビルドに対応できます。

今回は以下のルールを使います。

設定ファイル

Bazel で重要なのは WORKSPACEBUILD.bazel (BUILD) という2種類のテキストファイルです。

フォーマットは Python に似た Starlark と呼ばれる 言語で記述します。 Python の構文がすべて使えるわけではないので注意してください。例えば import 文は使えません。

WORKSPACE

WORKSPACE が配置されたディレクトリ配下は Bazel ではワークスペースとして認識されます。

このファイルにはプロジェクト全体に関する設定を書いていきますが、必要がなければ空でも構いません。

WORKSPACEでは主に以下のことを行います。

  • load と呼ばれる関数を使って必要なルールを読み込み

    • Python でいうなら from import のようなものと考えるとわかりやすい

  • 使用するルールを外部から読み込む http_archive, http_file の実行とそれらの設定を有効にする命令の実行

bazel-sample-project/WORKSPACE
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")

http_archive(
    name = "io_bazel_rules_go",
    urls = [
        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.22.2/rules_go-v0.22.2.tar.gz",
        "https://github.com/bazelbuild/rules_go/releases/download/v0.22.2/rules_go-v0.22.2.tar.gz",
    ],
    sha256 = "142dd33e38b563605f0d20e89d9ef9eda0fc3cb539a14be1bdb1350de2eda659",
)

load("@io_bazel_rules_go//go:deps.bzl", "go_rules_dependencies", "go_register_toolchains")

go_rules_dependencies()

go_register_toolchains()

http_archive(
    name = "bazel_gazelle",
    urls = [
        "https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/bazel-gazelle/releases/download/v0.20.0/bazel-gazelle-v0.20.0.tar.gz",
        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.20.0/bazel-gazelle-v0.20.0.tar.gz",
    ],
    sha256 = "d8c45ee70ec39a57e7a05e5027c32b1576cc7f16d9dd37135b0eddde45cf1b10",
)

load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")

gazelle_dependencies()

http_archive(
    name = "io_bazel_rules_docker",
    sha256 = "dc97fccceacd4c6be14e800b2a00693d5e8d07f69ee187babfd04a80a9f8e250",
    strip_prefix = "rules_docker-0.14.1",
    urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.14.1/rules_docker-v0.14.1.tar.gz"],
)

load(
    "@io_bazel_rules_docker//repositories:repositories.bzl",
    container_repositories = "repositories",
)

container_repositories()

load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps")

container_deps()

load(
    "@io_bazel_rules_docker//go:image.bzl",
    go_image_repos = "repositories",
)

go_image_repos()

http_archive(
    name = "com_google_protobuf",
    strip_prefix = "protobuf-master",
    urls = ["https://github.com/google/protobuf/archive/master.zip"],
)

load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")

protobuf_deps()

http_file(
    name = "grpc_health_probe",
    downloaded_file_path = "grpc_health_probe",
    urls = ["https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v0.3.2/grpc_health_probe-linux-amd64"],
)

go_repository(
    name = "co_honnef_go_tools",
    importpath = "honnef.co/go/tools",
    sum = "h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=",
    version = "v0.0.0-20190523083050-ea95bdfd59fc",
)

go_repository(
    name = "com_github_burntsushi_toml",
    importpath = "github.com/BurntSushi/toml",
    sum = "h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=",
    version = "v0.3.1",
)

go_repository(
    name = "com_github_census_instrumentation_opencensus_proto",
    importpath = "github.com/census-instrumentation/opencensus-proto",
    sum = "h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=",
    version = "v0.2.1",
)

go_repository(
    name = "com_github_client9_misspell",
    importpath = "github.com/client9/misspell",
    sum = "h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=",
    version = "v0.3.4",
)

go_repository(
    name = "com_github_cncf_udpa_go",
    importpath = "github.com/cncf/udpa/go",
    sum = "h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU=",
    version = "v0.0.0-20191209042840-269d4d468f6f",
)

go_repository(
    name = "com_github_envoyproxy_go_control_plane",
    importpath = "github.com/envoyproxy/go-control-plane",
    sum = "h1:rEvIZUSZ3fx39WIi3JkQqQBitGwpELBIYWeBVh6wn+E=",
    version = "v0.9.4",
)

go_repository(
    name = "com_github_envoyproxy_protoc_gen_validate",
    importpath = "github.com/envoyproxy/protoc-gen-validate",
    sum = "h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=",
    version = "v0.1.0",
)

go_repository(
    name = "com_github_golang_glog",
    importpath = "github.com/golang/glog",
    sum = "h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=",
    version = "v0.0.0-20160126235308-23def4e6c14b",
)

go_repository(
    name = "com_github_golang_mock",
    importpath = "github.com/golang/mock",
    sum = "h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=",
    version = "v1.1.1",
)

go_repository(
    name = "com_github_golang_protobuf",
    importpath = "github.com/golang/protobuf",
    sum = "h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=",
    version = "v1.3.5",
)

go_repository(
    name = "com_github_google_go_cmp",
    importpath = "github.com/google/go-cmp",
    sum = "h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=",
    version = "v0.2.0",
)

go_repository(
    name = "com_github_prometheus_client_model",
    importpath = "github.com/prometheus/client_model",
    sum = "h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=",
    version = "v0.0.0-20190812154241-14fe0d1b01d4",
)

go_repository(
    name = "com_google_cloud_go",
    importpath = "cloud.google.com/go",
    sum = "h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=",
    version = "v0.26.0",
)

go_repository(
    name = "org_golang_google_appengine",
    importpath = "google.golang.org/appengine",
    sum = "h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=",
    version = "v1.4.0",
)

go_repository(
    name = "org_golang_google_genproto",
    importpath = "google.golang.org/genproto",
    sum = "h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=",
    version = "v0.0.0-20190819201941-24fa4b261c55",
)

go_repository(
    name = "org_golang_google_grpc",
    importpath = "google.golang.org/grpc",
    sum = "h1:C1QC6KzgSiLyBabDi87BbjaGreoRgGUF5nOyvfrAZ1k=",
    version = "v1.28.1",
)

go_repository(
    name = "org_golang_x_crypto",
    importpath = "golang.org/x/crypto",
    sum = "h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=",
    version = "v0.0.0-20190308221718-c2843e01d9a2",
)

go_repository(
    name = "org_golang_x_exp",
    importpath = "golang.org/x/exp",
    sum = "h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=",
    version = "v0.0.0-20190121172915-509febef88a4",
)

go_repository(
    name = "org_golang_x_lint",
    importpath = "golang.org/x/lint",
    sum = "h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=",
    version = "v0.0.0-20190313153728-d0100b6bd8b3",
)

go_repository(
    name = "org_golang_x_net",
    importpath = "golang.org/x/net",
    sum = "h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=",
    version = "v0.0.0-20190311183353-d8887717615a",
)

go_repository(
    name = "org_golang_x_oauth2",
    importpath = "golang.org/x/oauth2",
    sum = "h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=",
    version = "v0.0.0-20180821212333-d2e6202438be",
)

go_repository(
    name = "org_golang_x_sync",
    importpath = "golang.org/x/sync",
    sum = "h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=",
    version = "v0.0.0-20190423024810-112230192c58",
)

go_repository(
    name = "org_golang_x_sys",
    importpath = "golang.org/x/sys",
    sum = "h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=",
    version = "v0.0.0-20200413165638-669c56c373c4",
)

go_repository(
    name = "org_golang_x_text",
    importpath = "golang.org/x/text",
    sum = "h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=",
    version = "v0.3.0",
)

go_repository(
    name = "org_golang_x_tools",
    importpath = "golang.org/x/tools",
    sum = "h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A=",
    version = "v0.0.0-20190524140312-2c0ae7006135",
)

備考

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 です。 必要なパッケージがここに指定されていないとビルドが通らないので注意です。

https://github.com/bazelbuild/rules_go/blob/master/go/core.rst#go-library

go_binary

mainパッケージに属するファイル群をビルドして実行ファイルを作ります。 実行するには bazel runbazel build を実行します。

https://github.com/bazelbuild/rules_go/blob/master/go/core.rst#go_binary

go_test

$ bazel test とすることでテストをビルドします。

ワークスペース内のすべてのテストを行う場合は bazel test --test_output=errors //... とします。 これは go test ./... と同じです。

https://github.com/bazelbuild/rules_go/blob/master/go/core.rst#go-test

gazelle

ビルドするのにルールなんていちいち覚えてられないし、 何よりファイルを作るたびにビルドの設定ファイルを手動で書き換えるのはめんどくさいですね。

この設定を自動生成するためのツールが gazelle です。

以下のようにセットアップします。

  • WORKSPACE で bazel_gazelle を取得

  • BUILD.bazel で gazelle を load して登録する

    bazel-sample-project/BUILD.bazel
    load("@bazel_gazelle//:def.bzl", "gazelle")
    
    # gazelle:prefix github.com/righ/go-sample-bazel-project
    gazelle(name = "gazelle")
    

これで 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 は 後のセクション で見ていくことにします。

備考

# 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.modGopkg.lock のようなパッケージのバージョンを管理するファイルを指定することで 依存パッケージのリポジトリ情報を WORKSPACE に書き出してくれます。

先程の WORKSPACE の下の方に書かれていた大量の go_repository はこれによるものだったのですね。

実際に使ってみる try

これまでの概要だけでは何が嬉しいのかイマイチ分かりづらいと思います。

以降のセクションではサンプルプロジェクトを動かして解説していきます。 最終的に k8s で動かすのを目標にします。

righ/bazel-sample-project

gateway サーバと echo サーバがあって、 gateway にきたリクエストを echo に gRPC で受け流してリクエストをそのままgatewayに返却するという簡単な構成です。

blockdiag clientgatewayechoHTTP RequestgRPC RequestgRPC ResponseHTTP Response

それぞれのプログラム自体は簡易ですが 別々のサーバなのでビルドはそれぞれで行わなければなりません。

bazel を使わずにビルドして実行してみます。

bazel-sample-project/gateway/main.go
package main

import (
	"context"
	"io/ioutil"
	"log"
	"net/http"
	"os"

	pb "github.com/righ/go-sample-bazel-project/protobuf"

	"google.golang.org/grpc"
)

func main() {
	echoHost := os.Getenv("ECHO_HOST")
	if echoHost == "" {
		echoHost = "localhost"
	}
	address := echoHost + ":8001"
	conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
	if err != nil {
		log.Fatalf("could not connect: %v", err)
	}
	defer conn.Close()

	cli := pb.NewEchoClient(conn)
	serve := func(w http.ResponseWriter, req *http.Request) {
		msg, err := ioutil.ReadAll(req.Body)
		if err != nil {
			log.Fatalf("could not read: %v", err)
			return
		}
		res, err := cli.Echo(context.Background(), &pb.Message{Message: string(msg)})
		if err != nil {
			log.Fatalf("could not receive: %v", err)
			return
		}
		_, err = w.Write([]byte(res.GetMessage()))
		if err != nil {
			log.Fatalf("could not write: %v", err)
		}
	}
	if err != nil {
		log.Fatalf("could not echo: %v", err)
	}
	http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
		w.Write([]byte("Hello world"))
	})
	http.HandleFunc("/echo", serve)
	http.ListenAndServe(":8000", nil)
}

bazel-sample-project/echo/main.go
package main

import (
	"context"
	"log"
	"net"

	pb "github.com/righ/go-sample-bazel-project/protobuf"

	"google.golang.org/grpc"
)

var conn pb.EchoClient

type message struct {
	Message string
}

type server struct {
	pb.UnimplementedEchoServer
}

func (s *server) Echo(ctx context.Context, in *pb.Message) (*pb.Message, error) {
	return in, nil
}

func main() {
	l, err := net.Listen("tcp", ":8001")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
		return
	}
	s := grpc.NewServer()
	pb.RegisterEchoServer(s, &server{})
	if err := s.Serve(l); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}
$ go run gateway/main.go
$ go run echo/main.go

localhost:8000 にアクセスしてから、以下のように /echoaaa を渡します。

./localhost8000.png

aaa が返ってきたので期待通りです。

bazel run

続いて bazel を使って動かしてみます。

bazel でビルドしたものをそのまま動かすには bazel run ターゲット のようにします。 ターゲットの名前部分(: の右側)を省略するとパッケージ名で補完されます。

以下は 実行対象の BUILD.bazel と登録された go_binary を呼び出す例です。

bazel-sample-project/gateway/BUILD.bazel
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
load("//:service.bzl", "service_image")

go_library(
    name = "go_default_library",
    srcs = ["main.go"],
    importpath = "github.com/righ/go-sample-bazel-project/gateway",
    visibility = ["//visibility:private"],
    deps = [
        "//protobuf:go_default_library",
        "@org_golang_google_grpc//:go_default_library",
    ],
)

go_binary(
    name = "gateway",
    embed = [":go_default_library"],
    visibility = ["//visibility:public"],
)

service_image("gateway")

bazel-sample-project/echo/BUILD.bazel
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
load("//:service.bzl", "service_image")

go_library(
    name = "go_default_library",
    srcs = ["main.go"],
    importpath = "github.com/righ/go-sample-bazel-project/echo",
    visibility = ["//visibility:public"],
    deps = [
        "//protobuf:go_default_library",
        "@org_golang_google_grpc//:go_default_library",
    ],
)

go_binary(
    name = "echo",
    embed = [":go_default_library"],
    visibility = ["//visibility:public"],
)

service_image("echo")
$ 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
$ 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 にアクセスしてから、以下のように /echoaaa を渡します。

./localhost8000.png

期待通りですがなんか普通のビルドよりも複雑になっている気がします。 --platforms=@io_bazel_rules_go//go/toolchain:darwin_amd64 ってなんなの?って思いましたよね。

実は最終的に Dockerのコンテナとして動かすことを想定しているため、Linuxのバイナリとして出力されるように .bazelrc を設定しているのです。

bazel-sample-project/.bazelrc
build --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64

ビルドだけなら動作しますがそのバイナリをMacの環境で動かそうとすると当然動かないのです。 (cannot execute binary file って言われる)

そこで私が使っている環境で動くように --platforms オプションでMac用バイナリを吐くように制御しています。 通常(Dockerで動かさない場合)は bazel run //xxxx だけでうまく動くはずです。

Container で動かす

コンテナを作るには (container_image と) go_image を使います。

が、今回は作成した BUILD.bazel には直接これらを書かず、service.bzl というマクロファイルに独自定義した service_image を介して利用しています。 わざわざ関数化した理由は単に複数回利用するからです。今回の例ではサービスは2つですが多くなると同じような処理を何回も書きたくありません。

マクロ関数は以下のようになっています。

bazel-sample-project/service.bzl
load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar")
load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_push")
load("@io_bazel_rules_docker//go:image.bzl", "go_image")

def service_image(name, **kwargs):
    pkg_tar(
        name = "tar",
        srcs = ["@grpc_health_probe//file"],
        mode = "0o755",
        package_dir = "/bin",
        visibility = ["//visibility:public"],
    )

    container_image(
        name = "base_image",
        base = "@go_image_base//image",
        tars = [":tar"],
        visibility = ["//visibility:public"],
    )

    go_image(
        name = "image",
        base = ":base_image",
        embed = ["//" + name + ":go_default_library"],
        goarch = "amd64",
        goos = "linux",
        visibility = ["//visibility:public"],
        **kwargs
    )

    container_push(
        name = "push_to_dockerhub",
        format = "Docker",
        image = ":image",
        registry = "docker.io",
        repository = "righm9/" + name,
        tag = "{BUILD_TIMESTAMP}",
    )

    container_push(
        name = "push_to_gcr",
        format = "Docker",
        image = ":image",
        registry = "gcr.io",
        repository = "righm9/" + name,
        tag = "latest",
    )

このマクロ関数は サービス名を受け取り、そのサービス名を元にビルドしたバイナリが指定されます。

実はただコンテナを作るだけであれば go_image だけでよいのですが、コンテナを tar 形式のファイルとして出力するために container_image で作ったイメージをベースに go_image を呼び出しています。 そして container_imagetars 引数に 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 引数で辞書形式で受け取れるが全環境で固定になってしまうのでホストから渡した環境変数を読みたい

というわけで、ビルドしたコンテナを docker コマンドで呼び出すようにしてみます。

docker network create test-networktest-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/ にアクセスできました。

./localhost8000.png

なんとか動いたようです。

bazel から直接呼び出せているわけではないのでなんか気持ち悪いですが、今回のゴールはここじゃないので目を瞑ってください。

備考

go_imagename, base, deps, binary, layers 以外のキーワード引数はすべて go_binary 関数に引き継がれます。

https://github.com/bazelbuild/rules_docker/blob/master/go/image.bzl

Push する

Bazelを使って任意のコンテナレジストリに Push することができます。

先程 container_push を書いたのはこれが理由です。

備考

container_push には go_image の名前を指定します。

container_image の名前を指定して Push しても イメージの CMDがなく No command specified と言われます。

今回のアップロード先は Docker Hub です。 予めログインを済ませて、リポジトリを作成しておく必要があります。

ここはレジストリによって操作が異なるのであんまり詳しくやりませんが、 私の場合は以下のようなリポジトリがある状態です。

repos.png

今回 DockerHub に Push するための、 push_to_dockerhubpush_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 でビルドしたコンテナを指定することができます。

以下のような設定ファイルを用意しました。

bazel-sample-project/skaffold.yaml
apiVersion: skaffold/v2beta1
kind: Config
deploy:
  kustomize:
    paths:
    - ./kube/skaffold
profiles:
- name: dev
  build:
    artifacts:
    - image: righm9/echo
      bazel:
        target: //echo:image.tar
    - image: righm9/gateway
      bazel:
        target: //gateway:image.tar
    local:
      push: false
      useBuildkit: true
  activation:
  - command: dev

artifacts の bazel.target にコンテナを指定できますが、 tar 形式である必要があります。 先程のマクロ関数で pkg_tar を使っていたのはこれが理由だったのです。

Kubernetes の解説になってしまいますが、 deploy.kustomize には Kustomization の定義を指定できるので、 kube/skaffold を指定しています。 これにより kube/skaffold/kustomization.yaml が読み込まれます。

  • 📁 kube
    • 📁 services
      • 📁 echo
        • 📁 base
          • 🗒 deployment.yaml
          • 🗒 kustomization.yaml
          • 🗒 service.yaml
        • 📁 overlays
          • 📁 local
            • 🗒 kustomization.yaml
          • 📁 production
            • 🗒 kustomization.yaml
            • 🗒 patch.yaml
      • 📁 gateway
        • 📁 base
          • 🗒 deployment.yaml
          • 🗒 kustomization.yaml
          • 🗒 service.yaml
        • 📁 overlays
          • 📁 local
            • 🗒 kustomization.yaml
            • 🗒 service.yaml
          • 📁 production
            • 🗒 kustomization.yaml
            • 🗒 patch.yaml
            • 🗒 service.yaml
    • 📁 skaffold
      • 🗒 kustomization.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo
spec:
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: echo
  template:
    metadata:
      labels:
        app: echo
      name: echo
    spec:
      containers:
      - image: righm9/echo
        name: echo
        ports:
          - containerPort: 8001
            name: grpc
            protocol: TCP
        resources:
          limits:
            cpu: 50m
            memory: 200Mi
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
apiVersion: v1
kind: Service
metadata:
  name: echo
spec:
  ports:
    - name: grpc
      port: 8001
      protocol: TCP
      targetPort: 8001
  selector:
    app: echo
  type: ClusterIP
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
patchesJson6902:
- path: patch.yaml
  target:
    kind: Deployment
    name: echo
    group: apps
    version: v1
- op: replace
  path: /spec/template/spec/containers/0/image
  value: gcr.io/righm9/echo:latest
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gateway
spec:
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: gateway
  template:
    metadata:
      labels:
        app: gateway
      name: gateway
    spec:
      containers:
      - image: righm9/gateway
        name: gateway
        env:
          - name: ECHO_HOST
            value: echo.default.svc.cluster.local
        ports:
          - containerPort: 8000
            name: grpc
            protocol: TCP
        resources:
          limits:
            cpu: 50m
            memory: 200Mi
      
      - image: nginx:1.17
        name: nginx
        ports:
          - containerPort: 80
            name: http
            protocol: TCP
        resources:
          limits:
            cpu: 50m
            memory: 200Mi
        volumeMounts:
          - name: nginx-conf
            mountPath: /etc/nginx/nginx.conf
            subPath: nginx.conf
      volumes:
        - name: nginx-conf
          configMap:
            name: nginx-conf

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
data:
  nginx.conf: |
    user nginx;
    worker_processes  3;
    error_log  /var/log/nginx/error.log;
    events {
      worker_connections  10240;
    }
    http {
      log_format  main
              'remote_addr:$remote_addr\t'
              'time_local:$time_local\t'
              'method:$request_method\t'
              'uri:$request_uri\t'
              'host:$host\t'
              'status:$status\t'
              'bytes_sent:$body_bytes_sent\t'
              'referer:$http_referer\t'
              'useragent:$http_user_agent\t'
              'forwardedfor:$http_x_forwarded_for\t'
              'request_time:$request_time';
      access_log	/var/log/nginx/access.log main;
      server {
          listen       80;
          server_name  _;
          location / {
              root   html;
              proxy_pass http://localhost:8000;
          }
      }
    }
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
apiVersion: v1
kind: Service
metadata:
  name: gateway
spec:
  ports:
    - name: app
      port: 8000
      protocol: TCP
      targetPort: 8000
    - name: server
      port: 80
      protocol: TCP
      targetPort: 80
  selector:
    app: gateway
  type: ClusterIP
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
patchesStrategicMerge:
- service.yaml
apiVersion: v1
kind: Service
metadata:
  name: gateway
spec:
  type: LoadBalancer
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
patchesStrategicMerge:
- service.yaml
patchesJson6902:
- path: patch.yaml
  target:
    kind: Deployment
    name: gateway
    group: apps
    version: v1
- op: replace
  path: /spec/template/spec/containers/0/image
  value: gcr.io/righm9/gateway:latest
apiVersion: v1
kind: Service
metadata:
  name: gateway
spec:
  type: LoadBalancer
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
  - ../services/echo/overlays/local
  - ../services/gateway/overlays/local

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 で稼働させています。

blockdiag clientNginxgatewayechoHTTP RequestHTTP RequestgRPC RequestgRPC ResponseHTTP ResponseHTTP Response

Nginxが待ち受けている ポート 80番 にアクセスしてみます。

./localhost80.png

うまく動いているようなので完了ですね

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 で解析する感じかな?

(資料だけおいて撤退

わかったら別の記事に上げます。もし知ってる方がいたら教えてもらえるとうれしいです。

その他の参考資料