先にまとめ
Go言語のtime.LoadLocation("Asia/Tokyo")
のような処理は、プログラム以外の外部環境のタイムゾーンのデータに依存しているそうです。
そのため、Dockerのマルチステージビルドなどで、Goのプログラムをピュアなalpine上で実行している場合、alpineにはタイムゾーンのデータが含まれておらず、実行プログラムから参照できなくunknown time zone Asia/Tokyo
というエラーになります。
Go 1.15でこのタイムゾーンのデータをビルド成果物に埋め込むことが可能になったそうです。ビルド時のオプションで-tags timetzdata
と付けることで、このデータが埋め込まれた形でビルドされ、alpine上で実行したときにもエラーが出なくなります。
問題の再現
東京のタイムゾーンで現在時刻を取得するプログラムを、簡単に以下のように作って実行してみます。
package main
import (
"fmt"
"time"
)
func main() {
loc, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
fmt.Println("!!ERROR!! ", err)
return
}
fmt.Println("Time: ", time.Now().In(loc))
}
ローカルにGoの環境があれば、実行すると画像のように現在時刻の情報が出力されます。
開発環境用にDockerの実行環境とdocker composeの設定も用意しておきます。
FROM golang:1.23.4-alpine3.21
WORKDIR /app
COPY ./main.go .
CMD go run main.go
services:
develop:
build:
context: .
dockerfile: ./dev.Dockerfile
image: test-golang-tzdata-dev
docker compose build
をして、docker compose run --rm develop
を実行してみるとこちらも現在時刻が表示されました。
おっけーそう👌、ということで本番用の実行環境をマルチステージビルドを利用して作ります。
################## ビルドステージ ##################
FROM golang:1.23.4-alpine3.21 AS builder
WORKDIR /app
COPY ./main.go .
RUN go build -o main main.go
################## 実行ステージ ##################
FROM alpine:3.21
WORKDIR /app
# ビルドしたバイナリをコピー
COPY --from=builder /app/main .
CMD /app/main
今回は検証用に、docker composeで本番環境も実行できるようにしておきます。
services:
develop:
build:
context: .
dockerfile: ./dev.Dockerfile
image: test-golang-tzdata-dev
+ product:
+ build:
+ context: .
+ dockerfile: ./prod.Dockerfile
+ image: test-golang-tzdata-prod
改めてdocker compose build
をしておいて、今度はproduct
のサービスを指定してdocker compose run --rm product
と実行してみます。すると今回はエラーになってしまいました!
原因
上の記事やスライドなどを読んでいると、プログラム中のtime.LoadLocation("Asia/Tokyo")
関数は、どうも外部依存(Goのプログラム以外への依存)があるそうです。具体的はOSなどに用意されているタイムゾーンの情報が書かれたファイルを読み込みます。Go言語自体のソースコードの中にもこの情報のデータは入っているようです。
これに対して、今回本番環境で使ったDockerのベースイメージはAlpine Linuxというものです。極端に言えば、できるだけ依存パッケージ等のファイルを減らし、セキュリティリスクの低減やリソース効率を高めたイメージです。
普通のUbuntuなどのディストリビューションであればタイムゾーンの情報がOSに用意されていましたが、Alpineではこのタイムゾーンの情報も含まれていないようで、参照できる情報が無いためunknown time zone
というエラーになってしまっていたようです。
開発環境のDockerコンテナも、golang:1.23.4-alpine3.21
というベースイメージでAlpine上のGo言語の環境で実行していましたが、ここにはGo言語自体のソースが含まれているため、その中に含まれるタイムゾーンデータを参照していたものと思われます。
対策
先ほどの記事などを参照すると、Goの1.15からこのタイムゾーンの情報をビルドの成果物に含められるオプションが追加されたようです。-tags timetzdata
というオプションをビルド時に付け、以下のように記述します。
################## ビルドステージ ##################
FROM golang:1.23.4-alpine3.21 AS builder
WORKDIR /app
COPY ./main.go .
- RUN go build -o main main.go
+ RUN go build -tags timetzdata -o main main.go
################## 実行ステージ ##################
FROM alpine:3.21
WORKDIR /app
# ビルドしたバイナリをコピー
COPY --from=builder /app/main .
CMD /app/main
これで実行すると、今回はエラーにならずに実行することができました。