Docker で /etc/localtime をホストに volume マウントしてハマった話

# /etc/localtime は迂闊に volume マウントしてはいけない。

[「タイムゾーンをホストと合わせるために /etc/localtime をホストに volume マウントしましょう (`-v /etc/localtime:/etc/localtime:ro`)」](https://www.google.com/search?q="%2Fetc%2Flocaltime%3A%2Fetc%2Flocaltime") という記事があちこちで見られます。
私もそれに従って volume マウントしていたのですが、そのためにハマった話です。

# under the hood で何が起きていたのか
Linux の特定のディストリビューションの特定のバージョンでは、`/etc/localtime` が `/usr/share/zoneinfo/Etc/UTC` (UTC の zoneinfo の実体) に symlink (シンボリックリンク) されています。そんな `/etc/localtime` をホストに volume マウントするとどうなるか? symlink である `/etc/localtime` がホストの `/etc/localtime` に置き換わると思いきや、実はリンク先のファイルが置き換わる、つまり、**UTC の zoneinfo の中身が JST のそれに置き換わってしまう**のです。
少しだけ詳しいことをこっちに書きました。
[「Docker で symlink をホストに volume マウントしたら...」](https://qiita.com/jeffi7/items/88a62e15abbf044e48a3)

```bash
# cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
# docker run --rm -it -v /etc/localtime:/etc/localtime:ro ubuntu:xenial-20190610 bash
root@ec52ace0eced:/# ls -l /etc/localtime
lrwxrwxrwx 1 root root 27 Jun 11 05:40 /etc/localtime -> /usr/share/zoneinfo/Etc/UTC
root@ec52ace0eced:/# cat /etc/localtime
TZiｫ?ﾝ
         ｼ
          ~~JDTJSTTZif2
                                e?p・ﾛｫ??ｼ
                                                                              LMTJDTJST
JST-9
root@ec52ace0eced:/# cat /usr/share/zoneinfo/Etc/UTC
TZiｫ?ﾝ
         ｼ
          ~~JDTJSTTZif2
                                e?p・ﾛｫ??ｼ
                                                                              LMTJDTJST
JST-9
```

# そしてどうなったか

[使っていた PostgreSQL のコンテナイメージ(非オフィシャル)](https://hub.docker.com/r/sameersbn/postgresql/) は、[9.6 のときのベースイメージが Ubuntu 14.04 (Trusty)](https://github.com/sameersbn/docker-postgresql/blob/9.6/Dockerfile) でした。Trusty の `/etc/localtime` は UTC の zoneinfo の実体のコピーだったので、中身が JST である ホストの `/etc/localtime` を volume マウントしても問題ありませんでした。
その状態のまま PostgreSQL のイメージを 10 にバージョンアップしたら問題が起きました。[ベースイメージが Ubuntu Xenial に上がっていた](https://github.com/sameersbn/docker-postgresql/blob/10/Dockerfile)のです。
(後で調べてわかったわけですが) その結果上述のように UTC の zoneinfo の中身が JST のそれに置き換わってしまい、セッションの timezone が UTC なのに `now()` や `timestamp with timezone` なフィールドの値が JST で返ってくるようになりました。
PostgreSQL を呼んでいたアプリ(某OSS) では、表示のタイムスタンプが9時間前になる、認可トークンのタイムスタンプが9時間前になって発行時点で既に期限が切れている、といった現象が発生しました。[^1]

[^1]: PostgreSQL 側の現象とアプリ側の現象が矛盾しているというか、時差のプラスマイナスが逆な気がします。そこの辻褄はアプリ(OSS) 側のロジックをもう少し分析しないとわかりませんが、現時点ではそこまでやれていません。

# ディストリビューションによる相違

いろいろなディストリビューションの `/etc/localtime` がどうなっているか、ざっと調べてみました。
下記で `判定` に × を付けたディストリビューション/バージョンは上記の問題が発生します。

|distribution|version|/etc/localtime|/usr/share/zoneinfo/Etc/UTC|判定|
|------------|-------|--------------|---------------------------|:--:|
|ubuntu|cosmic|なし|なし|
|ubuntu|bionic|なし|なし|
|ubuntu|xenial|/usr/share/zoneinfo/Etc/UTCへのsymlink|なし|×|
|ubuntu|trusty|UTCの実体|あり|
|debian|stretch|/usr/share/zoneinfo/Etc/UTCへのsymlink|あり|×|
|debian|jessie|UTCの実体|あり|
|debian|wheezy|UTCの実体|あり|
|alpine|3.1 - 3.5|UTCの実体|なし|
|alpine|3.6 - 3.9|なし|なし|
|centos|7.4.1708|../usr/share/zoneinfo/UTCへのsymlink|あり|×|
|centos|7.2.1511|../usr/share/zoneinfo/UTCへのsymlink|あり|×|

詳しく調べてはいないのですが、ディストリビューションにバンドルされている tzdata パッケージの版数によるようです。

# どうするのがよいのか

結論を先に言うと、いろいろググってみても諸説あって決定的な正解がみつかっていません。ディストリビューション毎に推奨の方法が異なっているようにも見えます。
上で問題になった ubuntu:xenial で `-e TZ=Asia/Tokyo` を付けて `docker run` してみましたが効果がありませんでした。というわけで、どのような方法にせよ、Dockerfile に細工して build する必要だけはありそうです。
究極の正解は UTC のまま使うことなんですが、ログのタイムスタンプを読み替えるのがめんどい....