今回は、TimeZoneを利用するコードを書いていたときに、Dockerコンテナ(Alpine Linux)上で`Asia/Tokyo’のローケーションが取得できなかったので、その理由や対処方法について紹介したいと思います。

Alpine Linuxとは 

Alpine Linuxは、軽量なLinuxディストリビューションの一つです。軽量なので、Dockerのコンテナサイズを小さくしたい場合などによく利用します。

timezoneを見るコード

timezoneを使うサンプル


package main
import (
        "log"
        "time"
)
func main() {
    loc,err := time.LoadLocation("Asia/Tokyo")
    if err != nil {
        log.Fatal("%w", err)
    }
    log.Printf("%v", loc)
}

このサンプルをmacなどで実行すると下記のような形でローケーションが取得できています。

$ 2021/01/19 23:26:18 Asia/Tokyo

Dockerfile


FROM golang:1.15 AS builder

WORKDIR /go/src/

ENV GOPATH="/go" \
    GO111MODULE="on"

ADD main.go .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o sample main.go

FROM alpine:latest

COPY --from=builder /go/src/sample .
CMD ["./sample"]

# build
$ docker build -t time-zone-sample .  
# run
$ docker run time-zone-sample                                                                                                                                                                                                                                     2021/01/19 14:28:07 erro %wunknown time zone Asia/Tokyo

このような形でローケーションがわからないというエラーになります。

Filesystem Hierarchy Standard(FHS)を思い出してみる

LinuxなどのUnix系OSでは、主なファイル構造が定義されているのです。そのあたりから時間関連に対応するものは、下記のようなものがあります。

  • /etc/localtime
  • /usr/share/zoneinfo/

/etc/localtimeというタイムゾーンを設定するファイルです。私が普段利用しているmacOSでは、下記のようになっています。

$ ls -al /etc/localtime                                                                                                                                                                                                                           
lrwxr-xr-x  1 root  wheel  36 12 16 02:24 /etc/localtime@ -> /var/db/timezone/zoneinfo/Asia/Tokyo

alpine linuxの実行結果です。

$ docker run -it --rm alpine /bin/ash
# ls -al /etc/localtime
ls: /etc/localtime: No such file or directory

/usr/share/zoneinfo/ディレクトリには、各タイムゾーンに対応したファイルが格納されています。私が普段利用しているmacOSでは、下記のようになっています。

$ ls /usr/share/zoneinfo/                                                                                                                                                                                                                                         
/usr/share/zoneinfo/@
$ ls /usr/share/zoneinfo/Asia/Tokyo 

alpine linuxの実行結果です。 こちらも存在しないです。

$ docker run -it --rm alpine /bin/ash
 # ls -al /usr/share/zoneinfo
ls: /usr/share/zoneinfo: No such file or directory

LoadLocation()の実装は、下記のようになっています。UTC, Localのタイムゾーンは、取得できるが、ZoneInfoから取得するようになっています。


// LoadLocation returns the Location with the given name.
  //
  // If the name is "" or "UTC", LoadLocation returns UTC.
  // If the name is "Local", LoadLocation returns Local.
  //
  // Otherwise, the name is taken to be a location name corresponding to a file
  // in the IANA Time Zone database, such as "America/New_York".
  //
  // 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.
  func LoadLocation(name string) (*Location, error) {
      if name == "" || name == "UTC" {
          return UTC, nil
      }
      if name == "Local" {
          return Local, nil
      }
      if containsDotDot(name) || name[0] == '/' || name[0] == '\\' {
          // No valid IANA Time Zone name contains a single dot,
          // much less dot dot. Likewise, none begin with a slash.
          return nil, errLocation
      }
      zoneinfoOnce.Do(func() {
          env, _ := syscall.Getenv("ZONEINFO")
          zoneinfo = &env
      })
      var firstErr error
      if *zoneinfo != "" {
          if zoneData, err := loadTzinfoFromDirOrZip(*zoneinfo, name); err == nil {
              if z, err := LoadLocationFromTZData(name, zoneData); err == nil {
                  return z, nil
              }
              firstErr = err
          } else if err != syscall.ENOENT {
              firstErr = err
          }
      }
      if z, err := loadLocation(name, zoneSources); err == nil {
          return z, nil
      } else if firstErr == nil {
          firstErr = err
      }
      return nil, firstErr
  }

対策

timezoneが存在しないので、インストールしてあげるとうい方法と、UTCであるということ日本時間がUTCから+9時間という性質を使ってあげるという2つの方法があります。

timezoneをインストールする方法

dockerのマルチステージビルドを使っている場合は、下記のようにビルドステージのゾーンファイルをコピーすることで解決できます。


FROM golang:1.15 AS builder

WORKDIR /go/src/

ENV GOPATH="/go" \
    GO111MODULE="on"

ADD main.go .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o sample main.go

FROM alpine:latest

COPY --from=builder /go/src/sample .
COPY --from=builder /usr/share/zoneinfo/Asia/Tokyo /usr/share/zoneinfo/Asia/Tokyo
CMD ["./sample"]

UTCであるということ日本時間がUTCから+9時間する  

下記のようにLocalのロケールを作成し、Azia/Tokyoのロケールを利用しようといる部分のロケールを’Local’からとるようにする。


package main
import (
	"time"
)
func main() {
    time.Local = time.FixedZone("Local", 9*60*60)
    loc, _ := time.LoadLocation("Local")
}

まとめ

Azia/Tokyoタイムゾーンがunknowになった理由などをFHSと紐付けながら、問題に対処してみました。お疲れ様でした。