何もしてないのにtimezoneが壊れまして候

# 久しぶりにdocker buildした

```docker
FROM golang:1.11.9-alpine as build

WORKDIR /go/app

RUN apk add --no-cache musl-dev git gcc

COPY . .

RUN go build -o app

FROM alpine:latest

RUN apk add --no-cache musl-dev git gcc tzdata curl

WORKDIR /app

COPY --from=build /go/app/app .

RUN apk --no-cache add tzdata ca-certificates \
  && cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
  && addgroup go \
  && adduser -D -G go go \
  && chown -R go:go /app/app

EXPOSE 8080

CMD ["./app"]
```


goでmysqlに接続するアプリをdockerで動作させていました。以下のようなデータが混ざったJSONを返すAPI Serverだと思ってください

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31388/c7ef344c-605b-9bc0-e1bb-0a69bb0fbd68.png)

特に問題も無くコードの変更もしてないのですが、ちょっとした変更がしたくて久しぶりにdocker buildを走らせた結果いきなりUTCに変わりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31388/6c2f0bab-c00c-ffd5-6f54-555e78900120.png)


# 接続情報等の確認

+ mysqlへ接続していたので
+ https://github.com/go-sql-driver/mysql
+ 接続情報になにか変更が入ってしまったのかと思い確認にいきましたが
+ ちゃんとAsia/Tokyoになっています

```go
parseTime=True&loc=Asia%2FTokyo
```

+ 段々意味が分からなくなってきたので
+ いったんtimezone周りの状況を整理しました

# goのlocalどんな感じに決まってるのか

+ 文字列が出力されるときに呼び出される
+ initLocalという関数の中で決まる
+ TZ環境変数
    + 無い場合
        + /etc/localtimeを参照してこちらの情報を元にtimezoneを決定する
    + 有る場合
        + TZ環境変数の中身とtime.zoneSourcesにあるディレクトリのリストを順に調査
        + 該当した場合はその情報を元にtimezoneを決める

```go
 21 var zoneSources = []string{
 22     "/usr/share/zoneinfo/",
 23     "/usr/share/lib/zoneinfo/",
 24     "/usr/lib/locale/TZ/",
 25     runtime.GOROOT() + "/lib/time/zoneinfo.zip",
 26 }
```

+ なのでTZ環境変数をしていなくても
+ tzdataをinstallさえして
+ /etc/localtimeへリンクをはるなりしておけば期待するtimezoneになります

## mysql driverの場合

+ mysql driverでlocを設定した場合
+ どうなるのか。
+ dsn.goで
+ time.LoadLocationへlocに設定した値が渡されて
+ configに設定されます

```go
450         // Time Location
451         case "loc":
452             if value, err = url.QueryUnescape(value); err != nil {
453                 return
454             }
455             cfg.Loc, err = time.LoadLocation(value)
456             if err != nil {
457                 return
458             }
```
+ ZONEINFO環境変数などが設定されていない場合に関しては
+ locに設定した値を元に
+ loadLocationが呼び出される

```go
656     if z, err := loadLocation(name, zoneSources); err == nil {
657         return z, nil
658     } else if firstErr == nil {
659         firstErr = err
660     }
661     return nil, firstErr
```

+ この場合もzoneSourcesのどこかに期待するtimezoneの情報があれば
+ 正常に動作します

# この時点で分かっていること

+ mysqlの取得する部分でtimezoneがおかしい問題がおきている
+ alpineでtzdataをinstallしていること
+ tzdata以外に参照するデータは無いこと

# とても怪しいところ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31388/c481e9e4-a596-c6cd-9091-4cde642faa9c.png)


+ 勘の良い方なら早々に気づいていたかと思いますが
+ 気づかないうちにalpineのバージョンが上がっていたようです
+ alpine:3.12.1にその時点で上がっていました

## alpine3.12.1でtzdataに何が起きたのか

+ tzdataが2020aから2020cへ変更されています
+ https://github.com/alpinelinux/aports/compare/v3.12.0...v3.12.1

```diff
diff --git a/main/tzdata/APKBUILD b/main/tzdata/APKBUILD
index 9725639106..f70b03cab1 100644
--- a/main/tzdata/APKBUILD
 b/main/tzdata/APKBUILD
@@ -2,8 ,8 @@
 # Contributor: Natanael Copa <ncopa@alpinelinux.org>
 # Maintainer: Natanael Copa <ncopa@alpinelinux.org>
 pkgname=tzdata
-pkgver=2020a
-_tzcodever=2020a
pkgver=2020c
_tzcodever=2020c
```

## tzdata2020a-2020cでは何が起きたのか

+ こちらのコミットに詳しいですが
+ https://github.com/eggert/tz/compare/2020a...2020c
+ https://github.com/eggert/tz/commit/6ba6f2117b95eab345a7ed9159cef939e30c4cd3
+ tzdataはzicというコンパイラを用いてバイナリデータに変換される
+ このzicのオプションにbがあり
    + https://man7.org/linux/man-pages/man8/zic.8.html
+ fatからslimへ変更されました
+ golang側でもこのissueでも扱われています
+ https://github.com/golang/go/issues/42138

# どう解決したのか

+ その時点でalpineのバージョンを有るところまで戻して固定する
+ もしくはTZを指定してtzdataを入れず zoneinfoを自前で入れた場合以下を見ます
+ runtime.GOROOT() + "/lib/time/zoneinfo.zip",
+ こちらへfat版を貰ってきます
+ 後者の方法をとることにしました

## goの lib/time/zoneinfo.zipを確認する

+ https://github.com/golang/go/commits/master/lib/time/zoneinfo.zip
+ こちらを確認すると
+ 特定のcommitでslimが入っています
+ なので特定のバージョンを落としてきます

```shell
export GOROOT=/usr/local/go
export TZ=Asia/Tokyo
curl -Lso/usr/local/go/lib/time/zoneinfo.zip https://github.com/golang/go/blob/5c9a8c0761ae643828a4526db764ac7a50a1a24d/lib/time/zoneinfo.zip?raw=true
```

## 確認

+ 無事指定のtimezoneになりました

```go
package main

import (
	"fmt"
	"time"
)

func main() {
	now := time.Now()
	fmt.Printf("%v\n", now)
}
```

```
2020-12-17 18:20:51.857490357 +0900 JST m=+0.000876904
```

# まとめ

+ 何もしてないのに壊れたになるので
+ latestでの運用はやめましょう
+ goのversionもあげておかないとdebugger使えなかったりして大変なのであげましょう

# 追記(重要)

## この問題は現在おきていません

+ ここを見て貰うと分かりますがfatに戻されています
+ https://github.com/alpinelinux/aports/commits/3.12-stable?after=434125893a86436a80f54545797bd57a667a1994+69&branch=3.12-stable

```docker
FROM golang:1.11.9-alpine as build

WORKDIR /go/app

RUN apk add --no-cache musl-dev git gcc

COPY . .

RUN go build -o app

FROM alpine:3.12.1

WORKDIR /app

COPY --from=build /go/app/app .

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

+ こちらで実行するとJSTになります

```
2020-12-17 18:31:55.460003303 +0900 JST m=+0.003871447
```


## goのslim対応はmasterに入っている

+ https://github.com/golang/go/commit/d83168eb385f7ae38710028fff5c84252267adbc#diff-831889cfa5253b4dd6d3df29582717acc1c9d0d83eeb34b579e6c322b0bf4c01
+ 恐らくこの次のバージョンぐらいに入るんじゃ無いかと期待しています