しばやん雑記

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

Azure Pipelines の Azure Resource Manager 接続は 2 年で期限が切れるので注意

Azure App Service や Azure Functions に対してデプロイする場合には、必ず Service connections に Azure Resource Manager を追加していると思います。

その際は大体が Azure Pipelines から Service Principal を自動生成していると思います。

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

ここから Service Principal を作成すると、Azure Portal の Azure AD から一覧を確認できます。

Azure Pipelines から作成された Service Principal の名前は組織名 + プロジェクト名 + サブスクリプション ID になるのでわかりやすいです。

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

重要なのがクライアントシークレットで、Azure Pipelines が作成したものは 2 年の期限付きなので Service connection を追加してから 2 年が経過すると、ログイン出来なくなります。

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

期限切れの通知などはしてくれないので、Pipeline の実行でエラーになってから気が付くことが多そうです。既に使ってる人は一度確認しておいた方がよいでしょう。

正直このあたりの情報を見た覚えがないので、既存の Service connection の更新方法と期限切れを起こさない Service Principal を作成して設定するまで試しました。

手動でクライアントシークレットを更新する

Service connection の設定には更新する機能は無さそうに見えますが、実は該当する Azure Resource Manager 接続から Edit を選ぶと更新が出来るようになってました。

Edit を選んで表示されるダイアログは、そのまま OK を押せるようになってます。OK を押すと、再度 Azure AD での認証が走るのでログイン画面が表示されるはずです。

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

しばらくするとダイアログが閉じられるので、その後 Azure AD から Service Principal のクライアントシークレットが更新されたことが確認できます。

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

Service connection の作成しなおしとは異なるので、既存の設定を壊すことなく更新できるのは良いです。更新日から 2 年間有効ですが、この作業がまた発生すると考えると面倒です。

無期限のクライアントシークレットを使う

Azure Pipelines から Service Principal を作成すると 2 年間のクライアントシークレットが作成されますが、手動の場合は無期限のクライアントシークレットを作成できます。

簡単に Service Principal を作成するのには Azure CLI がおすすめです。1 コマンドで作れます。

無期限という指定はないので、100 年間有効なクライアントシークレットを作成します。Azure CLI を以下のようなコマンドで実行すると作成されます。

az ad sp create-for-rbac -n "azpipeline" --role contributor --years 100

一応 Azure AD の画面から期限を確認しておくと、ちゃんと 100 年後になっています。

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

あとは Service connection の追加画面から "use the full version of the service connection dialog." というリンクを選ぶと、既存の Service Principal を設定できるダイアログになるので、作成されたクライアント ID とクライアントシークレットを入力します。

設定後は Verify connection を選んで Service Principal のチェックを行っておきます。

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

追加してしまえば、あとは通常の Service connection と同じように利用できます。YAML の編集時には Task アシスタントで Azure 上のリソース一覧を取得してくれますが、エラーが出ることなく動いてます。

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

実質無期限のクライアントシークレットを使っているので、最初の設定だけが少し面倒ですが 2 年毎に更新する必要がないので楽です。更新は簡単とはいえ高権限のユーザーが行う必要があるのが少しネックです。

期限が長すぎるのもどうかなと思いますが、シークレットの失効は行えるのでまあ良いかという気持ちです。

後悔しないための Azure App Service 設計パターン (2020 年版)

Azure App Service (Web Apps) がリリースされて 6 年、情報のアップデートを行いつつ気になった情報は適当にブログに書くという日々ですが、Regional VNET Integration や Service Endpoins が使えるようになって設計に大きな変化が出るようになったのでまとめます。

最近は Microsoft で HackFest を行うことも多いのですが、App Service をこれから使い始めたいという場合に、失敗しない構成を共有したい、知ってほしいという意図もあります。多いですが中身は単純です。

アーキテクチャという観点では App Service だけで収まるものではないのですが、今回は App Service 周りに限って書いています。単純に App Service は機能がめちゃくちゃ多いというのもあります。

ひとまず自分が思いついたものを一通り書いたはずですが、足りないものがあれば後で追記するかもしれません。一部 Preview な機能も混ざってはいます。

基本設定

64bit Worker は必要な場合のみ利用する

最初の方で地味に迷うのが 32bit / 64bit の設定だと思います。デフォルトでは 32bit になっていますが、大量にメモリを消費することがわかっているアプリケーション以外では 32bit のままで問題ありません。

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

64bit にすると扱えるメモリは増えますが、同時にメモリの使用量も増えます。

App Service は 2 コア以上の App Service Plan を使わない限り、大体は 4GB 以下のインスタンスなのを忘れないようにしましょう。あとアプリケーションだけではなく Kudu も同時に 64bit になるので注意。

FTP / Web Deploy をオフにする

FTP / FTPS の無効化はセキュリティ的な観点でも重要ですが、FTP / Web Deploy といったデプロイ方法は、今となってはアトミックではなくバージョン管理もされない悪手です。

利用できる状態にしていると、簡単にアプリを壊すことが出来るのでオフにします。

本当に最初期の開発ならともかく、それ以降でも Visual Studio から Web Deploy を使って新しいアプリケーションのデプロイを行うのは、あり得ない状態だと認識してください。

Always on を有効化する

App Service はアーキテクチャ的にコールドスタートは発生しうるものですが、Always on を有効化することでトラフィックが無い時にインスタンスが落ちるのを防ぐことが出来ます。

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

これで常にウォームスタートになるので、いつトラフィックが来ても素早く応答することが可能です。

ウォームアップリクエストを受け取ったタイミングで、コネクションやキャッシュといった必要な初期化処理を行っておくと更に効果的です。

ARR affinity をオフにする

Session Affinity のために自動的に App Service がクッキーを発行して、2 回目以降も同じインスタンスにルーティングする仕組みですが、自動的にインスタンスの入れ替えが発生する App Service とは根本的に相性が悪いです。全てのリクエストにクッキーが付くので効率的にも微妙です。

ステートレスなアプリケーション設計ならば不要なのでオフにします。

アプリケーションがセッションを利用している場合には、ちゃんと Redis Cache や Cosmos DB といったインスタンス間での共有ストアを用意して、リクエストを受けたインスタンスに依存した作りにしないことです。

当然ながら ARR Affinity をオンにしていると、ロードバランサーは偏ったルーティングを行うこともあるので、一部のインスタンスに負荷が集中してしまうことも容易に考えられます。

HTTP/2 の有効化を検討する

HTTP/2 を有効化したからと言って全てが改善するわけではないですが、App Service では HTTP/2 と HTTP/1.1 が簡単に切り替え出来るのでテストが行いやすいです。

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

実際のところケースバイケースという話が多いので、実際のアプリケーションでベンチマークを行っていくしかないですが、試してみる価値はあるでしょう。

Health Checks の有効化を検討する

プレビュー中の機能ですが、Health Checks を有効化すると App Service が設定されたエンドポイントへのリクエストを行い、失敗が続いているインスタンスは LB から切り離してくれるようになります。

LB から切り離されたインスタンスは、リクエストが成功すると再び LB に戻されます。この動作によって不良インスタンスがリクエストを処理するのを最小限に出来ます。

切り離されたインスタンスが 1 時間失敗し続けている場合には自動でリサイクルが行われて、完全に最初からアプリケーションの立ち上げが行われます。

タイムゾーンの設定を検討する

App Service に限らずパブリッククラウドを使っていて戸惑うことが多いのが、タイムゾーンが UTC で設定されていることだと思います。

日付をタイムゾーン込みで扱うのが理想ではありますが、国内のみ対象にした場合には割り切ってタイムゾーンを変更してしまうのが簡単です。

昔はタイムゾーンの設定は出来なかったので +9 時間するコードがよく書かれていましたが、今は App Service のアプリケーション単位での切り替えが出来るようになっています。

認証が必要な場合は App Service Authentication (Easy Auth) を利用する

関係者のみ見れるようにしたいステージングサイトや、本番向けでも認証が必要な場合には App Service Authentication (Easy Auth) を使うのが非常に簡単です。

後述する Deployment slot と組み合わせると、ステージングサイトを簡単に保護できるようになります。

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

昔は Basic 認証を行うモジュールを個別にインストールするケースもありましたが、Easy Auth を使うと数クリックで Azure AD に登録されているユーザーのみ閲覧可能に出来ます。

ドキュメントには記載されていませんが、OpenID Connect に対応しているので Azure Portal に設定項目がないプロバイダーでも対応可能です。Azure Functions との組み合わせが特に強力です。

アプリケーション設定 / 接続文字列

Web.config / appsettings.json にべた書きしない

開発環境用の設定や接続文字列であれば問題ないケースは多いですが、本番環境向けの設定は Web.config や appsettings.json といったリポジトリに入るファイルには書かずに App Service 側でオーバーライドします。

たまに全く App Settings / Connection Strings を使っていないケースもありますが、ファイルに書いていると簡単に事故るので分けて管理しましょう。

App Service に設定する方法は ARM Template や Terraform といった IaC を利用すると、コードで分かりやすく定義でき更にバージョン管理もされるのでお勧めです。

アプリケーションレベルで対応する場合には、Key Vault や App Configuration を直接利用するという方法もありますが、後述する Managed Identity と組み合わせても最低限の設定は必要となります。

Key Vault Reference の利用を検討する

SQL Database や Azure Storage などの接続文字列を個別に App Settings に追加するのではなく、Key Vault の Secret として追加して App Service からは Key Vault を参照する形にすると安全に一元管理が行えます。

一部制約があり、現在は Key Vault の Service Endpoints を有効化すると、App Service が VNET Integration を使っていてもアクセスできなくなります。

Managed Identity の利用を検討する

理想としては Managed Identity を使って App Service 単位でアクセス可能なリソースを割り当てることで、キーの管理自体を無くしてしまうことです。寿命の短い Bearer Token を使った認証になるので安全です。

いくつかの Azure サービスが Managed Identity を使ったアクセスに対応してきていますが、クライアントが Managed Identity に適した設計になっていないケースもあるので、まだ全面導入は難しい部分があります。

ネットワーク設定

HTTPS を常に有効化する

世界的に常時 HTTPS での通信は当たり前になっています。App Service はデフォルトでオンになっていないですが、チェックを入れるだけで常に HTTPS へリダイレクトさせることが出来ます。

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

TLS バージョンの選択も出来ますが、ここはデフォルトの TLS 1.2 で普通は問題ありません。こちらも世界的に TLS 1.0 / 1.1 はセキュアではないので、TLS 1.2 以降を利用する流れとなっています。

Regional VNET Integration を利用する

以前から VNET Integration は実装されていましたが、Point-to-Point VPN を使っていたので VNET ゲートウェイが必要で Service Endpoints や ExpressRoute が使えないなど制約が非常に多かったです。

新しい Regional VNET Integration ではそういった制約はなくなっています。

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

ASE や VM のように専用のインスタンスが VNET 内に直接配置されるわけではなく、あくまでもマルチテナントでの実行となりますが、これまでは ASE でのみ実現可能だったアーキテクチャをマルチテナントの App Service でも構築出来るようになりました。

IP 制限ではなく Service Endpoints を利用する

App Service では元から IP アドレスベースでのアクセス制限は行えましたが、Service Endpoints が追加されたことにより特定の VNET の Subnet からの通信のみ許可するといった設定が行えるようになっています。

フロントエンドに用意した App Service が VNET を経由して、Service Endpoints でアクセス元を制限したバックエンドの App Service を実行するという構成も簡単に構築できます。

Service Endpoints は既に数多くの Azure サービスで対応しているので、App Service で VNET Integration を使っていればアクセス元の制御が簡単に行えるようになっています。

特に SQL Database や Cosmos DB といったストレージ系のサービスでは IP アドレスでのアクセス制限ぐらいしか出来なかったのが、Subnet 単位で許可出来るようになったのはかなり大きいです。

VNET Integration と合わせて積極的に利用していきましょう。IP アドレスでの制限は全て捨てます。

CDN / Front Door の導入を検討する

App Service の前に CDN や Front Door を置くことで、コンテンツ配信の最適化をすることが簡単に行えるようになっています。設定も Azure Portal から行えるので迷うことは無いでしょう。

最近のアプリケーションは静的ファイルが非常に多くなってきているので、適切なキャッシュ戦略と CDN を使った最適化は必須になってきています。

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

特に Front Door は CDN と L7 LB を組み合わせたサービスなので、アプリケーションの負荷分散や DR にも使えます。ルーティングも柔軟に行えるので、アーキテクチャを変更した時にも Front Door で対応できます。

静的なコンテンツの配信には CDN / Front Door が圧倒的に有利なので、早めに導入を検討してお行きたいサービスです。Traffic Manager とは異なり Front Door は L7 LB なのでバックエンドの切り替えもすぐです。

スケーリング設定

本番環境では Premium V2 を利用する

App Service Plan はいくつも Tier があって最初は悩むかもしれませんが、最低でも Premium V2 の P1v2 を選んでおきましょう。Premium V2 はコストとパフォーマンスのバランスが取れています。

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

今は Premium V2 を使う予定はないからといって、Standard や Basic で作ってしまうと Regional VNET Integration が利用できない Scale unit に当たってしまうこともあるので、最初は必ず Premium V2 を指定してインスタンスを作るのが正解です。

作成後は Standard や Basic に変更しても問題ありませんが、Premium V2 とそれ以外の Tier 間の変更には Outbound IP アドレスの変更が伴うので、その点だけ注意が必要です。

今は変更のタイミングで警告メッセージが表示されるようになっていますが、IP 制限に使っている場合には問題となるので VNET Integration と Service Endpoints を使っておくと安心です。

オートスケーリング設定を検討する

今の App Service のオートスケーリング設定は Azure Monitor ベースで、様々なメトリックを元にルールを作成できるようになっています。それ以外にも曜日での固定スケーリングルールも作成できます。

App Service はスケーリングが非常に高速に行われるので、オートスケーリング設定との相性が非常に良いです。常時同じアクセスが発生するアプリケーション以外ではオートスケーリングを検討する価値があります。

Consumption / Premium Plan を活用する

イベントドリブンなアプリケーションの場合は Azure Functions と Consumption / Premium Plan を使って、イベントベースでのスケーリングを利用するようにします。

App Service Plan を使うよりスケーリングが早く、インスタンス数の上限も多いため大量のイベントを簡単に処理できるようになります。Premium Plan を使うとコールドスタートを避けつつも、イベントベースでのスケーリングが利用できるので検討しましょう。

アプリケーションのデプロイ

最低でも Zip Deploy を利用する

App Service には複数のデプロイ方法が用意されていて、FTP / Source Control / Web Deploy / Zip Deploy などがありますが、カジュアルな Web サイト以外では Zip Deploy を利用しましょう。

FTP / Web Deploy はデプロイがアトミックではないので、一部のファイルがデプロイ失敗すると整合性が破綻してアプリケーションが正しく動作しなくなります。

よくデプロイで発生する問題が古いファイルが残っていて、良くわからないエラーが発生することですが、FTP / Web Deploy を使っているから発生する問題と言えます。

特に Visual Studio からデプロイする際に使われる Web Deploy は今となってはパフォーマンスも悪く、デプロイに失敗しやすいのでメリットがほぼありません。CI を導入して Zip Deploy に切り替えると安定します。

Run From Package の利用を検討する

Zip Deploy ではアップロードされた Zip ファイルを展開して wwwroot 以下にデプロイする仕組みですが、Run From Package を使うと zip をそのままマウントするので、デプロイ時間の短縮や起動パフォーマンス改善につながります。もちろん単一ファイルをマウントするのでアトミックです。

Consumption / Premium Plan で Azure Functions を利用する場合には必須、App Service で Web アプリケーションを利用する場合にはローカルストレージへの書き込みを行わない場合に利用を検討出来ます。

App Service はローカルストレージのパフォーマンスが良くはないので、状態は外部のストレージに持たせる設計が多いはずです。その場合は Run From Package が使えます。

Deployment slot を使って Swap でのリリースを行う

開発中はともかく、本番環境へのデプロイ時には動作している App Service に対して直接デプロイするのではなく、別のスロットへのデプロイを行った後にスワップでリリースを行うようにします。

スワップのタイミングで App Service がウォームアップを行ってくれるので、初回のリクエストが遅いといった問題を回避しつつ、新バージョンでエラーが発生した場合には再スワップで元のバージョンに戻す運用が簡単に実現できます。

スワップ時のウォームアップへの対応や App Settings に追加すると良いキーなどいろんな情報がありますが、Ruslan のブログに書いてある内容が非常に参考になります。

Azure Pipelines / GitHub Actions などを利用して自動化する

初期の開発中はともかく、基本的には App Service へのデプロイは Azure Pipelines や GitHub Actions などの CI SaaS を使ってビルドとデプロイを自動化します。

App Service へのデプロイ自体は Task や Action が用意されているので簡単です。

Visual Studio や VS Code などを使って手動でデプロイするのはバージョン管理出来ていないですし、デプロイに必要な権限すら適切に管理出来てないことになります。

権限管理が出来ていないのはあり得ないので、予め CI 前提で考えていく必要があります。App Service のデプロイに必要な権限は実際かなり強いです。

リソース構築のコード化 (IaC)

ARM Template / Terraform を使って構築する

App Service は機能が多いので、その分設定項目も非常に多いです。なので複数の環境を用意する際に Azure Portal から手作業で作成するとミスが発生したり、設定が微妙に異なるという問題が簡単に発生します。

そういった問題を避けるためにも、最初から ARM Template や Terraform を使ってコード化して管理します。

App Settings や Connection Strings に関しても、ARM Template や Terraform を使うと管理できるため、実行時に動的に変更する必要がないものは IaC に寄せた方が管理しやすくなります。

Terraform を使う場合は、それ自体を Azure Pipelines などを使って自動デプロイするように構築します。

モニタリング / 診断

Application Insights を有効化する

アプリケーションに直接 Application Insights SDK を入れている場合はキーの設定だけ良いですが、それ以外の場合は Azure Portal から有効化するか、必要な App Settings を追加することで有効に出来ます。

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

Application Insights を利用したモニタリングと実践については去年の de:code で話しているので、使用したスライドを参考にしてもらえれば良いかと。

モニタリング系は問題が発生してからでは遅いので、きっちりと最初から設定しておく必要があります。

特に Application Insights は 5GB まで無料の容量課金なので設定しない手はありません。

Azure Monitor を使ったログ転送を有効化する

歴史的経緯から App Service のログはローカルストレージや Azure Storage にしか書き出せませんでしたが、Azure Monitor との統合が行われて他のサービスと同じ機能が使えるようになりました。

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

ファイルとして書き出すだけではなく、Log Analytics や Event Hub への書き出せるので、KQL を使って複雑な条件で絞り込むことも簡単に出来るようになっています。クエリを簡単に実行できるのは重要です。

Event Hub を使えばストリームでログを処理できます。異常検知を行いたい場合にも使えそうです。

App Service Diagnostics を積極的に活用する

App Service は組み込みの診断機能が非常に優秀で機能が豊富です。Azure Portal から "Diagnose and solve problems" を選ぶと対話式に診断を開始できます。

様々なメトリックから問題を検出してくれるので、App Service で動かしているアプリケーションに問題が発生した場合は、必ず最初に試しておくことを推奨しています。

特に TCP Connections は SNAT 絡みで問題となるケースが多いので便利です。一部の診断は Consumption で使えない制約がありますが、一時的に App Service Plan 上に移動してしまえば問題ありません。

API Management にカスタムドメインと SSL/TLS 証明書を追加してみた

これまでに API Management をちゃんと弄ったことがなかったのと、運用するとなるとカスタムドメインと証明書設定は必須になるので絞って確認してみました。

カスタムドメインは Azure の場合、確認用の CNAME / TXT レコードを作成する必要があることが多いので、その辺りを調べておきたいという意図です。特に証明書に関してはローテーションが必要になるので、出来れば Key Vault に寄せておきたいです。

ドキュメントによると Gateway / Portal / Management / SCM の 4 つに対して、個別にカスタムドメインの割り当てが可能と書いてありますが、基本は Gateway を抑えておけばよいです。

テスト用に API Management を作成しておきました。Consumption 以外はデプロイに 15 分ぐらいかかるので注意が必要です。バックエンドが Cloud Services なのでインフラ周りは改善してほしいです。

API Management を作成した後は、適当に Swagger 定義を読み込ませて API を作成しておきます。

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

ここで作成した API は Product と紐づけておけば利用できるようになります。API Management は結構重厚なサービスなので Subscription や Product といった要素があるので、理解するのは少し大変です。

とはいえ、使わないという方法や Consumption を使うとかなりの機能が制限されているので、基本的な API Gateway としてまず使うのが良いかと思いました。

Azure DNS に CNAME レコードを追加

先に DNS レコードの追加が必要かと思っていましたが、実は全く関係ないドメイン名でも API Gateway に追加可能のようです。App Service のように検証はしてくれなかったので、この点は注意が必要です。

Consumption 以外では A / CNAME の両方が使えるようですが、CNAME を使うのが安全です。

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

これで保存すれば、設定したドメイン名で API Management への解決が行えるようになります。

Key Vault に証明書を作成

証明書は API Management へのカスタムドメイン追加時に同時に指定する必要があるので、先に Key Vault 側に証明書を作成しておきます。

自己署名証明書でも問題ないかもしれませんが、今回は Let's Encrypt の証明書を使いました。

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

Azure DNS と Key Vault を使って Let's Encrypt を作るには、前に作った keyvault-acmebot を使いました。サクサク証明書を Key Vault に作成できるので便利です。

Key Vault を使うと証明書の一元管理が簡単に行えるようになるので、App Service で使う場合でも Key Vault 経由で証明書をインポートした方が便利なことが多いです。

API Management にカスタムドメインと証明書を追加

ここまでで DNS レコードと Key Vault 証明書の準備が完了したので、API Management に設定を追加していきます。Custom domains から Add を選ぶとドメイン追加の画面になります。

ドメインの割当先は Gateway にしておきます。Developer Portal への割り当ても考え方は同じなので、必要な場合は追加しておけばよいです。

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

ログインに OpenID Connect を使っている場合は、リダイレクト URL の変更も必要になるはずです。

証明書は PFX をアップロードする方法と Key Vault 証明書を参照する方法の 2 つが選べますが、PFX の手動管理は事故の元なので Key Vault を使うようにしていきましょう。

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

対象となる Key Vault と証明書を選ぶだけなので、非常に簡単に設定できます。

保存しようとすると、初回の場合は以下のようなメッセージが表示されると思うので、Yes を選んで Managed Identity と Key Vault のアクセスポリシーの設定を自動的に行ってもらいましょう。

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

ドキュメントでは自分で設定が必要という風に書いてありましたが、Azure Portal からの場合は全て自動でやってくれるので便利です。App Service と同様の仕組みですね。

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

Consumption Tier 以外の場合は設定完了までに 10 数分かかるようなのでしばらく待ちます。Cloud Services で動いてるインスタンスにデプロイしに行っているからか、割と時間がかかります。

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

設定が完了すれば、追加したカスタムドメインを使って API Management へのアクセスが行えるようになります。Key Vault 証明書を選んだ場合は、更新があれば自動的にアップデートしてくれるようでした。

API を呼び出して確認

設定が完了したので適当に API を呼び出して、カスタムドメインと証明書が設定されているか確認します。

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

ちゃんと Let's Encrypt で発行した証明書が使われていることが確認できます。

Protocol settings からは SSL/TLS の暗号スイートやプロトコル、HTTP/2 の有無なども設定できるので、要件に合わせて適宜選ぶことができます。デフォルトでは TLS 1.2 のみ有効になっています。

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

基本的には安全側に倒すような設定になっているので、変更する際はその副作用をしっかりと理解しておく必要があります。ほとんどのケースではデフォルト設定のままで良いはずです。

利用する際には Consumption Tier がかなり良いと感じているので、時間があればスケールについても調べてみたいですね。ちなみに Consumption は App Service 上に作られているので、多少は制限があります。

Azure Pipelines に追加された特定の Service Connection で承認を必須にする機能が便利

Azure DevOps の Sprint 158 Update で紹介されていた割に全然公開されていなかった Service Connection に対する承認機能が、最近になって自分のテナントでも使えるようになってました。

Sprint 158 Update は 9 月なので内容を既に忘れている人も居そうです。

紹介されているようなインターフェースとは異なっていて、Edit の横の「…」ボタンを選ぶと Approvals and Checks の項目が出てきます。機能としては Environments のものと同じです。

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

何が嬉しいかというと、本番向けの Azure Subscription など気軽に弄られたら困るような Service Connection に対して、ここで設定をしておけば Task で使われた場合に自動的に承認が必須になるという点です。

これまでは Environments を使うしかなかったので、必要な処理は Deployment job にまとめて実行する必要がありましたが、もっとシンプルに承認機能を使えるようになりました。

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

設定方法などは完全に Environments と同じなので、既に使ったことがある人は簡単に設定できるはずです。この辺りも徐々に改善されていて、今後もオプションの追加も予定されているとか。

Environments や Deployment job は今後 Kubernetes 以外のリソースへの対応や、Deployment strategy も追加予定なのでアプリケーションのデプロイ向けには便利ですが、それ以外の IaC などの用途では Service Connection を使った方が簡単に実現できるのでお勧めです。

色々と試しているうちに、複数の Task で Service Connection を使っている場合に、どのような挙動になるのかが気になったので思いついたパターンを検証してみました。

Stage が順番に動く場合

恐らく一番単純な、承認が必要な Stage が順番に動いていくパターンです。サンプルとして App Service へのデプロイを行っていますが、ここは Service Connection を使う Task なら何でも良いです。

- stage: Deploy_to_Dev
  jobs:
  - job: Deploy_to_Dev
    pool:
      vmImage: 'windows-latest'
    steps:
    - download: current
    - task: AzureWebApp@1
      inputs:
        azureSubscription: 'Azure Sponsorship'
        appType: 'webApp'
        appName: 'approvalstest'
        package: '$(Pipeline.Workspace)/**/WebApplication1.zip'
        deploymentMethod: 'auto'

- stage: Deploy_to_Prd
  jobs:
  - job: Deploy_to_Prd
    pool:
      vmImage: 'windows-latest'
    steps:
    - download: current
    - task: AzureWebApp@1
      inputs:
        azureSubscription: 'Azure Sponsorship'
        appType: 'webApp'
        appName: 'approvalstest'
        package: '$(Pipeline.Workspace)/**/WebApplication1.zip'
        deploymentMethod: 'auto'

dependsOn で依存関係を定義していないので、書かれた順に実行されていきます。動かすと 1 つめの Stage に差し掛かったタイミングで承認待ちの状態に入りました。

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

承認するかどうか選択する画面では、対象の Service Connection が表示されるので分かりやすいです。この辺りの UI は大きくは変わっていないように感じます。

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

ここで承認すると、現在の Stage の実行完了後に後続の Stage でも承認待ち状態になりました。

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

承認待ちは Stage 単位で毎回走るようです。1 度の承認で同じ Service Connection を使っている Stage が実行されても困るので、この挙動は安心しました。

Stage が同時に動く場合

次は dependsOn を設定して、2 つの Stage が同時に動く場合にどうなるかを試してみました。

- stage: Deploy_to_Dev
  dependsOn: Publish
  jobs:
  - job: Deploy_to_Dev
    pool:
      vmImage: 'windows-latest'
    steps:
    - download: current
    - task: AzureWebApp@1
      inputs:
        azureSubscription: 'Azure Sponsorship'
        appType: 'webApp'
        appName: 'approvalstest'
        package: '$(Pipeline.Workspace)/**/WebApplication1.zip'
        deploymentMethod: 'auto'

- stage: Deploy_to_Prd
  dependsOn: Publish
  jobs:
  - job: Deploy_to_Prd
    pool:
      vmImage: 'windows-latest'
    steps:
    - download: current
    - task: AzureWebApp@1
      inputs:
        azureSubscription: 'Azure Sponsorship'
        appType: 'webApp'
        appName: 'approvalstest'
        package: '$(Pipeline.Workspace)/**/WebApplication1.zip'
        deploymentMethod: 'auto'

同時に同じリソースにデプロイしてるのはテストなので気にしないでください。

実行してみると、この場合は 2 つの Stage が両方とも同時に承認待ち状態になりました。

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

そして承認するかどうかの画面には 2 つ表示されました。それぞれの Stage 毎に個別に承認・非承認を行えるようになっているみたいです。

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

なので片方だけ承認しないということが簡単に行えました。Stage の設計が重要になりそうです。

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

ちなみに承認しなかった Stage に表示されている Rerun failed jobs ボタンを押すと、もう一度承認待ちからやり直しになります。間違った時のリカバリーに使えます。

複数の承認待ちを扱えることは知らなかったので、興味深い結果でした。何 Stage まで同時に待てるのか気になりますが、検証が結構面倒なので確認まではしないでおきます。

1 つの Stage 内で 2 つの Job が動く場合

最後はこれまで使った 2 つの Stage を 1 つにまとめた場合です。具体的には Job を複数にします。

- stage: Deploy
  jobs:
  - job: Deploy_to_Dev
    pool:
      vmImage: 'windows-latest'
    steps:
    - download: current
    - task: AzureWebApp@1
      inputs:
        azureSubscription: 'Azure Sponsorship'
        appType: 'webApp'
        appName: 'approvalstest'
        package: '$(Pipeline.Workspace)/**/WebApplication1.zip'
        deploymentMethod: 'auto'

  - job: Deploy_to_Prd
    pool:
      vmImage: 'windows-latest'
    steps:
    - download: current
    - task: AzureWebApp@1
      inputs:
        azureSubscription: 'Azure Sponsorship'
        appType: 'webApp'
        appName: 'approvalstest'
        package: '$(Pipeline.Workspace)/**/WebApplication1.zip'
        deploymentMethod: 'auto'

これも実行してみると Stage の実行前に承認待ち状態になりました。Stage 単位でしか承認待ちは行えないので、1 つだけ表示されています。

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

この状態で承認すると 2 つの Job がそれぞれ同時に動き出しました。

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

従って Service Connection を 1 つだけ利用する場合は、適当に使っていても問題ないケースが多そうです。

しかし Azure Subscription などリソースグループ単位で権限が与えられるような場合は、複数の Service Connection を用意することもあると思います。その場合には Stage を分けておくのが安全です。

Deno を Azure App Service (Windows / IIS) で動かしてみた

Twitter で記事が流れてきたり、通知が飛んできたので Deno というランタイムを知ったわけですが、どうやらそのままだと Windows の App Service 上では動かなかったらしいです。

App Service はサンドボックス環境で動くので多少制限が厳しいですが、エラー内容に違和感を持ったので自分でも試してみることにしました。

Deno インストール用の posh はそのままだと動かなかったので、Kudu から curl と 7za を使って手動で D:\home\.deno\bin に exe を置きました。こういうのは Site Extension で撒いても良い感じです。

おもむろに実行してみると確かに Panic が発生して立ち上がってくれません。

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

Home Directory が取れないというのは謎いので、もうちょっと深堀してみます。

該当の Issue に当たってみると FOLDERID_Profile についてコメントが書かれていたので、ユーザープロファイル周りが怪しいと感じ始めました。

Rust のドキュメントには Windows の場合は SHGetKnownFolderPath を使っていると書かれていたので、App Service はデフォルトでユーザープロファイルを読み込んでないことが原因だと思いました。

Windows:
This function retrieves the user profile folder using SHGetKnownFolderPath.

dirs::home_dir - Rust

なので App Settings に WEBSITE_LOAD_USER_PROFILE の設定を追加します。あまり表には出てこないですが、証明書を読み込む場合などでたまに使うこともあるので知っておいて損はないです。

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

これで Worker Process がユーザープロファイルを読み込むようになるので、Deno をもう一度叩いてみると Panic を発生させることなく立ち上がりました。

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

プロセスが立ち上がってしまえば、後は HttpPlatformHandler でプロセスの管理と HTTP のプロキシをさせてしまえばよいので簡単です。

考え方は Node.js のデプロイの時と同じなので、特に難しいことはありません。

以下のような web.config を作成しました。HttpPlatformHandler の一般的な定義です。

example.ts は元記事のものをそのまま貰ってきて wwwroot 以下に保存しました。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="httpPlatformHandler" path="*" verb="*" modules="httpPlatformHandler" resourceType="Unspecified" />
    </handlers>
    <httpPlatform processPath="D:\home\.deno\bin\deno" arguments="--allow-net example.ts -p %HTTP_PLATFORM_PORT%" />
  </system.webServer>
</configuration>

これで全ての準備が整ったので、App Service にアクセスしてみると無事にページが返ってくるはずです。

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

Process Explorer を開くとフロント用の w3wp の下に deno がぶら下がっていることが確認できます。

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

コールドスタートに関しては Node.js よりも速い気がしたので、今後に期待したいです。

App Service では PATH を通すことは出来ないですが、Site Extension を使うとランタイムの追加は自由に出来るので、暇があれば作ってみたいところです。

Azure Cosmos DB v3 SDK の Bulk Support がとても使い勝手が良かった

少し前にリリースされた Azure Cosmos DB v3 SDK の Bulk Support を試そうと思いつつ放置してたのですが、仕事で割と良い感じのお題が降ってきたのでこれを機に試してみました。

開発チームの人がブログで色々と紹介してくれているので、一通り目を通しておくと良い感じです。Medium の方も同じようなタイトルですが、内容は違うので両方読んでおいた方が良いです。

v3 SDK の Bulk Support の特徴は、クライアント作成時にオプションを有効化すれば、後は透過的に複数のリクエストをバッチに変換して投げてくれるところです。

これまでも Bulk Executor を使うと Cosmos DB の RU を最大限に使って項目の追加が行えていましたが、未だに .NET Core への対応はプレビューのままで改善が期待できなかったり、初期化周りで準備が色々と必要でライブラリとしては使い勝手が悪かったです。

Bulk Executor のドキュメントにも v3 SDK を勧めるメッセージが表示されています。

If you are using bulk executor, please see the latest version 3.x of the .NET SDK, which has bulk executor built into the SDK.

Azure Cosmos DB: Bulk executor .NET API, SDK & resources | Microsoft Docs

同じように Change Feed も v3 SDK に組み込まれているので、v3 を勧めるメッセージが出てきます。

If you are using change feed processor, please see the latest version 3.x of the .NET SDK, which has change feed built into the SDK.

Azure Cosmos DB: .NET Change Feed Processor API, SDK & resources | Microsoft Docs

将来的に Bulk Executor と Change Feed Processor は v3 / v4 を使うコードに変更する必要が出てくるはずです。ただし Azure Functions の Trigger に関しては事情が複雑です。

Bulk Support が公開されたのは v3.4.0 なので、それより新しいライブラリをインストールしておきます。v3 SDK はかなり開発ペースが速いのであっという間に古くなるので注意。

そもそも v3 SDK をまだ使っていない人はサクッとアップデートしておきましょう。DocumentClient から v3 の CosmosClient に移行すると API がシンプルになっています。

基本的な使い方については GA のタイミングでまとめておいたので参考にしてください。

説明はこのくらいにして実際にコードを書いて試していきます。今回は適当なデータを大量に Cosmos DB に投入するコードを v3 SDK と Bulk Executor の両方で書いて試してみました。

折角なので Cosmos DB 側は Autopilot を有効にしたデータベースを用意しておきました。

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

Autopilot もちゃんと検証しておこうと思っていましたが、何だかんだで後回しにしていたので丁度良かったです。上の設定では最低 2000 RU/s から最大 20000 RU/s まで自動的にスケールします。

まだプレビューだからかもしれないですが、上限が固定値だったりメトリクスが分かりにくいので今後に期待してます。あと RU の単価が 1.5 倍になるのはちょっと高い気がします。

v3 SDK の Bulk Support を使ったバージョン

先に新しい v3 SDK を使ったコードです。1 万件のデータを Cosmos DB に投入するだけの単純なコードですが、AllowBulkExecution を設定するだけで内部的にバッチで処理されます。

コード的には Task.WhenAll を使って複数 Task の完了を待つようにしているだけです。この辺りのイディオムは C# では日常的に出てくるやつです。

class Program
{
    static async Task Main(string[] args)
    {
        var connectionString = "AccountEndpoint=https://xxxx.documents.azure.com:443/;AccountKey=xxxx;";

        var cosmosClient = new CosmosClient(connectionString, new CosmosClientOptions
        {
            AllowBulkExecution = true,
            SerializerOptions = new CosmosSerializationOptions
            {
                PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
            }
        });

        var container = cosmosClient.GetContainer("TestDB", "TestContainer");

        var tasks = Enumerable.Range(1, 10000)
                              .Select(x => new DemoItem
                              {
                                  Id = Guid.NewGuid().ToString(),
                                  Name = $"kazuakix-{x}",
                                  CreatedOn = DateTime.Now,
                                  Pk = Guid.NewGuid().ToString()
                              })
                              .Select(x => container.CreateItemAsync(x, new PartitionKey(x.Pk)));

        await Task.WhenAll(tasks);
    }
}

Bulk Executor と同じように消費した RU を見ながら適切に処理を行っているので、Cosmos DB を使っているときによく見る 429 が出続けたりすることはありませんでした。

今回は Autopilot で 20000 RU/s までスケールするので 429 は発生しにくいですが、固定 RU/s で AllowBulkExecution の設定をせずに実行すると、普通に 429 の連発になります。

古い Bulk Executor を使ったバージョン

そして今度は Bulk Executor を使ったコードです。処理自体は BulkImportAsync を呼び出すだけなのでシンプルですが、実行するまでの準備が地味に複雑です。

リトライ設定などを手動で上書きしないといけないのはかなり分かりにくいですし、DocumentClient ベースなので Database / Container の扱いが冗長です。

class Program
{
    static async Task Main(string[] args)
    {
        var endpoint = new Uri("https://xxxx.documents.azure.com:443");
        var accountKey = "xxxx";

        var connectionPolicy = new ConnectionPolicy
        {
            ConnectionMode = ConnectionMode.Direct,
            ConnectionProtocol = Protocol.Tcp
        };

        var client = new DocumentClient(endpoint, accountKey, connectionPolicy);

        var documentCollection = await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri("TestDB", "TestContainer"));

        client.ConnectionPolicy.RetryOptions.MaxRetryWaitTimeInSeconds = 30;
        client.ConnectionPolicy.RetryOptions.MaxRetryAttemptsOnThrottledRequests = 9;

        var bulkExecutor = new BulkExecutor(client, documentCollection.Resource);

        await bulkExecutor.InitializeAsync();

        client.ConnectionPolicy.RetryOptions.MaxRetryWaitTimeInSeconds = 0;
        client.ConnectionPolicy.RetryOptions.MaxRetryAttemptsOnThrottledRequests = 0;

        var documents = Enumerable.Range(1, 10000)
                                  .Select(x => new DemoItem
                                  {
                                      Id = Guid.NewGuid().ToString(),
                                      Name = $"kazuakix-{x}",
                                      CreatedOn = DateTime.Now,
                                      Pk = Guid.NewGuid().ToString()
                                  });

        await bulkExecutor.BulkImportAsync(documents);
    }
}

こちらも当然ながら 429 を連発することなく、スループットを見ながら最適な処理を行ってくれます。

それぞれのコードを実行してみたところ、処理時間はほぼ変わっていないので良い感じです。気になった点としては RU/s の消費が v3 SDK の方が多かったことですが、処理時間はほぼ同じなので問題ないです。

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

RU/s は時間課金なのでこういうケースの場合は上限まで使い切れた方が効率が良いです。書き込みデータサイズは同じはずなので、内部処理に違いが結構あるのかもしれません。

Azure Functions で v3 SDK を使う

前述したように Azure Functions の Cosmos DB Trigger は残念ながら v2 ベースなので、v3 の CosmosClient を使うためには手動でのインストールが必要です。

一応内部的には v3 への移行を行っているらしいですが、まだ時間がかかりそうです。

とはいえ、v3 を使った方が圧倒的にコードが書きやすいので、Change Feed 以外は CosmosClient を使って書いていくようにした方が良いです。

Azure App Service の .NET Core 3.1 対応と Azure Functions v3 が Go Live リリース

.NET Core 2.2 と 3.0 の EOL が近づいてきていますが、App Service と Azure Functions は無事に年内に LTS となる .NET Core 3.1 への対応が完了しました。

特に 2.2 の EOL は今年中なので、サクッと .NET Core 3.1 へのアップデートを行っておくと良いです。EOL になったからと言って即座にダメになるわけではないですが、数バージョン飛ばしのアップデートは検証が大変になるので定期的に行いましょう。

App Service の .NET Core 3.1 対応

今週中に App Service への .NET Core 3.1 デプロイが完了するという予定でしたが、順調に進んだからか今朝全てのリージョンへのデプロイが完了したらしいです。

例によって Azure Portal からは .NET Core 3.1 が選べないようですが、Windows 側にはインストールされているので問題なく利用できます。

.NET Core 3.0 の時と同様に SDK は含まれずランタイムだけデプロイされています。なので Kudu を使ったビルドでは .NET Core 3.x が使えないのはこれまで通りなので、Azure Pipelines などでビルドしたパッケージを Run From Package でデプロイというのが鉄板です。

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

既に .NET Core 3.0 への移行を行っている場合は TFM を netcoreapp3.1 に変えれば大体問題ないですが、Cookie の SameSite 周りで 1 点だけ影響範囲の大きい Breaking change があったので、ドキュメントを読んでからの設定と動作確認は必須です。

それ以外は特に影響は無さそうなので、3.0 へのアップデートが終わっている環境であれば完了です。2.2 から 3.1 へのアップデートは 3.0 での Breaking changes の対応を行う必要があるので割とやることが多いです。

Azure Functions v3 (.NET Core 3.1) が Go Live

数日前に .NET Core 3.1 が使えるようになって嬉しさ爆発してたんですが、あっという間に Go Live で使えるランタイムがリリースされました。Go Live と言ってますが、残りはツール周りの整合性を取るぐらいらしいので、ランタイム自体は実質 GA 相当のようです。

実質 GA になっているので、何もしなくても Visual Studio から v3 向けプロジェクトが作成できます。あの裏技っぽい環境変数の設定は必要なくなっています。

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

v3 の GA は来年の 1 月ぐらいになりそうです。v3 は高い後方互換性を持っているので移行は楽です。

この間書いたエントリがそのまま Go Live としてリリースされたランタイムを使っていたので、基本的な使い方はこっちを読んでおいてください。

v2 からの移行に関しては TFM と Function Version を変えて SDK をアップデートするぐらいです。

移行のドキュメントも公開されてますが、JavaScript はまあまあ Breaking changes があります。.NET Core に関しては普通に書いていれば問題ないレベルなので、特に気にしなくても良いです。

v2 から実装が変わった部分があるらしく、FUNCTIONS_V2_COMPATIBILITY_MODE という設定が追加されています。これに true をセットすると古い実装に戻るようですが、パッと見た感じでは使う必要なさそうです。

移行自体は楽ですが Entity Framework Core 3.1 が使えるようになったりするので、そういった部分の対応が必要となります。

その辺りはケースバイケースなので頑張るしかないですが、ASP.NET Core アプリと共有プロジェクトを持っている場合は、全てを .NET Standard 2.1 か .NET Core App 3.1 に統一できるので楽になるはずです。

ここまでは前回書いた内容とあまり違いはないですが、.NET Core 3.1 と対応した Azure Functions Runtime が App Service にデプロイされたので、Azure にデプロイして動かすことが出来るようになっています。

Azure Portal から Runtime Version が 3.0.12930 以降になっていれば使えます。

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

適当に作成した v3 Function をデプロイすると、サクッと .NET Core 3.1 上で実行されます。

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

動かしてみるとこれだけですが、Azure Functions v3 の魅力は .NET Core 3.x で追加された新しいライブラリや言語機能を使えるという点なので、積極的にアップデートしていきたいです。

Azure Pipelines の .NET Core 3.1 対応

App Service へデプロイするために重要なのが Azure Pipelines ですが、例によって .NET Core 3.1 SDK がインストールされた Hosted Agent の更新が遅れているようで、まだ手持ちの環境では入ってませんでした。

なので基本的には UseDotNet task を使ってバージョンを固定するのが良いです。

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

一応、今月リリースされた Image には .NET Core 3.1 SDK が含まれていましたが、実際に使えるようになるのが何時なのかは不明です。今月中にデプロイが間に合わない可能性もあります。

Hosted Agent の更新タイミングによってアップデートが左右されるのはイマイチなので、バージョンは明示的に指定してインストールするようにしましょう。

Azure Functions v3 で .NET Core 3.1 が利用可能になった

Ignite 2019 のタイミングで Azure Functions v3 のプレビューが正式に公開されましたが、Azure Functions SDK 側の問題で .NET Standard 2.1 ターゲットのライブラリ*1が使えなかったので弄って来ませんでした。

その後 Runtime も一時的に非公開になりましたが、最近全て解決したので試します。現在 v3 はプレビューとプレリリースの 2 種類が公開されています。

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

プレビューとプレリリースの差が分かりにくいですが、今のところはプレリリースの方が新しい Core Tools とテンプレートとなっています。それぞれの違いは大体以下の通りです。

  • v3-preview
    • .NET Core 3.0 (netcoreapp3.0)
    • Functions SDK v1.0.30-beta2
    • Core Tools v3.0.4
  • v3-prerelease
    • .NET Core 3.1 (netcoreapp3.1 も使える)
    • Functions SDK v3.0.1
    • Core Tools v3.1.0

次の更新ぐらいで v3-preview にも v3-prerelease の内容が降ってくると思うので、v3 のプレビュー中は v3-prerelease を使っておいた方が良さそうな気配です。

Azure Functions v3 の GA 時には .NET Core 3.1 がターゲットになるので、実際にデプロイしたいケースを除いて v3-prerelease 版が出た今となっては .NET Core 3.0 向けを弄る必要はないです。

Timelines
A public preview will be available in October 2019
General Availability, running on .NET Core 3.1 (LTS release), scheduled for Q1 2020

Azure Functions 3.0 · Issue #200 · Azure/app-service-announcements · GitHub

今のところ v3 に必要な .NET Core 3.1 が App Service にデプロイされていないのと、Functions Runtime が古いままなので v3-prerelease をデプロイ可能になるには少し時間がかかります。

App Service の .NET Core 3.1 対応は 12/13 頃に完了する予定らしいです。リージョンやスケールユニットによって多少は前後しそうですが、LTS 版が早めに展開されるのは喜ばしいですね。

前置きはこのくらいにして、実際に Entity Framework Core 3.1 を使う Azure Functions のコードを書いてみることにします。v3-prerelease を使っていきます。

Azure Functions v3 のプレビュー / プレリリースは環境変数を設定しないと出てこないので、以下の記事を参照して設定を追加する必要があります。ユーザー環境変数に追加して VS の再起動で OK です。

Functions SDK はこれまでの 1.0.x から一気に上がって 3.0.1 になりました。SDK はプレリリース版になっていないですが、依存関係は netcoreapp3.0 になっているので v3 専用です。

v3-prerelease を選んでプロジェクトを作成し、Microsoft.EntityFrameworkCore.SqlServer と DI 用の Microsoft.Azure.Functions.Extensions をインストールします。最近の Azure Functions ではインスタンスのライフサイクル管理のために DI を使うのが一般的です。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <AzureFunctionsVersion>v3-prerelease</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.1" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

デフォルトでは netcoreapp3.0 で作成されますが、手動で netcoreapp3.1 に変更しました。

DI 周りと Entity Framework Core を使うためのコードは以下のように用意しました。エンティティ周りは適当に用意して、接続文字列は SQL Database Serverless を作成して設定しました。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddDbContextPool<AppDbContext>(options =>
            options.UseSqlServer(Environment.GetEnvironmentVariable("DefaultSqlConnection")));
    }
}

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    {
    }

    public DbSet<Blog> Blogs { get; set; }
}

public class Blog
{
    public int Id { get; set; }

    [Required]
    public string Title { get; set; }

    [Required]
    public string Body { get; set; }

    [Required]
    public DateTime CreatedAt { get; set; }
}

あまり見ないであろう AddDbContextPool を使っているのは何となくです。作成してある SQL Database Serverless 側にテーブルを作成して、適当なデータを追加しておきました。

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

Function 本体はコンストラクタで AppDbContext を受け取って、関数の本体で利用しているだけです。単純に DB に入っているものを JSON として返すだけの簡単なコードです。

public class Function1
{
    public Function1(AppDbContext appDbContext)
    {
        _appDbContext = appDbContext;
    }

    private readonly AppDbContext _appDbContext;

    [FunctionName("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req,
        ILogger log)
    {
        log.LogInformation($".NET Core Version: {Environment.Version}");

        var list = await _appDbContext.Blogs.ToListAsync();

        return new OkObjectResult(list);
    }
}

Visual Studio からデバッグ実行すると、.NET Core 3.1 で動作していることがログから確認できます。

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

Entity Framework Core 周りでもエラーが出ることなく、DB からデータを取得して応答が返ってくることが確認できました。サクッと .NET Core 3.1 / .NET Standard 2.1 のライブラリが使えたので内容が地味です。

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

自分は使う予定が今のところないですが .NET Core に対応した Entity Framework 6 も .NET Standard 2.1 がターゲットなので、Azure Functions v3 で使えるようになっているはずです。

少し前に v2 と v3 で Cold start 周りの検証をしたところ、v3 の方が応答時間のばらつきが少なかったのでパフォーマンス面でも v3 のリリースを割と期待しています。*2

*1:Entity Framework Core 3.0 のこと

*2:https://twitter.com/shibayan/status/1195190127917592576

Azure Functions と Append Blob の組み合わせは相性が良かった話

ここ最近は Azure Functions と Append Blob を組み合わせて、非常に便利かつスケーラブルな処理をシンプルなコードで書くことが多かったので紹介します。

割と地味な立ち位置の Append Blob ですが、Serverless や Event-driven なコードとの相性が良かったです。

具体的には Azure Functions では Event Hubs / IoT Hub / Cosmos DB Change Feed などを使って、メッセージをストリームで処理することが多いのですが、そのデータの保存先として Append Blob は最適でした。

Append Blob の基本的なこと

あまり知られていない気がするので基本的な部分から。と言ってもリリース時のブログやドキュメントが参考になります。Append Blob の特徴としては書き込みを Block という単位で行い、追記に最適化されているので複数から同時に書き込むことが出来ます。

ただし書き込める Block 数に上限があって、1 Block あたり 4MB までの 50000 Blocks まで書き込めます。

なので以下のように 4MB 以上のデータを書き込もうとするとエラーになります。

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

従って最大ファイルサイズは以前の Block Blob と同じく約 195GB が上限になります。1 Block のサイズは問題になりにくいと思いますが、50000 Blocks の上限は書き込み側で考慮する必要があります。

単純に 1 行のデータを 1 Block として書いていけば 50000 行が上限になるので、時系列データの場合は適切な単位でのファイル分割が必要なります。1 つの巨大なファイルより複数のファイルの方がスケーリングを考えても有利なので、時間単位とかで分割するのが良いです。

複数からの書き込みと条件

基本的には Multi-writer に対応してますが、古い SDK の一部メソッドや Precondition を指定すれば Single-writer での書き込みを強制することも出来ます。

サンプルとして以下のような Append Blob に対して同時に書き込むコードを用意しました。

class Program
{
    static async Task Main(string[] args)
    {
        var connectionString = "DefaultEndpointsProtocol=https;AccountName=XXXXX;AccountKey=XXXXXX;EndpointSuffix=core.windows.net";

        var storageAccount = CloudStorageAccount.Parse(connectionString);

        var blobClient = storageAccount.CreateCloudBlobClient();

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

        var appendBlob = container.GetAppendBlobReference("sample.txt");

        await appendBlob.CreateOrReplaceAsync();

        var tasks = Enumerable.Range(0, 100)
                              .Select(x => appendBlob.AppendTextAsync($"{DateTime.Now}\n"))
                              .ToArray();

        await Task.WhenAll(tasks);
    }
}

実行すると以下のような例外が投げられて Append Blob への書き込みが失敗します。

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

古い SDK にある AppendBlock / AppendBlockAsync 以外のメソッドは Single-writer 向けに実装されているので、内部的に appendpos という書き込み位置を保持しているので同時に書き込むとエラーになります。

なので AppendBlockAsync を使うように修正すると、エラーが出ることなく書き込みが完了します。

他の Blob の内容を Append Blob へ追記する

面白い機能として別の Blob から Append Blob への追記が API レベルで提供されています。複数のファイルを結合して 1 つの Blob にする場合には、この API を使うと処理が Blob 側で終わるので非常に効率的です。

古い SDK には API が用意されていないので、新しい SDK を使ったサンプルは以下になります。

class Program
{
    static async Task Main(string[] args)
    {
        var connectionString = "DefaultEndpointsProtocol=https;AccountName=XXXXX;AccountKey=XXXXXX;EndpointSuffix=core.windows.net";
        var sourceUri = new Uri("https://xxxxx.blob.core.windows.net/content/function.txt?st=2019-11-29T09%3A05%3A43Z&se=2019-11-30T09%3A05%3A43Z&sp=rl&sv=2018-03-28&sr=b&sig=XXXXX");

        var appendBlobClient = new AppendBlobClient(connectionString, "append", "destination.txt");

        await appendBlobClient.CreateIfNotExistsAsync();

        await appendBlobClient.AppendBlockFromUriAsync(sourceUri);
    }
}

API 名からわかるように、1 つの Block として追加するので 4MB の制約は受けます。

4MB 以上のファイルを Append Blob へ追記したい場合には HTTP の Range Header を指定できるので、複数回の API 呼び出しに分ければ良いです。

Azure Functions と組み合わせて使う

本題の Azure Functions との組み合わせですが、最初から Blob Binding が Append Blob に対応しているので、引数の型を CloudAppendBlob に変えるだけで使えます。

サンプルとして QueueTrigger と組み合わせて、入ってきたデータを Append Blob に書く Function を書いてみましたが、コードは以下のようにシンプルです。

Event-driven なのでデータは細切れになってやってきますが、Append Blob の場合は簡単に追記が出来るので、難しいことを考えずに Azure Storage に保存できます。

public static class Function1
{
    [FunctionName("Function1")]
    [StorageAccount("StorageConnection")]
    public static async Task Run(
        [QueueTrigger("queue")] string queueItem,
        [Blob("append/year={datetime:yyyy}/month={datetime:MM}/day={datetime:dd}/hour={datetime:HH}/function.txt", FileAccess.ReadWrite)] CloudAppendBlob appendBlob,
        ILogger log)
    {
        if (!await appendBlob.ExistsAsync())
        {
            await appendBlob.CreateOrReplaceAsync();
        }

        await appendBlob.AppendBlockAsync(new MemoryStream(Encoding.UTF8.GetBytes($"{DateTime.Now} - {queueItem}\n")));
    }
}

Blob のパスが少し長いですが、日付でディレクトリを切るための指定です。

微妙に隠し仕様っぽいのですが、DateTime のバインド式ではフォーマットも指定できたので簡単でした。

このパスのフォーマットは Hive Streaming Conventions と Docs には書いてましたが、正しい名前は良くわからなかったです。Azure Monitor のログもこんな感じだったと思います。

Queue に適当なデータを入れると Function が走って Append Blob にデータが追記されていきます。

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

ディレクトリ構造は良い感じに日付単位で分けて Blob が作られているのが確認できます。

作成された Append Blob をダウンロードして中身を確認すると、ちゃんとデータが入っています。

2019/11/29 16:45:16 - kazuakix
2019/11/29 16:45:32 - daruyanagi
2019/11/29 16:45:38 - buchizo

サンプルは QueueTrigger を使いましたが、本命は Cosmos DB の Change Feed との組み合わせです。

Change Feed は Pull 型なので、ある程度まとまったデータを取ってきて 1 Block に書き込めるので効率的ですし、SQL Database に書き込む Change Feed と Storage に書き込む Change Feed を用意してしまえば、それぞれ並行稼働させることも簡単です。

惜しい点として Data Lake Storage Gen2 では Append Blob が使えないことですが、その辺りは Data Factory から Data Lake Storage Gen2 にコピーしてあげれば良いかなと思っています。Data Lake Storage に入れてしまえば後処理の自由度が高いです。

Ignite 2019 で発表された Azure App Service のアップデート

Surface Pro X を買うためにハワイに行ってたのでキャッチアップが遅れましたが、個人的な興味を持っている App Service 周りに関して Ignite 2019 での発表を軽くまとめました。

App Service Team のブログに関連するセッションリストが載っているので楽です。まあ少ないです。

数は少ないですが、結構良い感じの機能が追加されているので GA が楽しみという感じです。

Azure Monitor Integration (Preview)

これまで Local File storage / Blob / Table に書き出し設定が行えていたログ回りを、Azure Monitor の標準的な仕組みが使えるようになりました。

Blob に書き出す以外にもストリーム処理に適している Event Hub にも書き出せます。

Best practices and tips for operating and monitoring apps on Azure App Service

設定すると Blob の場合はコンテナが設定した項目別に作られて、その中は結構深いパスになっていますが日付時間別で細かく区切られて保存されます。中身は JSON なので扱いやすいです。

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

IIS ログや Azure Functions のログも簡単に転送できるので、Event Hub と組み合わせて Anomaly Detection に利用するとかも出来るのではないかと。

App Service はかなり初期のサービスなので、こういった今となっては標準的な仕組みに乗っかるのが遅れ気味ですが、Key Vault 連携など徐々に対応が進んできています。

Health Check (Preview)

昔から ARM にプロパティがあることは知っていましたが、上のセッションでひっそりと紹介されていました。Kudu の Wiki にもページがあったみたいですが、アナウンスが無いと気が付かないやつです。

これまでも App Service は動いているインスタンスに問題があったときには、自動で他の正常なインスタンスへフェイルオーバーしてくれましたが、この Health Check を設定すればアプリの問題をトリガーに LB からの切り離しと復旧を行ってくれるようになります。

現在は 2 分間隔のチェックで 5 回連続失敗した場合に切り離しが行われるらしいですが、もうちょっとカスタマイズ出来るようになってほしい感があります。

ヘルスチェックが通らない場合は 1 時間に最大で 1 インスタンス、1 日に最大 3 インスタンスまでが入れ替えされます。GA までにはこの辺りの挙動は変わると思われます。

Health Check の結果は Metrics から確認できます。値が微妙に分かりにくいですが % になっています。

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

上のグラフでは 100% 成功しているということになります。Split でインスタンス別に見れるのも良いですね。

Regional VNET Integration (Preview)

割と長い間プレビューが続いている新しい VNET Integration ですが、12 月の GA 予定が発表されました。そして Linux での対応は来年頭になるらしいです。

Web app network security made easy

同じサブネットに複数の App Service Plan を追加できないのは割と罠っぽいですが、将来的には改善されそうな気配ですね。Azure DNS Private Zone 対応もまだみたいです。

この新しい VNET Integration を使うことで Service Endpoint や Express Route と組み合わせることができるようになるので、これまでは ASE が必須だった構成を普通の App Service で組むこと可能です。

GA 後には VNET と Service Endpoint を組み合わせた App Service の構成が定番になると思っています。

Private Link (多分 Private Preview)

上のセッションでは VNET Integration と同時に App Service での Private Link についてもデモが行われました。今のところは Private Preview のようです。

Service Endpoint で良いのではと思いますが、プライベート IP でアクセスできるのは大きいですね。

Azure Portal では項目が出てこないですが、ARM Explorer などを使うと怪しい設定があります。

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

適当にいじれば使えるのかもしれないですが、当然ながら自己責任で試す形になります。

App Service Managed Certificate (Preview)

無料で証明書が発行できる Managed Certificate がプレビュー公開されました。Zone Apex に対応していないので微妙に使い勝手が悪いです。

既に証明書は発行しているので、6 か月後にちゃんと更新されるのかを見守っています。

Azure Functions Premium Plan (GA)

Premium Plan が GA して、ほとんどのリージョンで使えるようになっています。Japan East / West の両方で使えるので、これまで Consumption や Premium V2 を使っていた場合は、Premium Plan に変えることでパフォーマンス改善が図れます。

スケーリングは Function App 単位なので、複数の Function App を 1 つの Premium Plan に乗せると Cold start を避けつつも柔軟なスケーリングを行えるので便利になっています。

全体としてのコストを圧縮できるケースもありそうです。

Azure Functions v3 (Preview)

既にリリースはされていましたが、.NET Core 3.0 ベースになった Azure Functions v3 が発表されました。GitHub や Jeff のブログを読めば簡単に試すことができます。

これで .NET Standard 2.1 になった Entity Framework Core 3.0 が使えると思ったのですが、Metadata Generator 周りでエラーになってしまったので試せていません。

一応 Issue は上がっているので直るとは思いますが、地味に根が深そうな感じがしています。

Durable Functions v2 (GA)

Durable Entities / Durable HTTP が新規追加された Durable Functions v2 がリリースされました。v1 からの移行はインターフェース名や名前空間の変更があるので多少作業量は多いです。

実際に v1 から v2 に移行した際の Pull Request を参考までに紹介しておきます。

Durable Entities と Durable HTTP については既に試しているので一緒に紹介します。Durable HTTP は非同期 HTTP API を利用する際には結構便利だったので、使っていきたいです。

Ignite での発表全体から見ると App Service はあまりアップデートが無かったですが、重要な機能が多かったので既存のアプリケーションで使っていきたいと思います。

とりあえずは VNET Integration が 12 月に GA する前に色々な構成を試しておく予定です。Health Check の詳細な挙動も ASP.NET Core の Health Check API と組み合わせて検証してみたいところです。