Carpe Diem

備忘録

Bazelを使ってみる その3(docker imageのビルド)

概要

Bazel解説第3弾です。

Bazelを使ってみる その1(Goのビルド) - Carpe Diem
Bazelを使ってみる その2(protobufのビルド) - Carpe Diem

今回はDocker imageをビルドしてみます。

環境

  • Bazel v4.2.2

前提知識

通常docker imageを作成する際はDockerfileを使いますが、BazelではDockerfileを使わずにビルドします。
Dockerfile内で複雑にプロビジョニングする場合はDockerfileの方が直感的に書けてオススメですが、単純にバイナリを置くだけであればBazelも十分選択肢になります。

またBazelでバイナリを生成した場合はSandbox内に置かれるため、そこの連携という意味でもBazelに閉じる方がシンプルに用意できます。

container_imageとgo_image

Bazelにはcontainer_image*_image(go_image, java_imageなど)といった各言語のルールが用意されています。

container_imageの場合

まず先にcontainer_imageのやり方を説明します。

WORKSPACE

GitHub - bazelbuild/rules_docker: Rules for building and handling Docker images with Bazel の通りにWORKSPACEに以下を追記します。

# Docker
http_archive(
    name = "io_bazel_rules_docker",
    sha256 = "59536e6ae64359b716ba9c46c39183403b01eabfbd57578e84398b4829ca499a",
    strip_prefix = "rules_docker-0.22.0",
    urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.22.0/rules_docker-v0.22.0.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//container:container.bzl",
    "container_pull",
)

container_pull(
    name = "alpine_linux_amd64",
    registry = "index.docker.io",
    repository = "library/alpine",
    tag = "3.15",
)

io_bazel_rules_dockerrelease noteを見て最新版にしてください。

ベースイメージとして今回はalpineを取得しています。

container_pull(
    name = "alpine_linux_amd64",
    registry = "index.docker.io",
    repository = "library/alpine",
    tag = "3.15",
)

container_pull(
    name = "distroless_linux_amd64",
    registry = "gcr.io",
    repository = "distroless/base",
    tag = "latest",
)

のように複数用意することも可能です。

go_binaryのあるBUILD.bazel

次にgo_binary()のあるBUILD.bazelに以下を追記します。

load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_push")

go_library(
    name = "cmd_lib",
    srcs = ["main.go"],
    importpath = "github.com/jun06t/bazel-sample/docker/cmd",
    visibility = ["//visibility:private"],
)

go_binary(
    name = "cmd",
    embed = [":cmd_lib"],
    visibility = ["//visibility:public"],
)

+container_image(
+    name = "image",
+    base = "@alpine_linux_amd64//image",
+    entrypoint = ["/cmd"],
+    files = [":cmd"],
+    repository = "jun06t",
+)

+container_push(
+    name = "image-push",
+    format = "Docker",
+    image = ":image",
+    registry = "index.docker.io",
+    repository = "jun06t/bazel-sample-cmd",
+    tag = "latest",
+)

container_imageでdocker image用のtarを生成し、[container_push]で指定したDocker Registryにプッシュします。 (https://github.com/bazelbuild/rules_docker/blob/master/docs/container.md#container_push)

ポイント

ポイントは以下です。

  • container_image()entrypointにはgo_binary()nameを指定
  • container_image()filesにはgo_binary()nameを指定
  • container_push()imageにはcontainer_image()nameを指定

.bazelrc

alpine(=linux)環境なので、goもクロスコンパイルするようにします。

.bazelrcファイルを用意して以下を追記します。

build --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64

動作確認

$ bazel build //cmd:image
INFO: Analyzed target //cmd:image (1 packages loaded, 130 targets configured).
INFO: Found 1 target...
Target //cmd:image up-to-date:
  bazel-bin/cmd/image-layer.tar
INFO: Elapsed time: 75.432s, Critical Path: 44.93s
INFO: 6 processes: 4 internal, 2 darwin-sandbox.
INFO: Build completed successfully, 6 total actions

すると.tarが生成されます。

$ bazel run //cmd:image-push

するとdocker imageをpushします。

f:id:quoll00:20211224001822p:plain

Docker Hub

認証は?

pushする環境で~/.docker/config.jsonといったDocker Registryへの認証情報があればbazel側で特に意識する必要はありません。

CircleCIでGCPのContainer Registryにpushしたい、といった場合はcircleci/gcp-gcrといったOrbを使えば認証されてpushできるようになります。

go_imageの場合

次にgo_imageでのやり方を説明します。

WORKSPACE

WORKSPACEに以下を追記します。先程よりはやや少ないです。
go_imageはデフォルトだとdistroless/baseをベースイメージとして使います。

# Docker
http_archive(
    name = "io_bazel_rules_docker",
    sha256 = "59536e6ae64359b716ba9c46c39183403b01eabfbd57578e84398b4829ca499a",
    strip_prefix = "rules_docker-0.22.0",
    urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.22.0/rules_docker-v0.22.0.tar.gz"],
)

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

container_repositories()

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

_go_image_repos()

go_binaryのあるBUILD.bazel

次にgo_binary(go_libraryがあればそちらメイン)go_imageにリネームするような形で以下のように修正します。

load("@io_bazel_rules_go//go:def.bzl", "go_binary")
load("@io_bazel_rules_docker//go:image.bzl", "go_image")
load("@io_bazel_rules_docker//container:container.bzl", "container_push")

go_image(
    name = "image",
    srcs = ["main.go"],
    importpath = "github.com/jun06t/bazel-sample/docker-go-image/cmd",
    visibility = ["//visibility:private"],
)

container_push(
    name = "image-push",
    format = "Docker",
    image = ":image",
    registry = "index.docker.io",
    repository = "jun06t/bazel-sample-cmd",
    tag = "go_image",
)

細かい設定がしたい場合はgo_image-custom-baseを見てください。

.bazelrc

distroless(=linux)環境なので、goもクロスコンパイルするようにします。

.bazelrcファイルを用意して以下を追記します。

build --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64

動作確認

$ bazel build //cmd:image
Starting local Bazel server and connecting to it...
INFO: Analyzed target //cmd:image (77 packages loaded, 8213 targets configured).
INFO: Found 1 target...
Target //cmd:image up-to-date:
  bazel-bin/cmd/image-layer.tar
INFO: Elapsed time: 62.680s, Critical Path: 44.92s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action

すると.tarが生成されます。

$ bazel run //cmd:image-push

するとdocker imageをpushします。

f:id:quoll00:20211224004721p:plain

Docker Hub

その他

Tipsや検証する上で遭遇したエラーなど

docker imageをホスト環境に用意したい

bazel buildするとdocker imageの.tarが生成されますが、それをローカルでdocker imageとして扱いたい場合です。

docker loadで.tarをインポートする方法とbazelコマンドの2通りありますが、簡単なのはbazel runです。

$ bazel run //cmd:image
INFO: Analyzed target //cmd:image (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //cmd:image up-to-date:
  bazel-bin/cmd/image-layer.tar
INFO: Elapsed time: 0.462s, Critical Path: 0.01s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
Loaded image ID: sha256:9c18e5c241760317a4deecde7e63a7f2dcdc2d6b43b16241130a02f2aea533b3
Tagging 9c18e5c241760317a4deecde7e63a7f2dcdc2d6b43b16241130a02f2aea533b3 as jun06t/cmd:image

すると以下のようにホスト環境に用意されます。

$ docker images
...
jun06t/cmd     image     9c18e5c24176   52 years ago    26.5MB

欠点として作成日が大昔になる点と、container_pushと違ってrepositoryやtagが柔軟にいじれません。

image tagを外から注入したい

tagの箇所をこのように変数にして、

container_push(
    name = "image-push",
    format = "Docker",
    image = ":image",
    registry = "index.docker.io",
    repository = "jun06t/bazel-sample-cmd",
    tag = "$(IMAGE_TAG)",
)

bazelコマンド時に指定するようにします。

$ bazel run --define=IMAGE_TAG=v1.0.0 //cmd:image-push

fail: go_register_toolchains: version must be a string

WORKSPACEでcontainer_deps()などを以下の箇所に挿入したところ、

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

go_rules_dependencies()

// ここに入れるとエラー

go_register_toolchains(version = "1.17.2")

gazelle_dependencies()

次のようなエラーが発生しました。

ERROR: Traceback (most recent call last):
        File "/Users/jun06t/.go/src/github.com/jun06t/bazel-sample/docker/WORKSPACE", line 45, column 15, in <toplevel>
                container_deps()
        File "/private/var/tmp/_bazel_a13156/ee39864ec5fa85be79e557bb2ef72cc8/external/io_bazel_rules_docker/repositories/deps.bzl", line 32, column 12, in deps
                go_deps()
        File "/private/var/tmp/_bazel_a13156/ee39864ec5fa85be79e557bb2ef72cc8/external/io_bazel_rules_docker/repositories/go_repositories.bzl", line 34, column 27, in go_deps
                go_register_toolchains()
        File "/private/var/tmp/_bazel_a13156/ee39864ec5fa85be79e557bb2ef72cc8/external/io_bazel_rules_go/go/private/sdk.bzl", line 460, column 17, in go_register_toolchains
                fail('go_register_toolchains: version must be a string like "1.15.5" or "host"')
Error in fail: go_register_toolchains: version must be a string like "1.15.5" or "host"

go_register_toolchains(version = "1.17.2")とあるのになんでだろ、と思いつつも先にgo_register_toolchains()を呼んでからcontainer_deps()等を呼ぶようにしたところ直りました。

CI上でエラー

CircleCI上で実行したところ以下のエラーが出ました。

Error occurred while attempting to use the default Python toolchain (@rules_python//python:autodetecting_toolchain).
According to '/usr/bin/python -V', version is 'Python 2.7.16', but we need version 3. PATH is:

/root/.cache/bazelisk/downloads/bazelbuild/bazel-4.2.2-linux-x86_64/bin:/root/google-cloud-sdk/bin:/home/circleci/.local/bin:/home/circleci/bin:/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

これはエラーの通りpython3をインストールしたところ解消しました。

apt-get update
apt-get -y install python3 python3-setuptools python3-pip

exec format error

go_binaryをクロスコンパイルしてdocker imageと環境を合わせておかないと以下のエラーがでます。

$ docker run --rm jun06t/cmd:image
standard_init_linux.go:228: exec user process caused: exec format error

.bazelrcでビルド時のパラメータを設定しておきましょう。

サンプルコード

今回のコードは以下で全体を見ることができます。

container_image

github.com

go_image

github.com

まとめ

Bazelを使ったdocker buildとpush方法について説明しました。

container_image()go_image()かでいうと、

  • 成果物を意識しないで済む
  • 覚えるパラメータが最低限で済む

といった点からcontainer_image()の方が使いやすいかなと思います。

参考