BazelでGoプロジェクトのビルド。Gazelleのgo_repositoryで外部ライブラリの依存とBazelのgo_testでテスト。
BazelをつかったGoプロジェクトのビルドをまとめている。前回のエントリではバイナリのビルドとDockerイメージのビルドをまとめた。
今回は外部ライブラリをGoプロジェクトに依存させる方法とテストの方法をまとめていく。
Gazelleのgo_repositoryで外部のライブラリを依存させる
外部ライブラリの依存はGazelleのgo_repositoryをつかう。
bazel-gazelle/repository.rst at master · bazelbuild/bazel-gazelle · GitHub
依存させるライブラリはDIツールのwireを選んだ。
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ファイルに沿ってルールの追加をしていくと捗りそう。
コード
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_go
とpublic_kt
はHello Worldを出力するアプリケーションを配置する。common_go
とcommon_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は次のレポジトリから参照できる。
WORKSPACEで行っていることはbazel_toolsからhttp_archive
を読み込みio_bazel_rules_kotlin
の名前でrulesをインストールしている。そしてkotlin_repositories
とkt_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ファイルジェネレーターが用意されている。
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_go
とcommon_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ファイルの運用知見も引き続きまとめていきたい。
コード
このエントリで紹介したコード一式は次のレポジトリに置いてあるので参照してください。
Spring FuをGraalVMで動かす。起動速度に驚いた。
Serverlessプラットフォームが注目される中、その潮流にJava勢も乗っかりたいので起動速度が速いと注目のGraalVMを試してみた。
モチベーション
- GraalVMのアプリケーション起動までを理解したい。
- GraalVMでSpring Fuを動したい。
- GraalVMでHTTP/1.1とgRPCのエンドポイントが開いたサーバを動かしたい。
GraalVMコトハジメ
GraalVM周辺を理解する。
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のデメリット
- 動的なクラスなクラスロードができない。
- https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md
- 起動が速いメリットにより生まれたデメリットである。
- 事前にコンパイルするため実行時に動的なクラス参照ができない。
- Springframeworkなどは痛手であるが回避方法はある。
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を動かしているレポジトリを公開しているのでソースとともに確認してほしい。
gRPCサーバは動くのか
こちらのレポジトリで試したがnative imageの生成時にエラーを確認した。
oracle/graal
にissueを送ったので継続してgRPCサーバを動かすことにチャレンジしていきたい。
まとめ
- GraalVMのコトハジメをまとめた。
- JVMは起動速度がネックであったがServerlessプラットフォームに相性が良いGraalVMは利用が広がっていくのではないだろうか。
- ビルドの工程にnativeイメージを固めることが加えられるので時間は増すが起動時間短縮のメリットがある。
- 実践導入に向けた試験など課題はあるがスモールなサービスから投入して慣らしていきたい。