Azure Function(C#)の動作環境のタイムゾーンとWEBSITE_TIME_ZONEで変更できるか調査

背景
-----------

掲題の件について、AppServiceのドキュメントでは以下のようにあります。

https://docs.microsoft.com/en-us/azure/app-service/faq-configuration-and-management#how-do-i-set-the-server-time-zone-for-my-web-app-

アプリケーション設定 `WEBSITE_TIME_ZONE` で指定ができる。設定値はOSで異なります。

- Windows: `Tokyo Standard Time` (`tzutil /L`より)
- Linux: `Asia/Tokyo` (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)

さて、App Serviceの上で動作しているAzure Functionではどうなのかというと、同様にドキュメントがありますが、**Linuxの従量課金はサポートしていない**とのこと。

https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-app-settings#website_time_zone

では確認してみましょう、というのがこの記事です。AzureではデフォルトのタイムゾーンはUTCになっているとはよく聞きますが、そこもついでに。



### Azure Functionの環境情報

- OS: Windows/Linux
- ランタイム: .NET Core 3.1 (C#)
- プラン: 従量課金/Premium
- リージョン: 東日本


先に結果
---------

ドキュメント通り、**Linuxの従量課金のみWEBSITE_TIME_ZONEの設定が効かない**ことがわかりました。(2022/01/18)

デフォルトのタイムゾーンはUTCになっているようです。
WEBSITE_TIME_ZONE設定は、Windowsの従量課金、WindowsのPremium、LinuxのPremiumでできます。
またリージョンは関係ありませんでした。


| Region | OS      | Plan     | WEBSITE_TIME_ZONE   | "timezone(Id)"      |
| ------ | ------- | -------- | ------------------- | ------------------- |
| 東日本 | Linux   | 従量課金 | （なし）            | Etc/UTC             |
| 東日本 | Linux   | 従量課金 | Asia/Tokyo          | **Etc/UTC**         |
| 東日本 | Windows | 従量課金 | （なし）            | UTC                 |
| 東日本 | Windows | 従量課金 | Tokyo Standard Time | Tokyo Standard Time |
| 東日本 | Linux   | Premium  | （なし）            | Etc/UTC             |
| 東日本 | Linux   | Premium  | Asia/Tokyo          | Asia/Tokyo          |
| 東日本 | Windows | Premium  | （なし）            | UTC                 |
| 東日本 | Windows | Premium  | Tokyo Standard Time | Tokyo Standard Time |


検証
--------------

### 検証用コード

`func new`でC#用のHTTP Triggerのテンプレートを作り、少し書き換えてJSONを返すようにしました。

```cs
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Collections.Generic;

namespace az_function_datetime
{
    public static class ViewDateTime
    {
        [FunctionName("ViewDateTime")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            var now = DateTime.Now;
            var utcNow = DateTime.UtcNow;
            var timezone = TimeZoneInfo.Local;

            var envWebsiteTimeZone = Environment.GetEnvironmentVariable("WEBSITE_TIME_ZONE") ?? "(none)";

            return new OkObjectResult(new Dictionary<string, object>{
                ["now"] = now,
                ["utcNow"] = utcNow,
                ["timezone"] = timezone,
                ["envWebsiteTimeZone"] = envWebsiteTimeZone
            });
        }
    }
}
```

### func startでローカル実行（システム時間=日本）

``` json
{
  "now": "2022-01-18T13:17:10.6813074+09:00",
  "utcNow": "2022-01-18T04:17:10.681313Z",
  "timezone": {
    "Id": "Tokyo Standard Time",
    "DisplayName": "(UTC+09:00) 大阪、札幌、東京",
    "StandardName": "東京 (標準時)",
    "DaylightName": "東京 (夏時間)",
    "BaseUtcOffset": "09:00:00",
    "AdjustmentRules": null,
    "SupportsDaylightSavingTime": false
  }
}
```

まぁ、概ね期待通りの動作ですね。

### 各環境検証結果

このレスポンスをOS * Plan * 設定有無の8通りでレスポンスがどうなかったを調べたのが以下です。
思ったよりtimezoneinfoがノイズになってしまったので、gistにまとめました。結果は[最初に書いた通り](#先に結果)です。

https://gist.github.com/fukasawah/c8999bbf00bcee6f130149fe4df8d831

そういえば、ランタイム毎の違いはあるんでしょうか？私はAzure FunctionではC#で生きていくと決めたので調査はしません・・・


### 蛇足： C#のDateTime型はめんどくさい

C#の[DateTime](https://docs.microsoft.com/en-us/dotnet/api/system.datetime?view=net-6.0)型はtickと呼ばれる西暦0001年1月1日00:00:00からの100マイクロ秒単位の経過時間(62bit)と、[DateTimeKind](https://docs.microsoft.com/ja-jp/dotnet/api/system.datetimekind?view=net-6.0)(2bit)で日時を表現します。

**タイムゾーンやオフセットなどの情報は持っていないため**、Kind値が異なるDateTime型同士は正しく比較できません。（DateTimeOffset型はオフセットがありオフセットを考慮した比較も可能です。）

その代わり、DateTimeKindで「2=Local(システム時間)」「1=UTC」「0=Unspecified(その他)」の3つの区分値を使い、何の時間かを表現しています。
DateTime.Nowで得られる日時は「Local」になります。DateTime.UtcNowで得られる日時は「Utc」になります。UTCはわかりますね。Localは何でしょうか。
例えばOSなどのシステムのタイムゾーンが日本時間なら、「Local」は日本時間と見なされ、UTC時間よりも9時間(32400秒)進んでいることになります。Kindが異なる場合はこの差を足し引きしてLocalとUtcを変換できるようになっています。
実際、DateTime.Now.Ticks - DateTime.UtcNow.Ticksとすると、約324000000000Tickが得られます。（同じ時間なはずですが、そういう仕組みになっています。）
このおかげでLocalかUTCでの間であれば、[DateTime.ToUniversalTime](https://docs.microsoft.com/en-us/dotnet/api/system.datetime.touniversaltime?view=net-6.0)や[DateTime.ToLocalTime](https://docs.microsoft.com/en-us/dotnet/api/system.datetime.tolocaltime?view=net-6.0)を使うことで相互の変換は正しく行えます。

しかし時間はシステム時間とUTC時間だけではありません。
なので、[TimeZoneInfo](https://docs.microsoft.com/en-us/dotnet/api/system.timezoneinfo?view=net-6.0)の[ConvertTime](https://docs.microsoft.com/en-us/dotnet/api/system.timezoneinfo.converttime?view=net-6.0)を使うことで任意のタイムゾーンの変換をサポートしています。あるタイムゾーンに変換を行うと、システムorUTC→あるタイムゾーンの時差の差を元にしてTicksを再計算し、新しいDateTimeを返します。しかし、その瞬間DateTimeKindはUnspecifiedになります。
そして、ここから先は自己責任の世界となり、責任をもって「あるタイムゾーンのDatetime」として運用しなければなりません。
このDateTimeをうっかり、Utc時間やLocal時間に変換すると予期しない結果になります。これを戻すには、またあるタイムゾーンを使って[TimezoneInfo.ConvertTimeToUtc](https://docs.microsoft.com/en-us/dotnet/api/system.timezoneinfo.converttimetoutc?view=net-6.0)で一旦Utc時間に変換することです。

こんな感じでC#のDateTimeは小難しい事情を抱えています。わかればなんてことはないんですが、DateTimeという1個の型みてそこまで見抜くのは至難の業だと思います。もしUnspecifiedなDateTimeを見かけたら不幸に思いましょう。

というわけで（Unspecifiedの下りは全く関係なかったですが）、C#の「システム時間」は動作環境によるため、Azure Functionでの挙動を調べてみたかったということでした。

C#のDateTimeに疲れたら、[Nodatime](https://nodatime.org/)を使ってみるといいかもしれません。

（C#に限らず時間の扱いは言語、フォーマット、システムどれをとっても面倒なので別途まとめたい。素晴らしい記事は既にたくさんあるけども。）