Apline Linux の Docker コンテナで、Go の time.LoadLocation が失敗する仕組み【調査ログ】
はじめに
Go で文字列からタイムゾーンをパースするときに、実行環境起因でエラーとなりました。調べると Alpine Linux 環境で発生するようでした。今回、なぜエラーが発生するのか調べたメモを公開します。
調査過程をそのまま書いています。ライブ感を少しでも楽しんでいただけますと嬉しいです。また、調査先の記事の内容はよしなに日本語訳しています。
エラー内容
実装
例えば、以下のように文字列から time.LoadLocation
にてタイムゾーンをパースする処理があるとします。
package main
import (
"fmt"
"time"
)
func main() {
loc, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
panic(err)
}
fmt.Printf("loc = %v", loc)
}
この関数を、以下のような Docker イメージ環境で実行します。
本番環境でのコンテナ起動を想定し、マルチステージビルドを使用しています。実行環境のベースイメージが apline である点に注意してください。
FROM golang:1.24.2-bookworm AS builder
WORKDIR /app
COPY main.go .
ENV CGO_ENABLED=0
RUN go build -a -o main main.go
FROM alpine:3.21.3
WORKDIR /app
COPY /app/main .
CMD ["/app/main"]
エラー
以下のコマンドで Docker イメージをビルドし、実行してみましょう。
$ docker build -t go-timezone-sample .
$ docker run go-timezone-sample
panic: unknown time zone Asia/Tokyo
goroutine 1 [running]:
main.main()
/app/main.go:11 +0xac
エラー文はこの部分ですね。
unknown time zone Asia/Tokyo
おさらいすると、Go のコードは次のとおりでした。
time.LoadLocation("Asia/Tokyo")
Asia/Tokyo
は正しい TZ を表す文字列のように思えますが、なぜエラーが発生するのでしょうか?
なぜ発生するのか
順番に情報を整理しつつ、調べてみましょう。
Go の time.LoadLocation
そもそも、time.LoadLocation
はどのような関数なのでしょうか?公式ドキュメントの time.LoadLocation の説明を読んでみましょう。
LoadLocation は、与えられた名前の Location を返します。
名前が "" または "UTC" の場合、LoadLocation は UTC を返します。名前が "Local" の場合、LoadLocation は Local を返します。
それ以外の場合は、"America/New_York" のような、IANA タイムゾーンデータベースのファイルに対応するロケーション名とみなされます。
LoadLocation は、IANA タイムゾーン・データベースを以下の場所から順に探します。
- ZONEINFO 環境変数によって命名されたディレクトリまたは解凍されたzipファイル
- Unixシステム上では、システム標準のインストール場所
- $GOROOT/lib/time/zoneinfo.zip
- time/tzdataパッケージ(インポートされている場合)
time.LoadLocation
は、名前が空文字などの特殊な場合を除き、引数から IANA タイムゾーンデータベースのファイルを参照しているとのことです。さらに、IANA タイムゾーンデータベースは以下の順番で探索されるようです。
- ZONEINFO 環境変数によって命名されたディレクトリまたは解凍されたzipファイル
- Unixシステム上では、システム標準のインストール場所
- $GOROOT/lib/time/zoneinfo.zip
- time/tzdata パッケージ(インポートされている場合)
Unixシステム上では、システム標準のインストール場所
がよくわかりません。1~3つめはローカルのファイルを指しており、4つめは Go の time/tzdata パッケージを指しています。
状況をまとめると、引数の Asia/Tokyo
に一致する IANA タイムゾーンデータベースのファイルが存在せず、unknown time zone Asia/Tokyo
エラーが発生しているようです。つまり、ローカルに TimeZone を読み取るためのファイルが存在しないか、Go の time/tzdata パッケージを import していないがために、発生したエラー と言えそうです。
筆者は聞きなれない言葉だったのですが、そもそも IANA タイムゾーンデータベースとは何でしょうか?
IANA タイムゾーンデータベース
調べてみると、どうやら IANA という組織が管理しているデータベースのことを指しているようでした。IANA は Internet Assigned Numbers Authority の略称であり、IANA の公式サイト には次のように書かれていました。
DNSルート、IPアドレス、およびその他のインターネットプロトコルリソースのグローバルな調整は、インターネット割り当て番号機関(IANA)の機能として実行されます。
その名の通り「インターネット割り当て番号機関」のようですね。肝心のデータベースについては、IANA 公式サイトの Time Zone Database に次のように書かれています。
タイムゾーンデータベース (Time Zone Database) には、世界中の代表的な場所の現地時間の歴史を表すコードとデータが含まれています。タイムゾーンの境界線、UTCオフセット、サマータイム規則など、政治的機関による変更を反映するため、定期的に更新される。その管理手順は BCP 175: Procedures for Maintaining the Time Zone Database に文書化されている。
Asia/Tokyo
などの文字列が格納されたデータベースのようですね。IANA が管理し、策定しているようです。
さて、エラーを解決するためには、IANA タイムゾーンデータベースを以下の4箇所に格納する、もしくはデータベースに Asia/Tokyo
の値を保存したいのでした。
- ZONEINFO 環境変数によって命名されたディレクトリまたは解凍されたzipファイル
- Unixシステム上では、システム標準のインストール場所
- $GOROOT/lib/time/zoneinfo.zip
- time/tzdataパッケージ(インポートされている場合)
これ以上は「IANA タイムゾーンデータベース」で調べても情報がなさそうでした。エラー文で調べてみましょう。
Alpine の tzdata パッケージ
調べると Timezones failing to load in Go 1.13 がヒットしました。単純に tzdata というパッケージをインストールすれば良いとのことです。
$ apk add --no-cache tzdata
ここで結論を書きますが、tzdata パッケージのインストールでエラーは解決しました。
tzdata パッケージとはなんでしょうか? Apline のパッケージとして検索すると tzdata - Alpine Linux packages がヒットしましたが、具体的な説明はありません。サイト内に配置されている プロジェクトへのリンク をクリックすると、なんと先ほどの IANA の サイト (Time Zone Database) に遷移しました。
公式サイトからこれ以上の情報を得ることは難しそうです。tzdata パッケージをインストールすると問題が解決することから、IANA が配布している公式の IANA タイムゾーンデータベースということになりそうです。
Go の time/tzdata パッケージ
Go の time.LoadLocation
が、IANA タイムゾーンデータベースを探索する順序は次のとおりでした。
- ZONEINFO 環境変数によって命名されたディレクトリまたは解凍されたzipファイル
- Unixシステム上では、システム標準のインストール場所
- $GOROOT/lib/time/zoneinfo.zip
- time/tzdataパッケージ(インポートされている場合)
4つめの方法も調べてみましょう。time/tzdata パッケージのドキュメントには以下の説明があります。
パッケージtzdataはタイムゾーンデータベースの埋め込みコピーを提供します。このパッケージがプログラムのどこかにインポートされている場合、timeパッケージがシステム上でtzdataファイルを見つけることができなければ、この埋め込まれた情報を使用します。
このパッケージをインポートすると、プログラムのサイズが約450KB大きくなります。
このパッケージは通常、ライブラリではなく、プログラムのメイン・パッケージによってインポートされるべきです。ライブラリは通常、プログラムにタイムゾーン・データベースを含めるかどうかを決定すべきではありません。
このパッケージは、-tags timetzdata を付けてビルドすると自動的にインポートされます。
プログラムのサイズ容量が450KB大きくなることは、デメリットかもしれません。また、Apline Linux 以外のすでに IANA タイムゾーンデータベースが存在する環境では、無意味にプログラムのサイズを増やすことに繋がりそうです。
どうすればよいのか
tzdata パッケージのインストール
方法の1つめは tzdata パッケージのインストールです。
FROM golang:1.24.2-bookworm AS builder
WORKDIR /app
COPY main.go .
ENV CGO_ENABLED=0
RUN go build -a -o main main.go
FROM alpine:3.21.3
WORKDIR /app
COPY /app/main .
+ RUN apk add --no-cache tzdata
CMD ["/app/main"]
Go のコードはこちら
package main
import (
"fmt"
"time"
)
func main() {
loc, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
panic(err)
}
fmt.Printf("loc = %v", loc)
}
こちらで Docker イメージをビルドして実行すると、成功となりました🎉
$ docker build -t go-timezone-sample .
$ docker run go-timezone-sample
loc = Asia/Tokyo
time/tzdata パッケージの使用
もう1つの方法が time/tzdata パッケージの import です。
package main
import (
"fmt"
"time"
+ _ "time/tzdata"
)
func main() {
loc, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
panic(err)
}
fmt.Printf("loc = %v", loc)
}
Dockerfile のコードはこちら
FROM golang:1.24.2-bookworm AS builder
WORKDIR /app
COPY main.go .
ENV CGO_ENABLED=0
RUN go build -a -o main main.go
FROM alpine:3.21.3
WORKDIR /app
COPY /app/main .
CMD ["/app/main"]
こちらも Docker イメージをビルドして実行すると、成功となりました🎉
$ docker build -t go-timezone-sample .
$ docker run go-timezone-sample
loc = Asia/Tokyo
おわりに
そもそも、タイムゾーンの取得を time.LoadLocation
で行う設計意図は何でしょうか?おそらく「タイムゾーン文字列は IANA が管理するものであり、将来的に増減する可能性がある」からだと感じました。そのため、time パッケージに定数として用意するのではなく、利用者側で都度パースするようにしたのだと思います。time パッケージに定数として埋め込むことを避けることにより、古い Go のバージョンでも最新のタイムゾーンを使用することができるようになります。デメリットとして外部リソース (ホストマシンの IANA タイムゾーンデータベース) に依存する必要性が発生しますが、拡張性を優先した形なのだと理解しています。
またシンプルな疑問として、他のプログラミング言語ではどのように対応してるのでしょうか?Alpine Linux は軽量ベースイメージのため、IANA タイムゾーンデータベースさえ削っていることがわかりました。今回のケースと同様に、他のプログラミング言語も参照すべきタイムゾーンデータベースが存在しないことになります。また別の機会に調べてみようと思います。
毎度のごとく長くなってしまいました。どなたかのお役に立てましたら幸いです。
Discussion