Airflow資料抄訳(15)：タイムゾーン（Time Zones）

恥ずかしながら最近になって知ったワークフローエンジン [Apache Airflow](https://airflow.apache.org/)。日本語の紹介記事もちらほら出てきていますが、[公式ドキュメント](https://airflow.apache.org/docs/)をちょっとずつ抄訳しながら読んでいこうと思います。

15回目の今回は[タイムゾーン](https://airflow.apache.org/docs/apache-airflow/stable/timezone.html)（Time Zones）。
バージョン2.3.3時点のものです。

---

# タイムゾーン（Time Zones）

タイムゾーン・サポートはデフォルトで有効です。Airflowは日時情報を内部的にUTCで管理します。これによりタイムゾーンに依存したDAG実行が可能となります。今のところ、Airflowは日時情報をUI上でエンドユーザーのタイムゾーンへと変換することはなく、いつでもUTCで表示されます〔註：古い解説？実際とは異なる。少しあとのサブセクションで解説あり〕。オペレーター内で使用されるテンプレートも同様に変換を行いません。タイムゾーン情報をどう利用するかはDAG作成者が決めます。

あなたが運用するAirflowのユーザーが複数のタイムゾーンに属しており、各ユーザーの現地時刻に即して日時情報を表示したいという場合に便利です。

Airflowが単一のタイムゾーンで利用されているとしても、日時情報をUTCで管理するのはよいプラクティスです（Airflowがタイムゾーンに対応するまではこれは推奨ないし必須の設定でもありました）。主な理由は多くの国地域で夏時間（DST）を利用しており、現地時刻は春には前進し秋には後退するためです。ローカル時間で物事を考えていると、この時刻の遷移により、たぶん年に2回エラーに見舞われるわけです。（この件については、Airflowが利用する日時情報を扱うためのライブラリpendulumとpytzのドキュメントにより詳細な議論が掲載されています。）シンプルなDAGでは問題にならないかもしれませんが、例えば一日の終りの時刻を厳守しなくてはならない金融業界のシステムでは問題となるでしょう。

タイムゾーンは`airflow.cfg`で設定します。デフォルトではUTCが指定されていますが、システム時刻のタイムゾーンや任意のタイムゾーン（例えば ` Europe/Amsterdam `）を指定することもできます。Airflowのこの仕組は` pendulum `に依拠しています。このライブラリは`pytz`よりも正確に働きます。` pendulum `はAirflowとともにインストールされます。

## Web UI

デフォルトではWeb UIは日時をUTCで表示します。時刻表示は上方右側のメニュー（時計をクリックすると表示される）から変更できます：

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50571/de4f0c4f-da75-f2b2-a305-de85bc36ce18.png)

「Local」はブラウザのタイムゾーンから判断されます。「Server」はAirflowの設定ファイルの`[core]`セクションで設定された` default_timezone `に基づくものです。

ユーザーが選択したタイムゾーンはブラウザのLocalStorageに保存されます。したがってブラウザごとの設定になります。

:::note warn
Note
Airflowシステム全体のデフォルト・タイムゾーンの設定を変更し、かつ同じタイムゾーンでUIも利用したいという場合、設定ファイルの`[webserver]`セクション内にある` default_ui_timezone `を空文字列にするか、あるいは同じタイムゾーン名を記述してください。

（UIの動作に関してポイント・リリース間で整合性を持たせるため、現時点ではデフォルトはUTCになっています。）
:::

## 概念

### awareな日時とnaiveな日時

Pythonの` datetime.datetime `オブジェクトは`tzinfo`属性を持っています。これは` datetime.tzinfo `のサブクラスのインスタンスにより表現されるタイムゾーン情報を保持するための属性です。この属性に値が設定されUTCからの時刻のオフセットが示されているとき、当該の` datetime `オブジェクトは aware（アウェア）であると言い、そうでない場合はnaive（ナイーブ）であると言います。

日時オブジェクトがいずれであるかは`  timezone.is_localized()`や` timezone.is_naive()`を使って確認できます。

Airflowはタイムゾーンを考慮した日時（awareな日時）を利用します。Airflowに関連するPythonコードで日時オブジェクトを作るときはタイムゾーンを考慮した日時である必要があります。

```py
from airflow.utils import timezone

now = timezone.utcnow()
a_date = timezone.datetime(2017, 1, 1)
```

### naive日時の解釈

Airflowはタイムゾーンを考慮した日時（awareな日時）を使って処理をしますが、DAG定義の`start_dates`や`end_dates `にタイムゾーンを考慮しない日時（naiveな日時）を指定することも可能です。これは後方互換性のために残されているものです。タイムゾーンを考慮しない日時を使用した場合、デフォルトのタイムゾーンにおける日時として解釈されます。例えば、デフォルト・タイムゾーン設定が` Europe/Amsterdam `である場合、`  start_date `にタイムゾーンを考慮しない日時`  datetime(2017, 1, 1) `を指定すると、これは「アムステルダム現地時刻における2017年1月1日」とみなされます。

```py
dag = DAG(
    "my_dag",
    start_date=pendulum.datetime(2017, 1, 1, tz="UTC"),
    default_args={"retries": 3},
)
op = BashOperator(task_id="dummy", bash_command="Hello World!", dag=dag)
print(op.retries)  # 3
```

生憎、夏時間（DST）の遷移中はある日時が存在しなかったり、存在はしても曖昧であったりします。このような状況下では` pendulum `は例外を発生させます。可能なときはいつでもタイムゾーンを考慮した日時オブジェクトを使用すべきです。

実際のところこの制約が問題になることはほとんどありません。AirflowはDAGその他のコード内でタイムゾーンを考慮した日時オブジェクトを提供しており、ほとんどの場合、それらの日時オブジェクトをもとにて時刻差演算をすることで新しい日時オブジェクトが作成されます。それらのオブジェクトはあらかじめオフセットを持ちます。そうではなく一から日時オブジェクトを作成するのはふつう現在時刻を求めるためであり、そのために利用される`timezone.utcnow()`はタイムゾーンを考慮したオブジェクトを生成します。

### デフォルト・タイムゾーン

デフォルト・タイムゾーンは設定ファイルの`[core]`セクションにある`default_timezone`で指定します。インストール直後は`utc`が設定されておりこれが推奨値です。システム時刻のタイムゾーンを示す`system`や任意のタイムゾーン名（例えば、`Europe/Amsterdam`）も指定できます。DAGとその時刻情報はAirflowのワーカー群からアクセスされるので、すべてのノードでこの設定を同期しておく必要があります。

```
[core]
default_timezone = utc
```

:::note warn
Note
Airflowの設定について詳しくは[設定オプション]( https://airflow.apache.org/docs/apache-airflow/stable/howto/set-config.html)を参照ください。
:::

## タイムゾーンを考慮したDAG

タイムゾーンを考慮したDAGを作るのは難しいことではありません。` pendulum `のAPIを利用して` start_date `の値を指定するだけです。標準ライブラリの[timezone]( https://docs.python.org/3/library/datetime.html#timezone-objects)を使用しないでください。このライブラリには既知の制約があるので、DAGの中では使用できないようにしてあります。

```py
import pendulum

dag = DAG("my_tz_dag", start_date=pendulum.datetime(2016, 1, 1, tz="Europe/Amsterdam"))
op = EmptyOperator(task_id="empty", dag=dag)
print(dag.timezone)  # <Timezone [Europe/Amsterdam]>
```

タスクに`start_date`と`end_date`は指定できますが、データ区間（Data Interval）の計算には常にDAGのタイムゾーン設定もしくはグローバルなタイムゾーン設定（前者の方が優先）が使用される点に注意をしてください。`start_date`と`end_date`はまずそれらに関連付けられたタイムゾーン情報を使ってUTCに変換され、その後の処理ではこれらのタイムゾーン情報は無視されます。

###テンプレート

Airflowはテンプレートの中でもタイムゾーンを考慮した日時オブジェクトを返します。それらはローカル時刻に変換されません。その時刻情報をどのように処理するかはDAGに任されます。

```py
import pendulum

local_tz = pendulum.timezone("Europe/Amsterdam")
local_tz.convert(logical_date)
```

### cronスケジュール

cronスケジュールを使用するタイムゾーンを考慮したDAGは夏時間（DST）を踏まえて動作します。例えば、`US/Eastern`タイムゾーンで`0 0 * * * `というスケジュールで開始日時が指定されているDAGは、夏時間の期間中、UTC時刻で毎日04：00に実行される一方、夏時間の期間外にはUTC時刻で毎日05：00に実行されます。

###時差

`timedelta`もしくは`relativedelta`を使用するタイムゾーンを考慮したDAGは、開始日時に関して夏時間（DST）を踏まえて動作します。後続の実行スケジュールを決めるにあたって夏時間の調整を行いません。例えば`start_date`が`pendulum.datetime(2020, 1, 1, tz="UTC")`で `schedule_interval`が`timedelta(days=1)`であるとき、当該DAGは夏時間を顧慮せずUTC時刻で毎日05：00に実行されます。

---

日本国内で利用する場合、（今のところ）夏時間はあまり気にしなくてよいでしょうが、そもそものUTCとタイムゾーンの部分はきちんとユーザーに理解してもらう必要がありそうです。