【Go言語】本番環境でだけ`unknown time zone`というエラーが出る

## 先にまとめ
Go言語の`time.LoadLocation("Asia/Tokyo")`のような処理は、**プログラム以外の外部環境のタイムゾーンのデータに依存している**そうです。

そのため、Dockerのマルチステージビルドなどで、Goのプログラムをピュアなalpine上で実行している場合、alpineにはタイムゾーンのデータが含まれておらず、実行プログラムから参照できなく`unknown time zone Asia/Tokyo`というエラーになります。

Go 1.15でこのタイムゾーンのデータをビルド成果物に埋め込むことが可能になったそうです。ビルド時のオプションで`-tags timetzdata`と付けることで、このデータが埋め込まれた形でビルドされ、alpine上で実行したときにもエラーが出なくなります。

## 問題の再現
東京のタイムゾーンで現在時刻を取得するプログラムを、簡単に以下のように作って実行してみます。

```go:main.go
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の環境があれば、実行すると画像のように現在時刻の情報が出力されます。

![ローカルでの実行結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/3d119fad-935b-0284-b344-0029e6e0f04e.png)

開発環境用にDockerの実行環境とdocker composeの設定も用意しておきます。

```docker:dev.Dockerfile
FROM golang:1.23.4-alpine3.21

WORKDIR /app

COPY ./main.go .
CMD go run main.go
```

```yaml:compose.yaml
services:
  develop:
    build:
      context: .
      dockerfile: ./dev.Dockerfile
    image: test-golang-tzdata-dev
```

`docker compose build`をして、`docker compose run --rm develop`を実行してみるとこちらも現在時刻が表示されました。

![Dockerfileを使った開発環境での実行結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/96f14200-38dd-a1b9-2586-bdf066348144.png)

おっけーそう👌、ということで本番用の実行環境をマルチステージビルドを利用して作ります。

```docker:prod.Dockerfile
################## ビルドステージ ##################
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で本番環境も実行できるようにしておきます。

```diff_yaml:compose.yaml
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`と実行してみます。すると今回はエラーになってしまいました！

![Dockerfileを使った本番環境での実行結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/e9ee515f-d784-154f-ad3c-b00293bcd6c8.png)

## 原因

https://tutuz-tech.hatenablog.com/entry/2021/01/30/192956

https://techblog.szksh.cloud/golang-time/

https://speakerdeck.com/hiroakis/go-false-timezone-to-go-1-dot-15-false-tzdata-mai-meip-mi

上の記事やスライドなどを読んでいると、プログラム中の`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`というオプションをビルド時に付け、以下のように記述します。

```diff_docker:prod.Dockerfile
################## ビルドステージ ##################
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
```

![Dockerfileを使った本番環境での実行結果（修正後）](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351104/02390ff1-5157-f1ce-880e-661e413cc0c5.png)

これで実行すると、今回はエラーにならずに実行することができました。