GoのプロジェクトのDocker imageをDockerを使わずにBazelだけで作成する

# はじめに

この記事では[Go](https://golang.org/)のプロジェクトに対して[Bazel](https://bazel.build/)を用いることでDockerのimageを作成する方法をハンズオン形式で紹介する．

[前回の記事](https://qiita.com/akrisn/items/ebb77073824c3d6fcde1)で、Bazelによってbuildできるようにした[プロジェクト](https://github.com/stn/grpc-helloworld-bazel/tree/v1.0.1)を、ここでは再利用する。
このプロジェクトにはprotobufが含まれているが、それは今回の話では本質ではなく、protobufを使用しないプロジェクトにおいても以下の解説は同じである。

# BazelによるDocker imageの作成

## WORKSPACE

Bazelを用いるプロジェクトではトップディレクトリーにWORKSPACEというファイルがある。前回、このファイルにおいてGoのための設定を行ったが、さらに、Docker imageを作成するためのruleを読み込むため、以下の記述を追加する。

```python:WORKSPACE
# download Docker rules
http_archive(
    name = "io_bazel_rules_docker",
    sha256 = "29d109605e0d6f9c892584f07275b8c9260803bf0c6fcb7de2623b2bedc910bd",
    strip_prefix = "rules_docker-0.5.1",
    urls = ["https://github.com/bazelbuild/rules_docker/archive/v0.5.1.tar.gz"],
)

# load Docker
load(
    "@io_bazel_rules_docker//container:container.bzl",
    "container_pull",
    container_repositories = "repositories",
)

container_repositories()

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

_go_image_repos()
```

## BUILD

次に `greeter_server/BUILD.bazel` の最後に、以下の設定を加える。これがDocker imageを作成するための記述となる。

```python:greeter_server/BUILD.bazel
# Docker
load("@io_bazel_rules_docker//go:image.bzl", "go_image")

go_image(
    name = "greeter_server_image",
    embed = [":go_default_library"],
    importpath = "google.golang.org/grpc/examples/helloworld/greeter_server",
    goarch = "amd64",
    goos = "linux",
    pure = "on",
)
```

## BazelによるDocker imageの作成

`bazel`を実行することでDocker imageが作成される。

```bash
$ bazel build //greeter_server:greeter_server_image
```

### Mac / Windowsでのcross-compile

Linuxでは、問題ないが、MacやWindowsでは次のようなエラーが出る。Docker imageはLinuxなので、buildではcross-compileを指定する必要がある。

```
ERROR: /private/var/tmp/_bazel_stn/56853cb50709ed59d05b544de7cfcee8/external/org_golang_google_grpc/internal/channelz/BUILD.bazel:3:1: GoCompile external/org_golang_google_grpc/internal/channelz/linux_amd64_pure_stripped/go_default_library%/google.golang.org/grpc/internal/channelz.a failed (Exit 1)
GoCompile: missing strict dependencies:
	/private/var/tmp/_bazel_stn/56853cb50709ed59d05b544de7cfcee8/sandbox/darwin-sandbox/50/execroot/__main__/external/org_golang_google_grpc/internal/channelz/types_linux.go: import of "golang.org/x/sys/unix"
Known dependencies are:
	google.golang.org/grpc/connectivity
	google.golang.org/grpc/credentials
	google.golang.org/grpc/grpclog
Check that imports in Go sources match importpath attributes in deps.
Target //greeter_server:greeter_server_image failed to build
Use --verbose_failures to see the command lines of failed build steps.
INFO: Elapsed time: 37.508s, Critical Path: 16.71s
INFO: 48 processes: 48 darwin-sandbox.
FAILED: Build did NOT complete successfully
```

そこで、 `--platforms=@io_bazel_rules_go//go/toolchain:linux_amd64` を指定して再度buildする。

```bash
$ bazel build --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 //greeter_server:greeter_server_image
```

今度は、無事、imageが作成された。

```bash
INFO: Build options have changed, discarding analysis cache.
INFO: Analysed target //greeter_server:greeter_server_image (1 packages loaded).
INFO: Found 1 target...
Target //greeter_server:greeter_server_image up-to-date:
  bazel-bin/greeter_server/greeter_server_image-layer.tar
INFO: Elapsed time: 27.162s, Critical Path: 20.21s
INFO: 74 processes: 74 darwin-sandbox.
INFO: Build completed successfully, 79 total actions
```

ここまでのステップで、Dockerを使っていないことに気づいただろうか。この時点では、Docker daemonを起動しておく必要はなく、Dockerの入っていないシステムでもimageの作成はBazelだけでできる。


## Dockerでの実行

作成されたimageは　`bazel-bin/greeter_server/greeter_server_image-layer.tar` とtar形式になっている。このimageを実際に実行して動作を確認してみる。

imageを読み込むには、

```
$ docker import bazel-bin/greeter_server/greeter_server_image-layer.tar イメージ名:tag
```

でもできるが、先程の`bazel build`を`bazel run`に変えて、

```
$ bazel run --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 //greeter_server:greeter_server_image
```

とすればよい。 `docker ps`によって`bazel/greeter_server:greeter_server_image`というimageが読み込まれ`greeter_server`が実行されていることが分かる。

```
$ docker ps
CONTAINER ID        IMAGE                                       COMMAND                  CREATED             STATUS              PORTS               NAMES
4dfd4b02596b        bazel/greeter_server:greeter_server_image   "/app/greeter_server…"   5 seconds ago       Up 4 seconds                            tender_turing
```

また、走らせる必要はなく、imageを登録するだけなら、コマンドの最後に　`-- --norun`をつければよい。


### おまけ： portの話

Dockerのimageを作成して、実行するという点ではここまでだが、`greeter_server`に`bazel run //greeter_client`で接続を試みると失敗する。

```
$ bazel run //greeter_client
...
2018/10/14 08:37:14 could not greet: rpc error: code = Unavailable desc = all SubConns are in TransientFailure, latest connection error: connection error: desc = "transport: Error while dialing dial tcp [::1]:50051: connect: connection refused"
```

そう、ポートが開いていない。そこで、

```
$ docker run -p 50051:50051 bazel/greeter_server:greeter_server_image
```

とすることで、接続ができるようになる。

```
$ bazel run //greeter_client
...
2018/10/14 08:38:53 Greeting: Hello world
```


# まとめ

Bazelを用いて、GoのプロジェクトのDocker imageを作成することができた。

ここに書かれていないことなども調べたいときは、こちらを参考にしてください。
https://github.com/bazelbuild/rules_docker#go_image

また、ここまでの作業を行った結果をGithubにあげておきました。
https://github.com/stn/grpc-helloworld-bazel/tree/docker