BazelからCloud Native BuildpacksへDockerイメージのビルドを移行した話
現在のリポジトリの構成
こんにちは、バックエンドエンジニアのSです。
弊社ではGoで実装されたアプリケーションのDockerイメージをBazelにてビルドしていましたが、この度、Cloud Native Buildpacksへ移行しました。その経緯と移行に辺り工夫した点をご紹介いたします。
Bazelの概要
弊社ではrules_dockerを使用することにより、Dockerイメージをアプリケーションのエンドポイント毎にビルドしていました。Bazelのビルド設定となるBUILD.bazelファイルに関してはGazelleにより、自動生成します。エンドポイント毎に複数のDockerfileを管理する必要がないため、重複コードの排除を目的として使用していました。また、一度ビルドした後ではキャッシュにより、2回目以降のビルドが非常に高速です。
Bazelの問題点
Gazelle
以前、ブログの記事でも取り上げましたが、Gazelleはライブラリを自動更新するRenovateと相性が悪いです。アプリケーションのライブラリを更新する度にGazelleを実行することにより、BUILD.bazelファイルを更新する必要があります。しかし、Renovateはgo.modとgo.sumの更新しか行いません。そのため、弊社では追加のワークフローをGitHub Actionsへ定義することにより、Renovateの処理に加えてGazelleを実行していました。
confluent-kafka-go
GoのアプリケーションではKafkaクライアントのライブラリであるconfluent-kafka-goを使用しています。結論から申しますと、このconfluent-kafka-goとBazelの相性が非常に悪くCloud Native Buildpacksへの移行を決めた要因となります。
confluent-kafka-goは内部的にC++のライブラリlibrdkafkaを使用しています。Bazelからconfluent-kafka-goをビルドする場合、librdkafkaのバイナリをビルド環境に応じて選択できないため、エラーが発生してしまいます。rules_goではGoのライブラリに対して自動的にBUILD.bazelファイルを作成します。confluent-kafka-goのビルドエラーを避けるためには自動生成されたBUILD.bazelファイルに対するパッチを別途作成する必要があります。次に示すのはパッチの一部抜粋です。
--- kafka/librdkafka_vendor/BUILD.bazel
+++ kafka/librdkafka_vendor/BUILD.bazel
@@ -1,4 +1,27 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@rules_cc//cc:defs.bzl", "cc_library")
+
+cc_library(
+ name = "librdkafka_static",
+ srcs = select({
+ "@io_bazel_rules_go//go/platform:android": [
+ "librdkafka_glibc_linux.a",
+ ],
+ "@io_bazel_rules_go//go/platform:darwin": [
+ "librdkafka_darwin.a",
+ ],
+ "@io_bazel_rules_go//go/platform:ios": [
+ "librdkafka_darwin.a",
+ ],
+ "@io_bazel_rules_go//go/platform:linux": [
+ "librdkafka_glibc_linux.a",
+ "librdkafka_musl_linux.a",
+ ],
+ "//conditions:default": [],
+ }),
+ hdrs = ["rdkafka.h"],
+ visibility = ["//visibility:public"],
+)
go_library(
name = "librdkafka_vendor",
selectにより、ビルド環境となるOSから使用する静的ライブラリを決定します。開発環境はmacOSですが、動作環境となるDockerイメージのOSはLinuxのディストリビューションのひとつであるDistrolessです。macOSにてDockerイメージをビルドしてしまうと静的ライブラリが動作環境と適合しないため、動作しません。そのため、UbuntuをベースイメージとするBazelコンテナを用いてDockerイメージをビルドしていました。当然、Dockerを介してビルドを行うため、ビルドに要する時間が長くなってしまいます。
Cloud Native Buildpacks
Bazelによるビルドを廃止するに辺り、Cloud Native Buildpacksの導入を検討しました。Cloud Native BuildpacksもDockerfileを書かずにDockerイメージのビルドが可能です。しかしながら、一定の規則性に沿ったディレクトリ構成であることが求められるため、既存のアプリケーションへ導入するのは困難である場合があります。
Paketo Buildpacks
Paketo BuildpacksはCloud Native Buildpacksの実装のひとつです。今回はこのBuildpacksを採用することにしました。コマンド実行時の引数指定により、ビルド対象とするエンドポイントを指定できることが選定理由となります。また内部的にgo build
コマンドにより、バイナリをビルドしているため、confluent-kafka-goも問題なくビルドできます。
他にもGoogle Cloud Buildpacksがあります。2021年10月時点ではエンドポイント毎にDockerイメージをビルドできなかったため、採用を見送っています。
Dockerイメージのビルド時間を短縮
Paketo BuildpacksはDockerイメージ毎にコマンドを実行する必要があります。そのため、エンドポイントごとにコマンドを実行する必要があり、エンドポイントの数が増えるに従ってビルドに要する時間が長くなります。この問題を意図してなのか、エンドポイント毎のバイナリを全て含めたDockerイメージをビルドする方法が提供されていたりします。しかしながら、不要なバイナリをDockerイメージへ含めることはAttack surfaceを増やすことになるため、セキュリティ的によろしくありません。
Skaffold
今回はSkaffoldを用いてDockerイメージのビルドを行うことにより、ビルド時間の短縮を図りました。SkaffoldはDockerイメージのビルドからKubernetesへのデプロイまで行うことが出来るツールです。skaffold.yamlというファイルへDockerイメージのビルドを設定することにより、ビルドフローが簡略化されます。また、Local Buildにおけるconcurrencyを設定することにより、ビルドの並行処理数を決定できます。
Skaffoldはキャッシュについても簡単に管理ができます。ビルドを行うとホームディレクトリ配下のskaffold/cacheへキャッシュが生成されます。弊社では次のようなGitHub Actionsのステップを追加することにより、キャッシュを利用したバイナリとDockerイメージのビルドを行っています。
- name: Use cache for Skaffold
uses: actions/cache@v2
with:
path: |
~/.skaffold/cache
key: skaffold-${{ github.ref }}-${{ github.sha }}
restore-keys: |
skaffold-${{ github.ref }}-
skaffold-refs/heads/main-
skaffold-
まとめ
今回はDockerイメージのビルドをBazelからCloud Native Buildpacksへ移行した経緯と工夫点をご紹介しました。移行前は20分前後かかっていたビルドが移行後は10分前後まで短縮されています。キャッシュ利用時は5分前後まで時間が短縮されました。
Sprocketで働きませんか?
弊社ではカジュアル面談を実施しております。
ご興味を持たれましたら、こちらからご応募お待ちしております。
Discussion