しばやん雑記

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

Azure Data Factory を利用して Cosmos DB のコレクションをお手軽にコピーする

Azure の Cosmos DB を使っていて非常に困るのが、コレクションのパーティションキー設計を変更したい場合になった時です。パーティションキーは作成時にしか設定できず、後から変えることは不可能です。

設計ミスと言われると何とも言い返せないのですが、こういうのは使ってから発覚するものなのです。一応ある程度までは RU を監視して、クエリを最適化することで改善は行えました。

しかし、大体はクロスパーティションなクエリのせいで問題が発生するので、必要ない場合はパーティションキーを使わないという選択肢を取った方が良いことが多いように思います。

なので、新しくコレクションを作成してデータを全て移行すれば一応は解決するのですが、Cosmos DB にはコレクションのコピーといった機能は用意されておらず、パーティションキーと RU が関わってくるので自分で用意するのは現実的ではないように思います。

そこで Azure Data Factory を使います。コピーアクティビティを使ってコピー元に古いコレクションを、コピー先に新しいコレクションの指定出来るので簡単にコピーが可能でした。

ドキュメントには Cosmos DB => Cosmos DB に関しては書かれていないですが、特に設定さえ行えば問題ないようです。RU に関してもリトライを適切に行ってくれるみたいです。

とりあえず実際に Cosmos DB のコレクションコピーを行ってみます。パーティションキーを設定しているコレクションから設定していないコレクションへのコピーです。データは適当に用意しました。

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

パーティションキーは GUID にしたので、クロスパーティションクエリを使わない限り、このデータは取り出せません。このデータをそのまま新しいコレクションにコピーするのが目的です。

次に Data Factory を Cosmos DB と出来れば同じリージョンに作成して、専用のポータルにアクセスします。Copy Data という非常に分かりやすいボタンがあるのでクリックして設定を進めていきます。

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

ウィザード形式でパイプラインの作成が進んでいくので、良い感じに設定していきます。

コピータスクは基本的には 1 回しか実行しないので "Run once now" を選択しておきます。

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

次はコピー元のデータストアを選びます。Cosmos DB の接続を新しく追加するだけです。

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

接続を作成すれば、Cosmos DB の情報が表示されるようになるので、そこでコピー元になるコレクションを選択します。データはプレビューされるので便利です。

データの変換が必要な場合は、ここでスキーマやクエリを変更することも出来るみたいですが、今回は全データのコピーなので下の方にあるチェックを入れました。これを有効にするとそのままの JSON としてデータをエクスポートしてくれます。

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

次はコピー先の設定です。同じように Cosmos DB の接続を選ぶと、どのコレクションにコピーするか選べるようになります。この辺りは単純なので迷うことはなさそうです。

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

これで設定完了なので、後はボタンをポチポチ押していくと、パイプラインのデプロイと実行が行われます。1000 ドキュメントのコピーでしたが、一瞬で完了しました。

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

新しいコレクションのデータを確認すると、ちゃんとそのままの形でコピーされています。

Cosmos DB が管理用に作成するデータは変わっているので、タイムスタンプなどは失われています。

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

パーティションキーに使っていたデータなどはそのままの状態で、コレクション側のパーティションキーが無くなっているだけなので、非常に扱いやすいデータになりました。

例えばカラムを typo していたとか、構造を少し変えたいといった場合にも Data Factory のコピーが使える気がします。スキーマレスといってもデータが既にある場合には構造を変えるのは難しいものです。

Azure Functions で SendGrid の Dynamic Templates を使う

この間書いた SendGrid の Dynamic Templates ですが、今朝に対応した SendGrid の C# クライアントがリリースされていたので、早速 Azure Functions の SendGrid バインディングで使ってみました。

Dynamic Templates 自体については前回のエントリを参照してください。テンプレートもそのまま使います。

SendGrid C# クライアントの 9.10.0 から Dynamic Templates に対応しました。Azure Functions での SendGrid バインディングに関してはドキュメントがあるので、それを見れば簡単です。

Azure Functions v2 を使っているので、3 系のパッケージを入れる必要があります。

ちなみにこのバインディングは SendGrid C# クライアントの 9.9.0 以上を参照しているので、プロジェクト側で明示的に新しいバージョンをインストールすれば、そのバージョンが全体的に使われます。

実際に 9.10.0 をインストールすると、依存バージョンが適切に選択されていることが分かります。

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

それでは実際にコードを書いていくのですが、今回はモデル用のクラスを適当に作りました。

こまめに JsonProperty で名前を指定していますが、この辺りは Json.NET の設定で自動 camelCase が出来たはずなので、好みの問題という感じもします。

public class TemplateModel
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("message")]
    public string Message { get; set; }

    [JsonProperty("items")]
    public TemplateItemModel[] Items { get; set; }
}

public class TemplateItemModel
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("price")]
    public int Price { get; set; }
}

テンプレートは前回と同じなので、必要な構造を単純にクラスに起こしただけです。

実際にメールを送信する関数は以下になります。テンプレートから生成したら SendGridMessage を return する形で作られたので、今回はその方式で書きました。SendGrid のバインディングには何種類か送信する方法が用意されていますが、Dynamic Templates を使う場合には SendGridMessage が必要になります。

今回は簡単にするために HttpTrigger を使いましたが、テンプレートでは QueueTrigger を使う形で生成されるので、非常に Dynamic Templates と相性が良いと思います。

public static class Function1
{
    [FunctionName("Function1")]
    [return: SendGrid]
    public static SendGridMessage Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]
                                      HttpRequest req, ILogger log)
    {
        var model = new TemplateModel
        {
            Name = "kazuakix",
            Message = "8GB",
            Items = new[]
            {
                new TemplateItemModel { Name = "Item 1", Price = 1980 },
                new TemplateItemModel { Name = "Item 2", Price = 2980 },
                new TemplateItemModel { Name = "Item 3", Price = 3980 }
            }
        };

        var message = new SendGridMessage
        {
            From = new EmailAddress("from@example.com"),
            TemplateId = "TEMPLATE ID"
        };

        message.AddTo("to@example.com");
        message.SetTemplateData(model);

        return message;
    }
}

SendGrid のアクセスキーは AzureWebJobsSendGridApiKey という名前で App Settings に追加しておけば使われます。特に別名を付ける必要もないとおもうので、デフォルトで良いと思います。

この関数を実行すると、前回と同様にメールの文面がレンダリングされて届きます。

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

Azure Functions でメールを送信する場合にはテンプレートを用意するのが面倒かつ、RazorEngine を使った場合には新しくインスタンスが立ち上がる度にテンプレートのコンパイルが必要になるので無駄が多いです。テンプレートのコンパイルは結構なコストです。

その点、Dynamic Templates を使うと Azure Functions では Queue からデータを受け取って、SendGrid に流すという単純な処理になるのでスケーリングも行いやすくなります。

テンプレートの管理が必要無いという以外にもメリットは多いです。公式クライアントも出ましたし。

Azure Functions の Key Management について調べた

Azure Functions で HTTP Trigger や Webhook を使う時に指定する API Key ですが、主に Durable Functions を使う時に悩んだのでメモとして残しておきます。

Portal で適当な関数を選んで URL を取得する時に、どのキーを使うか選べます。

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

このキーはユーザーが自由に生成したり、削除や作り直しが出来ます。

Host Key を使うとどの関数に対しても同じキーでアクセスが出来るので、便利な反面危険でもあります。なので普通は Function Key を使うことになると思います。

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

Azure Portal 上では Function Key と Host Key しか確認出来ないですが、Azure Functions は内部で System Key も持っているので、実際には 3 種類のキーが存在していることになります。

ちなみに Azure Functions v2 から Key のデフォルト保存場所が共有ストレージから Blob に移動したので、Storage Explorer などを使うとファイルがあることを確認出来ます。

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

格納されているキーは暗号化されているので、そのままでは使うことは出来ません。

この辺りの変更に関しては Wiki にまとまっているので参照してください。

Changes to Key Management in Functions V2 · Azure/azure-functions-host Wiki · GitHub

App Settings の値を変えることで、これまで通りに共有ストレージに保存するように出来ます。

ここからが本題ですが、Durable Functions にはステータス確認やイベントを発生させるための API が用意されています。CreateCheckStatusResponse で返されるやつがそれです。

当然ながら管理用の API なので Key が必要になりますが、これら Durable Functions に用意されている各 API を実行するには System Key が必要になります。

このキーは Azure Portal からは確認出来ないため、CreateCheckStatusResponse を実際に叩くか Key Management API を利用して取得します。

ドキュメントには v1 と書かれていますが、今のところ v2 でも動作しました。

Key management API (V1) · Azure/azure-functions-host Wiki · GitHub

ぱっと見では Function Key と Host Key しか取得できなさそうですが、/admin/host/systemKeys にリクエストを投げると System Key の一覧が返ってきます。

当然この API も認証が必要なので、Authorization ヘッダーで Bearer Token を指定することになります。

curl -H "Authorization: Bearer TOKEN" https://***.azurewebsites.net/admin/host/systemKeys

ドキュメントには書かれていないのが罠なのですが、Bearer Token を取得する API が Kudu 側に用意されているのでそっちを先に叩いてトークンを取得後、Key Management API を叩くことになります。

取得方法は /api/functions/admin/token を叩くだけなので簡単です。

SCM 側なのでブラウザで直接アクセスしても取得できますが、コードからアクセスする場合には Basic 認証がこれまで通り使えます。このトークンの有効期限は短いので注意。

これで Key Management API を実行すると、Durable Functions 向けに自動生成されたキーを取得できます。

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

同じような名前のキーが 2 つあるのは、この間の Breaking Changes が関係しているのだと思いますが、System Key なので両方とも問題なく使えます。

これを利用して Durable Functions の実行状態を確認するためのダッシュボードを作ろうと思ったのですが、一瞬で飽きてしまいました。Rewind API が増えて需要はありそうなのですが残念。

Durable Functions v1.6 の Rewind API を使うと失敗した関数から再実行が可能に

Azure Functions v2 の Breaking changes に合わせて Durable Functions v1.6 がリリースされました。

今回の Breaking changes は Run From Packages で配布している場合に地味に大変でしたが、その話はまた今後どこかで書こうかと思いますが需要なさそう。

オーケストレーションのパフォーマンスが改善してたりするみたいなので、とりあえずアップデートはしておいた方が良いでしょう。Azure Functions v2 を使ってる場合は必須ですよ。

さて、メインは新しい Functions Runtime への対応っぽいですが、リリースノートには非常に興味深い機能が追加されていると書かれていました。失敗したオーケストレーションをやり直す機能です。

Rewind failed orchestrations (preview, #301): A new API has been added which allows you to "rewind" failed orchestration instances back to their previously good state. This can be a life-saver when you have long-running orchestrations that fail in unexpected ways.

Release Durable Functions v1.6.0 Release · Azure/azure-functions-durable-extension · GitHub

まだプレビューなので不具合があるかも知れないですが、これは待望の機能です。

これまで Durable Functions は Event Sourcing で実行の履歴が全て管理されているのに、失敗した場合には新しく最初からやり直すしか方法が無い点に不満を持っていました。

Added rewind endpoint to HTTP client response links by cmkerner20 · Pull Request #341 · Azure/azure-functions-durable-extension · GitHub

ちなみにこの Rewind 機能を実装したのはインターンの方らしいです。素晴らしいコントリビューションとして言いようがないですね。

使い方はまだドキュメントが公開されてないですが、実装を見ればすぐにわかりました。普通にオーケストレーションを開始すると、レスポンスに rewindPostUri が追加されています。

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

オーケストレーションが失敗した場合、このエンドポイントを POST で叩くと再実行されます。

とりあえず実際に動作を試して見ました。サンプルなので適当なオーケストレーターとアクティビティを用意して、2 つ目のアクティビティはわざと例外を投げて落ちるようにしています。

[FunctionName("Function1")]
public static async Task RunOrchestrator([OrchestrationTrigger] DurableOrchestrationContext context)
{
    var value = context.CurrentUtcDateTime.Ticks.ToString();

    await context.CallActivityAsync("Function1_Task1", value);
    await context.CallActivityAsync("Function1_Task2", value);
    await context.CallActivityAsync("Function1_Task3", value);
}

[FunctionName("Function1_Task1")]
public static void Task1([ActivityTrigger] string value, ILogger log)
{
    log.LogInformation($"Task1 running: Input {value}.");
}

[FunctionName("Function1_Task2")]
public static void Task2([ActivityTrigger] string value, ILogger log)
{
    throw new Exception();
    //log.LogInformation($"Task2 running: Input {value}.");
}

[FunctionName("Function1_Task3")]
public static void Task3([ActivityTrigger] string value, ILogger log)
{
    log.LogInformation($"Task3 running: Input {value}.");
}

アクティビティが失敗して、オーケストレーションが Failed になったのを確認後、Rewind API を叩いて再実行をリクエストします。reason には適当に理由を入れておけば、ログに出力されるので分かりやすいです。

実際にどのように実行されているかはログを見ないと理解しにくかったので、Application Insights でクエリを書いて分かりやすく並べました。まずは初回の失敗したログです。

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

Function1_Task2 が例外を投げたので、オーケストレーターも Failed していることが確認できますね。

次に Rewind API の実行後のログです。Instance Id は Rewind 後も変化しないので追跡が可能です。

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

オーケストレーターのステータスが Rewound に変化して、再実行が行われています。

前回の実行で成功した Function1_Task1 は再実行されずに Function1_Task2 から実行されていることが確認できます。もちろん状態も前回のままなのでオーケストレーターの作法に従って CurrentUtcDateTime などを使っていれば、安全に再実行が行えます。

アクティビティのコードに不具合があった場合には、修正をデプロイ後 Rewind API を使えば失敗した処理から続きを実行出来るので、リカバリの選択肢が広がりそうです。もちろん外部の API を叩いている場合も同様に使えるでしょう。

個人的にはこれで Durable Functions は完成形になったのではないかと思っています。使わないと損します。

Let's Encrypt 周りの自動化を行う Azure Functions の正式版をリリースした

前に作った Let's Encrypt の自動化を行う Azure Functions ですが、あれからもちょいちょい弄り続けた結果、先日やっと正式版を出せました。

いろんなバグやよくわからない Azure REST API の仕様などを乗り越えてきました。

今回の正式版リリースでは以下のような機能を実装しています。

  • 全ての App Service に対応
    • Web Apps / Functions / Web App for Containers など(Windows と Linux に対応)
    • Linux と Containers は Azure DNS が必要
  • ワイルドカード証明書の発行
    • これも Azure DNS が必要
  • wwwroot の readonly モードに対応
    • Run From Package を使った場合に readonly になる
  • RBAC で権限を付与すれば 1 つの Function App で全てに対応

前回のリリースからいろんな部分が大きく変わったのと、HTTP-01 だけじゃなく DNS-01 にも対応したので使い方も簡単に紹介しておくことにします。

必要なロールの変更

以前のバージョンでは Website Contributor だけ追加すれば動作していましたが、今のバージョンでは App Service が含まれているリソースグループには Website Contributor が、App Service Plan が含まれているリソースグループには Web Plan Contributor が必要になりました。

App Service と App Service Plan が同一リソースグループに含まれている場合は、両方のロールをリソースグループに付与します。別々のリソースグループに含まれている場合は少し注意。

Deploy to Azure Button の追加

以前は手動でデプロイしてくださいという形にしていましたが、今は ARM Template と Run From Package を利用して簡単に Function App のデプロイと初期設定が行えるようになっています。

リソースグループ、アプリ名、メールアドレスを入力すれば完了です。

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

サブスクリプション ID はデフォルトでデプロイ先のものが App Settings に設定されます。

Run From Package を使っているので、新バージョンへのアップデートは再起動するだけで終わります。

ワイルドカード証明書と Linux / Containers

Let's Encrypt ではワイルドカード証明書の発行には DNS-01 を使う必要があるので、同じサブスクリプションに設定済みの Azure DNS が必要です。そして IAM で DNS Zone Contributor の付与も必要です。

同様に発行したいドメインを App Service に追加さえしておけば、Function の実行で自動的に ACME Challenge 用の TXT レコードが作成されます。

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

カスタムドメイン用の A / CNAME レコードは事前に設定しておく必要があります。

Windows 以外の App Service や Web App for Containers の場合は HTTP-01 が使えないので、その場合も DNS-01 を使うことになるため Azure DNS が必要となります。

古い証明書の削除

証明書の期限まで 30 日を切っていて、どの App Service にもバインドされていない証明書を定期的に削除する Function を追加しています。

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

動作時刻は証明書の更新からずらしているので、同時には動かないようになっています。

おまけ

Let's Encrypt のアカウントキーは Function App の D:\home\.acme 以下に保存されています。

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

問題が発生した場合は削除すれば、また次回起動時に新しいアカウントが作成されます。

Azure Web Apps でデフォルト以外のパフォーマンスカウンターを Application Insights に送信する

Azure App Service では実行ユーザーの権限に制限がいろいろとかかっているために、Application Insights などを使ったパフォーマンスカウンターの送信はそのままでは出来ません。

しかし、App Service は環境変数を使ってパフォーマンスカウンターの値を渡してくれるので、それを使って Application Insights に値を送信できるようになっています。App Service 向けに専用の Collector が実装されているので、デプロイすればすぐに使えます。

対応しているカウンター名は実装から読み取った方が間違いが無くて正確です。

ASP.NET / Process / .NET CLR 周りのカウンターを公開してくれているので、Application Insights に送信したい場合には、ApplicationInsights.config に追加するだけで使えます。

例えば .NET CLR 周りのカウンターを送信する場合には以下のように書きます。

<Add Type="Microsoft.ApplicationInsights.Extensibility.PerfCounterCollector.PerformanceCollectorModule, Microsoft.AI.PerfCounterCollector">
  <Counters>
    <Add PerformanceCounter="\.NET CLR Memory(??APP_CLR_PROC??)\# Gen 0 Collections" ReportAs="# Gen 0 Collections" />
    <Add PerformanceCounter="\.NET CLR Memory(??APP_CLR_PROC??)\# Gen 1 Collections" ReportAs="# Gen 1 Collections" />
    <Add PerformanceCounter="\.NET CLR Memory(??APP_CLR_PROC??)\# Gen 2 Collections" ReportAs="# Gen 2 Collections" />
    <Add PerformanceCounter="\.NET CLR Memory(??APP_CLR_PROC??)\Gen 0 heap size" ReportAs="Gen 0 heap size" />
    <Add PerformanceCounter="\.NET CLR Memory(??APP_CLR_PROC??)\Gen 1 heap size" ReportAs="Gen 1 heap size" />
    <Add PerformanceCounter="\.NET CLR Memory(??APP_CLR_PROC??)\Gen 2 heap size" ReportAs="Gen 2 heap size" />
    <Add PerformanceCounter="\.NET CLR Memory(??APP_CLR_PROC??)\Allocated Bytes/ sec" ReportAs="Allocated Bytes/ sec" />
    <Add PerformanceCounter="\.NET CLR Memory(??APP_CLR_PROC??)\% Time in GC" ReportAs="% Time in GC" />
  </Counters>
</Add>

アプリケーションをデプロイして暫く待つと、Metrics から設定したパフォーマンスカウンターが見れるようになっているはずです。

後はグラフを描画したり、Analytics でクエリを投げたりといろいろと活用できます。

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

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

動作を確認していた時に気が付いたのですが、昔は表示されていなかった Available Memory が最新の Application Insights をインストールすると確認出来るようになっていました。

少し前のパッケージでは警告が出ていたので、比較的最近に対応が行われていたようです。

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

ちなみに現在プレビュー中の Web App for Containers の Windows 版では、現状は何故かカウンターの値が読めないです。報告済みなので GA までには何とかなると信じてはいます。

Windows Containers は Docker Image を作成する時には Container Administrator という管理者ユーザーとして動くので、GA すれば割と使い勝手は良くなると思います。

Web App for Containers (Linux / Windows) を ARM Template でデプロイする

絶賛プレビュー中の Web App for Containers の Windows 版を ARM Template でデプロイしようと調べていたところ、思ったより Linux 版の情報が無いことに気が付いたので一緒にまとめておきます。

基本的には Windows の App Service とほぼ同じですが、追加の設定が必要になります。例を挙げれば以下のような設定です。

  • WEBSITES_ENABLE_APP_SERVICE_STORAGE を false に
  • 利用する Docker Image を設定
  • SKU を適切なものに

特に Windows Containers の場合は Premium Container の SKU を指定しないと動かないので注意。

ARM Template の全体は Gist にアップロードしてあるので、ついでに Deploy to Azure ボタンも用意しておきました。これで簡単にテンプレートの動作を確認出来るようになっています。

Linux をデプロイする

Web App の定義はほぼ変更ないのですが、Service Plan に関しては少し特殊な指定が必要になります。

Linux の場合は kind = linuxproperties.reserved = true を追加すると、Linux 向けの Service Plan が作成されます。reserved は名前からは想像できませんが、Linux かどうかのフラグになっています。

ちなみに kind = linux と指定しつつ properties.reserved = false とすると Azure Portal 上は Linux なのに中身は Windows という不思議な Service Plan が完成してしまいます。

{
  "apiVersion": "2016-09-01",
  "type": "Microsoft.Web/serverfarms",
  "kind": "linux",
  "name": "[variables('servicePlanName')]",
  "location": "[resourceGroup().location]",
  "properties": {
    "name": "[variables('servicePlanName')]",
    "reserved": true,
    "numberOfWorkers": "1"
  },
  "dependsOn": [],
  "sku": {
    "Tier": "Standard",
    "Name": "S1"
  }
},
{
  "apiVersion": "2016-08-01",
  "type": "Microsoft.Web/sites",
  "name": "[parameters('siteName')]",
  "location": "[resourceGroup().location]",
  "properties": {
    "siteConfig": {
      "name": "[parameters('siteName')]",
      "appSettings": [
        {
          "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
          "value": "false"
        }
      ],
      "linuxFxVersion": "DOCKER|nginx:alpine"
    },
    "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]"
  },
  "dependsOn": [
    "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]"
  ]
}

ARM Template for Web App for Containers (Linux) · GitHub

https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fgist.githubusercontent.com%2Fshibayan%2F58e0b6ed541c3ad46faff12360f7dedc%2Fraw%2Fe6f2b661c614ed36d5aaf3bf1579f5c5d8591902%2Fazuredeploy_linux.json

肝となる linuxFxVersion には単一コンテナを使う場合には DOCKER|nginx:alpine のように Docker Image の名前を指定してあげます。

複数コンテナを使う場合には COMPOSEKUBE を指定して、定義ファイルの内容を Base 64 エンコードした後に連結して指定します。この時点ではフォーマットチェックは行われません。

Windows をデプロイする

Windows も追加の設定が必要ですが、Linux よりは少し分かりやすくなっています。

Service Plan に kind = xenonproperties.isXenon = true を追加しつつ、SKU を Premium Container にすると Windows Containers 向けの Service Plan が作成されます。

ちなみに xenon というのは Windows Containers 向けのコードネームです。

{
  "apiVersion": "2016-09-01",
  "type": "Microsoft.Web/serverfarms",
  "kind": "xenon",
  "name": "[variables('servicePlanName')]",
  "location": "[resourceGroup().location]",
  "properties": {
    "name": "[variables('servicePlanName')]",
    "isXenon": true,
    "numberOfWorkers": "1"
  },
  "dependsOn": [],
  "sku": {
    "Tier": "PremiumContainer",
    "Name": "PC2"
  }
},
{
  "apiVersion": "2016-08-01",
  "type": "Microsoft.Web/sites",
  "name": "[parameters('siteName')]",
  "location": "[resourceGroup().location]",
  "properties": {
    "siteConfig": {
      "name": "[parameters('siteName')]",
      "appSettings": [
        {
          "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
          "value": "false"
        }
      ],
      "windowsFxVersion": "DOCKER|microsoft/iis"
    },
    "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]"
  },
  "dependsOn": [
    "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]"
  ]
}

ARM Template for Web App for Containers (Windows) · GitHub

https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fgist.githubusercontent.com%2Fshibayan%2Fc6db9ae06d9279b73bf3916a194269c0%2Fraw%2F726d8e8ee527c1d3ced195a88839e9e997c41fdf%2Fazuredeploy_windows.json

使用する Docker Image は windowsFxVersion に Linux の時と同様のフォーマットで指定します。まだ複数コンテナには対応していないので DOCKER しか指定できません。

割と簡単な ARM Template で Web App for Containers のデプロイが出来ます。Docker Image の指定部分が初期リリースの時から変更されているので、古い ARM Template の場合は注意が必要です。

Azure Functions の新しい Run-From-Zip の仕組みと活用方法

Visual Studio 2017 の 15.8 で Azure Functions の Zip Deploy と Run-From-Zip に対応したので、そろそろちゃんとブログにまとめておこうかと思いました。

デプロイのタスク定義は Azure Functions の SDK に含まれているので、他のプロジェクトでは Zip Deploy は使えないので注意。

Zip Deploy は名前の通り zip ファイルを渡すと、それをそのままデプロイしてくれる機能です。これまでは Web Deploy 形式として作成しないといけなかったのですが、zip ならどの環境でも簡単です。

特に CI/CD を使う場合には有用ですね。MS Deploy はオプションが少し複雑です。

そして Run-From-Zip は zip ファイルをそのまま wwwroot 以下にマウントして実行に使ってしまうという、App Service の謎テクノロジーです。GitHub の Issue は非常に長くディスカッションが続いています。

Azure Functions の起動や実行が遅いのは、主にストレージが Azure Files が使われていて、通常の App Service よりもファイル I/O に対しては低速になっているのが原因です。

それに対して Run-From-Zip は App Service の起動時に zip をインスタンスのローカルストレージに落としてきて、それをマウントしているみたいなので Azure Files の I/O に引っ張られません。そしてローカルストレージは高速なので全体的なパフォーマンスが改善するというからくりです。

Run-From-Zip はこれまでのデプロイと比較して、以下のような点で優れていると言われています。

  • パフォーマンス改善
    • デプロイ
    • スタートアップ
  • デプロイのアトミック性
    • マウントする zip を切り替えるだけなので
  • バージョニング

バージョン管理はストレージ上に zip の履歴を持っているので、packagename.txt の中身を書き換えて App Service を再起動すればそのバージョンがデプロイされるという仕組みです。

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

この zip を最大いくつ保持してくれるのかは謎ですが、Visual Studio からデプロイするとファイル名がローカル時間のタイムスタンプになるのが少し気になります。

実際に Azure Functions を Run-From-Zip としてデプロイしてみます。15.8 がインストールされていれば、発行先の選択時に「ZIP から実行」というチェックボックスが増えているので、それにチェックを入れます。

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

その後はこれまで通りにデプロイ先を指定すれば、Zip Deploy 用の発行プロファイルが作成されます。

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

発行を実行すると、数秒でビルドからデプロイまで完了します。

デプロイの結果は Kudu のメニューから「Zip Push Deploy」を選べば最新の情報を確認出来ます。

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

Visual Studio からのデプロイに対応したので、これまでと同じように違和感なく使えるようになって良い感じです。まだプレビューという点だけは気になりますが、推奨されているので大丈夫でしょう。

Run-From-Zip は主に Azure Functions 用みたいに紹介されていますが、仕組み自体は Functions に依存するものではないので通常の Web App でも利用することが出来ます。

使い方は非常に簡単で、WEBSITE_RUN_FROM_ZIP キーを追加して Zip Deploy を使うだけです。

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

これで Web App でも Run-From-Zip が使えるようになりますが、ストレージが読み込み専用になるので App_Data に何かを書き込むようになっている場合は落ちます。特に PHP で多い wwwroot 以下にキャッシュを作るアプリの場合は無理でしょう。

元々 App Service のストレージに対して頻繁に I/O を行うのは避けた方が良いので、Redis Cache や SQL Database など外部のマネージドサービスを組み合わせれば十分実用的だとは思っています。

最後に Run-From-Zip に用意された 2 つの動作モードを紹介します。

1 つは Zip Deploy を使って Azure Files にマウントされたストレージに zip を保持するローカルモード、もう 1 つはアクセス可能な URL を指定して、実行時に zip をダウンロードしてくるリモートモードです。

リモートモードでは WEBSITE_RUN_FROM_ZIP に zip の URL を指定するだけです。App Service が再起動するたびに新しくデプロイされることになるので、ARM Template などで同時に配布する際に最適です。

実際に Azure Functions App の配布に使っていますが、CI/CD との相性が非常に良いので便利です。

Azure App Service で障害が発生した場合にやること

Azure App Service は非常に便利なのですが、アプリケーションに何らかの問題があった場合には VM などと比べると調査方法が若干特殊です。今は公式の診断機能がかなり強化されていますが、逆に多すぎて困ることもあるのでまとめました。

恐らく歴史的経緯によって機能がダブっている部分もあるので、その辺りについても書きました。

用意されているツール

Diagnose and solve problems を使う

何が起こっているのかわかっていない場合は、とりあえずこの自動診断機能を使えば大体の理由は判別できます。おおよその見当がついている場合は、該当するボタンを押せば確認出来ます。

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

帝国兵殿が Qiita に詳しい使い方を書いてくださっているので、こっちを読めば完璧です。

自動診断は非常に優秀ですが、CPU やメモリを大量に消費していることはわかっても、何故かという部分まではサポートしてくれないので、これから先は別のツールを使うことになります。

ASP.NET アプリケーションに関しては、診断系が他のプラットフォームよりも強化されています。App Service Team のブログにて紹介されています。

UI が用意されているのと、ブラウザで完結するのでお手軽に利用できます。

Support Tools を使う

Diagnose and solve problems のサイドバーに Support Tools が用意されているので、そこから個別にメトリクスを確認したり、アプリケーションのイベントログを表示できます。

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

Diagnostics as a Service の操作も行えます。ちなみに Diagnose and solve problems のダンプ取得などは DaaS を使っているので、Always On が必要になります。

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

Basic 以上のインスタンスが必要になるので、その点だけは注意なのと DaaS は WebJob として動き続けるので、プロセスを落としたりしないようにしましょう。

他にも、スケールアウトしているインスタンスのうち 1 台だけ異常な状態になった場合は、Advanced Application restart を使って特定のインスタンスだけを再起動することが出来ます。

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

根本的な解決ではないですが、問題を軽減することが可能です。他にも Mitigate から Auto Heal の設定が行えますが、それは後で紹介します。

Kudu を使う

最後の砦として、サンドボックス環境で許されている操作なら大体行えるインターフェースとして Kudu があります。もはや説明は不要感ありますが、改めて簡単に紹介を。

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

Debug Console を使うと、App Service にプリインストールされている診断用ツールを使えます。

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

ツールを直接使うと、用意されている全てのオプションが扱えるので、他のツールを使った場合には不可能なオプションの設定が出来ます。当然ながらコンソールで動くものに限ります。

Application Insights を使う

継続的にアプリケーションの問題を収集するためには Application Insights を使います。

通常ならメトリクスとエラー周りのみですが、Profiler と Snapshot Debugger をインストールするとパフォーマンスデータとクラッシュ時のメモリダンプを自動で取得してくれます。

両方ともダウンロードすれば Visual Studio で表示できるので、デプロイしたコードとバージョンを合わせておけば便利に使えます。

問題が発生すれば自動で収集してくれるので、特に考えることなく使えます。

パフォーマンス問題の場合

プロファイリングを実行する

Kudu を使うと IIS の ETW 付きのプロファイリングを実行出来ます。取得したデータはダウンロードして PerfView で確認出来るようになっています。

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

警告にもあるように、パフォーマンスへの影響が大きいのでカジュアルに実行は難しいでしょう。

実際にプロファイリングを実行して、原因を特定する例が去年のアドベントカレンダーで紹介されています。例では PHP ですが、ASP.NET でも同様にトレースできるはずです。

[Advent Calendar 2017 Day18] Azure Web Apps パフォーマンスチューニング – d99@Microsoft

面倒な場合は Visual Studio からリモートでプロファイリングを実行出来るので、こっちを使ってみるのも良いかもしれません。

結果は Visual Studio で確認出来るので、使い慣れている場合はこっちの方が分かりやすそうです。

メモリダンプを取得する

Diagnose and solve problems を使うと、DaaS 経由でダンプを取得し Debug Diag で解析した結果をダウンロード出来るので、誰でも簡単に解析できるようになっています。

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

これだけでは不足する場合には、Kudu からミニダンプとフルダンプの両方を取得可能です。

分かりにくいですが、Process Explorer を右クリックするとメニューが表示されるので、そこからダンプを取りつつダウンロードまで行えます。

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

ダウンロードしたダンプは Visual Studio や dotMemory に突っ込むもよし、WinDbg を使ってハードに扱っても良しなので、根本的な問題解決に役立つかと。

少し手間がかかる方法ではありますが、ProcDump を直接使うと例外フィルターを設定できるので、特定の例外が発生したタイミングでダンプを取るという方法も行えます。

Azure App Service: Generating memory dumps on first chance exception using Procdump | Unleashed

ちなみに ProcDump を使って手動でダンプを取る場合には、SMB でマウントされていない場所にダンプを書き出した方が、遥かに高速で取得できるのでお勧めです。

特に Premium V2 などを使っていると、ローカルが SSD になるのでさらに顕著でしょう。

Auto Heal を有効にする

根本解決に時間がかかりそうなので、一時的に問題の発生を抑えたいという場合には、大体は再起動が有効です。そんな場合には Auto Heal を有効化して、一定の条件で自動的に再起動するように設定します。

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

リクエスト数や CPU、メモリ使用量を条件に指定出来るので、適切に設定すれば問題の発生を上手く抑えられるでしょう。とはいえ、あくまでも緩和策でしかありません。

最近は Proactive Auto Heal がデフォルトで有効になっているので、逆に予期せぬタイミングで再起動が発生して障害になることもあるかも知れません。

その場合は設定を追加すればオプトアウトが行えるので、必要に応じて設定を追加しましょう。

これで App Service で使える診断周りの機能について大体まとめた気がします。後から足りないと気が付いたら足すかも知れません。

ACR Build を使って ASP.NET アプリケーションの Docker Image をビルドする

ついに App Service での対応によって Windows Containers を利用する機運が個人的に高まってきたので、前から気になっていた ACR Build を使って ASP.NET 向けのイメージをビルドしてみました。

VSTS や AppVeyor でも Docker Image を作ることは出来ますが、ACR Build の方が優れている部分がいくつかあります。自動で OS Image が更新されたときにビルドしてくれるとか最高ですね。

ドキュメントには Windows という文字が全く出てこないのですが、Azure CLI 2.0 を叩いていると OS を指定できるようになってるので、今は問題なく使えます。

とりあえずチュートリアルを参考にマニュアルでのビルドを試します。

これまで VSTS や AppVeyor ではタスクを定義したり、yml を書いたりしてイメージをビルドしてきましたが、ACR Build では Dockerfile を読んでビルドする機能しかないので、予め既存の Dockerfile を Multi-stage builds を使うように変更します。

軽く調べた感じでは ASP.NET の Multi-stage builds を見なかったので良い機会です。

ASP.NET アプリケーションを Multi-stage builds 化

先に Multi-stage builds を使うように既存の Dockerfile を修正します。公式ドキュメントがあるので、読みながら進めていけば大体問題ない感じです。それよりも .NET 固有の部分のがはまります。

要するにビルド用のイメージと実行用のイメージが必要になるのですが、ちょっと前にイメージが整理されて .NET Framework のビルドイメージが利用しやすくなっています。

今のバージョンはビルドは問題ないですが、一部のコンポーネントが正しくインストールされていないので動作しない機能もあります。特に Razor のプリコンパイル周りが動作しないので、Issue を上げています。

Dockerfile の中で nuget restore と MSBuild を実行すれば良いので、割とシンプルに書けます。

# escape=`

FROM microsoft/dotnet-framework:4.7.2-sdk as build

COPY . .

RUN nuget restore; `
    msbuild .\AspNetApplication\AspNetApplication.csproj /nologo /v:m /t:Build /p:DeployOnBuild=true /p:PublishProfile=FolderProfile


FROM microsoft/aspnet:4.7.2

COPY --from=build ./publish/ /inetpub/wwwroot

注意点としては .NET Framework のイメージを使うと、デフォルトのシェルが PowerShell になっていることぐらいです。なので、よく使われるように && でコマンドを繋ごうとするとエラーになります。

それ以外は特に書くことがありませんでした。プロジェクト全体は以下のリポジトリで公開しています。

AppVeyor でのビルド定義も同時に置いてあるので参考にしてください。

ACR Build でイメージを作成

ASP.NET アプリケーションを Multi-stage builds に変更したので、1 コマンドで ACR Build を使って Docker Image の作成が行えます。OS の指定だけ間違えないように注意です。

az acr build --registry aspnetsample --image aspnetapp:v1 --os Windows https://github.com/shibayan/appveyor-aspnet-docker.git

Cloud Shell でコマンドを実行しましたが、大体 7,8 分ぐらいでビルドが完了しました。最低限の Server Core イメージはキャッシュされているみたいなので、AppVeyor よりは多少高速です。

最後の方にイメージの依存関係を出力してくれます。ビルドタスクを組んでいると、イメージが更新されたタイミングでリビルドしてくれるらしいです。

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

プッシュされたイメージは Azure Portal から確認できます。

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

ちゃんとプラットフォームは Windows になっているので問題ないです。ACR Build を使うと面倒な権限周りの設定が不要なのも利点ですね。

Web App for Containers にデプロイ

最後に作成したイメージを Web App for Containers にデプロイします。といっても Azure Portal からドロップダウンで選択していくだけなので特に説明不要ですね。

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

保存して暫く待つとイメージが入れ替わって、ACR Build で作成したものになります。中々変わらない場合は再起動すれば大体はイメージを取り直してくれます。

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

ちゃんと ASP.NET MVC のアプリケーションが動作するところまで確認出来ました。

本来なら ACR Build でのイメージ作成が完了して、ACR にプッシュされたタイミングでステージングスロットにデプロイし、コンテナを起動したのちにスワップで本番リリースという流れを組みたいのですが、今のところは Web App for Containers 側の対応が足りないので残念です。

とはいえ、Windows Containers を使った開発から CI/CD、実行環境まで整った感があるので、後は Web App for Containers の進捗を待つという形になりそうです。