koで高速にGoのimage build

はじめに

こんにちは,今年の4月に新卒で入社しました,menu事業部で基盤開発ということをやっている窪田と申します.
突然ですが,皆さんは開発でGolangを使っていますか?
もし,Go言語を使われている場合,cloud上のdev/stg/prod環境にデプロイをする際,多くの場合Dockerでimageを作り,pushしていることと思います.(もしかしたら,サーバ上でgo run main.goをしているかもしれませんが...)
今回はそんな,imageの作成を高速に,簡単に行ってくれる ko(ko-build) というGolang専用のimage builderについて紹介させていただきます.

注意

  • 筆者はM1 Mac環境です.windowsIntel Macでの動作は確認しておりません.
  • Goのバージョンは1.18,koのバージョンはv0.12.0を用いています.
  • Localでの挙動確認のためには,Dockerが起動している必要があります.(Localでのimageのpush先にDockerのimage repositoryが使用されます)

koについて

koとは,

ko is a simple, fast container image builder for Go applications.

と公式のREADMEにもあるとおり,Goのために作られた,非常に高速なimage builderです.

Googleが開発を行っており,CNCFのsandbox projectにも採択されています.(参考リンク)

ko はローカルマシン上で go build を効果的に実行することでイメージを構築するため、docker のインストールは必要なく*1マルチプラットフォームビルドにも対応しています.

Readme上で

It's ideal for use cases where your image contains a single Go application without any/many dependencies on the OS base image (e.g., no cgo, no OS package dependencies).

とある通り,OS のベース イメージに多くの依存性を持たない単一の Go アプリケーションのbuildに対して特に最適なimage builderです.

koの利点

koの利点は大きく二つあります.

  • Dockerfileが不要
  • docker buildよりも大抵の場合2倍以上高速

以下ではこれらの利点を簡単にkoを使いながら実感してみましょう.

Dockerfile不要

koをインストールして簡単に使ってみましょう.
home brewを使用する場合には

brew install ko

home brewが無い環境の場合はgo installを用いて

go install github.com/google/ko@latest

でインストール可能です.

では,早速ko buildの便利さを実感してみましょう.
以下のようなmain.goを用意します.

package main

import "fmt"

func main() {
    fmt.Printf("Hello Gopher")
}

このコードのimageは

time ko build -L main.go

でbuildできます.-Lオプションをつけることで,imageの保存をLocalのdockerが用意しているimage repositoryに対して行ってくれます.(DockerがLocalで起動していないと失敗します)
実際に,imageを確認してみると,以下のようにlatestタグと,何らかのhash値を用いたtagをつけたimageが作成されているのが確認できます.

docker images | grep ko.local 

~~~以下出力~~~
ko.local/hello_world.go-758db53ba470591ced76d85d86b5f3fc   1a72d55ea703816a000073fa3e4323f4da7e1734b54be1d7493efb417840362d             3b524693e26c   14 hours ago    3.15MB
ko.local/hello_world.go-758db53ba470591ced76d85d86b5f3fc   latest                                                                       3b524693e26c   14 hours ago    3.15MB

ここで,Dockerでbuildしていた時のことを思い出してみましょう.
まず,Dockerfileが必要でした.
koのbase imageはデフォルトでcgr.dev/chainguard/staticとなっているので*2,Dockerfileを作るなら以下のようになるでしょうか

FROM golang:1.18 as build

COPY main.go main.go

RUN go build -o /app main.go


FROM cgr.dev/chainguard/static

COPY --from=build /app .

ENTRYPOINT ["./app"]

こちらのDockerfileをmain.goと同じディレクトリにおいて

docker build -t test .  

を実行することでimageが作れます.

ここまで見ていただいた通り,koの実行においてはDockerfileが不要になっています.

単純にGoのコードのimageをbuildするだけであれば,koではDockerfileのような設定ファイルを用意する必要がありません.

docker buildよりも高速

上の例でbuildにかかる時間を計測してみます.
簡単に測りたかったのでtimeコマンドを使用しました.*3
一番最後のtotalの手間に出ているのが,実行時間の総計になります.
以下がtime ko build -L main.goの実行結果です.

ko build -L main.go  0.31s user 0.51s system 16% cpu 4.934 total

以下がtime docker build -t test .の実行結果です.

docker build -t test .  0.23s user 0.32s system 4% cpu 12.210 total

これだけ単純なimageであってもko buildの方が早いことがわかるかと思います.

さらに少し大きめのプロジェクトとして,guestbookという,cloud codeのsample projectを用いた計測を行ってみましょう.*4*5
レポジトリをクローンしてsrc/backendディレクトリに移動してください.
Dockerfileで使用されているimageとbase imageを揃えるためにkoコマンドとしては以下を実行しました.*6

KO_DEFAULTBASEIMAGE=gcr.io/distroless/base ko build -L main.go 

timeコマンドを用いた結果が以下の通りです.

  8.58s user 1.85s system 118% cpu 8.811 total

一方でdocker buildの結果が以下の通りです.

 0.25s user 0.35s system 3% cpu 17.276 total

こちらも2倍ほどkoの方がbuildが早いことがわかるかと思います.

現在自分が開発中のプロジェクトにおいては,kubernetes上で動く複数のサービスに対して,それぞれサービス用のimageをbuildする必要があるのですが,docker buildを用いていたときには一イメージあたり30秒程度かかっていた各imageのbuild時間が,koの導入によりそれぞれ5~10秒以内で実行できるようになり,localでkubenertesを起動してR&Dをする際に待ち時間をかなり減らすことができました.

koでできること,できないこと

ここまでkoに関するいい点を話してきたので,koの導入にきっと興味を持ってくださっているかと思います.
ここからは,koのできること,できないことの中でも開発に関わってきそうな話に関して簡単に解説して行こうと思います

Goのみ

Go以外のプログラミング言語のbuildはもちろんできません.
Go以外のコードが共存する環境では通常通りdockerを用いる方が安全かと思います.

静的ファイルはimageに入れられる

htmlやcss,画像ファイルや変更が起こることのない実行ファイルなどは,imageに含めることができます.
main.goが存在するディレクトリ内にkodataというディレクトリを作成し,ko buildを実行すると,image内の/var/run/kodataというディレクトリ内に,localのkodataにおいたファイルが展開されます.なので,code内で/var/run/kodataを見にいくことで静的ファイルを使用することができます.

また,実行ファイルとしておくことが可能なコマンドなどに関しては,base imageに含めてしまうという手もあります.
base imageをあらかじめ作成しておき,docker hubなどにpushし,KO_DEFAULTBASEIMAGEで呼び出すことで,そのコマンドを持ったimageをbuildできます.
ただ,先に『OS のベース イメージに多くの依存性を持たない単一の Go アプリケーションのbuildに対して特に最適』と紹介した通り,こうした利用はあくまでも可能というだけで,公式としてもあまり推奨した使い方ではないように思われます.
base imageを自作するとそのbase imageを管理する手間も発生するため,可能な限り行わず,そうした必要がある場合には今まで通りDockerfileを使うようにした方が安全だろうと感じています.

imageのpush

作ったimageのpush先を指定できます.
-Lコマンドでlocalのdockerが起動しているimage repositoryへのpushが行われます.
一方で,KO_DOCKER_REPOにimageレポジトリのURLを設定することで,Docker Hub,GCPのArtifact RegistryやAmazon ECRなどremote環境へのimage pushも可能になります.
また,例えばGCPのArtifact Registryを用いる際などには,予めアクセス権限をworkload identityなどを用いて別途設定する必要があるため注意してください.

GitHub Actions

github.com

こちらのようにGitHub Actionsも用意されています.Readmeを読んで設定することが可能です.
Docker buildを行っている部分をkoコマンドを使うように書き換えるだけだったので,実装のコストはかなり低く感じました.

その他

こちらのサイトを参考にすることで,その他機能を確認できます.
このブログで紹介した機能以外にも

  1. koで作ったimageをdocker runで使ったりcloud serviceにdeployしたりする方法(参考リンク)
  2. kubernetesに対してのintegration(参考リンク)
  3. go build時のオプションの設定方法(参考リンク)
  4. .ko.yamlでの設定管理(参考リンク) などの機能が紹介されています.

最後に

koについての日本語の記事はまだまだ少なく,特にサービスコードに取り入れている例はほぼ公表されていないように見えましたので,今回koの紹介記事を書かせていただきました.
まだまだ,発展途中のプロジェクトでもあるため,公式ドキュメントにおいてわかりずらい点もあり,そういう際には,こちらのGoDocを参考にしたり,実際のコードを読みにいったりして,どういうdefault値が設定されているのかなどの確認などを行いました.

pkg.go.dev

Goを用いた開発をしているのであれば,是非koを使って快適にimage buildを行ってみてください.

また,弊社では様々なポジションのエンジニアを募集しています.
まずはカジュアルにお話を,という形でも結構ですので是非気軽にご応募ください.

reazon.jp

*1:imageのpush先として必要になる場合はあります.

*2:リンク先のOverriding Base Imagesを参照

*3:それぞれのコマンド実行前にdocker desktopのTroubleshoot機能内にあるClean / Purge dataを実行してimageの履歴を消し,cacheがない状態で計測しています.

*4:筆者環境では動作のためにgo.mod, Dockerfile内のgoのバージョンを1.18に下げています.

*5:cloud codeに関しては今後のtech blog内で紹介予定です.

*6:KO_DEFAULTBASEIMAGEという変数にimageのpathを設定することでbase imageを変更可能です