Zenn
⏱️

Apline Linux の Docker コンテナで、Go の time.LoadLocation が失敗する仕組み【調査ログ】

2025/04/10に公開

はじめに

Go で文字列からタイムゾーンをパースするときに、実行環境起因でエラーとなりました。調べると Alpine Linux 環境で発生するようでした。今回、なぜエラーが発生するのか調べたメモを公開します。

調査過程をそのまま書いています。ライブ感を少しでも楽しんでいただけますと嬉しいです。また、調査先の記事の内容はよしなに日本語訳しています。

エラー内容

実装

例えば、以下のように文字列から time.LoadLocation にてタイムゾーンをパースする処理があるとします。

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 イメージ環境で実行します。

本番環境でのコンテナ起動を想定し、マルチステージビルドを使用しています。実行環境のベースイメージが apline である点に注意してください。

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 --from=builder /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 パッケージのインストールです。

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 --from=builder /app/main .
+ RUN apk add --no-cache tzdata

  CMD ["/app/main"]
Go のコードはこちら
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 です。

main.go
  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 のコードはこちら
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 --from=builder /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

ログインするとコメントできます