ECS上のJava(JVM)アプリケーションで時刻が9時間ずれる問題の原因と対策

に公開

はじめに

この記事では、AWS ECS上で動作するJavaアプリケーション(SpringBoot)で、OSのタイムゾーンは日本時間(JST)なのに、アプリケーションが取得する時刻が9時間ずれる(UTCになる)という問題に遭遇し、解決した際の技術的なノウハウを記録します。

この記事で伝えたいこと

まず最初に伝えたいこと。

  • Dockerコンテナ上でJavaアプリケーションを動かす際は、OSのタイムゾーン設定だけでは不十分な場合がある。
  • JVM自体のタイムゾーンを明示的に設定する必要がある。

TL;DR

  • 問題: DockerコンテナのOSのタイムゾーンをJSTに設定しても、JVMがUTCで動作してしまう。
  • 原因: JVMのタイムゾーンは、OSの設定とは別に決定されるため。
  • 解決策: Javaの実行時オプション -Duser.timezone=Asia/Tokyo を追加するか、環境変数 TZ=Asia/Tokyo を設定する。

発生した問題

AWS ECSで動作させているJavaアプリケーションにおいて、「データベースに記録される時刻が9時間ずれている」という報告がありました。

具体的には、アプリケーションが LocalDateTime.now() などで現在時刻を取得しDBに保存する処理で、期待される日本時間(JST)ではなく、協定世界時(UTC)で記録されていました。

原因調査と切り分け

9時間のズレから、タイムゾーン設定が原因であると仮説を立て、以下の2段階で調査を行いました。

Step 1: コンテナOSのタイムゾーン確認と設定

まず、問題が発生しているコンテナのOS自体のタイムゾーンを確認しました。ECS Exec を使ってコンテナに入り、dateコマンドを実行したところ、UTC と表示されました。

# ECS Execでコンテナに入る
$ aws ecs execute-command --cluster ... --task ... --command "/bin/bash" --interactive

# コンテナ内でタイムゾーンを確認
$ date
Wed Jul  3 22:00:00 UTC 2024 # ← UTCになっている!

この問題を解決するため、DockerfileにOSのタイムゾーンをJSTに設定する処理を追加しました。

Dockerfile
# 環境変数を設定し、tzdataのインストールを非対話的にする
ENV TZ=Asia/Tokyo
ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && \
    apt-get install -y --no-install-recommends tzdata && \
    # 後片付け
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Note: apt-get install tzdata を実行すると対話形式で地域選択を求められることがあるため、ENV DEBIAN_FRONTEND=noninteractive を設定しておくのが定石のようです。また、ENV TZ=Asia/Tokyo を設定しておくと、OSだけでなく多くのプログラムがこのタイムゾーンを参照するようになります。

この修正後、コンテナの date コマンドは正しくJSTを指すようになりました。

Step 2: OS設定だけでは解決せず → JVMのタイムゾーン設定へ

しかし、アプリケーションを再デプロイしても、DBに記録される時刻は依然としてUTCのままでした。

ここで、「OSのタイムゾーンとJVMのタイムゾーンは別物である」という重要なポイントにたどり着きました。JVMは起動時にOSからデフォルトのタイムゾーンを継承しようとしますが、環境によってはそれがうまく機能せず、デフォルトのUTCが使用されることがあります。

そのため、JVMに対して明示的にタイムゾーンを指定する必要があります。

解決策

調べたところ、以下のいずれかの方法でJVMが使用するタイムゾーンをJSTに設定できることがわかりました。

解決策1: Java実行時オプションで指定する(今回採用した方法)

DockerfileCMD または ENTRYPOINT で、Javaの実行時オプション -Duser.timezone を追加します。

Dockerfile
# ... (省略) ...
CMD ["java", "-Duser.timezone=Asia/Tokyo", "-jar", "/data/app.jar"]

この修正を加えてデプロイしたところ、無事にアプリケーションが取得する時刻がJSTになり、問題は解決しました。

解決策2: 環境変数 JAVA_TOOL_OPTIONS を利用する

コンテナの実行環境(ECSのタスク定義など)で環境変数を設定できる場合、こちらの方法がより柔軟です。JAVA_TOOL_OPTIONS は、JVM起動時に自動的に読み込まれる環境変数です。

task-definition.json
"environment": [
  {
    "name": "JAVA_TOOL_OPTIONS",
    "value": "-Duser.timezone=Asia/Tokyo"
  }
]

この方法のメリットは、Dockerfileを再ビルドすることなく、環境変数でJVMの挙動を制御できる点です。

まとめ

Dockerコンテナ上でJavaアプリケーションを動作させる際のタイムゾーン問題は、「OSのタイムゾーン」と「JVMのタイムゾーン」の両方を意識する必要があります。

  • OSのタイムゾーン: ENV TZ=Asia/Tokyo などで設定する。
  • JVMのタイムゾーン: -Duser.timezone=Asia/Tokyo オプションで明示的に指定する。

この記事をまとめる時点で解決策2が存在することがわかりました。当時は本番環境で気づいた事象で、急いで対処する必要がありすぐに解決できる解決策1を採用しました。

ただ、特にECSのような環境では、コンテナの再利用性やポータビリティを考慮し、環境変数(TZJAVA_TOOL_OPTIONS)でタイムゾーンを外部から注入する設計が望ましいでしょう。(今なら、こちらの解決策2を採用すると思います。)

参考

GitHubで編集を提案

Discussion