しばやん雑記

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

Azure Cosmos DB Conf で Change Feed を中心としたアーキテクチャの設計と実装について話しました

今回が初の開催となる Azure Cosmos DB Conf で三宅さん (@kazuyukimiyake) と一緒に Change Feed を中心としたアーキテクチャの設計と実装について話しました。

Cosmos DB の利用シーンの幅広さを実感出来るようなセッションが多いので、是非他のセッションも見てもらいたいです。Live streaming 以外にも On demand セッションもあるのでかなり数が多いです。

イベントの配信自体は Learn TV と YouTube で行われました。YouTube の方がアーカイブ公開が早かったので、別件や忘れていて見られなかった方は以下からご覧ください。

開始時間をちょうど自分たちのセッション開始に合わせているので、素早く見られるはずです。

Learn TV の方は開始してすぐに一瞬途切れたと聞きましたが、YouTube の方は問題なさそうでした。

使用したスライドは以下で公開しています。リンクが生きているので扱いやすいと思います。

要するに、これまで色々なイベントで話してきて、ブログで機能を紹介してきた内容の集大成となります。

このブログで公開しているエントリでも少なくとも以下の 3 つは、このアーキテクチャに強く関連するものになります。細かいものを上げるときりが無いですが、それぐらい集中して取り組んできたということです。

これは特に Cosmos DB の Change Feed だけに関係するものでは無いですが、Azure Functions を使ったアプリケーションの回復性と信頼性を高めるためには、Retry policy や Graceful shutdown を適切に実装していきましょう。何回も言っていますが Azure Functions は月に何回も再起動しています。

Change Feed は Retry policy や Graceful shutdown との相性が非常に良いので、Azure Functions との組み合わせが最適です。入力データを Cosmos DB に投入することで後段の処理の信頼性が高まるとともに、サービス間の結合が弱まるので柔軟な運用が可能になります。

英語は喋れない代わりにスライドは全て英語で作成したので、英語圏の方にもそれなりに伝わったかなと信じています。コードならともかくアーキテクチャの説明はどこまでスライドに書くべきか悩みました。

イベントとしては APAC 時間の最後かつ唯一の日本語セッションということで、若干プレッシャーを感じていましたが嬉しいコメントを多く頂いたので「Change Feed をこんなにも深く使っているぞ」というアピールが出来たかなと思っています。

Deep Dive っぽさが足りないという方がとても多ければ、今回のセッションのフォローアップが別途 Hack Azure イベントとして開催されるかも知れません。

Azure CDN / Front Door の Subdomain Takeover 対策について

少し前に Azure CDN か Front Door を利用しているユーザー向けに以下のようなメールが届いていると思います。運用中の CDN / Front Door に関しては影響はないですが、削除する前にそれらをポイントしている DNS レコードを前もって削除しないといけなくなりました。

CNAME レコードとメールには記載されていますが、実際に試したところ A レコード + Alias record set の組み合わせの場合でも、CDN / Front Door 削除時にチェックが走ってエラーとなりました。

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

何故今回のような変更が行われたかというと、言わずもがな Subdomain Takeover への対策になります。

去年、主に Cloud Services と Traffic Manager に対しての Subdomain Takeover が大規模に行われていることが発見され、結構な大ごとになりました。当時のまとめは以下のエントリに記載しています。

Cloud Services や Traffic Manager と同様に CDN / Front Door は同名のリソースを再取得できるのと、カスタムドメインの追加時に CNAME のチェックしか行われないのでこのままだと攻撃対象にはなりますが、リソースの削除時にチェックを入れることで DNS レコードを確実に削除させる方向での対応になっています。*1

今回のアップデートに関しては Azure における Subdomain Takeover への対策をまとめたドキュメントへは反映されていないようですが、しばらくすればアップデートされると思います。

このドキュメントはちょいちょいアップデートされていて、当時は機能として存在しなかった Azure Defender for App Service を使った方法も紹介されています。Security Center はベストプラクティスを理解するのに有用なので、重要なリソースに対しては有効にしておきたい機能です。

既にカスタムドメインを当てている CDN / Front Door があったので、DNS レコードを削除することなくリソースの削除を試してみると、以下のようにエラーとなりました。

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

エラーメッセージも分かりやすいものになっているので、今後は CDN / Front Door に関しては Subdomain Takeover を受けるリスクが下がったかと思います。

ちなみに Cloud Services に関しては Extended support でも特に変化はないので、利用者に委ねられているのは変わらずです。早急に App Service などの対策が行われたマネージドサービスへ移行するのが良いです。

Azure Front Door Standard / Premium の場合

若干事情が異なるのが、少し前にプレビューとして公開された Front Door Standard / Premium です。

名前としては Front Door が付いているものの、リソースとしては全くの別物で ARM 的には CDN 寄りという違和感のある命名ですが、完全に新規ということでカスタムドメインの追加周りが大きく変わっています。

具体的には App Service のようにユニークな ID を使った検証が必要になりました。

CDN / Front Door も CNAME レコードを使った検証は必要でしたが、値が CDN / Front Door の FQDN なので Subdomain Takeover 対策としては意味が無いものでした。その点 Standard / Premium ではちゃんと意味のある対策になっています。

実際に適当なドメインを追加して確認してみました。Azure managed DNS というのは Azure DNS に対して自動で必要なレコードを作る機能なので、今回はあえて選びませんでした。

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

これでドメインを追加すると Validation state が Pending になって、ドメインの検証が追加で要求されます。

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

Pending 部分をクリックすると必要なレコードの情報が表示されるので、この情報に従って TXT レコードを作成すれば検証が終わります。レコードに設定する値がユニークな ID になっているので、もし削除を忘れていたとしてもチェックが通らないのでドメインを追加できないというわけです。

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

複数サブスクリプションでの検証はしていないですが、App Service と同様にサブスクリプション単位で TXT レコードに設定する値が変わるのだと思います。

基本的にカスタムドメインを設定可能なサービスに関しては追加時にユニークな ID を使ったチェック、削除時の DNS レコード消し忘れチェック、難しい場合は Azure Defender での保護になるのかと思います。

*1:既存のリソースとの兼ね合いで追加時のチェックを入れるのは難しかったのだと思われます

Microsoft Build of OpenJDK を Azure App Service にインストールして動かしてみた

ここ数日盛り上がり気味の Microsoft がビルドした OpenJDK ですが、現状の App Service はバージョンによっては Oracle と Azul が混在しているのが解消されるかと期待しています。

まだ Container Image が公開されていないので、アプリケーションで使うには若干手間がかかりますが Cloud Shell にはインストール済みらしいです。

予想では Preview が取れて正式版になると Early Access として App Service で使えるようになる気がしています。Early Access 自体は .NET 5 の時に使われましたが、任意のランタイム / SDK で利用可能な機能です。

暫く待てば使えるようになると思いますが、事前に試しておきたい人もいるはずなので App Service で利用する比較的簡単な方法を紹介しておきます。

公式サイトで公開されている Windows 向けのバイナリは zip と msi が用意されていますが、App Service では当然ながら msi を使ったインストールは行えないので、zip 版を使います。

こういったサポート対象外のランタイムを Windows 版の App Service で使う場合には、Site Extension としてインストールするものと相場が決まっています。

もしかしたら今後、公式で Site Extension がリリースされるかもしれないので、今回は手動でのインストール方法を紹介します。.NET Runtime に関しては既に 6.0-preview3 が Site Extension として公開されています。

適当に Kudu の Debug Console を立ち上げて、以下のようなコマンドを叩き込めば完了です。

set PATH=%PATH%;%SYSTEMDRIVE%\7zip
mkdir SiteExtensions\Java
cd SiteExtensions\Java
curl -LOs https://aka.ms/download-jdk/microsoft-jdk-11.0.10.9-windows-x64.zip
7za x microsoft-jdk-11.0.10.9-windows-x64.zip
rm microsoft-jdk-11.0.10.9-windows-x64.zip

Windows 版の App Service には curl と 7zip が予めインストールされているので、こういった処理を行う場合には便利です。この時ディレクトリは D:\home\SiteExtensions\Java となっているはずです。

これで OpenJDK のインストールは完了なので、最後の仕上げとして JAVA_HOME 環境変数をインストールした OpenJDK 向けに変更します。環境変数を弄るために以下のような applicationHost.xdt を作成します。

<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <system.webServer>
    <runtime xdt:Transform="Insert">
      <environmentVariables>
        <add name="JAVA_HOME" value="%XDT_EXTENSIONPATH%\jdk-11.0.10+9" />
        <!--<add name="PATH" value="%XDT_EXTENSIONPATH%\jdk-11.0.10+9\bin;%PATH%" />-->
      </environmentVariables>
    </runtime>
  </system.webServer>
</configuration>

Java アプリケーションを動かす場合には JAVA_HOME 環境変数だけを変更すればよいのですが、Debug Console から java を叩くとシステム側のものが使われるので、必要に応じて PATH も変更します。

applicationHost.xdt を作成後に、App Service を再起動すると変更が反映されます。

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

適当に Debug Console から java を叩くと、Oracle / Azul バージョンではなく Microsoft がビルドしたバージョンが使われていることが確認できます。

この時点で App Service にアクセスすると、デフォルトでデプロイされていたアプリケーションの表示が変わり、実行されている Java が Site Extension としてインストールしたものになっていることも確認できます。

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

後は好きなように Java アプリケーションを作成して、これまでと同じ方法でデプロイするだけで Microsoft Build of OpenJDK の検証が App Service 上で行えます。

気力が続かなかったのでアプリケーションのデプロイまではやっていませんが、App Service の JAR / WAR デプロイは進化しているので興味がある方は以下のエントリを参照してください。

Early Access で提供されるようになった時には、また試してみたいと思います。

Azure Functions の Binding / Trigger で必要なシークレットをより安全に扱う

Azure Functions は非常に便利ですが、Azure Storage や Cosmos DB などの Binding / Trigger を使う際に必要となる接続文字列は、管理を考えると若干扱いが面倒な部分があります。

App Settings に直接値を設定するのが簡単ですが、権限管理という観点では値が簡単に確認出来てしまうのと、複数のサービスを利用する場合にはそれぞれの接続文字列を設定する必要があるので、再生成や更新といった際の管理コストが高くなりがちです。

最近ではこの辺りの課題を解決するための手段が増えてきているので、実際に動作を試しておきました。

Key Vault Reference を使って参照する

接続文字列などを安全に管理する際には Key Vault Reference を使うのが最近の定番になっていると思います。App Settings に設定はしますが実体は Key Vault に格納されているのと、Key Vault 自体へのアクセスは Managed Identity で行うので最小限の権限で利用できます。

そんな Key Vault Reference にも弱点があって、Key Vault へのアクセスを Service Endpoint / Private Endpoint のみに限定している場合には、Azure Functions が Regional VNET Integration で VNET に参加していても利用できませんでした。つまりパブリックアクセスが必要でした。

最近になってその制約が解消されたので、App Service や Key Vault などを全て VNET に入れつつ、Key Vault Reference で安全に機密情報を扱えるようになりました。

前回の Hack Azure! #5 の時にも少し紹介しましたが、正式に GA したと発表があったので安心して使えます。

動作確認は Private Endpoint 経由のみ許可した Key Vault を用意して行いました。Private Endpoint を有効化しつつ、以下のように Firewall で全てを拒否するように設定することで実現出来ます。

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

事前に Azure Functions では Managed Identity を有効にして、Key Vault の RBAC 設定で Secret への読み込み権限を与えておきます。最近は Access Policy より RBAC を使うようにしています。

これで Key Vault Reference を設定すると、Managed Identity を使ってシークレットを取得できているのが確認できます。Private Endpoint のみ許可しているので、VNET 経由で取りに行っていることも確認出来ます。

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

注意点としては現時点はバージョン無しの Secret を参照している場合には、その更新を検知して再起動する機能は正しく動作しないようです。対応予定のようですが、今はバージョンまで指定する必要があります。

Function App から接続文字列を上書きする

Key Vault Reference が VNET 経由に対応したので使う場面が減った感はありますが、Azure Functions Runtime 3.0.15417 から Runtime Scale Monitoring を有効にしている環境で、Function App から Binding / Trigger が参照するシークレットを更新できるようになりました。

具体的には DI を使って Configuration Source をカスタマイズする形になります。

メリットとしては Configuration Source は Function App 側で実行されるので、Key Vault Reference のように VNET 周りの制約を受けることなく使えるという点があります。App Settings に 1 つずつ設定する必要が無いというのもメリットです。

Key Vault に保存されたシークレットを使うのは、最初からパッケージが用意されているので簡単です。

具体的に Azure Functions の Configuration Source に Key Vault を追加するのは以下のようなコードだけで行えます。当然ながら Managed Identity を使って Key Vault へアクセスしています。

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

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

        var secretClient = new SecretClient(new Uri(builtConfig["KeyVaultEndpoint"]), new DefaultAzureCredential());

        builder.ConfigurationBuilder.AddAzureKeyVault(secretClient, new KeyVaultSecretManager());
    }
}

Runtime Scale Monitoring の有効化を行わずにデプロイすると、Azure Portal に以下のようなエラーが表示されます。Scale Controller が参照する値とアプリケーションが参照する値が異なると困るので、基本は Binding / Trigger が参照するキーは変更不可になっているからです。

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

その後 Runtime Scale Monitoring を有効化すると、問題なく動作するようになります。

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

Cosmos DB や Storage を丸ごと Private Endpoint で閉じてしまうようなケースでは、Premium Plan を使っていても Scale Controller は利用できなくなるので Runtime Scale Monitoring が必要になります。

その場合は Key Vault Reference よりも Configuration Source を使った方が楽かもしれません。

Managed Identity を使って接続文字列を不要にする

現在プレビューで公開されている拡張を使うと、Storage などへのアクセスを Managed Identity で行えるようになるので、そもそも Key Vault で接続文字列を管理する必要が無くなります。これによって理想的な形で権限管理が行えるようになります。

ドキュメントには Blob / Queue / Event Hubs が Managed Identity に対応しているとありますが、恐らく Service Bus もプレビュー版が出ているので使える可能性が高いです。

基本的な使い方はプレビュー版の拡張をインストールしてしまえば、あとは接続文字列を Managed Identity 向けに変更するだけなので簡単です。プレビュー版の拡張は参照している Azure SDK が新しくなっているので、クラス名などに多少の破壊的変更があることだけ注意が必要です。

Managed Identity は接続文字列が不要ですが、接続するリソースに関する情報は必要なので serviceUrifullyQualifiedNamespace だけを設定するようにします。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "StorageConnectionString__serviceUri": "https://***.queue.core.windows.net/",
    "EventHubConnectionString__fullyQualifiedNamespace": "***.servicebus.windows.net"
  }
}

内部的には接続文字列は階層構造を持っているので、上のような定義が出来るようになっています。キー名に関しては拡張によって変わるのでドキュメントを参照する必要があります。

App Settings でも同様に設定を追加しておきます。接続先のリソース名は Key Vault で守るような情報ではないのでそのまま書いてしまいます。

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

Managed Identity を使うので Storage と Event Hubs に対して必要な権限を RBAC で追加すれば完了です。

適当に Queue や Event Hubs に対してメッセージを追加すると、接続文字列無しで動作していることが確認できます。若干地味な機能ですが、1 つの System Assigned Managed Identity で複数のリソースへのアクセスが出来るのは便利です。

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

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

順調に Binding / Trigger 周りで Managed Identity への対応が進んでいるので、将来的には Cosmos DB などでも Managed Identity でシンプルな設定と権限管理が実現されるはずです。

対応が完了すると Azure Functions は Regional VNET Integration と Managed Identity によってセキュアな PaaS / Serverless として大きな進化を遂げそうです。

Hack Azure! #5 - Geek of Azure Serverless フォローアップ

Ignite 2021 では特にアップデートがなく、なかなか話す機会が無かった Azure App Service や Azure Functions といった代表的な Azure Serverless なサービスについて、Hack Azure! #5 で話をしました。

これまでは大体ゲストを呼んで話していましたが、App Service / Azure Functions に関しては呼ぶ必要がないのでは?と思ったので、自分たちで話しています。

スライドを作っている最中はめっちゃ面白い内容と思っていたのですが、実際に喋ってみると地味でマニアックな内容が多かったので少し反省しています。

個人的には一度完全なフリーディスカッションで 1~2 時間話してみたいと思っているので、いつか機会があればやってみたいところです。

自分は色々な話をしたがるタイプでコロコロ話題が変わるので、出たとこ勝負も面白いかなと思っています。

使用したスライド

今回のスライドはリンクが動かないと厳しいので Azure Storage に HTML でアップロードしています。

リンクがない部分はちゃんとしたドキュメントが無い割に、実際には利用できる系のアップデートです。

Twitter まとめと YouTube アーカイブ

セッション中にハッシュタグ付きでツイートされたものと、ライブ配信のアーカイブは既に公開済みです。

ライブ配信の方は今回も大平さんが頑張ってくれたので、いい感じにツイートも一緒に見られるようになっています。セッションの内容と同期して見られるのが良いですね。

最後の方は Azure Serverless の回ということで App Service / Azure Functions 関係なく CDN や Cosmos DB の話もねじ込んでいます。若干無理やり感あったかなとは思っています。

App Service / Azure Functions 最新情報の入手

App Service / Azure Functions に関しては最近は大規模イベントでの発表がほぼ無い印象ですが、アップデート自体はガンガン行われています。公式情報を得る場合は以下の 3 つを押さえておくと良い感じです。

特に Azure Functions に関してですが、最近は定期的に YouTube で配信されている Azure Functions Live での発表がメインになっています。新機能と予定されているアップデートの両方が発表されることが多いです。

それぞれを追うのが面倒な時は、最低限ブチザッキを見るようにしておけば公式情報は追えます。

ブチザッキは公式からの発表以外は載せないらしいので、ひっそりとドキュメントが更新されていた系は載らないです。ドキュメント系の更新も書いて欲しい。

Key Vault Reference のアップデート

Secret Version 無しで指定すると、いい感じに最新バージョンをチェックして更新するようになってくれましたが、個人的にはアクセス制限された Key Vault への対応のが欲しかったものになります。

今回の配信では Private Endpoint からしかアクセスできない Key Vault を用意して、Regional VNET Integration などを有効化した Azure Functions Premium Plan から Key Vault Reference 経由でのアクセスを試していました。Private Endpoint の利用方法は以下のエントリを参照してください。

Azure Portal や Kudu から該当の App Settings / Connection Strings を見ると、ちゃんと Key Vault から値を解決できていることが確認できます。

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

ドキュメントが更新されていないので詳細は分かっていませんが、仕組みから考えると Premium Plan のイベントベースでの高速スケールを利用する場合は Runtime Scale Monitoring の有効化が必要な気がしています。

Azure Functions の .NET 5 サポートと今後

配信中にも話したように .NET 7 からは Azure Functions の .NET 実行環境は In-Process から Isolated Process に変更になります。変更は多くなるとは思いますが、その頃には .NET Isolated Worker も成熟していると思うので安心して使えるようになっているはずです。

今のところパフォーマンスは 2 倍ぐらい In-Process の方が良いですが、まだ Worker 側の最適化は行われていないらしいので差は今後縮まっていくでしょう。

Isolated Process への変更理由としては以前書いたように、ランタイムが使っているアセンブリとアプリケーションが使っているアセンブリでバージョンが異なることで競合が発生することが多いので、完全に分けることで根本的な対応を行いたいようです。

ランタイムから独立したプロセスとして実行されるので、バージョン問題は発生しなくなるはずです。

Visual Studio からデバッグが簡単に出来ないのはかなり辛いので、この辺りの対応が終われば移行方法などを含めた確認をしていきたいと思っています。

HttpTrigger での Open API 定義生成

自分の記憶では Visual Studio 16.9 ぐらいから追加されたと思っているのですが、HttpTrigger の追加時に Open API サポートを有効に出来るようになっています。

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

実体としては以下の拡張機能がインストールされるだけではありますが、Azure Functions の HttpTrigger 実装から Open API 定義を実装したいことは多かったので期待しています。

元々は Cloud Advocate の人が個人で作っていたものを公式化したもののようです。CLI を使ってビルド時に生成出来そうな雰囲気もあるので楽しみです。

バックエンド Storage Account の閉域化

Azure Functions は Regional VNET Integration への対応で、ほぼ全ての通信を VNET や Private Endpoint 経由に出来るようになりましたが、最後まで出来なかったのが AzureWebJobsStorage の閉域化でした。

Web Worker が該当 Storage Account が持っている Files を SMB でマウントする仕組みなので、ここを VNET 経由にするのはかなり難しいとは思っていましたが、去年 West Europe で Early Preview が開始されました。

それが最近になって West Europe 以外でも使えるようになったみたいです。ドキュメントも独立したページにリライトされているので、GA が近いか既にしているのかなと思っています。

この機能に関してはアップデートが発表されそうなので待つ予定です。

Azure Storage SDK のアップデート関連

Azure Storage SDK は何回かアセンブリ名や名前空間を含めた変更が行われていますが、今では v12 と呼ばれている SDK のみがサポートされている状態です。この辺りは以前書いたので参照してください。

要約すると Azure Functions と Azure Storage SDK の関係は以下の通りになります。

  • めっちゃ古い ← 一部でまだ残っている
    • WindowsAzure.Storage
  • 古い ← 現状
    • Microsoft.Azure.Storage.*
    • Microsoft.Azure.Cosmos.Table
  • 新しい ← 移行作業が進行中
    • Azure.Storage.*
    • Azure.Data.Tables

ランタイム側と Storage 拡張の両方で作業が進んでいますが、重要なのは Storage 拡張側の方になります。

多くの場合は気にする必要はないと思いますが、Binding で CloudBlockBlob などを直接使うようにしていると、拡張のアップデートを行ったタイミングで影響を受けます。まだ拡張自体がプレビューなので大丈夫ですが、正式リリース後には注意が必要になります。

Static Web Apps について

Static Web Apps に関しては三宅さんのブログを読んでおけば全て理解できる感じです。今回のメインは Static Web Apps CLI なので、後編が公開されるのを読みながら待ちましょう。

CLI は以下のリポジトリで非常にアクティブに開発されていて、Issue / PR もいい感じに処理されているようなので、気になる点があればフィードバックすると良いと思います。

App Service Authentication 周りもローカルで再現できるのはかなり便利そうなので、正直なところ App Service / Azure Functions 向けにも同じような機能が欲しいです。

Azure AD B2C にカスタムドメインを設定して MSAL (C# / JavaScript) から使ってみた

恐らく Azure AD B2C を使っている人全員が待ち望んでいたカスタムドメイン対応ですが、Front Door と組み合わせる形にはなりますが Preview として公開されました。

単純に Front Door をリバースプロキシとして使うだけなのですが、割と現実的な落としどころな実装になった感があります。Front Door 分の課金は必要になりますが、こればかりは仕方ありません。

カスタムドメインの設定方法のドキュメントが公開されているので、これに従って実際に設定する方法と、設定後の Azure AD B2C を MSAL から使うところまで試してみました。

今回のカスタムドメイン対応と同じタイミングで、Azure AD B2C のログイン画面を iframe で使うための機能も Preview として公開されています。

SPA ではログイン時に必ず Azure AD B2C 側へのリダイレクトが必要でしたが、iframe を使うことで画面遷移なしにログインさせることが出来ます。UI のカスタマイズ性も向上するでしょう。

流石にこっちは試しませんが、暇なタイミングで UI に詳しい人と弄ってみたいと思います。

Active Directory にカスタムドメインを追加

とりあえずドキュメントに従って Active Directory に設定したいカスタムドメインを追加します。この辺りは Azure AD を使っていれば設定したことがある人は多そうです。

通常なら Azure AD B2C などにはサブドメインを割り当てると思いますが、Active Directory への追加時には Apex ドメインを最初に登録する必要があります。

Apex ドメインを追加すると検証プロセスが走るので、検証後にサブドメインを追加可能になります。

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

今回は使い道のないドメイン login.daruyanagi.com を使うので、daruyanagi.com から先に追加します。

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

使用するドメインが検証済みになれば Azure AD B2C 側の設定は完了です。この設定は Front Door で転送されてくるホスト名の許可リストになるようです。

Make primary を押したくなりましたが、いろいろと壊れそうな気がしたので止めておきました。

Front Door にカスタムドメインと B2C へのルーティングを追加

Azure AD B2C に使用するカスタムドメインを追加すれば、後は Front Door にカスタムドメインを割り当てて、B2C へのルーティングを追加するだけです。

特に説明も必要ない気はしますが、一応スクリーンショットを撮っておいたので簡単に書きます。Front Door で新規 Backend pool を作成して、Backend として b2clogin.com で終わるホスト名を設定します。

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

それ以外はデフォルトのままで問題ありません。Status が Enabled になっていることだけは確認します。

Backend pool では Health probes を無効化して保存します。単純にチェックの必要はありません。

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

後は Front Door の Frontend にカスタムドメインと証明書を設定して、ルーティングルールで先ほど作成した Backend pool を設定するだけで、Azure AD B2C のカスタムドメイン化が完了します。

実際には Azure AD に設定したカスタムドメインでもアクセス可能にしているだけなので、Azure Portal には b2clogin.com のエンドポイントしか表示されませんが、手動でホスト名を書き換えて確認します。

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

ログインを行って取得できた id_token を確認すると iss もカスタムドメインで設定したものになっています。ここはログインに使用したドメインがそのまま使われるようになっているようです。

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

これまで b2clogin.com を使っていたものをカスタムドメインに置き換える場合には、iss のチェックで既存のトークンがエラーになる場合がありそうなので、リリース計画を立てる必要がありそうです。

Identity Provider 側の設定を修正する

Facebook や Google といった OAuth 2 / OpenID Connect に対応した Identity Provider を使っている場合は、カスタムドメイン設定後はリダイレクト URL が変わるので、以下のようなエラーとなるはずです。

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

IdP の設定からリダイレクト URL をカスタムドメインを使用したものを追加するか、変更すればこれまで通りログイン可能です。忘れやすい部分だと思うので注意したいです。

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

リダイレクト URL は複数設定できることが多いので、変更ではなく追加のが安心です。

MSAL からカスタムドメインで利用する

最後に実際のアプリケーションから使うことを考えて、MSAL を使って試しておきました。まずは良く使いそうな MSAL.js でカスタムドメインを試してみます。

Azure AD B2C と MSAL.js 2.x の組み合わせは、少し前から認可コードフロー + PKCE 対応になっているので非常にすんなり使えます。

カスタムドメインになったからといっても、単純に MSAL に渡す設定に含まれている各 URL をカスタムドメインに置き換えるだけで問題なく扱えました。

const msalConfig = {
  auth: {
    clientId: "<clientid>",
    authority: "https://login.daruyanagi.com/<tenantid>/<policyname>",
    knownAuthorities: ["login.daruyanagi.com"]
  }
}

実行してみるとポップアップがカスタムドメインになっていることが確認できます。もちろんログインも問題なく行えたのでトークンが取得できました。

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

同様に MSAL.NET でも試してみます。デスクトップアプリケーションで試しているので設定は若干異なっていますが、内容としては MSAL.js の時と同じようにカスタムドメインに置き換えただけです。

WebView だとリダイレクト URL は表示されないので気にする必要はないのですが、折角なのでカスタムドメイン版に置き換えておきました。

var app = PublicClientApplicationBuilder.Create("<clientid>")
                                        .WithB2CAuthority("https://login.daruyanagi.com/tfp/<tenantid>/<policyname>")
                                        .WithRedirectUri("https://login.daruyanagi.com/oauth2/nativeclient")
                                        .Build();

var result = await app.AcquireTokenInteractive(null)
                      .ExecuteAsync();

Debug.WriteLine(result.AccessToken);
Debug.WriteLine(result.IdToken);

Azure AD B2C のアプリケーションのデフォルト設定では、カスタムドメイン版のリダイレクト URL は追加されないので、手動で追加して対応しました。

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

この設定で MSAL.NET でもカスタムドメインで問題なく動作しました。

全体的に見ると Front Door の設定が必要になるのは若干面倒な感じですが、カスタマイズした WAF を追加できる*1のは魅力になるかなと思いました。GA までにはデフォルトドメインでのアクセス拒否機能が組み込まれて欲しいですね。

*1:Preview 中は WAF を追加するのは推奨されていないっぽいみたいですが

私が Azure Functions アプリケーションの開発時に意識していること

ここ数年は Azure Functions をフルに活用したアプリケーションを実装することが多かったのですが、同時に Azure Functions を失敗しないように使う方法も分かってくるので、ここらでちゃんと言語化しておきます。

最近は特に Azure Light-up というハッカソンを行うことが多いのですが、Azure Functions を使う場合には必ずこの辺りは毎回説明するようにしています。要するに Azure Functions の利点・特性を理解して賢く使いこなそうという話です。

図があった方がわかりやすい気もしましたが、作成する元気がなかったので長文になりました。あくまでも私が意識していることなので、こういう考え方もあると捉えて貰えれば良いです。

Binding / Trigger で実現出来ないか考える

Azure Functions 最大の特徴といえる、豊富かつ拡張可能な Binding / Trigger は要件に合えば必ず使っています。Azure Storage や Event Hub などの SDK の使い方を知らずとも使える便利な機能です。

HttpTriggerTimerTrigger はほぼ使ったことがあると思いますが、今ではそれ以外にもかなりの数が提供されています。最近では CosmosDBTrigger を使うことが非常に多いです。

Blob や Queue の読み書きはほぼ同じコードになるので割と退屈な部分の割に、コード量はそこそこ必要になるので Binding / Trigger を使って隠してしまいます。この辺りはアプリケーションの本質的ではないコードになるので、実際のビジネスロジックに集中するためにも重要です。

Function の実装は出来る限り小さく保つ

1 つの Function の実装が大きいと、冪等性の担保やリトライが難しくなることが多いので、基本的に 1 つの Function には 1 つの処理だけをさせるようにしています。

単機能の Function を Queue Storage や Cosmos DB Change Feed で組み合わせることで、目的のアプリケーションを実装するようにしています。Function の責務をはっきりさせないとデプロイすら難しくなります。

今では Durable Functions を使うことで、Queue やインスタンスを意識することなく複数 Function を操作するアプリケーションを実装できるので、以前より Function の実装を小さく保ちやすいです。

ただし実際のビジネスロジックは別途サービスやリポジトリクラスとして分離して、それらを DI で組み合わせて作り上げていくので、コード量自体はもう少し多くなります。

最近は Azure Functions でも DI が使いやすくなっているので初期化が肥大化気味ですが、各 Function App から共通で参照されるクラスライブラリを作成し、DI の登録周りと処理の共通化を行うようにしています。

リトライのしやすい実装を重視する

Function を単機能として実装することで、考えることが減るのでリトライへの対応が簡単になります。冪等性の担保も同様にしやすくなります。

Azure SDK には最初からリトライは組み込まれているので、基本は組み込みをそのまま使いつつ、必要に応じて Polly や Azure Function の Retry Policy を利用するようにしています。

特に最近は避けるようにしている実装として、1 つの Function が複数のストレージへの書き込みを行うようなケースがあります。何らかの処理結果を SQL DB に書き込みつつ Blob へも書き込むような実装は、後者の処理が失敗した場合にリカバリーが難しくなります。実装もそれぞれに対してのエラーハンドリングが必要になるため、異常に見通しが悪くなることがあります。

そういった場合には Blob であれば Event Grid を使って、イベントドリブンで Blob への書き込みを検知して後続の書き込みを行うようにします。

最近では Cosmos DB の Change Feed を使って、大本の Function では Cosmos DB への書き込みを行い、SQL DB や Blob への書き込みはそれぞれ別の Function として実装するケースが多いです。それぞれの Function の処理がシンプルになるので、リトライが行いやすくなります。

最新の .NET での作法に沿ったコードを書く

.NET の作法といっても最新の C# 言語機能を使うというわけではなく、ベースとなっているフレームワークに沿ったコードを書くように意識しています。

Azure Functions v3 は .NET Core 3.1 / ASP.NET Core 3.1 ベースなので、その作法に従っておくとシンプルなコードに出来ます。特に DI は必須になっているので、Visual Studio を作成した時に生成される static なクラスとメソッドは即座に捨てています。

ただし複雑な DI の使い方をすると逆効果になるので、やりすぎない DI としてシンプルな依存関係の解決と、インスタンスの生存期間管理をメインに使っています。

昔は App Settings を取るのにも苦労をしましたが、最新の Microsoft.Azure.Functions.Extensions をインストールするとシームレスな統合が実現されているので、積極的に使うようにしています。上記の static なクラス削除と DI の設定はテンプレートに入っていて欲しいぐらいです。

少し .NET から外れますが Managed Identity や Key Vault も利用するようにしています。

Graceful Shutdown に対応したコードを書く

昔は稼働中の Azure Functions へのデプロイを行う前に、Application Insights で処理が行われていないことを確認することもありましたが、少し前に Graceful Shutdown が正しく動作するようになったので対応するように実装して、デプロイはタイミングを意識せずに行えるようにしています。

Azure Functions は .NET で標準となっている CancellationToken を使ってシャットダウンを検知できるので、処理が壊れないように必要な処理だけを行って Function を完了させることが出来ます。

英語ブログの方に実際にアプリケーションを実装して得た知見を、Graceful Shutdown のベストプラクティスとしてまとめています。常時データが流れてくるアプリケーション上で入念に検証を行いました。

Deployment Slot を使えば Graceful Shutdown は不要と考える方もいると思いますが、App Service / Azure Functions の Deployment Slot は本番ではない側のスロットのウォームアップが完了後、ルーティングを入れ替えて本番だったスロットを再起動するため、Graceful Shutdown に対応したコードを書いていない場合は処理中でも一定時間後に問答無用でシャットダウンされます。

従ってスワップ前に確実に処理が正常終了する保証はないため、Deployment Slot を使ったデプロイの場合でも Graceful Shutdown への対応は重要です。

それ以前の話として、App Service / Azure Functions はプラットフォーム更新などの要因で、1 か月に数回自動的にインスタンスの移動が行われて、アプリケーションの再起動が行われています。そのタイミングは制御不可能なので、アプリケーションはシャットダウンに備える必要があります。

機能単位で Function App プロジェクトを分ける

正確にはスケーリングとデプロイの単位で Function App プロジェクトを分けるのが正解なのですが、最初からその辺りが見えるケースは少ないのでまずは機能単位で分けることを考えるようにしています。

たまに 1 つの Function App に大量の Function を実装しているアプリケーションを見ますが、もし 1 つの Function App にまとめている理由がコストなら検討しなおしたほうが良いです。

非常に誤解されやすく、正しく理解されていないことが多い App Service や Azure Functions の課金単位ですが、簡単にまとめると以下の通りになります。

  • Consumption Plan は実行回数と実行時間 (GB 秒) で課金
  • App Service Plan はインスタンスのサイズと数で課金
  • Premium Plan は最小インスタンスのサイズと数 + 追加インスタンスは使用した秒単位で課金

重要なのは App Service Plan 単位での課金であって、その上で動作する App Service / Azure Functions の数は無関係ということです。従って 1 つの App Service Plan に対して 5 つの Function を持つ Function App を 1 つデプロイするのと、1 つの Function を持つ Function App を 5 つデプロイするのは同じ価格になります。

当然ながら App Service Plan が持つリソースは有限なので、大量の Function App を載せるのはパフォーマンス面でも現実的ではないですが、1 つの Function App に全ての機能をまとめたからと言って安くならないことは知っておく必要があります。

そもそもどのプランを選ぶべきか分からない場合は、公式ドキュメントにそれぞれの機能比較表があるので、どのプランを使えば要件が満たせるか確認してください。

本番向けのアプリケーションであれば、大体の場合は App Service Plan か Premium Plan になると思います。特に最近は VNET Integration が必要となるケースが多いので、まず Consumption Plan は候補から消えます。

インスタンスのサイズはアプリケーションの特性によって変更しますが、ほとんどのケースでは P1V2 か P1V3 で十分なことが多いです。スケールアップは簡単なのでリソースを確認しつつ対応しています。

早いうちから CI / CD パイプラインを組む

もはや言うまでもないと思いますが、ごく小規模のアプリケーションを除き大体は Function App は 1 つ以上作成することが多いです。それらに Visual Studio から手動でデプロイすることは現実的ではないので、GitHub Actions や Azure Pipelines を使って自動化します。

デプロイ自体は Action / Task が用意されているのと、非常に簡単なので特に悩むこともないはずです。最近ではハッカソンの最中に CI / CD パイプラインまで組むことも多々あるぐらいです。

普段ハッカソンで話している内容を出来るだけまとめたつもりですが、抜け漏れがあるかも知れないので追記するかもしれません。Azure Light-up に興味がある場合は連絡ください。

Azure Cosmos DB における接続文字列の管理が RBAC サポートでついに不要に

Ignite は PaaS / Serverless 周りの話が少なかったですが、Cosmos DB に関してはいくつかアップデートがありました。その中でも RBAC サポートはアクセスキーや接続文字列を管理したくない勢としては待望の機能なので、使い勝手を確認しておきました。

Cosmos DB チームは相変わらずドキュメントがしっかり書かれているので、ドキュメント通りに作業を進めるだけでサクッと動きました。なので、あまり細かい使い方は書きません。

今のところは Azure Resource Manager の RBAC のように組み込みのロールは用意されていません。先に必要なカスタムロールを作成してから割り当てる必要がありますが、細かく権限管理が行えるので安心です。

アプリケーションから RBAC を使ってアクセスするためには最新のプレビュー版 SDK が必要になるので、間違えて正式版をインストールしないようにします。バージョンを明示的に指定しないと入りません。

Visual Studio の Azure サービス認証や Managed Identity を使う場合には、Cosmos DB SDK と同時に Azure.Identity のインストールが必要です。

早速サンプルコードを出していきますが、CosmosClientTokenCredential を受け取るコンストラクタが追加されているので、新しい Azure SDK を使ったことがある方なら余裕だと思います。

class Program
{
    static async Task Main(string[] args)
    {
        var tokenCredential = new DefaultAzureCredential();

        var cosmosClient = new CosmosClient("https://***.documents.azure.com:443/", tokenCredential, new CosmosClientOptions
        {
            SerializerOptions = new CosmosSerializationOptions
            {
                PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
            }
        });

        var container = cosmosClient.GetContainer("HackAzure", "TodoItem");

        var iterator = container.GetItemLinqQueryable<TodoItem>()
                                .ToFeedIterator();

        while (iterator.HasMoreResults)
        {
            var items = await iterator.ReadNextAsync();

            foreach (var todoItem in items)
            {
                Console.WriteLine($"{todoItem.Id},{todoItem.Title},{todoItem.Body}");
            }
        }
    }
}

まずは Visual Studio でのデバッグ実行を行いたいので、ログインしているユーザーに対して Azure CLI で Read Write が行えるカスタムロールを割り当てました。

少し迷ったのが --role-definition-id に指定する値ですが、カスタムロール作成時に返ってきた id プロパティではなく name プロパティの値を指定するのが正解です。

az cosmosdb sql role assignment create -a *** -g *** --scope "/" --principal-id "ユーザーの Object ID" --role-definition-id "作成したカスタムロールの ID (GUID)"

カスタムロールを割り当てると F5 でのデバッグ実行だけで問題なく動作します。ロールを割り当てていない場合は 403 が返ってきてエラーとなります。

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

接続文字列のために Key Vault Reference などを使う必要がなくなり、Cosmos DB のエンドポイントだけ設定に持っておけば良いので扱いが楽です。

同じように Managed Identity を有効にした App Service / Azure Functions でも利用することができます。使い方はコンソールアプリケーションの時と全く同じですが、最近の Azure Functions っぽい書き方をします。

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

        builder.Services.AddSingleton(provider =>
        {
            var tokenCredential = new DefaultAzureCredential();

            return new CosmosClient(context.Configuration["CosmosEndpoint"], tokenCredential);
        });
    }
}
public class Function1
{
    public Function1(CosmosClient cosmosClient)
    {
        _cosmosClient = cosmosClient;
    }

    private readonly CosmosClient _cosmosClient;

    [FunctionName("Function1")]
    public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req, ILogger log)
    {
        var container = _cosmosClient.GetContainer("HackAzure", "TodoItem");

        var iterator = container.GetItemLinqQueryable<TodoItem>()
                                .Take(3)
                                .ToFeedIterator();

        var items = await iterator.ReadNextAsync();

        return new OkObjectResult(items);
    }
}

トークンを取得するのに DefaultAzureCredential を使っているので、同じコードのまま Visual Studio でのデバッグ実行と Azure Function 上の両方で動作します。Azure CLI が入っている環境でも動作します。

実際に Azure Functions にデプロイしてテスト実行をしてみると、接続文字列無しで問題なく動作しました。

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

基本的な RBAC 周りの動作は確認したので、アプリケーションから操作可能な Container と処理を制限するカスタムロールを作成してみます。

個人的には Change Feed をよく使うので、Change Feed 経由での読み取りのみ可能なカスタムロールを以下のように作りました。readMetadata は Cosmos DB を使う上で必要なものなので入れておきます。

{
    "RoleName": "ChangeFeedOnly",
    "Type": "CustomRole",
    "AssignableScopes": ["/"],
    "Permissions": [{
        "DataActions": [
            "Microsoft.DocumentDB/databaseAccounts/readMetadata",
            "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/readChangeFeed"
        ]
    }]
}

このカスタムロールを割り当てればサクッと Change Feed のみ可能になるかと思い、一般的な Change Feed Processor のコードを書いて試してみましたが実際にはエラーになりました。

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

エラーになった理由としては単純で、Change Feed Processor は実際には Lease Container を必要としていて、Lease に対しては読み書きの権限が必要だからという話でした。

なので Change Feed Processor を利用するために、2 つのカスタムロールを 2 つの Container をスコープとして設定して割り当てました。Lease に対しては読み書き可能なロールを、Change Feed で読み取る Container に対しては作成した Change Feed のみ可能なロールを割り当てています。

accountName='Cosmos DB アカウント名'
resourceGroupName='リソースグループ名'
principalId='User / Managed Identity / Service Principal の ObjectId'
changeFeedRole='Change Feed のみ許可する Role Definition Id'
readWriteRole='Read Write を許可する Role Definition Id'
az cosmosdb sql role assignment create -a $accountName -g $resourceGroupName --scope "/dbs/HackAzure/colls/TodoItem" --principal-id $principalId --role-definition-id $changeFeedRole
az cosmosdb sql role assignment create -a $accountName -g $resourceGroupName --scope "/dbs/HackAzure/colls/Lease" --principal-id $principalId --role-definition-id $readWriteRole

このロールの割り当てで問題なく Change Feed Processor が動作するようになりました。実際にここまで細かく RBAC で制限するかは要件次第だと思いますが、ガチガチに制限することが出来るのは良いことです。

ちなみに Change Feed Processor ではなく Change Feed の Pull model を使う場合には、Change Feed のみ可能なロールの割り当てだけで動作します。Pull model は Lease を必要としないのが理由ですが、結局はどこかに継続トークンを保存する必要があるので、何かしらの読み書き権限は必要になります。

App Service Managed Certificate が Apex ドメインに対応したので試した

Apex ドメインに対応するまでは GA しないオーラが漂っていた App Service Managed Certificate ですが、Ignite 合わせと見せつつ全く関係ない感じで Apex ドメインに対応したと発表がありました。

最初に公開されたのが 2019/11 なので非常に長いプレビューになっています。GA は未定っぽいです。

公式ブログの内容と実際に試した結果から、分かったことを簡単にまとめておきました。Web アプリケーション向けには問題なく使えると思っていますが、注意点も少しあります。

  • http-token を使ったドメイン所有の検証を行っている
    • 公式ブログの通り IP Restriction を使っている場合は検証リクエストが通らないので失敗する
  • Windows / Linux に関係なく利用可能
    • Web Worker ではなく前段の ARR で検証リクエストを処理しているっぽい
    • つまり Easy Auth の影響は恐らく受けない(Easy Auth は Web Worker での処理なので)
  • Azure Functions (Consumption 以外) でも使えるはず
    • Consumption Plan はそもそも A レコードが使えないので関係ない
  • サブドメインに関してはこれまで通り CNAME が必須
    • A レコード + http-token に対応ではなく、Apex ドメイン + http-token に対応という形
  • Traffic Manager を入れた構成には非対応
    • http-token を使った検証がどのインスタンスに振られるか不定だからだと思う
  • ワイルドカード証明書は未対応
    • 今後も対応されない気がする
  • App Service Environment も非対応のままっぽい
    • http-token を使った方法なら対応できそうな気もするが謎

前段の ARR でこういった処理を行えるのは App Service のメリットという感じがしますね。わかりやすい仕組みなのに、対応までに非常に時間がかかったのはちょっと謎です。

汎用的な作りにはなっておらず、完全に Apex ドメインに特化した実装のようなので、サブドメインに関しては CNAME 必須のままでした。試すと以下のようなエラーになるので、若干わかりにくいかも知れません。

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

普通に Apex ドメインに対して使う分にはサクッと発行できるので、要件が合う場合には積極的に使って行けばよいと思います。個人的には最初の更新が上手くいくのかを、見届けてからかなという気持ちです。

実際に Linux の App Service を新しく作成して、Apex ドメインへの証明書発行を試しておきました。これまでは Apex ドメインはエラーになっていたはずですが、問題なくチェックが通ります。

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

ちなみにワイルドカードドメインは一覧に表示すらされません。後は Create を実行するだけで証明書が作成されますが、自分が何回か試した限りでは 3,4 分ぐらいかかりました。

発行された証明書はこれまで通り DigiCert 発行の 180 日有効なものでした。例によって CAA には注意。

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

単純な証明書であればこれで十分という感じですね。実際に更新が上手くいくか若干怪しいと思っていますが、最悪手動で再発行もできるので何とでもなります。

Traffic Manager が必要なシナリオや、ワイルドカードや SANs といった複雑な証明書が必要な場合は、これまで通り購入するか Let's Encrypt などを使うことになると思います。

障害に強い Azure の運用を考える (2021 年版)

Azure の日本リージョン 7 周年の日に Japan East のストレージ障害が発生するという、なんともアレな出来事がありましたが、障害発生後はアーキテクチャを見直すいい機会だと思うので色々書きます。

まだ RCA は公開されていないですが、おそらくぶちぞう RD がブログに書くのでリンクを貼ります。

4 年前の同じような時期にも Japan East で大規模なストレージ障害が発生したので、同じようなものを書きましたが流石にいろいろと進化しているので古さを感じます。

今回の障害は影響範囲はさほど大きくなかったようで、主に Azure Storage と Virtual Machines がステータスには上がってきていました。実際には Application Insights や Log Analytics にも影響がありましたが、Azure Storage は全ての基本なので仕方ない感はあります。*1

全てを書くのは無理なので、いくつかサービスをピックアップして紹介します。

要するに App Service / Azure Functions を使えという話ではありますが、Storage 周りにも 4 年前とは違ってサービスや機能が増えているので多めです。VM は察してください。

App Service / Azure Functions

以前に書いた App Service の設計パターンまとめにほとんど書いているのですが、いくつかピックアップしてもうちょっと具体的に紹介します。構築前に読んでおくといいことがあると思います。

これを書いた時から大きくは変わっていないので、そのまま使えるはずです。変わったら更新します。

基本は App Service Diagnostics に従う

既に何回も書いていますが App Service Diagnostics が非常に優秀なので、まずはここから始めていけば良いです。最近だと Risk alerts という形で推奨事項を確認できます。

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

可用性に対するリスクが検出されているので、以下のように複数インスタンスで実行させることや、Auto heal を有効化することが推奨されています。

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

ベストプラクティスの塊なので、いろいろ考える前に App Service Diagnostics を確認しましょう。

Health Check を有効化する

Health Check は去年に GA した新しい機能ですが、アプリケーション側で適切にヘルスチェック用のエンドポイントを実装していれば、App Service が自動的に再起動や VM の入れ替えなどを行ってくれるので、回復性が高まります。詳しくはドキュメントを参照してください。

最近の Azure Portal では Essential の部分に Health Check が表示されているので、重要度の高い機能であることが理解できると思います。設定自体は以下のように非常にシンプルです。

注意点としては不良 VM の入れ替えなどは 1 インスタンスでは実行してくれないので、2 インスタンス以上で動作させておく必要があることでしょう。該当する App Service では警告メッセージが出ます。

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

Load Balancer から削除されるまでの時間は、実質的にはヘルスチェックが 2 分に 1 回行われるので、連続して何回失敗したときに行うかという設定になります。ヘルスチェックが 2 分に 1 回は若干長いと思うのですが、今のところ変更は出来ません。

2 インスタンスで実行している App Service に対して、特定のインスタンスに対してヘルスチェックを失敗させるようにしたアプリケーションをデプロイして、動作を確認したのが以下のグラフです。

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

DC83 のインスタンスはヘルスチェックが通らなくなりましたが、App Service によって自動的に新しいインスタンスが用意されて入れ替えが行われました。この時ヘルスチェックの失敗回数が設定した回数を上回っているので、ヘルスチェックが落ちているインスタンスは LB から削除されています。

共有ストレージの利用を最低限にする

App Service / Azure Functions は Azure Storage ベースの共有ストレージがマウントされているので、使用している Azure Storage に障害が発生すると当然ながら影響を受けます。従ってアプリケーションが共有ストレージに頻繁に読み書きする場合は、サービスの提供を継続できません。

ごく小規模のアプリを除いては、共有ストレージは読み込み専用で利用するのが最近のお勧めになります。特にアプリケーションのデプロイには Run From Package を使うのは非常に良いです。

Run From Package でデプロイされたアプリケーションは、一度起動されるとデプロイやインスタンスが入れ替わらない限りはローカルストレージにキャッシュされたパッケージが使われるので、ストレージ障害の影響を最小限に抑えられる可能性があります。

マルチインスタンス・マルチリージョン構成を組む

障害が App Service のインスタンスと共有ストレージの両方に運悪く影響した場合は、基本的は障害の復旧を待つしかなくなってしまいます。Azure Storage は様々なサービスのバックエンドで使われているので、どうしても影響範囲が広くなりがちです。

可用性を高めるためには、最低限 2 インスタンス以上で Always On を有効にした状態で稼働させるか、更に高可用性が必要なアプリケーションの場合は Japan East と Japan West の両方に同一構成のアプリケーションを用意するのが最適解となります。

Traffic Manager や Front Door を入れておくことで、フェールオーバーも簡単に行えますし、最近は CI / CD や ARM Template / Terraform などを使うことで同一構成を別リージョンに丸ごと作ることも簡単です。

Storage (Blob / Queue / Table)

Azure Storage で障害が発生した場合 Blob や Queue を使うアプリケーションでは回避が難しいです。

特に Azure Functions はバックエンドを Storage に完全に依存しているので、アーキテクチャを見直してマルチリージョン構成にする必要も出てくるでしょう。

ZRS 構成を組む

昔は GRS とか RA-GRS を使うぐらいしか選択が無かったですが、今は ZRS が使えるようになったので LRS に近い形で高可用性を実現できるようになっています。

GRS の場合はフェールオーバーが正直だるい感じですが、ZRS は特定のゾーンが落ちた場合には Azure によって自動的に DNS レベルで再設定が行われるので、クラウドデザインパターンに則ってリトライを実装しておけば幸せになれそうです。

Azure CDN / Front Door と組み合わせる

単純に Blob を使ってコンテンツを配信している場合には、前段に Azure CDN や Front Door を用意してキャッシュさせることで多少時間稼ぎが出来ますし、オリジンを切り替えることで同一 URL のまま別ストレージアカウントを見るようにできます。

Blob は Object Replication を使うと、GRS よりも分かりやすく別リージョンへ Blob のレプリケーションを実現できるので、コンテンツ系の場合はこの方法が RA-GRS などを使うよりは良いでしょう。

レプリケーション先は読み込み専用になるため、フェールオーバー後にプライマリに昇格みたいな動作にはならないですが、コンテンツ配信系のサービスでは問題にならないケースもあるかと思います。

Cosmos DB の利用を検討する

Queue と Table に関しては Cosmos DB とその Change Feed を使うことで、同等の処理を実現できます。Cosmos DB の可用性は非常に高く、必要に応じてマルチリージョン構成も簡単に組めるため、価格以外では Azure Storage よりもメリットがあります。

具体的には Queue には Change Feed を使い、Table には Cosmos DB の Table API を使う形になります。最初から Cosmos DB を使うと決めている場合は SQL API がお勧めですが、一応 Table と互換性のある SDK が用意されているので、既存のコードも比較的簡単に移行できるはずです。

Virtual Machines

基本的には VM を素の状態で運用するのは可用性が下がるだけなので、独自に高可用性のアーキテクチャを組めない場合は PaaS / Serverless といったマネージドサービスを使った方が良いです。

App Service / Azure Functions で実現出来ないか検討する

以前の App Service / Azure Functions は仮想ネットワーク周りの機能が足りておらず、実現可能なアーキテクチャに限界がありましたが、Regional VNET Integration や Private Link で出来ることが増えています。

したがって最初から VM を使う方法を選ぶのではなく、App Service / Azure Functions で実現出来ないかを検討するのがベストです。非常に大量のインスタンスが必要なケースや gRPC などの、どうしても App Service で実現できない要件の場合に、初めて他のソリューションを検討しても遅くはありません。

可用性オプションを適切に利用する

どうしても VM が必要な場合には、要件に合わせた可用性オプションを利用していきましょう。Japan East には Availability Zone が 3 つ用意されているので、AZ を有効にした VM をデプロイすることで、障害の影響を最小限に抑えることができます。

他にもゾーンまではいかなくとも Availability Set を使えば同じデータセンター内でも、物理的に分離されたハードウェア上に分離してデプロイすることも出来ます。ドキュメントは各種用意されているので、VM を使う場合は熟読が必要でしょう。

VMSS を使うことで比較的簡単に実現は出来ますが、それなりに管理コストがかかることを覚悟する必要があるでしょう。素の VM を適切に運用するのはかなり難しいです。

その他のサービス

可用性のチェックリストが Well-Architected Framework カテゴリに用意されているので、参考までに目を通しておくとよいと思います。代表的なサービスが載っています。

Azure には最初から高可用性を実現するためのサービスや機能は揃っていますが、それを上手く使えるかどうかはアーキテクチャ次第です。適当に作るだけで実現出来るような甘い世界ではないので、障害発生を無駄にせずベストプラクティスやデザインパターンを学んでいきましょう。

*1:ステータスに載らないのは許されないが、Azure Service Health には載っていた