YAMAGUCHI::weblog

海水パンツとゴーグルで、巨万の富を築きました。カリブの怪物、フリーアルバイター瞳です。

『効率的なGo』という本が出版されました #efficient_go

はじめに

こんにちは、Google Cloudのオブザーバビリティ/SRE担当者です。出張中で発売日にきちんとした記事が書けなかったのですが、去る2月24日に私が翻訳しました『効率的なGo―データ指向によるGoアプリケーションの性能最適化』という書籍がオライリー・ジャパン社より出版されました。書店ならびに各社オンラインストアでご購入いただけます。

www.oreilly.co.jp

電子書籍版はオライリー・ジャパンのサイトにPDFおよびEPUBでの提供がありますので、そちらよりご確認ください。

『効率的なGo』をなぜ翻訳しようと思ったのか

私は業務において、SREやオブザーバビリティに関わる各種プラクティスの啓蒙や、それらの各種製品(Google Cloudのプロプライエタリ製品やオープンソースソフトウェア、その他関連製品なんでも)を使った実践などを解説したりしています。ここ数年でのオブザーバビリティに対する注目が高まったこともあり、計装やAPMに関する情報はだいぶ増えてきたように思います。一方で、ボトルネックの究明を行った後の最後の一歩、ボトルネックの改善をどう行うのかについては、アプリケーション開発の文脈に渡されてしまい、あまり一般的な解説が得難い領域でした。

そんな中、『オブザーバビリティ・エンジニアリング』の翻訳の第2校がちょうど終わる頃に、原著 "Effecient Go" の出版が決まり、急いでその内容を確認したところ、まさにその解説が得難い領域をテーマとした書籍であったこと、そして内容もGoに限らない、アプリケーション性能改善一般に触れる書籍であったことから、翻訳の企画をオライリー・ジャパンへと持ち込みました。

ここ最近私が関わったオライリー・ジャパンでの翻訳書籍は、企画が立ち上がった順序で言うと、『SLO サービスレベル目標』『オブザーバビリティ・エンジニアリング』の順だったのですが、ちょうどこの順序でサービス全体のマクロな視点の目標設定から始まり、それを効率よく観察するためのオブザーバビリティの獲得、そして問題がある場合の原因の究明までは理解ができますが、最後の性能改善の部分が足りないとと考えていました。そこにおあつらえ向きに本書が出版され、まさに福音でした。

また本書がGoで解説していたことも大きいです。自分が最も使う頻度が高く、長らく関わっているプログラミング言語なので、『Go言語による並行処理』と同様に、内容の理解は他の言語で解説されたものよりもできるからです。

こういった偶然が重なり、本書を翻訳する機会を得ることとなりました。

「効率的なGo」はどのような本か

本書は次のような読者に有益であると考えています。

  • Goによって開発されたプログラムのパフォーマンスを改善したいと考えているエンジニア
  • 他の言語でのパフォーマンス改善方法を知っているが、Goでの方法を知らないエンジニア
  • パフォーマンス改善一般について理解したい方
  • Goがどのようにリソースを使うか、理解を深めたいエンジニア

本書は書籍タイトルにもあるとおりGo製のプログラムを中心として、そのパフォーマンス改善手法について解説していますが、Goに限らない、プログラムのパフォーマンス改善において汎用的な考え方が紹介されています。

また本書はGoのランタイムからOSまでという、これまであまり解説がまとまった形で得られなかった低レイヤーの解説にもある程度のボリュームが割かれている書籍なので、初級者向けの書籍では刺激が得られないエンジニアにも、非常に興味深い内容になっていると思います。

関連図書

本書の関連図書として私からいくつか挙げてみます。

まず先にも紹介しましたし、本書の訳者まえがきにも書いたのですが、『SLO サービスレベル目標』『オブザーバビリティ・エンジニアリング』は真っ先に挙げたい書籍です。

もちろん自分が翻訳に関わったからでもありますが、先にも紹介した通り、一連のオブザーバビリティの獲得と性能改善というシナリオを大局的に理解するために必要な情報はこの3冊で網羅されています。

そしてプログラムの性能問題の調査に関しては『詳解 システム・パフォーマンス』を外すわけにはいきません。非常に分厚く、また価格も高いので購入がためらわれるかもしれませんが、逆にこの内容の充実ぶりで7000円を切る価格で販売されているというのは破格と言っても過言ではありません。内容も、非常に丁寧に解説されていますし、頭から通して読まなくても、辞書的に使えるところが素晴らしいです。一人一冊とまでは言わないまでも、一社に一冊は備えておくことをおすすめしたいです。

本書にならんでGoの内部挙動を紹介する書籍として紹介したいのは『Goならわかるシステムプログラミング』です。本書より少しだけ上のレイヤーで広くGoのランタイムの解説をしています。システムコールのレベルでGoのランタイムとOSの関係性を知りたい場合にはおすすめの書籍です。

また本書で解説されている内容の中で、GoプログラムとCPUに関する章(第4章)がありますが、そちらに興味を持たれた方は『プログラマーのためのCPU入門 』をおすすめします。より一般的な立場からプログラムとCPUはどう連携して動くのかを深く解説されている書籍です。

おわりに

私は職業柄、システム全体、サービス単体、関数1つと様々なレベルでの性能最適化に関わる話をすることがありますが、一貫して広まってほしいと思っている考え方は「性能を改善するためには計測し目標を立てること」です。本書が、その普及の一助になることを期待しています。

Goの単一メソッドインターフェースと関数型について

はじめに

こんにちは!!Google Cloudでオブザーバビリティを担当しているものです。年に一度のGoアドベントカレンダーの時期がやってきましたね!本記事は Goアドベントカレンダー 2022 の12日目の記事です。昨日は @Maki_Daisuke さんの担当でした。

Goアドベントカレンダーもついに今年で10年目です!これまでに書いた記事を見るとなかなか懐かしいトピックがあったりしますね。

今年は何を書こうかなあと思ったときに、久々に原点に戻って、Goの設計プラクティスに関して最近同僚と話していて面白かった「単一メソッドインターフェースと関数型」について書こうと思います。

宣伝

ところでGo Conference 2023のCfPがオープンしました。開催は来年の6月、CfPの締切は来月末です。皆様のセッションプロポーザルをお待ちしております!

gocon.jp

単一メソッドインターフェース

単一メソッドインターフェース(Single Method Interface)というのは正式な名称ではないのですが、次のようなインターフェースを指します。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
type Reader interface {
    Read(p []byte) (n int, err error)
}

いずれも標準パッケージの net/httpio から持ってきたものですが、名前の通りメソッドを1つしか持たないインターフェースです。Goにはこういう単一メソッドインターフェースが非常に多くて、これを駆使することで非常に柔軟で強力な型システムが実現されています。

関数型

一方で関数型(Function Type)と呼んでいるのは、あるシグネチャを持った関数を個別の型として宣言しているものです。同様に標準パッケージから例を持ってくると次のようなものがあります。

type HandlerFunc func(ResponseWriter, *Request)
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)

これらはそれぞれ net/httpbufio から引用しています。関数型は単純に特定の関数シグネチャエイリアスするというだけではなく、「特有の型を作ることで、値として渡すときに制限を設ける」ことができます。

単一メソッドインターフェースと関数型の比較

これらは「特定の関数シグネチャを型として明示的に区別する」という点では同じですが、その性質は異なります。

アサーション

interface{} 型から型アサーションするときに、単一メソッドインターフェースのほうが挙動がわかりやすいです。たとえば次のような例の場合、結果はどうなるでしょうか。

type T struct{}

func (T) ServeHTTP(http.ResponseWriter, *http.Request) {}

func ServeHTTP(http.ResponseWriter, *http.Request) {}

func isHandler(data interface{}) {
    _, ok := data.(http.Handler)
    if ok {
        fmt.Println("a http.Handler")
    } else {
        fmt.Println("NOT a http.Handler")
    }
}

func isHandlerFunc(data interface{}) {
    _, ok := data.(http.HandlerFunc)
    if ok {
        fmt.Println("a http.HandlerFunc")
    } else {
        fmt.Println("NOT a http.HandlerFunc")
    }
}

func main() {
    sh := (T{}).ServeHTTP
    sh2 := ServeHTTP

    isHandler(sh)
    isHandlerFunc(sh)

    isHandler(sh2)
    isHandlerFunc(sh2)
}

よく訓練されたGopherの皆様であれば、なんのつまづきもなくこれらの結果を予想できると思いますが、Goまだ馴染んでいない方だと特にHandlerFuncかどうかの判定で戸惑うのではないかと思います。 sh では、メソッドのレシーバーがあるので実際には func(T, http.ResponseWriter, *http.Request) となっているのでだめ、ということ、後者は実装のまま func(http.ResponseWriter, *http.Request) として見られているのでだめ、ということになります。

これを見るとまだHandlerかどうかのほうがわかりやすい結果になっているかと思います。

型変換

型変換をする場合には、関数型は関数シグネチャだけ合っていれば型変換できるのに対して、インターフェースはメソッド名まで合っていないといけません。次の例を見てみてください。

type T struct{}

func (T) ServeHTTP(http.ResponseWriter, *http.Request) {}

type N struct{}

func (N) ServeDebugHandler(http.ResponseWriter, *http.Request) {}

func main() {
    var (
        t T
        n N
    )

    var _ http.Handler = t
    var _ http.Handler = n // 動作しない

    var _ http.HandlerFunc = t.ServeHTTP
    var _ http.HandlerFunc = n.ServeDebugHandler
}

拡張性

構造体はインターフェースで定義されたメソッドを実装しているかぎり、インターフェースを満たしているとみなされるため、状態を保持するためのフィールドを定義したり、他の補助的なメソッドを追加したりなど自由に拡張できます。一方で、関数型の場合はそもそも関数シグネチャに縛られているので、拡張性はインターフェースと比較すると著しく乏しくなります。

宣言

インターフェースの実装はトップレベルでしか宣言できないのに対して、関数型は関数内でも定義可能です。

type T struct{}

func (T) ServeHTTP(http.ResponseWriter, *http.Request) {}

func ServeHTTP(http.ResponseWriter, *http.Request) {}

func main() {
    var (
        _ http.Handler     = new(T)
        _ http.HandlerFunc = ServeHTTP
    )

    type innerT struct{}

    // 関数内で構造体のメソッドは実装できない
    // func (innerT) ServeHTTP(http.ResponseWriter, *http.Request) {}
    // var _ http.Handler = new(innerT)

    f := func(http.ResponseWriter, *http.Request) {}
    var _ http.HandlerFunc = f
}

メソッド値と関数型

型キャストのところでも少し触れましたが、メソッド値は関数値として渡すことが可能なので便利です。

type Feature struct {
    DryRunMode bool
}

func (f *Feature) RenderDebug(w http.ResponseWriter, req *http.Request) {}

func New(mux *http.ServeMux) *Feature {
    f := &Feature{}
    mux.HandleFunc("/featurez", f.RenderDebug)
    return f
}

使い分け

以上は言語仕様から各々の特徴を確認しただけで、どちらが優れているということはありません。しかしながら、拡張しやすいという点や、総合的な挙動の理解のしやすさを考えると、一旦単一メソッドインターフェースを用意してあげるのがまずは安全なのかなあと思いました。

一方で、逆に拡張性を絞って渡す関数に制限を加えたい場合には、関数型を定義してあげるのも良いかなと思いました。

これらを考えた上で、さらに net/http パッケージの実装を見てみると、関数型自体が単一メソッドインターフェースを実装している(そして、これは単一メソッドインターフェースでないと実装は無理だと思う)関係性が http.HandlerFunchttp.Handler に見られて、本当に美しいな、などと思うのでした。(おそらくこういった実装をしている例は、標準パッケージ内で公開インターフェースで行っているのはおそらく net/http しかないと思います)

// In net/http:
// type HandlerFunc func(http.ResponseWriter, *http.Request)
//
// type (f *HandlerFunc) ServeHTTP(w ResponseWriter, r *Request)
//
// type Handler interface {
//     ServeHTTP(ResponseWriter, *Request)
// }
f := func(http.ResponseWriter, *http.Request) {}
var _ http.Handler = http.HandlerFunc(f)

おわりに

今年の記事は久々にGoの設計についての考察を書きました。Goは言語仕様が軽量なこともあり、こうした考察がしやすい言語になっていると思います。来年も仕事でGoをたくさん活用したいと思います。

明日は @otiai10 さんの記事です。

OpenTelemetryでgRPCのヘルスチェックのトレースを無視する

はじめに

OpenTelemetryを使ってgRPCのトレースを楽に取ろうと思うと otelgrpc を使ってよしなにリクエストのトレースを取っていることと思います。

たとえばサーバー側であれば

interceptorOpt := otelgrpc.WithTracerProvider(otel.GetTracerProvider())
srv := grpc.NewServer(
        grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor(interceptorOpt)),
        grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor(interceptorOpt)),
    )

クライアント側であれば

interceptorOpt := otelgrpc.WithTracerProvider(otel.GetTracerProvider())
*conn, err = grpc.DialContext(ctx, addr,
    grpc.WithTransportCredentials(insecure.NewCredentials()),
    grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor(interceptorOpt)),
    grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor(interceptorOpt)),
)

といった具合にOpenTelemetry用のインターセプターを挿入することになると思います。(あえて明示的にTracerProviderを指定していますが、globalなものを使うのであればこれは必要ありません)

しかし、これをこのまま実行すると、KubernetesなどでgRPCのヘルスチェックをしているようなときに、そのトレースまで送られてしまいます。つぎのスクリーンショットで大量の grpc.health.v1.Health/Check のトレースが見えています。(分布図の最下部あるトレースがそれ。スクリーンショットは1つだけハイライトしたもの。)

実際にこれが鬱陶しいのでフィルターをOpenTelemetry側でしてほしいという要望がいくつか出ています。(1つは私が起票したものですが...)

これは当然で、単純に無駄なトレースが大量に表示されてダッシュボードが見づらくなるだけでなく、従量課金になっているようなサービスではコストの無駄になります。そこでワークアラウンドが欲しくなります。

WithSamplerワークアラウンドする

特定のトレースを取得するかどうかをAPIで差し込める余地は実は殆ど無く、あるとしたら

  • 自らexporterを書いて特定のトレースはバックエンドに送らないようにする(あるいはexporterによってはそのようなオプションがあるかも)
  • Samplerを自分で書く

くらいしかありません。前者は実現したい事柄に対してのワークアラウンドが大きすぎるので非現実的です。後者は多少コードは多くなるけれど、まだ許容できる範囲なので、こちらでやっていこうと思います。

import sdktrace "go.opentelemetry.io/otel/sdk/trace"

func IgnoreWithNameSampler(targets []string) sdktrace.Sampler {
    return ignoreWithNameSampler{
        targets: targets,
    }
}

type ignoreWithNameSampler struct {
    targets []string
}

func (s ignoreWithNameSampler) Description() string {
    return "drop all spans with the name that contains one of targets."
}

func (s ignoreWithNameSampler) ShouldSample(p sdktrace.SamplingParameters) sdktrace.SamplingResult {
    ts := trace.SpanContextFromContext(p.ParentContext).TraceState()
    for _, t := range s.targets {
        if strings.Contains(p.Name, t) {
            return sdktrace.SamplingResult{
                Decision:   sdktrace.Drop,
                Tracestate: ts,
            }
        }
    }
    return sdktrace.SamplingResult{
        Decision:   sdktrace.RecordAndSample,
        Tracestate: ts,
    }
}

これを作ったらあとは普通にTraceProviderにSamplerとして指定するだけです。

tp := sdktrace.NewTracerProvider(
    sdktrace.WithSampler(IgnoreWithNameSampler([]string{"grpc.health.v1.Health/Check"})),
    sdktrace.WithBatcher(exporter),
)

紹介してきましたが、本来であれば TraceIDRatioBasedなどと組み合わせて簡単にやりたいはずなので、標準で持っていてくれていいはずなのにとは思います。

追記(2022.07.12 14:30)

レポートと共にPull Requestを出した。

github.com

追記(2022.08.17 16:30)

Pull Requestが無事にマージされたのでこれからは go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/filters を使えば簡単にフィルターできます。

github.com

OpenTelemetryでSpanのレイテンシーを取得する

はじめに

分散トレースでリクエスト全体の中でのボトルネックを発見すると同時に、ユーザーに対するSLOとしてトレース全体のレイテンシーを取得することもあると思います。その場合OpenTelemetryではすんなりとルートSpanのレイテンシーを取得できないため、その方法をメモしておきます。

バージョン

通常の計装

まずこういう典型的な計装があったとします。

func sleepHandler(w http.ResponseWriter, r *http.Request) {
    tracer := otel.Tracer("handler.sleep")
    ctx := context.Background()
    ctx, span := tracer.Start(ctx, "request.sleep")
    defer span.End()
    ...(なにかする)...
}

この span にかかったレイテンシーを取得したいとします。しかし go.opentelemetry.io/otel/trace.Span には開始時刻と終了時刻のタイムスタンプを取得するメソッドが定義されていません。

しかし直感でも分かる通り、また仕様でも定義されている通り、Spanはその開始時刻と終了時刻を保持しているので、アクセスするためのインターフェースがどこかにあるはずです。

github.com

tracesdk.ReadOnlySpanに変換する

で、よくコードを読んでみると go.opentelemetry.io/otel/sdk/trace.ReadOnlySpan にはそのためのインターフェースが定義されています。

pkg.go.dev

したがって、上記計装から次のようにすれば spanレイテンシーを取得できます。

func sleepHandler(w http.ResponseWriter, r *http.Request) {
    tracer := otel.Tracer("handler.sleep")
    ctx := context.Background()
    ctx, span := tracer.Start(ctx, "request.sleep")
    ...(なにかする)...
    span.End()
    ro := span.(tracesdk.ReadOnlySpan)
    start, end := ro.StartTime(), ro.EndTime()
    duration := end - start
    ...(durationをメトリクスとしてバックエンドに送る)...
}

注意することとしては span.End() を先に呼んでいないとspanのendtimeが打刻されないので、defer span.End() にしていた部分は何かしら書き換えないといけないということです。

他の言語でも同様

上記の例はすべてGoで書いていたけれど、OpenTelemetryは仕様が全言語でインターフェースレベルで共通で実装されているので、おおよそこういった実装は他の言語でも共通になっています。

2022.06.15 追記

OpenTelemetryの仕様 によると、No-op Tracerの場合はそもそもReadOnlySpanにキャストできない様子なので、その場合の対応は考える必要がありそうです。

Go製アプリケーションのコンテナ化にはkoを推したい

はじめに

こんにちは、Google Cloudでオブザーバビリティを担当しているものです。Cloud Operations suiteをよろしくおねがいします。(宣伝終わり)

この記事はGo Advent Calendar 2021 その1の22日目の記事です。昨日は @sago35tk さんの「ESP32 向けに TinyGo をセットアップする」でした。TinyGoのコアな情報を日本語で教えてくれるtakasagoさんには本当にいつも感謝しています。

さて、今日はGo製のアプリケーションをdockerlessでコンテナ化できるkoの紹介をします。koは本当にイチオシのツールで、みんなに使ってもらいたいのでぜひ使ってください。

github.com

DockerによるGo製アプリのコンテナ化

まず最もポピュラーと思われるDockerを用いた場合のGo製アプリケーションのコンテナ化の方法についておさらいします。Go製のアプリケーションで最もシンプルな構成の場合、go build をして特定のパスに置いた後、アプリケーションが使うポートを EXPOSE して、CMD もしくは ENTRYPOINT でそのパスを指定してあげる、というような形になります。

具体的には、次のような簡単なGoのアプリケーションがあった場合

package main

import (
    "log"
    "net/http"
)

const port = "8888"

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, world!"))
    })
    log.Println("starting server on :" + port)
    if err := http.ListenAndServe(":"+port, nil); err != nil {
        log.Fatalf("error running server: %v", err)
    }
}

Dockerfileとして次のようなものを用意する、ということです。

FROM golang:1.17.5-bullseye as builder
WORKDIR /dist
RUN go build -o server

FROM gcr.io/distroless/base-debian11
WORKDIR /app
COPY --from=builder /dist/server /app/server
EXPOSE 8888
CMD ["/app/server"]

koを使った場合

しかしながら、これだけのためにわざわざDockerをいれたいかと言われたら、自分は入れたくありません!そこでkoを使います。

github.com

本日二回目の埋め込みリンクです。koはGo製アプリケーション専用のDockerlessコンテナビルドツールなのですが、たとえば上のアプリケーションの場合は、go build を叩くかのように、設定ファイル等用意する必要なく、この ko publish コマンド一発でコンテナが作れます。

$ ko publish --local --base-import-paths .
2021/12/22 01:08:18 Using base gcr.io/distroless/static:nonroot for test
2021/12/22 01:08:19 Building test for linux/amd64
2021/12/22 01:08:20 Loading ko.local/test:4a9444968723e9d5d24d25b07aaaa504c9ea8a1921273a3753028e5e6d9f5dc9
2021/12/22 01:08:20 Loaded ko.local/test:4a9444968723e9d5d24d25b07aaaa504c9ea8a1921273a3753028e5e6d9f5dc9
2021/12/22 01:08:20 Adding tag latest
2021/12/22 01:08:20 Added tag latest
ko.local/test:4a9444968723e9d5d24d25b07aaaa504c9ea8a1921273a3753028e5e6d9f5dc9

いろいろオプションが付いていますが、これは気にしないことにして、このコマンドによって ko.local/test というイメージができました。これは go.mod でモジュール名を test にしているからです。また ko.localレジストリ名が自動でprefixになっているということです。( --local オプションをつけたことによる。)

たとえば KO_DOCKER_REPO という環境変数gcr.io/foo/bar を指定して、アプリケーションのモジュール名を github.com/xxx/yyy とした場合には、コンテナイメージ名は gcr.io/foo/bar/github.com/xxx/yyy としてくれる、ということです。

koのコマンドがGo製のアプリケーションをビルドした後、よしなにそれをコンテナ内に配置し、エントリーポイントの設定をしてくれ、設定したコンテナレジストリ向けの名前でイメージを作成してくれます。簡単ですね!

ko のインストール

さて、koは簡単そうだ、という雰囲気がわかったところで、ko自体のインストール方法ですが、これも超簡単です。ko自体もただのGo製ツールですので、Goが手元にインストールされている人であれば

go install github.com/google/ko@latest

これでおしまいです。Dockerも必要ありません。macOSな環境では本当にこれは便利ですね!

当然バイナリ配布もされているので、手元にダウンロードしてくればそれで終わりです。各種方法は公式ドキュメントを見てください。

ko の挙動のカスタマイズ

とりあえず最低限動かせるコマンドを紹介したので、次に ko の挙動を変えたい場合にどうするかを紹介します。たとえば、koで作るコンテナのベースイメージを変更したい場合にはどうしたらいいでしょうか。(デフォルトは gcr.io/distroless/static:nonroot )あるいはGoのビルドの際に必要な環境変数を渡したい場合にはどうしたらいいでしょうか。こうしたカスタマイズは .ko.yaml というYAMLファイルで設定します。例えば次のような具合です。

defaultBaseImage: gcr.io/distroless/base-debian11

builds:
  - id: main
    dir: .
    main: .
    env:
      - CGO_ENABLED=0

ko とコンテナランタイムとの連携

さらに ko が便利なのはコンテナランタイムと連携する場合です。たとえば Kubernetes の deployment のYAML内でコンテナのイメージを指定しますが、そこを ko の記法を使ってコンテナを指定しておくことができます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 3
  ...
  template:
    spec:
      containers:
      - name: my-app
        image: ko://github.com/my-user/my-repo/cmd/app

これを ko resolve -f deployment.yaml で渡してあげると、まず github.com/my-user/my-repo/cmd/app にあるアプリケーションを ko でビルドして、環境変数 KO_DOCKER_REPO で指定しているコンテナレジストリにプッシュして、さらに ko://... の文字列をそのイメージのパスで置き換えるということをした deployment.yaml の値を返してくれます。これを kubectl apply にパイプで渡してあげたりするわけです。

apply をするだけであれば ko apply というショートカットも用意してくれています。とにかく雑にGo製アプリケーションをコンテナ化してどこかにデプロイするのがとても簡単になります。

Dockerに投げる場合でも

docker run --rm -p 8888:8888 $(ko publish --local .)

というようなワンライナーで上げられたり、Cloud Runに投げるときは

gcloud run deploy --image=$(ko publish --local .)

という具合にできます。

ko とツールの連携

さきほどちらりと kubectl との連携させるための ko resolve というコマンドを紹介しましたが、Kubernetesを扱う場合、たとえば自分は skaffold を使ってテスト環境を上げたりしています。

skaffoldでは最近 ko がサポートされたので、skaffold.yaml にkoを指定するだけで一気通貫でコンテナのビルドからKubernetesクラスタへのデプロイまでいけます!

build:
  artifacts:
  - image: my-simple-go-app
    ko:
      fromImage: gcr.io/distroless/base-debian11
      labels:
        org.opencontainers.image.licenses: Apache-2.0
        ...

おわりに

大変雑にkoを紹介しましたが、これは概ね ko の公式ドキュメント(=README)に記載されているものです。

github.com

ただいかんせんぱっと見が分かりづらいと思われるので「本当に簡単に使えます!」ということだけにフォーカスして紹介しました。来月にはDocker Desktopの有料化も始まります。コンテナ化を行うだけであれば、いまや選択肢は数多くありますので、koもその一つとして知っておいて損はないと思います。ぜひ試してみてください。