コンテナのzoneinfoとGoのLocationについて

以前書いたAlpine Linuxで時刻をJSTに設定する(Dockerfile)で、コンテナ内の時刻をJSTとして扱えるようにした。

しかし、以下が気になった。

  • コンテナのイメージ作成においてタイムゾーンを変更するような記述を見かけたことがなかった
  • コンテナのタイムゾーンを変更しなくても、アプリ内でJSTとして扱うことはできるので、不要なセットアップではないか

ということでアプリケーション(Go)の方で対処できないか、検証してみた。

結論

結果、以下のような挙動となった。

  • time.LoadLocationはコンテナの中に該当するzoneinfoがないと失敗する
    • Alpine LinuxのデフォルトではAsia/Tokyoなどは存在しないため確実に失敗する
    • /etc/localtime/usr/share/zoneinfo/Asia/Tokyoなどで上書きされていても、 /usr/share/zoneinfo/Asia/Tokyo自体がないとやはり失敗する
  • time.FixedLocationは直接time.Locationを生成するためzoneinfoの有無の影響を受けない

軽量コンテナを使用する際にはUTCを前提とするか、time.FixedLocationを使用した方が良さそう

もしJSTをデフォルトとしたコンテナを作成したかったら、以下のようなDockerfileにすべき。

FROM alpine:latest

# インストールしたtzdataは削除しない
RUN apk --no-cache add tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

以下、検証した内容。

Goのプログラム

以下のように4回時刻を出力するようにした。

package main

import (
    "fmt"
    "time"
)

func main() {
    t := time.Now()
    fmt.Println(t)

    jst, err := time.LoadLocation("Asia/Tokyo")
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(t.In(jst))
    }

    jst = time.FixedZone("JST", 9*60*60)
    fmt.Println(t.In(jst))

    time.Local = jst
    fmt.Println(time.Now())

}

表示は以下のようになると予想していた。

  • time.Nowではコンテナのタイムゾーンに基づいた時刻表示
  • t.In(jst)ではJSTの時刻表示がされる
    • time.LoadLocationでerrが起きるとは思ってなかった
  • 最後のtime.NowではJSTの時刻が表示される

バイナリは以下のようにしてtime-testとして出力しておく。

GOOS=linux GOARCH=amd64 go build -o time-test

検証1: zoneinfoを特に設定しない

まずはコンテナを作っとく。Dockerfileは以下の通り。

FROM alpine:latest
 
COPY ["time-test", "/"]
CMD [ "/time-test" ]

docker buildして、jst-time-test1というタグにしておく。

docker build -t jst-time-test1 -f Dockerfile1 .

docker runで実行する。

$ docker run --rm jst-time-test1
2018-08-30 00:36:56.4097601 +0000 UTC m=+0.000328601
open /usr/local/Cellar/go/1.9.2/libexec/lib/time/zoneinfo.zip: no such file or directory
2018-08-30 09:36:56.4097601 +0900 JST
2018-08-30 09:36:56.4098805 +0900 JST m=+0.000448301

最初はUTCが出力され、最後はJSTが出力された。しかしtime.LoadLocationではエラーが出力された。

open /usr/local/Cellar/go/1.9.2/libexec/lib/time/zoneinfo.zip: no such file or directoryと表示されるのは、time.LoadLocationの説明にあるとおりで、

The time zone database needed by LoadLocation may not be present on all systems, especially non-Unix systems. LoadLocation looks in the directory or uncompressed zip file named by the ZONEINFO environment variable, if any, then looks in known installation locations on Unix systems, and finally looks in $GOROOT/lib/time/zoneinfo.zip.

最終的に$GOROOT/lib/time/zoneinfo.zipを参照するということなので、システムのzoneinfoもないし、$GOROOT/lib/time/zoneinfo.zipもない(GoのSDKは存在しない)ためエラーとなった様子。

検証2: JSTをローカルタイムに設定しzoneinfoを削除した状態

今度はAlpine Linuxで時刻をJSTに設定する(Dockerfile)で設定した環境で、dateコマンドだとJSTが表示される状態になっている。Dockerfileは以下の通り。

FROM alpine:latest

RUN apk --no-cache add tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    apk del tzdata

COPY ["time-test", "/"]
CMD [ "/time-test" ]

docker buildする。

docker build -t jst-time-test2 -f Dockerfile2 .

実行する。

$ docker run --rm jst-time-test2
2018-08-30 09:37:09.7907743 +0900 JST m=+0.000254901
open /usr/local/Cellar/go/1.9.2/libexec/lib/time/zoneinfo.zip: no such file or directory
2018-08-30 09:37:09.7907743 +0900 JST
2018-08-30 09:37:09.7908918 +0900 JST m=+0.000372901

最初はJSTが出力され、最後はJSTが出力された。しかしtime.LoadLocationではエラーが出力された。

このコンテナでは/etc/localtimeは上書きされているが、zoneinfo(/usr/share/zoneinfo/Asia/Tokyo)がないから、やはりエラーとなった様子。

検証3: JSTをローカルタイムに設定しzoneinfoを残した状態

検証2の結果から、Dockerfileを以下のように修正した。

FROM alpine:latest

# 前回実施していた apk del tzdata を消去
RUN apk --no-cache add tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

COPY ["time-test", "/"]
CMD [ "/time-test" ]

docker buildする。

docker build -t jst-time-test3 -f Dockerfile3 .

実行してみる。

$ docker run --rm jst-time-test3
2018-08-30 09:37:15.3019175 +0900 JST m=+0.000325101
2018-08-30 09:37:15.3019175 +0900 JST
2018-08-30 09:37:15.3019175 +0900 JST
2018-08-30 09:37:15.3020498 +0900 JST m=+0.000457301

今度はうまくいった!やはり参照するzoneinfoがなかったからエラーになっていたのだろう。

time.FixedLocationを使用した場合にはすべて問題なく実行できたのも確認できた。

終わりに

今回検証した辺りのzoneinfo(tzdata)について詳しくなかったので、勉強になった。

とりあえず今後は

package main

import "time"

func init() {
    time.Local = time.FixedZone("JST", 9*60*60)
}

としておくようにしよう。