しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure Functions におけるロガーの扱いとフィルタの注意点

最近の Azure Functions 開発ではバインディングやトリガーなどに必要なものだけをメソッドのパラメータとして定義して、それ以外は全て DI を使ってコンストラクタから取るようにしていますが、Azure 上のみロガーから Application Insights に送信されない現象にはまったので残します。

結局のところは仕様で、ちゃんとドキュメントにも記載されているのですが、特定の環境では問答無用で Application Insights にログが出力される問題があったため解決に無駄な時間を要しました。

恐らく一番シンプルな DI を使ったロガーのパターンは以下のようなものになります。

Azure Functions は歴史的にメソッドのパラメータとして ILogger を取るようになっていたため、たまに下位のメソッド呼び出し時にも ILogger を渡し続けていくコードが書かれたこともありました。

しかし今は DI 対象のクラスはコンストラクタで ILogger<T> を受け取れるので、ログを有効にしたまま Function の実装からさらに別クラスのメソッドを呼び出すのも簡単です。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddSingleton<IGreetingService, GreetingService>();
    }
}

public class GreetingService : IGreetingService
{
    public GreetingService(ILogger<GreetingService> logger)
    {
        _logger = logger;
    }

    private readonly ILogger<GreetingService> _logger;

    public string SayHello(string name)
    {
        _logger.LogInformation($"Invoke {nameof(SayHello)} method with {name}.");

        return $"Hello, {name}!";
    }
}

サービスクラスは抽象化された ILogger<T> だけ受け取れれば良いので、ASP.NET Core と Azure Functions 間での共有が簡単に出来るようになっているのも大きなポイントです。

実際に Web API は ASP.NET Core で実装しつつも、タイマーやイベントドリブンが必要な処理は Azure Functions を使う例で、同じ実装を DI 経由で利用できるのがかなり便利でした。

Function の実装クラスでは DI に登録したクラスを受け取りつつ、ILogger<T> も同時にコンストラクタで受け取るようにします。以下の例ではログの出力の違いを説明するためにメソッドにも ILogger を用意していますが、本質的には不要です。

public class Function1
{
    public Function1(IGreetingService greetingService, ILogger<Function1> logger)
    {
        _greetingService = greetingService;
        _logger = logger;
    }

    private readonly IGreetingService _greetingService;
    private readonly ILogger<Function1> _logger;

    [FunctionName("Function1")]
    public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req, ILogger log)
    {
        _logger.LogInformation("Logging from DI ILogger<T>.");

        log.LogInformation("Logging from Bind ILogger.");

        var message = _greetingService.SayHello(req.Query["name"]);

        return new OkObjectResult(message);
    }
}

この実装を Azure に作成した Function App に対してデプロイして実行してみると、バインディング経由で受け取った ILogger の内容しか Application Insights に出力されていないことが分かりますね。

f:id:shiba-yan:20200919173531p:plain

ちなみに重要になるのがログのカテゴリです。Azure Functions はデフォルトで出力するログカテゴリを内部で保持しているので、それ以外のカテゴリは明示的に設定する必要があります。

f:id:shiba-yan:20200919174239p:plain

もう原因は分かっているので host.json を弄って、今回のアプリケーション名前空間がカテゴリになっているログで Information 以上のレベルの場合は Application Insights に送信するようにします。

{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingExcludedTypes": "Request",
      "samplingSettings": {
        "isEnabled": true
      }
    },
    "logLevel": {
      "FunctionApp16": "Information"
    }
  }
}

修正後に再度デプロイを行い、実行してみると今度はちゃんと全てのログが出力されています。

f:id:shiba-yan:20200919174814p:plain

カテゴリが名前空間 + クラス名になっていることも確認できます。

f:id:shiba-yan:20200919174824p:plain

これだけ見ると簡単な話だったのですが、最悪なことに Azure Functions Runtime 3.0.14492 より以前のバージョンは、名前空間が Function から始まればフィルタをすり抜けてしまう問題がありました。

今回は検証のために Visual Studio で新しく Function App を作ったのですが、デフォルトの名前空間が FunctionApp** なので正しく再現することが出来なかったというオチです。

しかも 2 週間ほど前に修正が行われて、この問題ではまった次の日にはデプロイされて直りました。

最後になりましたが、ローカル開発環境では何もしなくても全てのログが出力されます。

f:id:shiba-yan:20200919174115p:plain

最近のランタイムアップデートで --verbose を付けないと Function が実行されたことすら表示されなくなったので、正直少し戸惑っています。少し注意が必要だと感じています。

新しい Azure Table SDK がリリースされていたので試した

Cosmos DB の Premium Table な SDK を使えと言われ続けてきた Table Storage ですが、予告されていた通りに新しい Azure SDK の仕様に則った専用の SDK が公開されました。

Blobs や Queues と同じような API になっているので、特にドキュメントを読まずとも IntelliSense だけを見て利用できました。機能は今のところ最低限という感じです。

ドキュメントとサンプルコードは Azure SDK の repo に用意されていますが、用意されている API が分かりやすいので自分は読む必要なかったです。

Azure Functions 周りが v11 SDK ベースになっているのであまり移行する気にはならないですが、将来的には Storage Extension も新しい Azure SDK ベースになるはずなので軽く触っておきました。

基本となる Table へのエンティティ追加を行うコードを書きました。特に説明は必要ないでしょう。

class Program
{
    static async Task Main(string[] args)
    {
        var tableServiceClient = new TableServiceClient("UseDevelopmentStorage=true");
        var tableClient = tableServiceClient.GetTableClient("sample");

        await tableClient.CreateIfNotExistsAsync();

        for (int i = 0; i < 100; i++)
        {
            await tableClient.AddEntityAsync(new TableEntity("sample", Guid.NewGuid().ToString())
            {
                { "IntValue", i },
                { "StrValue", "buchizo" }
            });
        }
    }
}

古い SDK にも TableEntity クラスは存在していましたが、所詮 PartitionKeyRowKey などの必須プロパティが実装されているだけのクラスでした。

しかし新しい SDK では IDictionary<string, object> が実装されているので、専用のエンティティクラスを用意する必要なくカジュアルに扱えるようになっています。

上のコードを実行すると、Table Storage にエンティティが書き込まれることが確認できます。

f:id:shiba-yan:20200912171747p:plain

古の WindowsAzure.Storage の Table SDK よりは使いやすいなと思いました。TableEntity を使えば割と雑にデータの読み書きが出来るのは結構好きな感じです。

今回は追加を行いましたが、基本的な CRUD な API は用意されているので必要に応じて使えば良いです。

f:id:shiba-yan:20200912175114p:plain

後は Table Storage からのデータ読みこみを行っておきます。PartitionKeyRowKey で読み込む方法ではなく、任意のフィルタリングを行って読み込む方法を試しておきます。

とりあえずシンプルに PartitionKey を指定して取得するコードを書きました。

型引数は必須なので TableEntity を最低でも指定する必要がありますが、Indexer を使ってカラム名でアクセス出来るので便利です。最近の SDK らしく非同期版は IAsyncEnumerable<T> を返します。

class Program
{
    static async Task Main(string[] args)
    {
        var tableServiceClient = new TableServiceClient("UseDevelopmentStorage=true");
        var tableClient = tableServiceClient.GetTableClient("sample");

        await tableClient.CreateIfNotExistsAsync();

        var result = tableClient.QueryAsync<TableEntity>("PartitionKey eq 'sample'");

        await foreach (var entity in result)
        {
            Console.WriteLine($"{entity.PartitionKey},{entity.RowKey},{entity["IntValue"]},{entity["StrValue"]}");
        }
    }
}

実行すると先ほど追加したエンティティが取得されて、コンソール画面に表示されます。簡単ですね。

f:id:shiba-yan:20200912171758p:plain

使用できるフィルタ式はドキュメントに書いてありますが、OData で書く必要があるので正直面倒です。

クライアントサイドでのフィルタリングは行われないようなので、安心して使えます。一応 LINQ も使えますが、エンティティクラスを定義すると使いやすいです。

エンティティクラスを定義して使う

古の SDK のようにエンティティクラスを定義すると、タイプセーフに値を扱えます。この時 TableEntity を継承するのではなく ITableEntity インターフェースを実装すればよいです。

public class SampleEntity : ITableEntity
{
    public string PartitionKey { get; set; }
    public string RowKey { get; set; }
    public DateTimeOffset? Timestamp { get; set; }
    public ETag ETag { get; set; }
    public int IntValue { get; set; }
    public string StrValue { get; set; }
}

データの追加は TableEntity の時と同様に行えます。こちらの方が型が決まっているので安全に扱えます。

class Program
{
    static async Task Main(string[] args)
    {
        var tableServiceClient = new TableServiceClient("UseDevelopmentStorage=true");
        var tableClient = tableServiceClient.GetTableClient("sample");

        await tableClient.CreateIfNotExistsAsync();

        for (int i = 0; i < 100; i++)
        {
            await tableClient.AddEntityAsync(new SampleEntity
            {
                PartitionKey = "sample",
                RowKey = Guid.NewGuid().ToString(),
                IntValue = i,
                StrValue = "kazuakix"
            });
        }
    }
}

エンティティクラスを定義しておくと、LINQ が使いやすくなります。TableEntity の時も Indexer でカラムを参照できますが、キャストが必要になるので冗長になります。

最初のサンプルのように PartitionKey で絞り込むクエリは以下のように書けます。対応していないクエリを書いた場合には例外になります。

class Program
{
    static async Task Main(string[] args)
    {
        var tableServiceClient = new TableServiceClient("UseDevelopmentStorage=true");
        var tableClient = tableServiceClient.GetTableClient("sample");

        await tableClient.CreateIfNotExistsAsync();

        var result = tableClient.QueryAsync<SampleEntity>(x => x.PartitionKey == "sample");

        await foreach (var entity in result)
        {
            Console.WriteLine($"{entity.PartitionKey},{entity.RowKey},{entity.IntValue},{entity.StrValue}");
        }
    }
}

まだ Batch や Entity Group Transaction は実装されていないですが、最近はこの辺りの機能が必要なら素直に Cosmos DB を使った方が良いと思うので、個人的にはあまり重要視していません。

Durable Functions が古い SDK に依存せざるを得ない状態になっているので、新しい Table SDK によって依存関係が整理されることを期待しています。

Visual Studio で Azure Functions を作成すると文字化けする問題を EditorConfig で直す

Visual Studio や Azure Functions Core Tools などで新しい Azure Functions を作成すると、一般的な C# クラスとは異なり UTF-8 BOM 無しのファイルが生成されます。

UTF-8 BOM が必要なのかどうかは議論しませんが、C# では UTF-8 BOM が無いとマルチバイト圏の人間は困るようになっています。具体的には以下のように日本語を書いた時です。

f:id:shiba-yan:20200911025410p:plain

ファイルに UTF-8 BOM が付いていない場合、Visual Studio はデフォルトのエンコーディング、すなわち Shift_JIS として保存してしまいます。最悪な挙動ではあります。

f:id:shiba-yan:20200911025419p:plain

Git で差分を確認しようとすると、まともに表示されないので不便です。GitHub や Azure Repos などで確認した時にも文字化けが発生するので、結構困ります。

f:id:shiba-yan:20200911025523p:plain

全てを日本語の Windows 上で完結する場合には気が付きにくいですが、ビルドを GitHub Actions や Azure Pipelines を行っている場合には、デフォルトエンコーディングが異なるのでビルドしたバイナリに文字化けしたデータが含まれます。

この辺りは以前にぶちぞう RD が踏んで、ブログでブチギレていることで有名な挙動です。

これまで書いたように、Azure Functions のテンプレートが UTF-8 BOM 付きのファイルを生成しないのが根本原因ですが、Issue を上げても理解してもらえずにクローズされたので諦めています。

公式の対応が完全に期待できないので、ふと EditorConfig で強制できるのではないかと思って調べると、Visual Studio は charset に対応していました。

文字コードとしても utf-8-bom が使えるので、これを使ってファイルエンコーディングを強制します。

最低限の .editorconfig ファイルは以下のようなものを用意しました。これで *.cs ファイルは全て UTF-8 BOM として保存されるようになります。

# top-most EditorConfig file
root = true

[*.cs]
charset = utf-8-bom

作成した .editorconfig が読み込まれたことを確認し、適当に編集して保存すると UTF-8 BOM として保存されるので、Git の差分確認でも文字化けが発生しなくなります。

f:id:shiba-yan:20200911030213p:plain

一応ファイルエンコーディングを確認しておきます。ちゃんと UTF-8 BOM になっています。

f:id:shiba-yan:20200911030222p:plain

これで新しく作成した Function も常に UTF-8 BOM になるため、安心してコメントなどで日本語を使って書いていくことが出来るようになりました。

あまり関係ないですが、UTF-8 を Git でちゃんと扱うためには、環境変数 LANG の設定が必要です。

f:id:shiba-yan:20200911031835p:plain

EditorConfig はファイルエンコーディングだけだともったいないので、もっと本格的な定義を作成して一緒に入れておくのが良いです。自分は CoreFx などを参考にしてカスタマイズしたものを使っています。

暇な時に近代的な Azure Functions なプロジェクト構成を、テンプレとして作っておこうかと思いました。

Terraform Cloud を使った Azure リソースの管理を試した

今後も Terraform を使っていくことが多そうですが、状態ファイルの意識した CI / CD パイプラインの作成は地味に手間です。その辺りを簡単にするために Terraform Cloud を試してみました。

正直 GitHub Actions や Azure Pipelines で良いと思っていましたが、当然ながら Terraform に特化した機能が用意されているので結構便利に使えました。5 ユーザーまで無料です。

無料以上のプランにするとユーザー数課金になるので、思ったより高くなりそうだなという印象です。

今回は GitHub で公開していた以下のリポジトリを使って試しています。Monorepo で dev / prd を管理するようになっていますが、Monorepo はあまりお勧めしないようなことがドキュメントに書いてありました。

一先ずは GitHub で Pull Request を作成して、それをマージすれば Azure 上の環境に反映されるというところまで試しました。最低限の内容ですが、はまるポイントは状態ファイルの扱いなので十分です。

環境毎の分離や、本番環境でのフロー構築は Terraform Cloud の機能で十分実現できそうだったので、そっち方面にはあまり深掘りはしていません。

Workspace を作成する

Terraform CLI にも workspace という機能がありますが、Terraform Cloud の Workspace はまた別の概念になっています。Workspace 単位で状態が管理されるので、こっちの方が分かりやすいと思います。

1 repo に対して複数の Workspace を作れるので、環境毎に分けるのが基本的な構成でしょう。

f:id:shiba-yan:20200910155619p:plain

Workspace を作成すると Configuration のチェックが走って、しばらくすると terraform plan を UI から実行できるようになります。CI SaaS ではよくある手動トリガーですね。

f:id:shiba-yan:20200910160002p:plain

とりあえず適当に実行してみると、よく見る Terraform CLI の実行結果が表示されます。Terraform 専用だからか CI SaaS を使うよりも、スピンアップがかなり速いように感じます。

f:id:shiba-yan:20200910160040p:plain

今回のサンプルは envs/dev 以下にメインとなる tf ファイルを置いているので、Terraform Cloud でも Workspace の設定から Terraform CLI を実行するディレクトリを設定しておきます。

この辺りの構成についてはドキュメントにも書いてあるので読んでおくと良いです。Monorepo にも Terraform Cloud は対応できますが、構成ごとに repo を分けつつ、Private Module を利用することを推奨しています。

Terraform CLI を実行するディレクトリは設定にある "Terraform Working Directory" で指定します。

f:id:shiba-yan:20200909174746p:plain

これで正しく Terraform CLI の実行が行えるようになります。必要な remote backend の設定は Terraform Cloud が自動的にやってくれるため、tf ファイルに backend を追加する必要はありません。

Azure Resource Manager の認証情報を設定する

Workspace の設定後に Plan を実行すると、今度は Azure CLI 周りのエラーが出ます。

f:id:shiba-yan:20200910160432p:plain

これはお馴染みの Azure 認証情報が未設定のエラーなので、対象となるサブスクリプションに対する権限を持つ Service Principal を作成して環境変数に設定します。

Service Principal の作成方法と環境変数名はドキュメントにまとめられています。

https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_client_secret

作成した Service Principal の情報は Workspace の Variables から環境変数として設定します。

f:id:shiba-yan:20200909174606p:plain

メジャーなパブリッククラウド向けには入力補助があると嬉しいなと思いました。これで再度 Terraform Plan を実行すると、今度は正常に終了します。

f:id:shiba-yan:20200910160747p:plain

デフォルトでは Terraform Plan が成功すると、今度は Apply 待ちの状態になるため手動で承認するかどうかを選ぶ必要があります。この辺りは CLI での実行に近いです。

f:id:shiba-yan:20200910161020p:plain

ここで Apply を選ぶと、Azure にリソースが作成されて正常に実行されたことを確認できます。

f:id:shiba-yan:20200910161713p:plain

GitHub Actions や Azure Pipelines を使って自前で Terraform の CI / CD パイプラインを作成するよりも、Terraform Cloud なら圧倒的に簡単に用意できました。

作成された tfstate ファイルは UI から簡単に確認できますし、差分も分かりやすく見られます。

f:id:shiba-yan:20200910194954p:plain

Azure の場合は Blob Storage に tfstate を保存するための設定やリソースを用意する必要がありましたが、その辺りも Terraform Cloud では不要なのがかなり良いです。

GitHub との連携を確認する

最後に基本的な GitHub を利用したフローを確認しておきます。と言っても Pull Request ベースでの話です。

Terraform Cloud の設定済みの repo で Pull Request を作成すると、他の CI SaaS を利用した時と同様に Terraform Plan が走ります。GitHub から結果のサマリーを確認できるのは地味に良いです。

f:id:shiba-yan:20200910162159p:plain

当然ながら Pull Request の場合は Terraform Plan までの実行で Apply までは進みません。何故かこのログは Workspace の実行履歴からは見られないので、Pull Request から飛ぶ必要があります。

マージを実行すると Apply まで進みますが、デフォルトの設定だと以下のように手動での承認が必要です。

f:id:shiba-yan:20200910162516p:plain

Plan の確認が終わった後にマージされるのが基本なので、設定を変えて自動で Apply されるようにします。デフォルトでは Manual apply になっているので、ここを Auto apply に変更します。

f:id:shiba-yan:20200910162609p:plain

実際の運用フェーズに入ると GitHub と Terraform Cloud を行ったり来たりはしたくないので、マージ出来る人を GitHub で制限する方が便利そうです。

これで再び Pull Request を作成して、マージすると自動で Apply まで実行されて変更が反映されます。

f:id:shiba-yan:20200910163450p:plain

Terraform は状態を常に 1 つだけ持つので、GitHub の Branch protection rules から "Require branches to be up to date before merging" を有効化しておくと、Pull Request のマージ先ブランチが更新された時に、取り込まないと進めなくなるので安全です。

f:id:shiba-yan:20200910163352p:plain

最初にも書きましたが Terraform Cloud は 5 ユーザーまで無料で使えるのと、はまりやすい状態ファイルの管理から解放されるので、利用を検討する価値はあると思います。

アプリケーションのビルドとデプロイは GitHub Actions、リソースの管理は Terraform Cloud という形での使い分けが一番良いのかもと思い始めました。

こういうドメインに特化したサービスは今後さらに増えそうです。便利なので使っていきたいです。

Azure AD B2C が MSAL.js v2 に対応したようなので試した

MSAL.js v2 の RTM 時には Azure AD B2C は CORS 周りの機能がデプロイされていないため非対応になっていましたが、既存のテナントへの CORS 対応のデプロイが進められているようです。自分のテナントで使えるようになっていたので試しました。

MSAL.js には種類がありますが、今回試しているのは MSAL Browser と呼ばれるブラウザで動くやつです。

これまで MSAL.js と SPA の組み合わせでは Implicit Flow のみ利用可能でしたが、最近は Implicit Flow が非推奨になっているので Authorization Code Flow + PKCE を使うようになっています。

Azure AD B2C の設定画面でも SPA を追加すると、その旨が記載されています。

f:id:shiba-yan:20200905222023p:plain

Authorization Code Flow の場合は CORS に対応する必要があるので、Azure B2C 側の対応待ちという話でした。この辺りに関しては近日中に公式リリースが出る気がします。

とりあえずミニマムなコードで MSAL.js v2 と Azure AD B2C の組み合わせを試します。

流れとしては PublicClientApplication を作成して loginPopup を実行するとポップアップ内でログインが行われます。await でシームレスに扱えるので便利です。

<!DOCTYPE html>
<html>
<head>
  <script src="https://alcdn.msauth.net/browser/2.1.0/js/msal-browser.min.js" integrity="sha384-EmYPwkfj+VVmL1brMS1h6jUztl4QMS8Qq8xlZNgIT/luzg7MAzDVrRa2JxbNmk/e" crossorigin="anonymous"></script>
</head>
<body>
  <script>
    const msalConfig = {
      auth: {
        clientId: "CLIENT_ID",
        authority: "https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/B2C_1_SignUp_SignIn_v2",
        knownAuthorities: ["yourtenant.b2clogin.com"]
      }
    }

    const loginRequest = {
      scopes: ["openid", "offline_access"]
    }

    const msalInstance = new msal.PublicClientApplication(msalConfig);

    async function loginAsync() {
      try {
        const loginResponse = await msalInstance.loginPopup(loginRequest);

        console.log(loginResponse);
      } catch (err) {
        console.error(err);
      }
    }

    loginAsync();
  </script>
</body>
</html>

設定としてはいつも通りの clientIdauthority として User Flow 名付きの URL、そして knownAuthorities が必要です。具体的な値はサンプルコードを見てもらった方が早いです。

このサンプルではトークンのキャッシュを行っていないので毎回ログインされてしまいますが、実際のアプリケーションではトークンの有効期限が切れるまではキャッシュを使うのが基本的な流れです。今回は MSAL.js なら簡単に実装出来るのと、B2C に関係ない部分なので省略します。

適当にホストしてブラウザからアクセスすると、B2C のログイン画面がポップアップ表示されます。

f:id:shiba-yan:20200906003724p:plain

ユーザー登録、もしくはログインを行うと Authorization Code を使ってトークンを取りに行きますが、この時の Pre-flight request で CORS 対応されているのが確認できます。

以前は Pre-flight でエラーになっていたので、トークン取得まで進めませんでした。

f:id:shiba-yan:20200906014628p:plain

もちろん CORS に対応しているので、その後のトークン取得も問題なく行われます。ちゃんと id_tokenrefresh_token などが返ってきていることが確認できます。

f:id:shiba-yan:20200905233002p:plain

実際の loginPopup の戻り値ではクレームがデコードされた状態で入ってくるので、アプリケーションから簡単に使えるのはこれまで通りです。

SPA に MSAL.js v2 を組み込むことで、Azure AD B2C の利用がセキュアかつシンプルになるので良い感じです。ちゃんと PKCE に対応しているのも良いです。

App Service の実行基盤が Cloud Services から VMSS に変更されつつある話(非公式)

Private Endpoint や Azure Functions Premium Plan の検証用に West US 2 に新しくリソースを作成していた時に気が付きましたが、一部のリージョンには Web Worker が Worker Role から VMSS に変更された Scale unit がデプロイされ始めているようです。

具体的には Kudu でシステムドライブを眺めていた時に、普段とディレクトリ構成が違うことに気が付きました。この名前のディレクトリがあるなら VMSS だろうと思いました。

f:id:shiba-yan:20200830144800p:plain

ほぼ意識することなく VMSS な Scale unit がデプロイされていくのだと思いますが、変わりつつあることは知っておかないと予想外の部分ではまりそうなので、非公式ですが書いてみました

App Service のアーキテクチャは昔はよく紹介されていましたが、最近は話を聞かなくなってきたので古い記事ですが引っ張ってきました。Web Worker が VMSS になったのかと思いましたが、どうやらフロントの ARR 含め VMSS になっている気配があります。

一番わかりやすいのは App Service のホスト名を nslookup で引いてみた時の結果です。古い Scale unit 上で動いている場合には Cloud Services が使われているので cloudapp.net が返ってきます。

f:id:shiba-yan:20200830144152p:plain

一方 VMSS で動いていると思われる App Service の場合は cloudapp.azure.com が返ってきます。

これは VM の場合に割り当てられるホスト名なので、少なくとも Cloud Services ではないことが分かります。

f:id:shiba-yan:20200830144144p:plain

ついに App Service も Cloud Services から脱却しつつあることが確認できました。おめでたい。

ぶっちゃけ Cloud Services から VMSS に移行したからと言って、当然ながら App Service として普通に使う分には全く違いはありません。自分も Kudu で調べるまで全く気が付かなかったぐらいです。

1 つ大きく変わっている点としては、システムドライブが D:\ から C:\ に変更されていることです。理由は知らないですが Cloud Services はシステムドライブが D:\ に設定されていたので、App Service も同様に D:\ ベースになっていました。

f:id:shiba-yan:20200830144305p:plain

VMSS ベースになることで、このよく分からないシステムドライブ構成が一般的な C:\ に変更されています。Kudu を開いて勘の良い人はこのタイミングで気が付きます。

f:id:shiba-yan:20200830143947p:plain

アプリケーションで App Service の wwwrootD:\home\site\wwwroot へ決め打ちしている場合には、新規作成時や将来的に動かなくなる可能性が高いので、環境変数 HOME を使うようにしましょう。

Program Files 系は以下のように D:\ 以下にシンボリックリンクが用意されているので、Java などでフルパスを指定していた場合でも、問題なく動作するようになっているようです。

f:id:shiba-yan:20200903231349p:plain

ぱっと見で問題になりそうなのは C:\cacertC:\devtools にいろいろ便利なものがあることを知っている人ぐらいかなと思いました。cacert.pem を App Service にデプロイされているものを使っている場合は、恐らくフルパスで指定しているので死にます。

GitHub 上のリポジトリを眺めていると ASE 周りにもアップデートがありそうなので、公式情報を待ちたいと思います。久し振りに App Service のアーキテクチャについて話して欲しい気持ちが高まっています。

ASP.NET Core 5.0 に向けて Azure 関連パッケージのアップグレードを行っておく

.NET Conf 2020 の日程も発表されて、既に Daily Build では RC や 6.0 が出ている .NET Core 周りですが、そろそろ 5.0 へのアップグレードに向けた作業を行っておくかーという気分になっています。

ちなみに .NET Conf 2020 は 11 月 10-12 日での開催です。.NET 5 Launch イベントです。

Azure Functions がどうなるのかちょっと怪しいですが、最悪 netstandard2.1 でライブラリを書いておけば共有できるようになっています。

.NET 5 は LTS ではありませんが、毎年 .NET Core のバージョンアップが予定されているのと、例によってパフォーマンス改善や API 追加などが非常に広範囲に行われているのでアップグレードは行っておきましょう。

ASP.NET Core はかなり成熟してきたので 5.0 になっても大きな変更はなく、すんなりと TFM を変えてパッケージの更新を行えば 5.0 へ移行できるはずです。ただし Azure 関連のパッケージが廃止予定になっているため、この点に関しては修正の必要があります。

Issue にも書いてあるように、既存のパッケージは 5.0 ではサポートされません。しかし新しいパッケージは 2.1 から 5.0 までサポートされるので、先に移行しておくことが可能です。

Data Protection の Key Storage として Blob を使っていることが多いと思うので、この辺りの移行を先にしておくと ASP.NET Core 5.0 への移行が楽になります。

Configuration Provider

使っている人が多いと思うのが Key Vault Secrets を Configuration として読み込むパッケージでしょう。接続文字列や API キーなどは Key Vault に格納しつつ、App Service から Managed Identity でアクセスするのが割と鉄板感あります。

App Service には Key Vault Reference という機能がありますが、Service Endpoint / Private Endpoint 経由では使えないので、この Configuration Provider を使って解決しているケースが多そうです。

新しいパッケージは全て Azure.* という Prefix が付いているので分かりやすいです。最近の Azure SDK の命名規約に沿ったパッケージ名になっています。

まだドキュメントは更新されていないので、サンプルコードを載せておきます。メソッド名は変わっていないですが、パラメータが変わっているので渡し方には注意が必要です。

サンプルコードでは本番のみ Key Vault と Managed Identity を使うようにしています。

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, config) =>
            {
                if (context.HostingEnvironment.IsProduction())
                {
                    var builtConfig = config.Build();

                    config.AddAzureKeyVault(new Uri(builtConfig["KeyVaultEndpoint"]), new DefaultAzureCredential());
                }
            })
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

開発環境でも Visual Studio の Azure サービス認証や Azure CLI 経由で使えますが、一般的には User Secrets を使えば良いので本番だけに限定します。デバッグ開始までに時間もかかるのでストレスが溜まります。

上の例では DefaultAzureCredential を使っていますが、これは Azure.Identity パッケージをインストールする必要があるので忘れないようにしてください。

Key Storage Provider

App Service の場合はデフォルトで特別対応してくれていますが、Deployment Slot を使っている場合には Data Protection のキーを Blob に保存する設定を追加していると思います。

Docker で運用している場合にもキーの永続化が必要なので、同じような設定を行っているはずです。

キーを Blob に保存するパッケージと Key Vault をキーの保護に使うパッケージが新しくなっているので、使っている場合にはそれぞれをインストールしなおします。

Key Vault の方を使っている人はあまり多くない気がしますが、この二つはセットで使います。

これもドキュメントがまだ更新されていないので、サンプルコードを載せておきます。メソッド名は Configuration Provider と同様に変わっていませんが、パラメータは異なります。

新しいパッケージの方が分かりやすいパラメータになっているので良い感じです。

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        services.AddDataProtection()
                .PersistKeysToAzureBlobStorage(Configuration.GetConnectionString("DefaultStorageConnection"), "secrets", "keys.xml")
                .ProtectKeysWithAzureKeyVault(new Uri(Configuration["KeyIdentifier"]), new DefaultAzureCredential());
    }
}

Blob へのアクセスには SAS や Managed Identity が使えますが、ここはこれまで通り接続文字列を使うことにしました。以前は CloudBlobClient を作って渡す必要がありましたが、手間が省ける形になっています。Key Vault へのアクセスは Managed Identity を使うようにしています。

これで適当にデバッグ実行すると、指定した Blob にキーが保存されます。ファイルフォーマットに依存しないパッケージなので、アップグレードしても問題なく読めます。

f:id:shiba-yan:20200903181054p:plain

難しくはないですが、ドキュメントがまだ更新されていないというのだけがネックでした。使用されている Azure Storage / Key Vault SDK も新しいバージョンなので安心です。

古い WindowsAzure.Storage SDK から新しい Storage SDK (v11 / v12) へ移行する

太古の昔から使われてきた WindowsAzure.Storage SDK は未だに使えますが、そろそろ新しい SDK (v11 / v12) に移行しておくかという気分になってきたので、実際のプロジェクトで移行を行いました。

まだ使っているのかと言われると反論は難しいのですが、Azure Storage SDK は非常に広い範囲で使われていて依存関係が複雑かつ、アップグレードで得られるものが少なかったのもあります。特に Azure Functions は最近になって v11 へのアップグレードが行われたので、それに合わせる形にします。

サンプルとして以下のような Blob / Queue / Table を使うよくあるコードを用意しました。これを v11 と v12 へアップグレードしていくことにします。

using System;
using System.Threading.Tasks;

using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Queue;
using Microsoft.WindowsAzure.Storage.Table;

namespace ConsoleApp22
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var storageAccount = CloudStorageAccount.Parse("UseDevelopmentStorage=true");

            await BlobSampleAsync(storageAccount);
            await QueueSampleAsync(storageAccount);
            await TableSampleAsync(storageAccount);
        }

        private static async Task BlobSampleAsync(CloudStorageAccount storageAccount)
        {
            var blobClient = storageAccount.CreateCloudBlobClient();

            var container = blobClient.GetContainerReference("sample");

            await container.CreateIfNotExistsAsync();

            var blob = container.GetBlockBlobReference("buchizo.txt");

            await blob.UploadTextAsync("buchizo");
        }

        private static async Task QueueSampleAsync(CloudStorageAccount storageAccount)
        {
            var queueClient = storageAccount.CreateCloudQueueClient();

            var queue = queueClient.GetQueueReference("sample");

            await queue.CreateIfNotExistsAsync();

            await queue.AddMessageAsync(new CloudQueueMessage("buchizo"));
        }

        private static async Task TableSampleAsync(CloudStorageAccount storageAccount)
        {
            var tableClient = storageAccount.CreateCloudTableClient();

            var table = tableClient.GetTableReference("sample");

            await table.CreateIfNotExistsAsync();

            var operation = TableOperation.Insert(new SampleEntity
            {
                PartitionKey = "sample",
                RowKey = DateTime.UtcNow.ToString("yyyyMMddHHmmss"),
                Content = "buchizo"
            });

            await table.ExecuteAsync(operation);
        }
    }
}

新しく書くアプリケーションでは v12 の SDK を使うことをお勧めしますが、v11 から v12 へのアップデートは Managed Identity を使う場合以外には、積極的に行いたい理由はあまり無いように感じます。

ただし Blob Change Feed と Azure Data Lake Storage Gen 2 を使う場合には実質的に v12 以外の選択肢が無いです。どれかを v12 にした場合は全てアップグレードしましょう。

v11 SDK

既にレガシーと呼ばれていますが Azure Storage SDK v11 は実質的には WindowsAzure.Storage の名前空間の変更とアセンブリを機能ごとに分離したものなので、Blob と Queue に関しては新しいパッケージに入れなおして名前空間を変更すれば大体問題ないです。

ただし Table に関しては Storage SDK として提供されなくなりました。なので Table をこれまで通り使うには Cosmos DB の Table API の一機能として提供されている SDK を使う必要があります。

ドキュメントが用意されているのと、クラス周りはほぼ同じなので使い方は大体わかるはずです。

非常に厄介なのが Blob / Queue の SDK と Cosmos の Table SDK で同じ名前のクラスが存在することです。具体的には CloudStorageAccount が確実にコンフリクトします。

今回は using を使って Cosmos 側の CloudStorageAccount にエイリアスを付けることで対応しました。

using CloudStorageAccount = Microsoft.Azure.Storage.CloudStorageAccount;
using CloudTableStorageAccount = Microsoft.Azure.Cosmos.Table.CloudStorageAccount;
static async Task Main(string[] args)
{
    var storageAccount = CloudStorageAccount.Parse("UseDevelopmentStorage=true");

    await BlobSampleAsync(storageAccount);
    await QueueSampleAsync(storageAccount);

    var tableStorageAccount = CloudTableStorageAccount.Parse("UseDevelopmentStorage=true");

    await TableSampleAsync(tableStorageAccount);
}

この修正でコンパイルが通り、これまで通り動作するようになります。

しかし Azure Storage を使うために Cosmos DB 向けの SDK を使うというのは、非常に違和感があります。さらに古い Cosmos DB SDK が使われているので、二重で嫌なライブラリとなっています。

Azure Functions における v11 SDK

Azure Functions では最近まで WindowsAzure.Storage が使われていましたが、WebJobs Extensions v4 などで v11 SDK への対応がようやく行われました。

Visual Studio で Azure Storage を利用する Azure Functions を作成すると、以下のようにいくつかのライブラリで WindowsAzure.Storage が参照されていることが確認できます。

f:id:shiba-yan:20200831004458p:plain

WindowsAzure.Storage への参照を無くすためには、以下の 2 つのライブラリを更新する必要があります。

この両方を更新することで古い参照が無くなり、v11 SDK のみが使われるようになりました。綺麗に参照が消えていることが確認できますし、IntelliSense でも出てこなくなります。

f:id:shiba-yan:20200831005007p:plain

バインディングで CloudTable などの Storage SDK のクラスを使っている場合には名前空間が変わっている関係上、以下のような警告が出てくるようになります。単純に using を追加すれば解消します。

f:id:shiba-yan:20200831202753p:plain

Storage SDK を直接使っていない場合は、パッケージの更新だけで v11 への移行が完了しました。

v12 SDK

Azure Storage SDK v12 は新しい規約に従って実装されているライブラリで、Key Vault や Cosmos DB v4 などいくつかのサービスはリリースされています。

最近のドキュメントは v12 ベースになっているので、新しいアプリケーションでは v12 を使っていけばよいです。API は v11 から v12 でシンプルになっていますが、互換性は無いので慣れるまで少しかかります。

この辺りの違いは実際のコードで知った方が速いと思うので、サンプルコードを出します。

今回は Storage の接続文字列を使っていますが、Managed Identity が必要な場合は Azure.Identity によって全ての新しい SDK で同じ方法で扱えます。

class Program
{
    static async Task Main(string[] args)
    {
        var connectionString = "UseDevelopmentStorage=true";

        await BlobSampleAsync(connectionString);
        await QueueSampleAsync(connectionString);
        await TableSampleAsync(connectionString);
    }

    private static async Task BlobSampleAsync(string connectionString)
    {
        var blobServiceClient = new BlobServiceClient(connectionString);

        var containerClient = blobServiceClient.GetBlobContainerClient("sample");

        await containerClient.CreateIfNotExistsAsync();

        var blobClient = containerClient.GetBlobClient("buchizo.txt");

        await blobClient.UploadAsync(new MemoryStream(Encoding.UTF8.GetBytes("buchizo")));
    }

    private static async Task QueueSampleAsync(string connectionString)
    {
        var queueServiceClient = new QueueServiceClient(connectionString);

        var queueClient = queueServiceClient.GetQueueClient("sample");

        await queueClient.CreateIfNotExistsAsync();

        await queueClient.SendMessageAsync("buchizo");
    }

    private static async Task TableSampleAsync(string connectionString)
    {
        // 同じなので省略
    }
}

基本的には **Client というメソッドやクラスを使って処理を行います。多少オーバーロードが減っているので、単純な移行とはいきませんが難しくはないです。

Table Storage に関しては v11 と同じく v12 でも SDK がリリースされていませんが、今後追加される予定なのでその時にまた試してみたいと思います。ちなみに 9 月にプレビュー版がリリースされそうです。

この辺りがリリースされると Durable Functions 周りも v12 へのアップグレードが行われそうです。暫くは完全に WindowsAzure.Storage を無くすことは出来ませんが、あともう少しで完了しそうです。

2020 年 8 月の Azure Functions Live で発表されたアップデート

2020/8 の Azure Functions Live でかなりインパクトの大きいアップデートについて話があったので、触れておきたいと思います。Azure Functions でイマイチだと思っていた部分がかなり改善されているので、今後は使うのが当たり前になりそうな機能ばかりです。

例によって自分の興味ある部分しか触れないので、YouTube で本編をちゃんと見ておいた方が良いです。

新機能と今後の予定でいくつか紹介されていますが、以下の 6 つに絞って試した結果をまとめておきます。まだ何も公開されていなさそうですが、OAS から Function を作るのは結構面白そうです。

特に Configuration や Checkpoint control などは全ユーザー待望だと思うので嬉しいです。

ReadyToRun

Tiered Compilation は Function Host では有効化されていませんが、Function App での ReadyToRun 済みアセンブリの読み込みに対応したようです。Function Host 自体は R2R でビルドされています。

実際にどのくらいの効果があるかは検証中ですが、原理上は悪くなることはないかなと思っています。規模の大きい Function の方が効果が出やすそうです。

この辺りの情報は以前書いたので適当に参照しておいてください。

ドキュメントにあるように csproj に 2 つ設定を追加すると、発行時に自動的に ReadyToRun 済みのアセンブリを作成してくれます。R2R には多少時間はかかりますが、最近のマシンだと無視できるレベルでしょう。

f:id:shiba-yan:20200822234056p:plain

csproj に単純に足すと、デバッグ実行時にも ReadyToRun を行ってしまうので、CI などで dotnet publish の実行時のみ ReadyToRun 済みアセンブリを作成するという戦略が良いでしょう。

以下のような単純なコマンドで ReadyToRun を行いつつビルドできます。

dotnet publish -c Release -o ./publish -r win-x64 -p:PublishReadyToRun=true

ドキュメントでは RID として win-x86 を使っていますが、Function SDK に問題がありインストールされている .NET Core の bitness に合わせないと正常にビルド出来ません。

原因は extensions.json の生成時にアセンブリを読み込む仕組みになっていますが、当然ながら bitness の違う ReadyToRun アセンブリは読み込めないためです。

殆どの開発者は x64 版 .NET Core をインストールしているはずなので、RID は win-x64 を指定する必要があるということです。解決には時間がかかりそうな気がします。

IFunctionConfigurationBuilder

Azure Functions ユーザー待望の Configuration カスタマイズ機能がついに提供されました。

これで ASP.NET Core でお馴染みの appsettings.json を使って設定を自由に読み込めるようになりました。local.settings.json は特殊なのでオブジェクトを書けなくて結構辛かったです。

App Service Plan 以外で Binding / Trigger の接続文字列などを変更しようとすると、Azure Portal に以下のようなエラーが表示されて Function Host が起動しなくなります。

f:id:shiba-yan:20200822232527p:plain

これは Scale Controller が外にいる構造上、仕方ない挙動です。ただし Premium Plan + Runtime Scale Monitoring が有効なら Scale Controller がリソースにアクセスする必要が無いので、このあたりの制限を緩和出来ないか提案をしています。

制限が緩和されれば VNET Integration を使いつつ、Key Vault に Service Endpoint or Private Endpoint を追加してアクセス元を絞ることが出来るようになるはずです。

ドキュメントなどでは IFunctionsConfigurationBuilder が待望の機能過ぎて扱いが雑になっていますが、今回 FunctionsHostBuilderContext というクラスも追加されて、Startup 内で以下の情報を簡単に参照できるようになりました。

  • ApplicationRootPath
  • Configuration
  • EnvironmentName

特に ApplicationRootPathConfiguration を触れるようになったのは、非常にありがたいです。

これにより Startup 内の Options パターンが非常に扱いやすくなっています。正直なところ、これまで IConfiguration を取る手段がなかったのがハードモード過ぎました。

拡張メソッドとして用意された GetContext を呼び出すと、ConfigureConfigureAppConfiguration の両方で参照できます。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        var context = builder.GetContext();

        builder.Services.Configure<SampleOptions>(context.Configuration.GetSection("Sample"));
    }
}

public class SampleOptions
{
    public string Value { get; set; }
}

これですっきりしましたね。自分で ConfigurationBuilder を使って組み立てる必要が無くなったのはかなり良いです。これでやっと DI をフルに使った Azure Functions の開発が行いやすくなった感あります。

ただし Configuration の値は ConfigureAppConfiguration の時点では完全な情報ではありませんので、Key Vault を参照する場合は ASP.NET Core の場合と同様に一度 Build する必要があります。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
    }

    public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
    {
        var builtConfig = builder.ConfigurationBuilder.Build();

        var tokenProvider = new AzureServiceTokenProvider();
        var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(tokenProvider.KeyVaultTokenCallback));

        builder.ConfigurationBuilder.AddAzureKeyVault(builtConfig["KeyVaultEndpoint"], keyVaultClient, new DefaultKeyVaultSecretManager());
    }
}

これで Key Vault に保存された Secret を Managed Identity を使って読み込むことが出来ます。

App Service に用意されている Key Vault Reference では Service Endpoint / Private Endpoint を越えられませんが、アプリからの場合は問題なくアクセス出来ます。

Per app scale limits for all plans

これまでも WEBSITE_MAX_DYNAMIC_APPLICATION_SCALE_OUT という設定がプレビューで提供されていましたが。新しくちゃんと動く設定が追加されたようです。

All plans と言っているので App Service Plan でも使えそうな気がしますが、軽く試した感じではちゃんと動いてなさそうでした。Consumption / Premium Plan 限定かもしれません。

ドキュメントにあるように SQL Database など接続数の上限が存在するサービスを利用する場合に、スケールアウトの上限を設定できるのは安心という感じです。

Durable Functions monitoring in portal

Durable Functions を使っている人にとっては待望の Orchestrator のモニタリングが Azure Portal で行えるようになりました。モニタリングのソリューションはいくつかありましたが、どれも決定打に欠けました。

今回のリリースで Azure Portal から設定不要でモニタリング出来るようになったのはかなり大きいです。

f:id:shiba-yan:20200822221433p:plain

既に完了している Orchestrator についても Running と表示されていますが、正直大した問題ではありません。使われている KQL が修正されれば終わるような話です。

一覧から日付を選ぶと、Orchestrator の実行ログをさらに細かく見ることが出来ます。この辺りはフィードバックで良くなっていく部分だろうと思います。

f:id:shiba-yan:20200822221441p:plain

Azure Portal に組み込まれたという点が非常に重要なので、これからの進化に期待しています。

Restrict storage to VNet

Function App のバックエンドで使われている Storage Account に対して、Service Endpoint や Private Endpoint でアクセス制限を行うと正しく動作しない問題があります。

ドキュメントにも Storage Account に対して Service Endpoint を追加しないように書かれています。

Service Endpoint に関しては使えるようになっている気がします。対応済みかもしれません。

とはいえ App Service の Regional VNET Integration は他のサービスに比べて挙動が特殊なことが多いので、事前に動作確認をしておく必要があります。

特に Private Endpoint が絡むとさらに複雑になる傾向にあります。

Checkpoint control for Event Hubs / Cosmos DB

Checkpoint control と言ってはいますが、早い話リトライについてです。Function 自体にはそもそもリトライという概念はなく、Queue でのメッセージ再追加や Event Grid 側のリトライに任せるなど、外部サービスを使う形で行われていました。

特に困っていたのが Cosmos DB で、Change Feed の処理が失敗した場合にはリトライなどは行われず、そのまま次の処理に進んでしまう問題がありました。それが以下の PR で解消されようとしています。

というか、何故失敗しているのに Change Feed が先に進むのだという感想しかないですが、これまではアプリケーション側にリトライを実装するという形で乗り切ってきたのが不要になります。

設計としては外部サービス側にリトライを任せた方が美しいですが、現実問題として Event Hubs と Cosmos DB の場合はどうしようもなかったということでしょう。割と待望の機能です。

Azure Cosmos DB Serverless が Preview になったので試した

Build 2020 で発表があった Cosmos DB の Serverless がプレビューとして公開されました。Autoscale が 1 時間毎の最大 RU で課金がされるのに対して、Serverless は完全に消費した RU によって課金が行われます。

Cosmos DB チームはブログとドキュメントを頑張っているので、この二つで大体理解できるはずです。

ある程度予想はしていましたが、それなりに制限が多いので使う際には注意したいところです。プレビュー中のみの制約もあると思いますが、気になる点はフィードバックを投げると GA に向けて検討されるようです。

個人的に気になった点と注意した方が良い点をピックアップしておきました。

  • 単一のリージョンでのみ利用可能
    • Geo Replication は設定できない
  • Serverless 専用の Cosmos DB アカウントを作成する必要がある
    • GA の時にはこの辺りは変わりそう (Autoscale みたいに変更可能とかになるのでは?)
  • SQL API でのみ利用可能
    • Change Feed とかはちゃんと使える
    • プレビューの期間中の制約っぽい
  • Container 単位でキャパシティ上限が決まっている
    • 最大 5,000 RU と 50GB のストレージ
    • 物理パーティションキーとの兼ね合いが気になる
  • RU の単価はリージョンによって異なるが大体 10 倍ぐらい
    • 1,000,000 RU 当たりで金額が決まっている
    • 1,000,000 RU 当たりの単価とストレージ 1GB の単価が大体一致している

この 5000 RU と 50GB という上限はプレビュー中の制限ではないかと思います。GA では変わりそうです。

ユーザー側で RU の上限が指定できないのは結構怖いですね。Azure Monitor などで使用量をきっちりと把握しておかないと、アプリの不具合などで異常に RU を消費するコードになっていても 5000 RU までは問題なく動いてしまいます。アラートの設定は必要になるでしょう。

実際に Serverless な Cosmos DB を作成して、大量にデータを書き込んで RU の消費と 429 周りの確認をしておきました。メータリングは正しいことが確認できたので安心です。

Serverless 用アカウントを作成する

現在は Serverless 用アカウントは Azure Portal でしか作成できないらしいので、CLI 派の人も諦めて Azure Portal からポチポチ作る必要があります。ARM 側の対応が出来ていないのでしょう。

作成時に Provisioned throughput と Serverless を選べるようになっています。後から変更は出来ません。

f:id:shiba-yan:20200820022054p:plain

あまり関係ないですが、Azure Portal だと作成にどのくらい時間がかかるかを教えてくれるようになっていました。正確さは不明ですが、作成に時間がかかるサービスだと不安になるので安心出来ます。

f:id:shiba-yan:20200820022102p:plain

教えてくれた通り大体 3 分ぐらいで作成完了したので、適当な名前で Database と Container を作成していきます。RU の指定が不要なので、異常にすっきりした UI になりました。

f:id:shiba-yan:20200820022119p:plain

Container 単位での RU とストレージ上限になっているので、上手く TTL と組み合わせつつ使うのが良さそうです。まだ慣れないですが消費した RU で課金されるので、Change Feed の Leases Container を同じ DB に入れる必要は無かったりします。

RU 上限まで書き込みをテスト

作成した Container に対してちゃんと 5000 RU 以上を書き込んでみるために、Cosmos DB SDK の Bulk API を使って大量のデータを短時間に書き込むことにします。

ただし項目のサイズがあまりにも小さい場合は件数が増えてしまって、ネットワークレイテンシの影響が無視できなくなるので大き目のデータを書き込むようにします。

既に Bulk API については以前書いていたのでサンプルコードは載せませんが、10000 件の項目を書き込みつつ 495200 RU を消費することが出来ました。

上限の 5000 RU を超えたため 429 が発生し、Azure Portal には以下のような警告が出ました。429 が出ない世界は難しいですが、最近は SDK が優秀なのであまり問題はないです。

f:id:shiba-yan:20200820022133p:plain

Provisioned throughput の場合は RU を増やすように言われていたと思いますが、Serverless の場合は対応しようがないのでメトリックの確認へ誘導されます。

相変わらず Cosmos DB の画面から確認できるメトリックは分かりにくいので、個人的には Azure Monitor 側から確認するのをお勧めしています。こっちなら合計 RU も簡単に確認できます。

f:id:shiba-yan:20200820022450p:plain

Bulk での書き込みを 2 回行ったので、約 99 万 RU を消費したことが確認できますね。結構な量のように見えますが、実際にいくらぐらい課金されるのか計算してみます。

Serverless の価格について

ドキュメントには価格についての情報があまりないですが、Azure の Pricing の方に Serverless の情報が既に追加されているので、ここから各リージョンの金額を調べることが出来ます。

ちなみに Japan East の Serverless の金額は以下のようになります。日本円にすると 1,000,000 RU で約 32 円ですが、これが高いのか安いのかはこのままだとあまり想像が付きませんね。

1,000,000 Serverless request units (RU) $0.285

比較のために通常の Provisioned throughput と 1 時間当たりの金額を合わせてみることにします。Japan East だと 100 RU/s 当たりの金額は以下のようになります。

100 RU/s single-region account 1 x $0.009/hour

Provisioned throughput は 1 時間当たりの金額なので、100 RU/s の場合は最大で 360000 RU を消費できることになります。ここから Serverless との RU 単価差を計算できます。

Provisioned throughput
  360,000 RU = $0.009

Serverless
1,000,000 RU = $0.285
  360,000 RU = $0.1026

分かっていたことですが、Serverless は RU の単価だけを見ると 10 倍近く高いです。しかし Serverless は Provisioned throughput とは異なるワークロードに適用されるため、単価だけでの比較は意味がありません。

先ほどの Bulk API での実行でかかった金額を計算してみることにします。Provisioned throughput では 1 秒使っただけでも最低 1 時間分は課金されることに注意が必要です。

Provisioned throughput
5000 (RU) / 100 * $0.009 = $0.45

Serverless
990,000 (RU) / 1,000,000 * $0.285 = $0.28215

実際に消費された RU 分で計算を行うと、Serverless の方が圧倒的に安いことが分かりますね。ストレージ容量は含まれていませんが、これは両方とも同じ金額のようなので省いています。

Provisioned throughput で 5000 RU を設定しているのは、Serverless と同じ時間で処理を完了させるためです。つまり時間方向に処理を平準化すれば、また金額は変わってきます。Serverless はこのバースト性が非常に大きなメリットでもありますね。

ドキュメントでもいくつかの例と共に金額について説明が行われているので、こちらも参照してください。

それぞれの使い分けですが、事前にアプリケーションの要件を理解して設計することがますます重要になってきたという印象です。要件をしっかり理解しないとデータモデリングは出来ないし、適切な課金モデルすら選べないようになってきた感があります。

個人的にはもっと大規模なサービスにも適用できるものを期待していましたが、これはこれで Free Tier と Provisioned throughput の間をきっちり埋めることが出来るのではないかと。