0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

バイナリ1つだけのscratchコンテナでタイムゾーンもHTTPS通信も扱いたい

0
Last updated at Posted at 2026-06-20

はじめに

私は、コンテナイメージについてイメージサイズと同梱されるファイル数が小さければ小さいほど美しいと考えています。

ですが、この美学を阻むのが各種ライブラリです。

例えば、glibcといった基本的なライブラリ。世界中のタイムゾーンを扱うためのtzdata
そして昨今の通信では欠かせないHTTPS通信を行うためのca-certificatesなどです。

本記事では、これらのパッケージをバイナリに埋め込んだものを作成し、scratchコンテナイメージにファイル1つをコピーするだけで動作するシングルバイナリの作り方をGo言語を使用して説明します。

記事要約

  • Goを使うとglibcに依存しないシングルバイナリが作れる
  • Goのtime/tzdataパッケージを使うとタイムゾーン情報をバイナリに埋め込める
  • golang.org/x/crypto/x509roots/fallbackパッケージを使うとCA証明書をバイナリに埋め込める

本記事で目指すゴール

以下の項目を全て満たすシングルバイナリを作ることをゴールとします

  • シングルバイナリで起動してexample-go-scratch-containerと表示できること
  • 現在時刻を取得して日本時間としてRFC3339形式で表示できること
  • https://example.comにアクセスしてHTTPステータスである200 OKが返ってくること

成功した場合には以下のような出力が出るものとします。

example-go-scratch-container
now: 2026-06-20T15:44:23+09:00
200 OK

使用した環境

  • macOS Tahoe 26.5.1
  • Go 1.26.3
  • docker: 29.2.0

プログラムを作る

まずは以下のようなプログラムを作成します。

main.go
package main

import (
	"fmt"
	"net/http"
	"time"
)

func main() {
	fmt.Println("example-go-scratch-container")
	jst, err := time.LoadLocation("Asia/Tokyo")
	if err != nil {
		panic(err)
	}
	now := time.Now().In(jst)
	fmt.Printf("now: %s\n", now.Format(time.RFC3339))

	resp, err := http.Get("https://example.com")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	fmt.Println(resp.Status)
}

こちらをgolang:1.26.3-trixieイメージでビルドして実行してみましょう。

Dockerfile
FROM golang:1.26.3-trixie

WORKDIR /build
COPY . .
RUN go build -trimpath -ldflags="-s -w" -o example-go-scratch-container .
ENTRYPOINT [ "/build/example-go-scratch-container" ]
$ docker build -t example-go-scratch-container:0.0.0 .
$ docker run -it --rm example-go-scratch-container:0.0.0
example-go-scratch-container
now: 2026-06-20T16:25:50+09:00
200 OK

ゴールの要件を満たすバイナリが作成できました。

それではこちらをDockerのマルチステージビルドを使用して、先ほどと同じイメージでビルドします。
ビルドされたバイナリをscratchイメージの中に同梱して実行してみましょう。

Dockerfile
FROM golang:1.26.3-trixie AS build

WORKDIR /build
COPY . .
RUN go build -trimpath -ldflags="-s -w" -o example-go-scratch-container .

FROM scratch

COPY --from=build /build/example-go-scratch-container /example-go-scratch-container
ENTRYPOINT [ "/example-go-scratch-container" ]
$ docker build -t example-go-scratch-container:0.1.0 .
$ docker run -it --rm example-go-scratch-container:0.1.0
exec /example-go-scratch-container: no such file or directory

no such file or directoryと表示されますね。ファイルがあるのか確認してみましょう。

entrypointを書き換えて、存在しないファイルを指定して実行してみました。

$ docker run -it --rm --entrypoint="" example-go-scratch-container:0.1.0 /aaa
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "/aaa": stat /aaa: no such file or directory

Run 'docker run --help' for more information

本当にファイルがない場合にはstat /aaa: no such file or directoryと表示されるみたいです。
つまり、ビルドされたファイルはコンテナ内にありそうです。

では、ビルドされたexample-go-scratch-containerがどのようになっているのか確認してみましょう。
先ほど動作確認をしたイメージを使用して確認してみます。

$ docker run -it --rm --entrypoint="" example-go-scratch-container:0.0.0 sh
# ldd /build/example-go-scratch-container
        linux-vdso.so.1 (0x0000f7e29b1c3000)
        libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000f7e29afc0000)
        /lib/ld-linux-aarch64.so.1 (0x0000f7e29b186000)

libc.so.6を動的リンクして動作しているようですね。
scratchイメージはなにもないイメージのため、このように動的リンクしているバイナリは動作しません。
先ほどのno such file or directoryも、ライブラリが見当たらないことを指し示しているのかもしれません。

CGO_ENABLED=0を使って静的バイナリを作成する

GoにはCGO_ENABLEDという環境変数があります。
こちらをCGO_ENABLED=0と設定してビルドすると、使用するライブラリを内包した静的バイナリが作成できます。
こちらの環境変数を使用してイメージを作り直してみましょう。

Dockerfile
 FROM golang:1.26.3-trixie AS build

 WORKDIR /build
 COPY . .
-RUN go build -trimpath -ldflags="-s -w" -o example-go-scratch-container .
+RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o example-go-scratch-container .

 FROM scratch

 COPY --from=build /build/example-go-scratch-container /example-go-scratch-container
 ENTRYPOINT [ "/example-go-scratch-container" ]

イメージをビルドしなおして実行してみます。

$ docker build -t example-go-scratch-container:0.2.0 .
$ docker run -it --rm example-go-scratch-container:0.2.0
example-go-scratch-container
panic: unknown time zone Asia/Tokyo

goroutine 1 [running]:
main.main()
        example-go-scratch-container/main.go:13 +0x1e4

実行したところ、example-go-scratch-containerと表示されました。
しかし、その後の時刻表示とHTTPS通信の部分は実行できていません。
これらを修正していきましょう。

結果:

  • :white_check_mark: シングルバイナリで起動してexample-go-scratch-containerと表示できること
  • :x: 現在時刻を取得して日本時間としてRFC3339形式で表示できること
  • :x: https://example.comにアクセスしてHTTPステータスである200 OKが返ってくること

time/tzdataでタイムゾーンをバイナリに組み込む

先ほどのイメージを実行した際のエラーを見てみましょう。

panic: unknown time zone Asia/Tokyo

goroutine 1 [running]:
main.main()
        example-go-scratch-container/main.go:13 +0x1e4

unknown time zone Asia/Tokyoと表示されています。

実は、タイムゾーンを扱う際にはtzdataというパッケージが必要になります。
このパッケージには世界中のタイムゾーンに関するデータが入っており、基本的なマシンにはこのパッケージがインストールされています。
Goではこのパッケージを参照し、そこからタイムゾーンのデータを取得するようになっています。
scratchイメージにはタイムゾーンの情報はどこにもないため、エラーしたという状況です。

ところが、Go 1.15にてtime/tzdataというパッケージが追加されました。

このパッケージには名前の通り、tzdataパッケージに相当するデータが組み込まれています。
このパッケージをブランクインポートすることにより、tzdataパッケージ相当のデータをバイナリに組み込むことができます。

main.goimportを以下のように編集します。

main.go
 import (
        "fmt"
        "net/http"
        "time"
+       _ "time/tzdata"
 )

ではこちらのmain.goをビルドし、scratchイメージにてバイナリを実行してみましょう。

$ docker build -t example-go-scratch-container:0.3.0 .
docker run -it --rm example-go-scratch-container:0.3.0
example-go-scratch-container
now: 2026-06-20T17:22:32+09:00
panic: Get "https://example.com": tls: failed to verify certificate: x509: certificate signed by unknown authority

goroutine 1 [running]:
main.main()
        example-go-scratch-container/main.go:21 +0x1c4

バイナリが実行され、日本時間の現在時刻も取得できました。
ですが、HTTPS通信のところでエラーしています。
こちらを修正していきましょう。

結果:

  • :white_check_mark: シングルバイナリで起動してexample-go-scratch-containerと表示できること
  • :white_check_mark: : 現在時刻を取得して日本時間としてRFC3339形式で表示できること
  • :x: https://example.comにアクセスしてHTTPステータスである200 OKが返ってくること

golang.org/x/crypto/x509roots/fallbackでCA証明書をバイナリに組み込む

先ほどのエラーを確認しましょう。

panic: Get "https://example.com": tls: failed to verify certificate: x509: certificate signed by unknown authority

goroutine 1 [running]:
main.main()
        example-go-scratch-container/main.go:21 +0x1c4

HTTPS通信を行う際、サーバーが提示してきた証明書の検証ができなくて失敗しています。
サーバーが提示してきた証明書を検証する際には、ca-certificatesというパッケージに同梱されているCA証明書を使って検証します。
こちらもtzdataと同様に、GoではOSに同梱されているCA証明書を探して見つかったものを使用します。
もちろんこちらもscratchイメージには存在しないため、検証用のCA証明書が見つからずにエラーしたという状況です。

Goプロジェクトのサブプロジェクトとしてgolang.org/x/crypto/x509roots/fallbackというパッケージがあります。
こちらもtime/tzdataと同様にCA証明書の内容を組み込むことができます。

main.goのimportを次のように編集します。

main.go
 import (
        "net/http"
        "time"
        _ "time/tzdata"
+
+       _ "golang.org/x/crypto/x509roots/fallback"
 )

こちらは外部モジュールとなるのでgo mod tidyなんかを忘れずに。

ではこのmain.goをビルドして、scratchコンテナ上で動かしてみましょう。

$ docker build -t example-go-scratch-container:0.4.0 .
$ docker run -it --rm example-go-scratch-container:0.4.0
example-go-scratch-container
now: 2026-06-20T17:43:39+09:00
200 OK

バイナリが実行され、日本時間の現在時刻も取得でき、HTTPS通信もできるようになりました。
これにて、本記事のゴールを達成することができました。

結果:

  • :white_check_mark: シングルバイナリで起動してexample-go-scratch-containerと表示できること
  • :white_check_mark: : 現在時刻を取得して日本時間としてRFC3339形式で表示できること
  • :white_check_mark: https://example.comにアクセスしてHTTPステータスである200 OKが返ってくること

さいごに

本記事ではGo言語のCGO_ENABLED=0time/tzdatagolang.org/x/crypto/x509roots/fallbackを使うことによって、タイムゾーンやHTTPS通信が行えるシングルバイナリを作成していきました。

このシングルバイナリはscratchイメージに同梱しても動くため、とても美しいコンテナイメージが完成したと思います。

そしてGoのクロスコンパイルの容易性や、可搬性なんかが考えられている側面に触れられたと思います。

もちろん、この手法についてのデメリットもあります。
それは本来OSで共有すべきデータをバイナリに組み込んでいるため、バイナリ自体の容量が大きくなってしまいます。
せっかくなので今回ビルドしたイメージを用いて簡単に比較してみましょう。

 イメージ名 説明  イメージサイズ
example-go-scratch-container:0.1.0 scratchにコピー(動的リンク) 7.81MB
example-go-scratch-container:0.2.0 CGO_ENABLED=0を設定 7.73MB
example-go-scratch-container:0.3.0 CGO_ENABLED=0, time/tzdataを追加 8.21MB
example-go-scratch-container:0.4.0 CGO_ENABLED=0, time/tzdata, golang.org/x/crypto/x509roots/fallbackを追加 8.45MB

time/tzdataを追加すると480KBほど増加し、golang.org/x/crypto/x509roots/fallbackを追加すると240KBほど増加するみたいですね。
とはいえGoのバイナリはランタイム自体が大きいので、これらのパッケージ追加によって増える増分を考えると微々たるものとして考えてもいいかもしれません。

それでは皆さん、よい美しいコンテナイメージライフをお過ごしくださいませ。

今回使用したコード

付録

軽量で有名なalpineイメージと、Googleが提供している必要最小限のdistroless/staticに同梱してみたときのイメージサイズと同梱ファイル数を比較してみます。
distroless/staticにはca-certificatestzdataが同梱されています。
しかし、alpineイメージにはデフォルトでca-certificatesは入っていますが、tzdataに相当するものは入っていません。こちらはapkにて導入して、イメージを測定することにします。

ベースイメージ ビルド設定 イメージサイズ  同梱ファイル数
gcr.io/distroless/static-debian13:latest CGO_ENABLED=0のみ 14.1MB 1378
alpine:3.24.1 CGO_ENABLED=0のみ 23MB 1134
alpine:3.24.1 golang1.26.3-alpine3.23でビルド。CGO_ENABLED=1 23 MB 1134
(参考) scratch CGO_ENABLED=0, time/tzdata, golang.org/x/crypto/x509roots/fallbackを追加 8.45MB 1

どれも一般的なコンテナイメージとしては小さめですが、やっぱりscratchでビルドしたものにはイメージサイズと同梱ファイル数どちらも最小となる結果になりました。
というか意外とdistroless/static-debian13ってファイル数が多いんですね。

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?