BazelでGoプロジェクトのビルド。Gazelleのgo_repositoryで外部ライブラリの依存とBazelのgo_testでテスト。

BazelをつかったGoプロジェクトのビルドをまとめている。前回のエントリではバイナリのビルドとDockerイメージのビルドをまとめた。

blog.soushi.me

今回は外部ライブラリをGoプロジェクトに依存させる方法とテストの方法をまとめていく。

Gazelleのgo_repositoryで外部のライブラリを依存させる

外部ライブラリの依存はGazelleのgo_repositoryをつかう。

bazel-gazelle/repository.rst at master · bazelbuild/bazel-gazelle · GitHub

依存させるライブラリはDIツールのwireを選んだ。

github.com

WORKSPACE

WORKSPACEにgo_repositoryを有効にしてruleを追加する。

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

gazelle_dependencies()

go_repository(
    name = "com_github_google_cloud",
    commit = "2152f209f3c907645f7ebdcacdb2c18cd89e6fa8",
    importpath = "github.com/google/go-cloud",
)

nameはWORKSPACE内の固有をつける。commitはgoogle/go-cloud 0.5.0 バージョンのハッシュ値を設定する。import_pathはGoコードでwireを利用するときにimport文のパスをセットする。

wireをつかってDIするコードを用意する。

簡易的なstruceのインスタンスをDIするコードを用意する。

# pkg/public_go/usecase/greet_usecase.go

package usecase

import (
        "github.com/google/go-cloud/wire"
)

var GreetUsecaseSet = wire.NewSet(ProvideUseCase)

type GreetUsecase struct {
    Msg string
}

func ProvideUseCase(msg string) GreetUsecase {
    return GreetUsecase{
        Msg: msg,
    }
}

上記のコードを追加してgazelleコマンドを実行する。

(bazel-multiprojects) $ bazel run gazelle

コマンドを実行するとGazelleはWORKSPACEに追加したgo_repositoryの依存とgreet_usecase.goのwireの利用を理解して次のようなBUILD.bazelファイルを生成してくれる。

# pkg/public_go/usecase/BUILD.bazel


load("@io_bazel_rules_go//go:def.bzl", "go_library")

go_library(
    name = "go_default_library",
    srcs = ["greet_usecase.go"],
    importpath = "github.com/soushin/bazel-multiprojects/pkg/public_go/usecase",
    visibility = ["//visibility:public"],
    deps = ["@com_github_google_cloud//wire:go_default_library"],
)

greet_usecase.goをソースにしたライブラリのruleが定義されている。depsにはWORKSPACEで定義したcom_github_google_cloudのレポジトリが参照されている。

Gazelleの魅力

Bazel + Gazelleの魅力はこのようにWORKSPACEで定義した依存とコードの中間の役割のBUILDファイルを自動で生成してくれるところだ。

最終的にはmainパッケージからusecaseパッケージを参照することになる。wireで生成したwire_gen.goコードは次のようになる。

# pkg/public_go/wire_gen.go

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

import (
    "context"
    "github.com/soushin/bazel-multiprojects/pkg/public_go/usecase"
)

// Injectors from injector.go:

func initializeGreetUsecase(ctx context.Context, greet string) (usecase.GreetUsecase, error) {
    greetUsecase := usecase.ProvideUseCase(greet)
    return greetUsecase, nil
}

mainパッケージにusecaseが依存した状態で再度、gazelleコマンドを実行するとmainパッケージのBUILD.bazelは次のような差分になる。

diff --git a/pkg/public_go/BUILD.bazel b/pkg/public_go/BUILD.bazel
index 06f7a04..b50739a 100644
--- a/pkg/public_go/BUILD.bazel
+++ b/pkg/public_go/BUILD.bazel
@@ -2,10 +2,16 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")

 go_library(
     name = "go_default_library",
-    srcs = ["main.go"],
+    srcs = [
+        "main.go",
+        "wire_gen.go",
+    ],
     importpath = "github.com/soushin/bazel-multiprojects/pkg/public_go",
     visibility = ["//visibility:private"],
-    deps = ["//pkg/common_go/util:go_default_library"],
+    deps = [
+        "//pkg/common_go/util:go_default_library",
+        "//pkg/public_go/usecase:go_default_library",
+    ],
 )

 go_binary(

depsに//pkg/public_go/usecase:go_default_libraryが追加されている。usecaseパッケージに定義しているgo_libraryを参照している。

Gazelleは各パッケージの依存とWORKSPACEの外部依存を理解してBUILDファイルを生成するだけでなくパッケージ間の依存も理解をしてくれる。

Bazelのgo_testでテスト

テストのビルドもBazelで行う。

次のようなstringのテストコードを用意する。

# pkg/common_go/util/string_test.go

package util

import "testing"

func TestAdd(t *testing.T) {
    actual := Add("test")
    expected := "test - built by Bazel!"
    if actual != expected {
        t.Errorf("invalid text: got %s want %s", actual, expected)
    }
}

このテストコードを配置した状態でgazelleコマンドを実行するとutilパッケージのBUILD.bazelは次のような差分になる。

diff --git a/pkg/common_go/util/BUILD.bazel b/pkg/common_go/util/BUILD.bazel
index ce90025..d48f3ca 100644
--- a/pkg/common_go/util/BUILD.bazel
+++ b/pkg/common_go/util/BUILD.bazel
@@ -1,4 +1,4 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

 go_library(
     name = "go_default_library",
@@ -6,3 +6,9 @@ go_library(
     importpath = "github.com/soushin/bazel-multiprojects/pkg/common_go/util",
     visibility = ["//visibility:public"],
 )
+
+go_test(
+    name = "go_default_test",
+    srcs = ["string_test.go"],
+    embed = [":go_default_library"],
+)

Gazelleがパッケージ内のテストコードを解釈してgo_testのruleを追加してくれる。このgo_testは次のように実行する

(bazel-multiprojects) $ bazel run //pkg/common_go/util:go_default_test 
INFO: Analysed target //pkg/common_go/util:go_default_test (4 packages loaded).
INFO: Found 1 target...
Target //pkg/common_go/util:go_default_test up-to-date:
  bazel-bin/pkg/common_go/util/darwin_amd64_stripped/go_default_test
INFO: Elapsed time: 0.244s, Critical Path: 0.01s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
exec ${PAGER:-/usr/bin/less} "$0" || exit 1
Executing tests from //pkg/common_go/util:go_default_test
-----------------------------------------------------------------------------
PASS

まとめ

  • Gazelleを使ったGoプロジェクトの外部ライブラリ依存とテストの方法をまとめた。
  • GoプロジェクトにおいてはGazelleの働きが素晴らしくビルドルールの追加からビルドまでストレスフリーで開発が行える印象。
  • KotlinプロジェクトにおいてもGazelleが生成するBUILDファイルに沿ってルールの追加をしていくと捗りそう。

コード

github.com

GoとKotlinのマルチプロジェクトをBazelでビルドする

Googleが開発するビルドツールのBazelを試していく。

Bazel - a fast, scalable, multi-language and extensible build system" - Bazel

モチベーション

GoやKotlinでつくるマイクロサービス開発は1つのサービスに1つのレポジトリで行ってきた経緯がある。またCIではDockerfileをレポジトリに内包することでビルド時にDockerイメージの錬成を行いRegistryへのPushを行う。GoやKotlinのビルド環境が異なることやイメージのビルドがDockerfileドリブンであるためサービス対レポジトリが1:1の関係であった。

Bazelは複数言語のビルドをサポートする。またDockerfileを使わずにイメージをビルドができる。

1つのレポジトリにGoとKotlinのマイクロサービスを構造化したプロジェクトのビルドをBazelで実現できるのか?を検証することがエントリのモチベーションである。

ゴール

簡易的なHello Worldを出力するアプリケーションをGoとKotlinでつくる。また言語ごとにアプリケーションから参照するライブラリを作りマルチプロジェクトの構成も取り入れる。そしてBazelでアプリケーションをDockerイメージにビルドするまでをゴールと定義する。

プロジェクト構成

pkg以下にアプリケーションを配置する構成を定義する。

./
├── README.md
└── pkg
    ├── common_go
    │   └── util
    │       └── string.go
    ├── common_kt
    │   └── util
    │       └── string.kt
    ├── public_go
    │   └── main.go
    └── public_kt
        └── main.kt

public_gopublic_ktはHello Worldを出力するアプリケーションを配置する。common_gocommon_ktは各言語が参照するライブラリを配置する。

Kotlin

まずはKotlinからビルドしていく。Bazelのインストールは調べるとキャッチアップできるので割愛する。

BazelはWORKSPACEというファイルをプロジェクトの最上位に配置する。そしてBUILDというファイルを各アプリケーションに配置することでビルドを行う。

WORKSPACE

WORKSPACEはビルド全般の設定を定義するものと理解すると分かりやすい。

# ./WORKSPACE

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

rules_kotlin_version = "87bd13f91d166a8070e9bbfbb0861f6f76435e7a"

http_archive(
    name = "io_bazel_rules_kotlin",
    urls = ["https://github.com/bazelbuild/rules_kotlin/archive/%s.zip" % rules_kotlin_version],
    type = "zip",
    strip_prefix = "rules_kotlin-%s" % rules_kotlin_version
)

load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kotlin_repositories", "kt_register_toolchains")

kotlin_repositories()

kt_register_toolchains()

pkgと同じ階層に上記のWORKSPACEファイルを配置する。Bazelはrulesという各言語ごとのビルド定義を用意している。Kotlinは次のレポジトリから参照できる。

github.com

WORKSPACEで行っていることはbazel_toolsからhttp_archiveを読み込みio_bazel_rules_kotlinの名前でrulesをインストールしている。そしてkotlin_repositorieskt_register_toolchainsを有効にしている。

Bazelの良いところはWORKSPACEに定義したビルド設定を各自が実行すればビルド環境を簡単に再現できることである。リリース前のrulesを試したければrules_kotlin_versionを個人で更新して試せば良い。他者に影響することなくローカルにインストールできる。

BUILD

次に各アプリケーションを見ていく。pkg/public_kt/main.ktはHello Worldを出力する簡単なコードで動いている。

package com.github.soushin.multipojects.publickt

import com.github.soushin.multipojects.commonkt.util.add

fun main(args : Array<String>) {
    println("Hello kotlin!".add())
}

Stringクラスの拡張関数であるadd()をつかっている。この拡張関数はpkg/common_kt/util/string.ktのライブラリを参照している。このようなマルチプロジェクトな仕組みをBazelで実現する方法を整理する。


まずはcommon_kt/util/string.ktをライブラリとしてビルドする方法をまとめる。

# ./pkg/common_kt/BUILD

package(default_visibility = ["//visibility:public"])

load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_jvm_library")

kt_jvm_library(
    name = "common_kt_lib",
    srcs = ["util/string.kt"],
)

このBUILDファイルでadd()を含むライブラリがビルドできる。kt_jvm_libraryを有効にしてcommon_kt_libの名前でビルドしている。このビルドはpublic_ktから参照するのでvisibilityをpublicに定義している。

このcommon_ktをビルドするにはbazelコマンドを実行する。

 (bazel-multiprojects) $ bazel build //pkg/common_kt:common_kt_lib

上記のコマンドはWORKSPACEが配置されたプロジェクトのルートディレクトリで実行している。//pkg/common_ktがBUILDファイルまでのパスでcommon_kt_libがBUILDファイルで定義したビルドタスクである。

bazel-multiprojects/pkg/common_ktのディレクトリで実行する場合は次のコマンドになる。

(bazel-multiprojects/pkg/common_kt) bazel build common_kt_lib

ライブラリとしてビルドしたcommon_kt_libを利用するpkg/public_kt/BUILDは次のようになる。

# ./pkg/public_kt/BUILD

package(default_visibility = ["//visibility:public"])

load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_jvm_library")

kt_jvm_library(
    name = "public_kt_lib",
    srcs = glob(["main.kt"]),
    deps = ["//pkg/common_kt:common_kt_lib"],
)

java_binary(
    name = "java_bin",
    main_class = "com.github.soushin.multipojects.publickt.MainKt",
    runtime_deps = [":public_kt_lib"],
)

kt_jvm_libraryを使いmain.ktを含むpublic_kt_libを定義している。public_kt_libではdeps"//pkg/common_kt:common_kt_lib"が追加 してcommon_ktを依存させている。

depsの定義によりmain.ktからadd()の拡張関数が参照できるようになる。

java_binaryはjarを錬成するruleである。main_classにはmain()関数があるクラスを定義する。runtime_depsではkt_jvm_libraryでビルドしたpublic_kt_libを追加する。

これまで複数のruleを定義してきたがHello Worldを出力するビルドはjava_binaryのruleで定義したjava_binaryを実行するだけである。java_binaryはpublic_kt_libに依存しているのでBazelが依存関係をよしなに解決して実行してくれる。

java_binを実行した結果が次のようになる。

 (bazel-multiprojects) $ bazel run //pkg/public_kt:java_bin
INFO: Analysed target //pkg/public_kt:java_bin (2 packages loaded).
INFO: Found 1 target...
Target //pkg/public_kt:java_bin up-to-date:
  bazel-bin/pkg/public_kt/java_bin.jar
  bazel-bin/pkg/public_kt/java_bin
INFO: Elapsed time: 8.150s, Critical Path: 7.60s
INFO: 2 processes: 2 darwin-sandbox.
INFO: Build completed successfully, 6 total actions
INFO: Build completed successfully, 6 total actions
Hello kotlin! - built by Bazel!

錬成したjarを実行すると失敗する。

 (bazel-multiprojects) $ java -jar bazel-bin/pkg/public_kt/java_bin.jar
 bazel-bin/pkg/public_kt/java_bin.jarにメイン・マニフェスト属性がありません

これは単体で動かないjarのため単体で動かすには_deploy.jarをタスクに追加する。

 (bazel-multiprojects) $ bazel build //pkg/public_kt:java_bin_deploy.jar
 INFO: Analysed target //pkg/public_kt:java_bin_deploy.jar (0 packages loaded).
 INFO: Found 1 target...
 Target //pkg/public_kt:java_bin_deploy.jar up-to-date:
   bazel-bin/pkg/public_kt/java_bin_deploy.jar
 INFO: Elapsed time: 0.359s, Critical Path: 0.13s
 INFO: 1 process: 1 darwin-sandbox.
 INFO: Build completed successfully, 2 total actions

このjarを実行すると正常にHello Worldが出力したjarが起動する。

 (bazel-multiprojects) $ java -jar bazel-bin/pkg/public_kt/java_bin_deploy.jar
Hello kotlin! - built by Bazel!

name_deploy.jarの仕組みは次のDocsにまとまっているので参照してほしい。

Java Rules#Implicit output targets

Go

ここまでjarをBazelで錬成できるところまでまとめた。Dockerイメージのビルドは最後にまとめるのでGoプロジェクトを整理する。

WORKSPACE

# ./WORKSPACE

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
    name = "io_bazel_rules_go",
    url = "https://github.com/bazelbuild/rules_go/releases/download/0.16.2/rules_go-0.16.2.tar.gz",
    sha256 = "f87fa87475ea107b3c69196f39c82b7bbf58fe27c62a338684c20ca17d1d8613",
)

http_archive(
    name = "bazel_gazelle",
    urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/0.15.0/bazel-gazelle-0.15.0.tar.gz"],
    sha256 = "6e875ab4b6bf64a38c352887760f21203ab054676d9c1b274963907e0768740d",
)

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

go_rules_dependencies()

go_register_toolchains()

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

gazelle_dependencies()

http_archiveで必要なruleをインストールしているがGoではgazelleというGoプロジェクトに特化したBUILDファイルジェネレーターが用意されている。

github.com

Gazelleの威力

KotlinではHello Worldを出力するアプリケーションとライブラリの依存関係をBUILDファイルに定義したがgazelleはプロジェクト内のgoファイルを解析してBUILDファイルを錬成してくれる!

WORKSPACEと同階層に次のようなBUILDファイルを配置する。

# ./BUILD

load("@bazel_gazelle//:def.bzl", "gazelle")

# gazelle:prefix github.com/soushin/bazel-multiprojects
gazelle(name = "gazelle")

gazelleを有効にしている箇所でgazelle:prefixでgoのimportパスのprefixを定義している。このBUILDファイルを配置した状態で次のコマンドを実行する。

 (bazel-multiprojects) $ bazel run gazelle

上記のコマンドを実行すると自動でpublic_gocommon_goのgoコードを解析してBUILDファイルが錬成される。錬成したBUILDファイルを見ていく。

まずはcommon_goを見ていく。

# ./pkg/common_go/BUILD.bazel ※ gazelleが錬成したファイルは`.bazel`が含まれる。

load("@io_bazel_rules_go//go:def.bzl", "go_library")

go_library(
    name = "go_default_library",
    srcs = ["string.go"],
    importpath = "github.com/soushin/bazel-multiprojects/pkg/common_go/util",
    visibility = ["//visibility:public"],
)

go_libraryのruleが定義されている。importpathはgazelle:prefixで定義したプレフィックスが適応されている。

このgo_default_libraryをpublic_ktに依存させていく。


次にpublic_goのBUILDファイルは次のようになっている。

# ./pkg/public_go/BUILD.bazel

load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")

go_library(
    name = "go_default_library",
    srcs = ["main.go"],
    importpath = "github.com/soushin/bazel-multiprojects/pkg/public_go",
    visibility = ["//visibility:private"],
)

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

main.goはmainパッケージに配置しているのでgo_binaryのruleが追加になっている。このgo_binaryを実行してみると次のようになる。

 (bazel-multiprojects) $ bazel run //pkg/public_go:public_go    
INFO: Analysed target //pkg/public_go:public_go (0 packages loaded).
INFO: Found 1 target...
Target //pkg/public_go:public_go up-to-date:
  bazel-bin/pkg/public_go/darwin_amd64_stripped/public_go
INFO: Elapsed time: 0.571s, Critical Path: 0.37s
INFO: 2 processes: 2 darwin-sandbox.
INFO: Build completed successfully, 3 total actions
INFO: Build completed successfully, 3 total actions
Hello World!

Hello World!が出力できた。次にcommon_goを依存させてHello kotlin! - built by Bazel!と出力するようにしてみる。


錬成したライブラリを依存させるにはKotlinと同様にdepsを用いる。

go_library(
    name = "go_default_library",
    srcs = ["main.go"],
    importpath = "github.com/soushin/bazel-multiprojects/pkg/public_go",
    visibility = ["//visibility:private"],
    deps = ["//pkg/common_go/util:go_default_library"],
)

depsにgo_default_libraryのライブラリを追加した。この状態でgo_default_libraryをビルドするとmain.goからcommon_go/util/stirng.goが参照できるようになる。

package main

import (
    "fmt"

    "github.com/soushin/bazel-multiprojects/pkg/common_go/util"
)

func main() {
    fmt.Println(util.Add("Hello World!"))
}

最後にgo_binaryを実行する。

 (bazel-multiprojects) $ bazel run //pkg/public_go:public_go 
INFO: Analysed target //pkg/public_go:public_go (0 packages loaded).
INFO: Found 1 target...
Target //pkg/public_go:public_go up-to-date:
  bazel-bin/pkg/public_go/darwin_amd64_stripped/public_go
INFO: Elapsed time: 0.629s, Critical Path: 0.41s
INFO: 2 processes: 2 darwin-sandbox.
INFO: Build completed successfully, 3 total actions
INFO: Build completed successfully, 3 total actions
Hello World! - built by Bazel!

無事、util/stirng.goがmain.goに依存できた。

Dockerイメージのビルド

KotlinとGoのDockerイメージのビルドを見ていく。今回はビルドしたイメージをローカルにインストールするところまでをまとめていく。

Kotlin

WORKSPACEに次のhttp_archiveを追加する。

# ./WORKSPACE

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

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"],
)

Dockerをビルドするためのruleをインストールする定義である。

Kotlinプロジェクトで錬成したjarを動かすJavaベースのDockerイメージをプロジェクト内にインストールしたい。そのような場合は次のようにWORKSPACEに追加する。

# ./WORKSPACE

load("@io_bazel_rules_docker//container:container.bzl", "container_pull")

container_pull(
    name = "java_base_image",
    registry = "index.docker.io",
    repository = "library/openjdk",
    tag = "12-ea-18-jdk-alpine3.8",
)

container_pullのruleを有効にしてdocker.ioにあるalpineベースのjdkイメージをjava_baze_imageとしてプロジェクトにインストールした。

ここまでがWORKSPACEに追加するKotlin向けのプロジェクト全般の定義である。

BUILD

container_pullしたjava_base_imageをベースにHello Worldを出力するアプリケーションを内包したDockerイメージを錬成していく。

次のようなcontainer_imageのruleを有効にして定義する。

# ./pkg/public_kt/BUILD

load("@io_bazel_rules_docker//container:container.bzl", "container_image")

container_image(
    name = "public_kt_image",
    base = "@java_base_image//image",
    files = [":java_bin_deploy.jar"],
    cmd = [
        "java",
        "-jar",
        "java_bin_deploy.jar",
    ],
)

baseはWORKSPACEに追加したjava_base_imageを設定している。filesにはjava_binのタスクで錬成するjarファイルを設定しDockerイメージ内に配置されるようにしている。あとはイメージ内に配置したjarを実行するcmdを設定している。

このタスクを実行すると次のようになる。

 (bazel-multiprojects) $ bazel run //pkg/public_kt:public_kt_image
 INFO: Analysed target //pkg/public_kt:public_kt_image (14 packages loaded).
 INFO: Found 1 target...
 Target //pkg/public_kt:public_kt_image up-to-date:
   bazel-bin/pkg/public_kt/public_kt_image-layer.tar
 INFO: Elapsed time: 5.201s, Critical Path: 2.85s
 INFO: 4 processes: 4 darwin-sandbox.
 INFO: Build completed successfully, 5 total actions
 INFO: Build completed successfully, 5 total actions
 84c6c9b35317: Loading layer [==================================================>]  972.8kB/972.8kB
 Loaded image ID: sha256:a5cf4f2786c475fe57d020a86d74f2b686a85701908e9c9128c24219b078e809
 Tagging a5cf4f2786c475fe57d020a86d74f2b686a85701908e9c9128c24219b078e809 as bazel/pkg/public_kt:public_kt_image

ローカルにbazel/pkg/public_kt:public_kt_imageの名前とタグでイメージがビルドできた。このDockerイメージを起動する。

 (bazel-multiprojects) $ docker run -it bazel/pkg/public_kt:public_kt_image
 Hello kotlin! - built by Bazel!

Bazelで錬成したjarをBazelによって錬成したDockerイメージに内包することができた。

Go

GoのDockerイメージの錬成を見ていく。

WORKSPACEに次のhttp_archiveを追加する。

# ./WORKSPACE

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

Kotlinとは異なりBazelが提供するイメージを利用する。

BUILD
# ./pkg/public_go/BUILD

load("@io_bazel_rules_docker//go:image.bzl", "go_image")

go_image(
    name = "public_go_base_image",
    embed = [":go_default_library"],
    importpath = "github.com/soushin/bazel-multiprojects/pkg/public_go",
    goarch = "amd64",
    goos = "linux",
    pure = "on",
)

load("@io_bazel_rules_docker//container:container.bzl", "container_image")

container_image(
    name = "public_go_image",
    base = ":public_go_base_image",
)

go_imageのruleを有効にしてGoのDockerイメージを錬成している。embedにはライブラリのgo_default_libraryを設定している。このpublic_go_base_imageをcontainer_imageのbaseにしてローカルにDockerイメージがインストールする。

go_imageのpublic_go_base_imageだけでもDockerイメージはインストールできるのだがcontainer_imageはportやenvなどの細かいビルド設定が定義できる。今回のエンドリでは利用用途がなく割愛しているがHTTP Serverのようなアプリケーションではもう少し設定が増えると思う。

public_go_imageのタスクを実行すると次のようになる。

 (bazel-multiprojects) $ bazel run //pkg/public_go:public_go_image
INFO: Analysed target //pkg/public_go:public_go_image (0 packages loaded).
INFO: Found 1 target...
Target //pkg/public_go:public_go_image up-to-date:
  bazel-bin/pkg/public_go/public_go_image-layer.tar
INFO: Elapsed time: 1.137s, Critical Path: 0.49s
INFO: 2 processes: 2 darwin-sandbox.
INFO: Build completed successfully, 3 total actions
INFO: Build completed successfully, 3 total actions
84ff92691f90: Loading layer [==================================================>]  10.24kB/10.24kB
Loaded image ID: sha256:71560af2643cc9a1f0593ff72940ae99711d5d572156187c930bfdb6aa740bbf
Tagging 71560af2643cc9a1f0593ff72940ae99711d5d572156187c930bfdb6aa740bbf as bazel/pkg/public_go:public_go_image

ローカルにDockerイメージがインストールできたのでDockerイメージを起動する。

(bazel-multiprojects) $ docker run -it bazel/pkg/public_go:public_go_image
Hello World! - built by Bazel!

Kotlinと同様にバイナリの錬成からDockerイメージのビルドまでBazelのみで完遂できた。

まとめ

最終的なプロジェクトの構造は次のようになった。

./
├── BUILD
├── README.md
├── WORKSPACE
├── bazel-bazel-multiprojects -> /path/to/symlink
├── bazel-bin -> /path/to/symlink
├── bazel-genfiles -> /path/to/symlink
├── bazel-out -> /path/to/symlink
├── bazel-testlogs -> /path/to/symlink
└── pkg
    ├── common_go
    │   └── util
    │       ├── BUILD.bazel
    │       └── string.go
    ├── common_kt
    │   ├── BUILD
    │   └── util
    │       └── string.kt
    ├── public_go
    │   ├── BUILD.bazel
    │   └── main.go
    └── public_kt
        ├── BUILD
        └── main.kt

※Bazelが出力するbazel-*のシンボリックリンクのパスは/path/to/symlinkに置換しています。

プロジェクトのルートにWORKSPACEが配置され各アプリケーションとライブラリにBUILDファイルが配置されているのが分かる。BazelはGoとKotlinの言語が混合したプロジェクト構造であってもビルド環境は1つなので言語環境が競合することなく1つのレポジトリに配置することができる。

GoとKotlinが言語間でやり取りすることはないので1つのレポジトリに配置することはメリットがないかもしれない。あるとすればWORKSPACEの設定は共有されるのでGoとKotlinプロジェクトのビルド環境の差異を防げるくらいでだろうか。このエントリを通して今後プロジェクトに導入して事例を貯めていけるようなフェーズに入れたので引き続きBazelを推進していきたい。

今回では触れられなかったところを備忘録としてリストする。

  • grpcを利用するのでBazelのビルド定義は今のところ分からない。grpc-javaやgoもexampleはあるようなので見ていきたい。
  • container_imageのビルドまでをまとめたが実際のプロジェクトではcontainer_pushを利用することになる。CI視点の知見も貯めていきたい。
  • KtorやSpring-fu、Goであれば各種ライブラリが依存するような実戦形式のBUILDファイルの運用知見も引き続きまとめていきたい。

コード

このエントリで紹介したコード一式は次のレポジトリに置いてあるので参照してください。

github.com

Spring FuをGraalVMで動かす。起動速度に驚いた。

Serverlessプラットフォームが注目される中、その潮流にJava勢も乗っかりたいので起動速度が速いと注目のGraalVMを試してみた。

モチベーション

  • GraalVMのアプリケーション起動までを理解したい。
  • GraalVMでSpring Fuを動したい。
  • GraalVMでHTTP/1.1とgRPCのエンドポイントが開いたサーバを動かしたい。

GraalVMコトハジメ

GraalVM周辺を理解する。

https://www.graalvm.org/

GraalVMとは

  • Polyglot VM。多言語用の仮想マシンである。
  • JavaのJVMもScala/Java/Kotlinの複数言語を実行できたがGraalVMはJavaScript、Python、Rubyなどの言語も実行できる。
  • KotlinでRubyの関数を呼び出すことが可能。
  • Twitterは本番環境に導入しているらしい。

GraalVMとGraal

  • GraalとGraalVMを混同してしまうが別物である。
  • GraalはJavaのJITエンジンの1つでGraalVMはJITコンパイラにGraalを使っている。
  • Java9のAOTコンパイラはGraalをつかっている。

AOTコンパイラ

  • GraalVMではAOTコンパイラをつかってnative imageを作れる。
  • これにより起動が爆速になる。
    • JVMのJITコンパイラ(Just-in-time:実行時コンパイル)は実行時にコンパイルされるがAOTコンパイラ(Ahead-of-time:事前コンパイル)は事前コンパイルのため予めVM上で動く機械語にコンパイルされるため起動が速い。

GraalVMのメリット

  • 多言語を1つのランタイムで動かせること。
  • デプロイ構成や監視体制など言語ごとに整える必要がなくなる。
  • 起動が爆速。KnativeなどのServerlessプラットフォームと相性が良い。

GraalVMのデメリット

  • 動的なクラスなクラスロードができない。

GraalVMでSpring Fuを動かす

  • Spring Fuで実装したアプリケーションをGraalVMで動かす
  • サンプルで試したアプリケーションではHandlerをbean化しているがnativeイメージ化するためには一工夫必要である
native-imageのコマンド
 native-image -H:Name=app \
  --delay-class-initialization-to-runtime=io.netty.handler.codec.http.HttpObjectEncoder,org.springframework.core.io.VfsUtils \
  -H:ReflectionConfigurationFiles=graal-app.json \
  -Dio.netty.noUnsafe=true \
  -H:+ReportUnsupportedElementsAtRuntime \
  -Dfile.encoding=UTF-8 -cp ".:$(echo spring-fu-on-graalvm/BOOT-INF/lib/*.jar | tr ' ' ':')":spring-fu-on-graalvm/BOOT-INF/classes me.soushin.app.ApplicationKt

-H:ReflectionConfigurationFiles=graal-app.jsonがポイントである。 このjsonに次のようにbean化するクラスを定義する。

  {
    "name": "me.soushin.app.EchoHandler",
    "allDeclaredConstructors": true
  }

こうすることでSpringの世界で bean<EchoHandler>()を読んでも動的クラス読み込みが成功する。

起動にかかった時間を確認する

04:15:42.131 [main] INFO org.springframework.boot.SpringApplication 
  - Started application in 0.012 seconds (JVM running for 0.012)

0.012!! 爆速。

Spring Fuを動かしているレポジトリを公開しているのでソースとともに確認してほしい。

github.com

gRPCサーバは動くのか

こちらのレポジトリで試したがnative imageの生成時にエラーを確認した。

github.com

oracle/graalにissueを送ったので継続してgRPCサーバを動かすことにチャレンジしていきたい。

github.com

まとめ

  • GraalVMのコトハジメをまとめた。
  • JVMは起動速度がネックであったがServerlessプラットフォームに相性が良いGraalVMは利用が広がっていくのではないだろうか。
  • ビルドの工程にnativeイメージを固めることが加えられるので時間は増すが起動時間短縮のメリットがある。
  • 実践導入に向けた試験など課題はあるがスモールなサービスから投入して慣らしていきたい。