しばやん雑記

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

Azure Front Door Premium (Preview) で追加された Private Link 対応を試した

これまで Azure Front Door には SKU が存在していませんでしたが、先週に Front Door に新しく Standard と Premium の SKU が追加されたバージョンが Public Preview として公開されました。

事前に REST API リファレンスには登場していたので知っていましたが、ARM 的にはこれまでの Front Door とは異なり CDN 寄りなのが大きな違いのように感じます。

機能的には今のところはこれまでの Front Door と大きな差はないのですが、Premium では Private Link を使ったオリジンへの安全なアクセスが行える機能が追加されています。これが今回の目玉機能でしょう。

本当にそれ以外は大きな差は無いと思います。Rule set を優先順位付きでの複数指定や、証明書の管理がわかりやすくなったなどいろいろありますが、CDN としての機能を見ると差はないです。

話を Private Link に戻すと、これまでオリジンのロックダウンには以前書いたように、アクセス元の広い IP Range を登録するか、特殊な HTTP リクエストヘッダーを見るしか方法が無かったです。

しかし Premium の場合は Private Link 経由でのアクセスを設定できるので、確実かつ簡単に Front Door 以外からのアクセスを遮断することが出来ます。CDN の各エッジから Private Link で見る仕組みが謎ではありますが、セキュリティ要件で確実に遮断が必要な場合には有効でしょう。

早速 Premium の Front Door を作成して App Service と Storage Account に対して Private Link 設定を試してみるわけですが、リソースの作成画面が大きく変わりました。単純な SKU だけではなく、ユースケースによって適切な Front Door / CDN を選べるようになっています。

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

とはいえ今回は Premium を試すと決めているので、Preview 版を直接選択してリソースを作成します。

SKU 自体はこの後に選択できるようになっています。説明にもあるように Premium はセキュリティを重視する場合に使うという認識で問題ないです。もちろん Standard に比べると高いです。

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

新しく Front Door を作成するタイミングで一緒にオリジンの追加も出来るので、前もって作成していた App Service を追加しておきます。"Enable private link service" にチェックを入れると、Private Link の追加設定が必要になりますが、大体は勝手に入ってくれます。

この辺りは Private Endpoint を作成する時と同じなので、追加したことがある人は見慣れたものでしょう。

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

ちなみに現在 Private Link が使えるリージョンは East US / West US 2 / South Central US だけなので、デプロイ先には注意しましょう。

新しい Front Door Premium (Preview) の Azure Portal はこれまでとは大きく変わっているので、若干戸惑うかもしれませんが Front Door Designer が細分化されたと考えればスムーズに扱えるかと思います。

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

これで Front Door のデプロイが完了しましたが、エンドポイントにアクセスしてもエラーになると思います。まだ Private Link が Approve されていないので、Front Door からアクセスできないのが原因です。

Private Link の設定を行ったリソースの Private Endpoint connections を確認すると、Front Door からのリクエストが追加されているはずなので、選択してから Approve する必要があります。忘れずに Approve しないといつまで待ってもアクセスできないので注意です。

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

ドキュメントには Private Link を有効化してからとあったので、とりあえず Private Endpoint を手動で作成しておいたのですが、これは実際には必要ありませんでした。意図としては Premium V2 / V3 などの Private Link に対応した SKU が必要というだけで、VNET と Private DNS Zone は不要という話だったようです。

実際にもう一つ VNET や Private Endpoint の設定が行われていない、まっさらの App Service を作成してみたところ、問題なく Front Door からの Private Link 接続が有効化できました。

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

この画面に表示されている Private Endpoint は Front Door が内部で管理しているものなので、確認することはできません。Front Door からオリジンを削除すれば自動的に Private Endpoint も削除されます。

Private Endpoint を Approve すれば、Front Door からアクセスできるようになっているはずです。App Service は Private Endpoint を追加すると、自動的に全てのパブリックアクセスは無効化されるので、安全にアクセス出来ていることが確認できます。

次は Storage Account をオリジンに追加して試してみます。単なる Blob ではなくて、使う機会が多いと思われる Static Website をターゲットにして試していますが、現在は Custom で追加するしかないです。

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

Private Link の設定は App Service とほぼ同じですが、Target sub resource の設定だけは気を付ける必要があります。今回は Static Website を使うので web を指定する必要があります。

オリジンを追加すると App Service の時のように Private Endpoint へのリクエストが追加されているので、同じように Approve することで Front Door からアクセスできるようになります。

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

ただし Storage Account は Private Endpoint を追加しただけではパブリックアクセスは閉じられないので、Firewall の設定からパブリックアクセスを拒否するように変更する必要があります。

正直なところ Preview だからか Private Link 周りの動作は若干不安定で、反映されるまでに時間がかかったりしますが、しばらく放置した後にアクセスできるようになりました。

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

オリジンのロックダウンが Private Link で行えるようになったのはかなり良い機能だとは思いますが、Premium が思ったより高いなという印象だったので、基本はこれまで通り Access Restriction などを使って HTTP リクエストベースでロックダウンをしつつ、高いセキュリティレベルが要求される部分は Private Link という使い分けが必要だと思います。

とはいえオリジンへのアクセスを Private Link でシンプルに行えるのは魅力的ですね。

Azure App Service の Access Restriction を使って Front Door からのアクセスのみ許可する

ドキュメントを適当に読んでいたところ、App Service の Access Restriction で特定の Front Door からのアクセスのみを許可する方法がプレビューとして記載されていたので、この辺りの確認をしておきました。

Front Door のドキュメントにもバックエンドへのアクセスを Front Door のみにロックダウンする方法は書かれていますが、ちょっと前から Access Restriction の機能追加で簡単に実現できるようになったという話です

初期のころは Front Door のバックエンドに割り当てられている 147.243.0.0/16 を追加する方法が多かったですが、少し前から Service Tag を使った制限に対応しているので、IP Range を使った方法はドキュメントから消されたようです。早めに Service Tag へ移行したほうが良いでしょう。

Service Tag のサポートについては以前に書いたので、こっちを参照してください。

結局のところ Service Tag だけでは Front Door からのトラフィックであれば、どのインスタンスか判別出来ないため全て素通りさせてしまう問題がありましたが、ドキュメントにもあるように X-Azure-FDID を併用することで一応は解決できます。

やっと本題ですが App Service の Access Restriction に、特定の HTTP ヘッダーの値によるアクセス制限を追加できるようになったので X-Azure-FDID による制限を組み込みやすくなったという話です。

この機能追加は割と前から ARM 上は実装されていたのですが、まともな使い方の紹介は存在しなかったのでスルーしていました。実質 Front Door 向けの機能です。

HTTP ヘッダーであれば何でも使えるわけではなく、以下の 4 つに限定されているようです。基本的にはリバースプロキシの下にいる場合を想定したヘッダーですね。

  • X-Forwarded-Host
  • X-Forwarded-For
  • X-Azure-FDID
  • X-FD-HealthProbe

実際に他のヘッダー名を追加しようとするとバリデーションでエラーになりました。

この中でも X-Forwarded-For は最初は少し違和感がありましたが、Access Restriction の処理が実行されているのは App Service の Web Worker の前にいる ARR なので、ARR の前に更にリバースプロキシが居ると Access Restriction の IP 制限は意図した通りに動かないので必要という話です。

そもそも Front Door や CDN を置いている場面で IP 制限が必要なのかというのは別の話です。

特定の Front Door からのアクセスのみ許可する

前置きが非常に長くなったので Front Door を使って HTTP ヘッダーでのアクセス制限を有効化してみます。基本はドキュメント通りではありますが、ARM Explorer の便利な使い方を書きたかったので。

Front Door がバックエンドに送る HTTP ヘッダーの一覧は以下の通りです。

今後は Front Door の Service Tag と X-Azure-FDID を Access Restriction に設定するといい感じに幸せになれます。出来れば Private Link とかで直接繋がってほしい気持ちはあります。

肝心の Front Door ID と呼ばれる GUID は Azure Portal から確認できるようになっていました。少し前までは無かったと思うので、最近追加されたのかもしれません。

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

ドキュメントでは Azure PowerShell を使って Access Restriction に追加していましたが、Azure PowerShell の使い勝手はイマイチなので ARM Explorer の Raw モードを使いました。

https://resources.azure.com/raw/

このモードを使うと VS Code の REST Client 拡張のように ARM REST API を実行できます。送信したい部分を選択して Ctrl+S を押せばリクエストが実行されます。

URL を直接指定するので自由に API Version を指定できます。API Version は何だかんだで結構更新されているので、ARM Explorer の標準モードでは出てこないプロパティなども多いです。

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

案外 ARM REST API を直接叩くのは面倒なので、ARM Explorer の Raw モードを使うのが簡単です。

config/web エンドポイントに対して以下のような JSON を PUT で送信すれば反映されます。

{
  "properties": {
    "ipSecurityRestrictions": [
      {
        "ipAddress": "AzureFrontDoor.Backend",
        "action": "Allow",
        "tag": "ServiceTag",
        "priority": 100,
        "name": "Azure Front Door",
        "headers": {
          "X-Azure-FDID": [
            "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
          ]
        }
      }
    ]
  }
}

ARM Explorer 上では以下のようになるので、リクエストボディと URL を同時に選択して送信します。

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

特にエラーが出ていなければ設定完了なので、Front Door のバックエンドに設定した App Service に直接ブラウザでアクセスすると、以下のように 403 エラーが返ってくるようになります。

もちろん Front Door 経由でアクセスした場合には正しくアクセスできます。

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

Front Door ID が ARM REST API を叩けば取れるので、Terraform や ARM Template でも問題なく扱えます。

Service Tag と X-Azure-FDID を同時に指定しているので、設定した Front Door 以外からバックエンドへのアクセスはこれで塞ぐことができました。

Azure CDN と特定のホスト名からのアクセスのみ許可する

Front Door の場合は専用のヘッダーが用意されていましたが、同じインフラを利用している Azure CDN (Standard Microsoft) ではそういったものは用意されていないので、Front Door を使うようにするか X-Forwarded-Host と組み合わせるぐらいの選択肢しかありません。

Azure CDN (Standard Microsoft) は Front Door と同じインフラを利用しているので、Service Tag も Front Door と同じものが使えます。したがって以下のような JSON を用意することで、ある程度バックエンドのロックダウンが行えます。

{
  "properties": {
    "ipSecurityRestrictions": [
      {
        "ipAddress": "AzureFrontDoor.Backend",
        "action": "Allow",
        "tag": "ServiceTag",
        "priority": 100,
        "name": "Azure CDN",
        "headers": {
          "X-Forwarded-Host": [
            "daruyanagi.com"
          ]
        }
      }
    ]
  }
}

値のマッチングルールはヘッダーによって変えられていて、X-Forwarded-Host の場合は DNS や証明書と同様のワイルドカードでの指定もできるようです。

App Service 直接アクセスの場合はカスタムドメインを割り当てていても X-Forwarded-Host は送信されないので、結果的に Azure CDN などのリバースプロキシ経由の場合のみ通るようになります。

しっかりとしたロックダウンが必要な場合には Front Door を使った方が良いとは思いますが、価格が CDN と比べると高くなるので必要に応じて使い分けていけばよいと思います。

Azure App Service の新しい Authentication 設定を試した

毎日、何回も Azure Portal から App Service / Azure Functions のブレードを開いているので、新しい項目があればすぐに気が付きます。なので今回の Authentication (Preview) も発見が早かったです。

何の変哲もない App Service Authentication (Easy Auth) の設定が新しくなりつつあるという話なのですが、若干の新機能もあったので試しておきます。

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

ちなみに基本的な考え方は OpenID Connect に対応した新しい Authentication 周りを Azure Portal からも設定できるようにした、という感じです。ファイルベースは気軽ですが IaC との相性は悪かったです。

App Service Authentication の OpenID Connect 対応については以前書いたので紹介しておきます。

ファイルベースの場合は有効化のために ARM Explorer などで設定を変える必要がありましたが、Azure Portal からの場合は当然ながらそう言った設定は不要なので気楽です。

とりあえず新しい Authentication (Preview) を使ってみました。Authentication (Preview) を有効にすると古い Authentication / Authorization の設定は使えなくなるので注意が必要です。

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

現在の Preview では Microsoft しか Identity Provider を選べないですが、この辺りは今後徐々に増えていくようです。ひっそりと GitHub が増えているのが古い設定との違いです。

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

よく見ると GitHub が Github になっていて若干 typo していました。そのうち直ると思います。

OpenID Connect がグレーアウトされた選択肢にすら入っていないのが気になりましたが、Twitter で Chris が間もなく追加されると教えてくれたので安心です。

API 的には OpenID Connect 対応 Provider は他と違って複数追加できるようになるはずなので、その辺りでちょっと時間がかかりそうな気配はあります。この辺りは Azure AD B2C っぽさもありますね。

既に新しい Authentication (Preview) で使われている API は当然ながら ARM Template からも使えるようになっています。リファレンスも公開されているので、読めばどんなことができるのか想像がつきます。

一先ずは現在利用可能な Microsoft を選んで設定を行っていきます。この Microsoft というのは Azure AD のことを指していますが、古い設定でいうところの MSA っぽいので若干混乱します。

この設定画面から Azure AD アプリケーションを作成できるのはこれまで通りですが、Supported account types に Microsoft account 向けの項目が追加されました。

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

古い Authentication に慣れていたので、この辺り分かりにくいと Twitter で話していたら Chris が背景を説明してくれたのでクリアになりました。

詳細はツイートを読んでもらえばよいですが、今回のアップデートで AAD v2 エンドポイントを使うように変わったので、MSA も Azure AD で同じように扱えるようになったらしいです。

これまでは v1.0 エンドポイントが使われていたので、MSAL と組み合わせる時にいろいろと苦労がありましたが、新しい Authentication (Preview) では解消されそうです。

後の設定は未認証時の動作設定ぐらいですが、これまで 302 でログイン画面にリダイレクトさせるぐらいしかなかったのが、401 や 403 を明示的に返すオプションが増えていました。

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

User-Agent によってこれまで 302 リダイレクトさせるかどうかを判別していたので、Web API を App Service Authentication で認証させる場合には明示的な User-Agent の加工や、X-Request-With をリクエストヘッダーに付ける必要がありましたが不要になります。

最後に Azure AD アプリケーションを新規作成する場合は、Graph API のパーミッションを同時に指定できるようになっていました。この辺りは通常の Azure AD アプリケーションを作成する流れと同じでした。

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

油断すると User.Read のパーミッションが無くなるので気を付けたいところです。というか Azure Portal 側の不具合っぽいのでフィードバックして直してもらおうかと思います。

これで保存すれば Azure AD を使ったログインが有効になるので難しいことはないですね。基本的には UI の変更がメインではありますが、1 点だけ大きな変更点として Client Secret などのセキュアな情報は App Settings に保存されるように変わりました。

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

OpenID Connect を使うときと同じ仕組みに統一されたという話ですが、知っておかないと ARM Template や Terraform などで設定する時にはまりそうです。

最後になりましたが、既存の Authentication の移行には一度設定を全て削除してから、新しい Authentication の画面から再度行う必要があるので若干面倒です。設定が排他になっているので仕方ないですが、作業中は一瞬認証が外れるので注意する必要があります。

Azure Pipelines での .NET Core アプリケーションのビルド時に NU1101 エラーが出るのを回避する

少し前から Azure Pipelines の Windows ワーカーを使っている場合に、以下のような NuGet パッケージの復元時にエラーが出ることが増えてきました。

参照している NuGet パッケージが多いと大量に出るので結構邪魔ですし、マルチステージでビルドしている場合など、後半でエラーになれば最初からやり直しになるためストレスが溜まります。

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

NuGet.org 上にしかないパッケージを何故か Visual Studio のオフラインのキャッシュから取ろうとして、エラーになっているという状態です。挙動としては謎ですが、最初に考えられるのは NuGet.org からパッケージのダウンロードが出来なかったという可能性です。

NuGet パッケージの復元時に出るエラーは NuGet の CDN が調子悪いことが原因なことが多いので、エラーの度に NuGet のステータスページを確認していますが、特に障害は報告されていない時にも発生します。

この問題は Visual Studio がインストールされることで、ローカルキャッシュも構築されることに原因があると考えられるので、Windows のワーカー以外では恐らく発生しないでしょう。

Visual Studio がインストールされている場合は、dotnet nuget list source の結果は 2 つ出てきます。

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

それに比べて Ubuntu 上で確認してみると、公式の NuGet.org しか出てこないです。

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

この差がエラーの原因と思われるので、Windows でもローカルのキャッシュを削除してしまいます。

対応方法は非常にシンプルでソリューションがあるディレクトリで dotnet new nugetconfig を実行して、新しく nuget.config を作成するだけで完了です。デフォルトの設定では以下のように元のソースをクリアしてから NuGet.org を追加するようになっています。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <!--To inherit the global NuGet package sources remove the <clear/> line below -->
    <clear />
    <add key="nuget" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
</configuration>

<clear /> を忘れるとマージされてしまって意味が無いので注意が必要です。

他にも設定項目は用意されているので、興味がある方は以下のドキュメントを参照してください。

この修正を含めて Azure Pipelines でビルドを行うと、Visual Studio が持つキャッシュが使われなくなるため、よく分からない原因の復元エラーが出なくなりました。

常に NuGet.org からオンデマンドで取る割に早いので正常な時は気にもしませんが、CDN 周りで障害が発生した時には何も出来なくなってしまうので、そういった際に備えて Azure Pipelines 側でキャッシュを有効にしておくのもありだと思います。

キャッシュしたからといって常にビルドパフォーマンスが改善するという話ではなく、むしろオーバーヘッドの方が大きいのですが、障害時にも CI を回し続けられるというのはメリットとなるでしょう。

NuGet パッケージの復元に関しては Windows ワーカーの挙動に謎な部分が多いので、これからは常にリポジトリのルートには nuget.config を明示的に作成しておくことになりそうです。

NuGet のロックファイルと CI でのパッケージキャッシュ

基本的に NuGet に関しては CI でのパッケージのキャッシュがあまり効果的ではないのですが、推奨設定での使い方をちゃんと試しておこうと思ったので残します。

GitHub Actions や Azure Pipelines には NuGet 向けのサンプル定義が用意されています。中身はほぼ一緒なのでサンプル自体も非常に似通ったものになっています。

今回は GitHub Actions で試すので actions/cache@v2 の方のサンプルを持ってきましたが、見慣れない packages.lock.json というファイルのハッシュを使ってキャッシュキーを生成しています。

- uses: actions/cache@v2
  with:
    path: ~/.nuget/packages
    key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
    restore-keys: |
      ${{ runner.os }}-nuget-

ぶっちゃけ今回はこの packages.lock.json に関して試すのがメインです。ファイル名の通り NuGet パッケージの依存関係ロックファイルなのですが、使い方や説明を見た記憶があまりありません。

基本的には NuGet 公式ブログの以下の記事が実質的なリファレンスという感じです。

最近のパッケージマネージャではロックファイルで依存関係のバージョンを固定化して、CI などで利用するのが一般的になっているので、それを NuGet でも利用可能にしたという話です。

特に Floating Version を使っている場合には、ビルドのタイミングでバージョンが変わる可能性もあるので、ロックファイルを使って CI ではバージョンを固定しつつも、開発中には自動で最新バージョンを使うという流れは重要になります。

Docs にも一部だけロックファイルについての説明があるので、上の記事と併せて読みましょう。

メリットはブログにもいろいろと書いてありますが、全てのパッケージ依存関係が書き込まれているので、CI での利用を考えるとキャッシュキーとして最適という訳です。ちなみに csproj はパッケージ以外の情報も含まれるため、キャッシュキーとしては若干非効率です。

早速ロックファイルを有効化してみますが、いくつか方法がある内の csproj にプロパティを追加する方法を選びました。単純に RestorePackagesWithLockFile を追加するとパッケージの復元・更新時にロックファイルが生成されるようになります。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

csproj に追加しておくと、これまでと同じようにパッケージの追加や更新の時にロックファイルも自動的に更新されるので、特に意識する必要が無く便利です。

最近の .NET CLI はビルド時にパッケージの復元も自動的に行うので、プロパティを追加してビルドすると以下のようにプロジェクトのルートに packages.lock.json が生成されます。

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

ロックファイルはちゃんとリポジトリに含めてコミットするようにしましょう。Visual Studio 向けの .gitignore には含まれていないので勝手に入るはずですが、忘れないようにします。

実際にロックファイルを使ってパッケージの復元を行うには dotnet restore--locked-mode スイッチを渡すのが簡単です。これは Node.js で言うところの npm ci に相当するコマンドになります。

# restore と build を分ける
dotnet restore --locked-mode
dotnet build --no-restore

# build + MSBuild パラメータを渡す
dotnet build -p:RestoreLockedMode=true

依存関係の再評価をロックファイルを使う場合は行う必要が無いので、復元のパフォーマンスが向上するのではと期待しましたが、自分が試した範囲では特に変化を感じませんでした。インストールされたパッケージの数にも寄るかもしれませんが、少し残念でした。

ロックファイルを有効にすると NuGet の Fallback Folder 周りでエラーが出ることもありますが、その場合は DisableImplicitNuGetFallbackFolder プロパティを追加して Fallback Folder を使わないようにします。

この辺りのコマンドを使い分けるのが面倒な場合は Directory.Build.props を以下のような内容で用意しておくと、csproj への設定も既存の Workflow への変更もなしにロックファイルを有効化できます。

<Project>

  <PropertyGroup>
    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
    <RestoreLockedMode Condition="'$(CI)' == 'true'">true</RestoreLockedMode>
  </PropertyGroup>

</Project>

GitHub Actions 向けになっているので CI 環境変数を見るようにしていますが、TF_BUILD を見るように変えれば Azure Pipelines 向けにも出来ます。

実際に GitHub Actions で NuGet パッケージのキャッシュを試してみましたが、予想通りアプリケーションに左右される部分と Agent に依存する部分が多すぎるので、明確な差は出ませんでした。早い時もあれば遅い時もあるので、大体は外部要因でしょう。

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

もう少し規模の大きいプロジェクトの場合は差が出るかもしれませんが、当然キャッシュの保存と復元にもコストがかかるので、その辺りを上手く吸収できるかは怪しいです。ビルドのパフォーマンス改善という面ではあまり期待できないでしょう。

ただしデフォルトだと常に NuGet からパッケージをダウンロードしているので、NuGet に障害が発生すると CI が通らなくなりますが、パッケージのキャッシュを行うと構成を変えない限りは影響を受けません。

NuGet の障害対策で有効化するのは要件によってはアリだと思います。事前に検証はちゃんとしましょう。

Azure Functions の .NET 5 対応と関係する注意点

.NET 5 がリリースされて少し経ちますが、App Service は Early Access という形ですが .NET 5 への対応が行われたのに対して、Azure Functions は今のところ .NET Core 3.1 までの対応となっています。

少し前から Azure Functions の .NET 5 対応に関して GitHub では悪い意味で盛り上がっているのと、パッケージの更新を行うと動かなくなってしまうケースがあるため、注意喚起のために書きます。起こっていることは以下の Issue から辿れば分かります。

先に軽く概要を書いておくと、Azure Functions は v1 から v3 までのランタイムが存在していて、それぞれが .NET Framework / .NET Core 2.1 / .NET Core 3.1 をターゲットにしています。基本的には LTS 向けにリリースするという方針です。

しかし .NET 5 は LTS ではないため、Function チーム的にはあまり需要は無いと見ていたようです。しかし現実には .NET 5 への要望が非常に多かったため、Node.js などのようにランタイムと切り離す方向で非 LTS バージョン向けにプレビュー版がリリースされたのが最近の話です。

個人的には .NET 5 への対応は割とどうでも良いと思っていて、これまで通り .NET Standard 2.1 に対応したパッケージが使えれば問題ないと考えていましたが、実際には .NET Standard 2.1 対応のパッケージでも動作しないものが出てきたので困っています。

ここからは実際に Azure Functions と .NET 5 周りで発生したトラブルと対処法のまとめです。

.NET Standard 対応なのに使えないパッケージ

最近の .NET / .NET Core では Microsoft.Extensions.* というパッケージが同時にリリースされているのですが、このパッケージの参照するバージョンによっては .NET Standard 2.1 であっても Azure Functions では実行時エラーとなります。

具体的には Microsoft.Extensions.HttpMicrosoft.EntityFramework.Core などのパッケージで発生します。これらは .NET 5 と同時にリリースされたので、参照する Microsoft.Extensions.* のバージョンは同じく 5.0.x になっています。

これらのパッケージは .NET Standard 2.0 や 2.1 をターゲットにしているので、本来であれば .NET Core 3.1 の Azure Functions でも問題なく動作するはずです。

実際に Visual Studio で作成した HttpTrigger のみを持つ Function App に Microsoft.Extensions.Http をインストールして実行すると、以下のようなエラーが発生して起動しません。

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

原因としては Azure Functions は .NET Core 3.1 なので、参照している Microsoft.Extensions.* のバージョンは大体 3.1.x となっていますが、Function App 側は 5.0.x なのでバージョンが合いません。

Function App のライブラリは実行時に AssemblyLoadContext によって読み込まれるので、本来であれば別々のバージョンが読まれても問題はないのですが、ランタイムから Function App へ DI やバインディングでインスタンスを渡すケースがあるため、上手く動かないという話です。

従ってパッケージのバージョンを 3.1.x に下げると問題なく動作します。

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

NuGet からのインストール時にバージョンを明示的に指定しない場合は自動的に最新版がインストールされるため、何気なく Entity Framework Core や HttpClientFactory を使おうとするとはまります。

HttpClientFactory ぐらいであればバージョンを下げれば良いのですが、Entity Framework Core は 5.0 でかなりのアップデートが行われているため、バージョンを下げて対応というのは無理な話です。なのでフィードバックをしていますが反応は微妙です。

依存関係は問題がないのに使えないパッケージ

特に依存関係には問題がないのに、インストールすると実行時エラーになるパッケージも一部存在します。代表例は System.Text.Encoding.CodePages 辺りです。

実際に Function App にインストールして動かしてみると、以下のようにエラーが発生します。

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

これの原因は単純で、最近の Azure Functions SDK はビルド時にランタイムが持っているアセンブリは自動的に削除するようになっているため、ランタイムより新しいバージョンを使う場合に問題となります。

回避にはアセンブリの自動削除を無効化すれば良いので、csproj ファイルに _FunctionsSkipCleanOutput を追加するだけで動くようになります。この辺りは以前書いたエントリを参照してください。

_FunctionsSkipCleanOutput を追加して再度実行してみると今度は問題なく動作しました。

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

バージョンをチェックせずに同名なら一律で消すという処理が、まだ改善されていないことに残念さを感じますが、こっちに関してはひとまず回避方法があるので何とかなります。

.NET 5 対応 Worker はまだまだ発展途上

ランタイムの .NET 5 への対応が LTS ではないので行われない代わりに、Node.js などと同様の仕組みで .NET 5 が使える Worker が去年の末にプレビューとして公開されています。

アーキテクチャ的に Worker はかなりのオーバーヘッドがありますし、.NET 5 でパフォーマンスが改善したとはいっても確実にオーバーヘッド分の方が大きくなるので、個人的に使うメリットはないと感じています。

一応試しては見ましたが使えるトリガーはまだ僅かですし、この Worker を使っても Entity Framework Core 5.0 は使えなかったので完全に見切りました。

事故らないために出来ること

Floating Version を使って指定する

今回の内容を要約すると .NET 5 と同時にリリースされたバージョンの Microsoft.Extensions.* パッケージを参照すると動かなくなるため、既存のパッケージのアップデート時に注意する必要があります。

HttpClientFactory や Entity Framework Core はセキュリティアップデートなどで更新も行われるので、Floating Version を使うことでそもそもアップデートの必要が無くなるので多少回避できます。

バージョンの一部をアスタリスクで指定すると、その部分の最新版が自動的に解決されるので LTS の .NET Core 3.1 向けではあります。csproj を書き換えても良いですし、インストール時に指定も出来ます。

dotnet add package Microsoft.EntityFrameworkCore.SqlServer -v 3.1.*

これでインストールされた Entity Framework Core は 3.1 系の最新版が常に使われるようになります。

Version Range を明確に指定する

Floating Version だけでは Visual Studio から NuGet パッケージの一括更新が行われた場合には、メジャーバージョンを含めた最新版へアップデートされてしまうので、回避するためには利用可能なバージョンを明示的に指定する方法があります。

若干書き方が難しいですが、以下のように書けば Entity Framework Core と HttpClientFactory は 3.1.x の最新版かつ、5.0.0 未満しかインストール出来なくなるため安全です。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="[3.1.*,5.0.0)" />
    <PackageReference Include="Microsoft.Extensions.Http" Version="[3.1.*,5.0.0)" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

Floating Version やバージョンの範囲を限定した場合でも、Visual Studio のソリューションエクスプローラーから、どのバージョンがインストールされたか確認出来ます。

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

.NET Core から .NET への過渡期というのもあり、問題が複雑になっている感はありますが、パッケージのバージョンアップには気を付ける必要が当分はあるという話でした。

Azure Functions SDK の実装が雑なのも混乱を深めている感じです。直るまでフィードバックし続けます。

Managed Identity を使った SQL Database の認証がとても簡単になった話

App Service のドキュメントには Managed Identity を使って SQL Database を利用するサンプルが載っていますが、ここのサンプルコードは結構いい加減で特に .NET Core 向けでは使う気がしないものでした。

サンプルコードがダメなだけで SQL Database の設定周りは特に問題ないので、Managed Identity を使って SQL Database を利用したい場合は、このドキュメントを参考に Azure AD のユーザー作成や管理者の追加などを行ってください。

具体的に何がダメかというと .NET Core の場合は以下のような実装を載せているところです。

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    {
        var connection = (SqlConnection)Database.GetDbConnection();

        // この例だと同期で取るしか方法がない
        connection.AccessToken = new AzureServiceTokenProvider().GetAccessTokenAsync("https://database.windows.net/").Result;
    }

    public DbSet<TodoItem> TodoItems { get; set; }
}

この方法だとローカル開発環境でも Managed Identity が強制されますし、そもそもコンストラクタ内で同期処理でトークン取得してを設定させるのは意味が分からないですね。サンプルとは言えとにかく酷いです。

しかし Microsoft.Data.SqlClient の v2.1.0 からはプロバイダに Managed Identity 対応が追加されたため、上のサンプルのような最悪なコードを書く必要が無くなりました。

最近追加されたのであまり知られていないようですが、ちゃんとドキュメントも用意されています。

同様に Microsoft.Data.SqlClient のリリースノートにも記載されています。

要約すると接続文字列に Authentication=Active Directory Managed Identity を追加するだけで、後は自動的に Managed Identity を使った認証を行ってくれるようになりました。最高ですね。

ちょっと前は App Service や Azure Functions などの System Assigned Managed Identity だと正常に動かなかったようですが、Microsoft.Data.SqlClient v2.1.1 で修正されたとリリースノートに書いてあります。

大きなメリットとして接続文字列を変更するだけで、本番では System Assigned Managed Identity を使いつつも、ローカル開発環境では ID / パスワードや Windows 統合認証でアクセス出来るところにあります。

サンプルコードを書いて試してみましたが、これまでのコードと全く同じで接続文字列のみ違うという状態です。接続文字列の修正だけで Managed Identity が使えるため、移行などの手間が発生しません。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        var connectionString = "Server=***.database.windows.net,1433;Database=***;Authentication=Active Directory Managed Identity;";

        builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(connectionString));
    }
}

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

    public DbSet<TodoItem> TodoItems { get; set; }
}

public class TodoItem
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
}

NuGet パッケージに関しては Entity Framework Core が参照しているバージョンが古いため、明示的に新しいバージョンをインストールする必要はありますが、大した手間ではありません。

おまけ的に Function の実装も載せておきます。単純に DI で AppDbContext を受け取って、LINQ でレコードを取得しているだけなので説明するまでもないですが。

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)
    {
        var todoItems = await _appDbContext.TodoItems.AsNoTracking().Take(10).ToArrayAsync();

        return new OkObjectResult(todoItems);
    }
}

この Function を Managed Identity が有効になっている Azure Function にデプロイして、適当に実行してみると正しくレコードが取得できていることが確認できました。

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

Application Insights で E2E ログを確認してみるとトークンを取得しているのが確認できます。

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

SQL のログは残念ながら出ていませんが、これは Microsoft.Data.SqlClient で EventSource 名が変わったことに Application Insights 側が追従できていないことによります。

最新のプレビュー版 SDK だと修正されているようなので、ASP.NET Core アプリケーションで使う場合は試してみると良いでしょう。Azure Functions では Runtime がアップデートされるのを待つ必要があります。

既存のコードを変更することなく、パッケージのアップデートと接続文字列の変更だけで Managed Identity を使った認証が行えるようになったので、劇的に使い勝手が良くなりました。IaC との相性もかなり良い感じなので、今後は使っていきたいと思います。

GitHub Actions / Azure Pipelines で Pull Request に特定のラベルが付けられた時だけ処理を行う

個人的によく使っていて時々 Pull Request も投げている Durable Functions の開発リポジトリでは、全ての Pull Request に対しては基本的なテストのみ実行し、full-ci というラベルが付いた時のみ全てのテストを実行するようになっています。

実際に以前投げた Pull Request は影響範囲の広い修正だったので、full-ci ラベルが付けられてテストを全て実行し、パスしたのを確認してマージされました。

理想的には全ての Pull Request で全てのテストを実行するべきなのでしょうが、テストに関しては時間的な制限もあって難しいので、この運用は個人的にかなり良い感じだと思っていました。

常に全てのテストを実行する必要がないことは開発中していて気が付きますし、テストに時間がかかってマージやリリースが遅れ始めるとテストが邪魔扱いされかねません。そういう事態を防ぐためにも、レビュアーが全てのテストを実行させるか判断して、ワーカーという有限なリソースを適切に割り振れるのも良いです。

直近で同じようなフローを組み込みたい案件があったため、GitHub Actions と Azure Pipelines の両方で同じ動作を実現する Workflow を書いてみました。結論としては GitHub Actions なら非常に簡単でした。

GitHub Actions

まずは GitHub Actions を使った方法ですが、流石 GitHub という感じで最初からいろいろな情報が取れるようになっていました。PR に特定のラベルが含まれているかの判定は以下の回答の通りに簡単に書けます。

この判定を Workflow 内の if の条件式として渡せば完成です。簡単すぎました。

on:
  pull_request:
    types: [opened, synchronize, labeled]
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2

    - name: Build projects
      run: dotnet build -c Release

    - name: Test projects
      run: dotnet test -c Release
      if: contains(github.event.pull_request.labels.*.name, 'full-ci')

ラベルは自由に設定出来るので、柔軟性が高くて良い感じですね。特に GitHub Actions では Pull Request にラベルが付けられたイベントも用意されているので、本当にラベルを付けるだけで済むのが良いです。

今回はサンプルとして単体テストの実行をラベルの有無で実行するかを判定しています。まずはラベル無しで Pull Request をそのまま作成してみた例です。

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

ラベルが付いていないので単体テストはスキップされているのが確認できます。

この状態で full-ci ラベルを付けると、即座に Workflow が実行されます。ラベルのイベントを拾うようにしているので、Workflow の手動での再実行は必要ありません。

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

結果を見ると意図したとおり単体テストが実行されていることが確認できます。

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

この後にコミットを追加しても、既にラベルが付いているので常に単体テストが実行されるようになります。テストに時間がかかる場合にはレビューのタイミングで付けるのがバランスが良いでしょう。

Azure Pipelines

GitHub Actions の場合は簡単に実現できましたが、Azure Pipelines の場合は Pull Request のラベルは変数で渡される仕組みになっていないので、何とか取得する必要があります。

Azure Repos でも一応 GitHub のようにラベルは付けられるようになっているので、REST API を Pipeline 内で実行して取得するようにします。以下の API で PR に付いているラベル一覧を取得できます。

Azure Pipelines にも GitHub Actions のように API 実行のためのアクセストークンは用意されているので、REST API を実行するのは比較的簡単に行えます。

この辺りの処理は Template を使って分離することにしました。これで再利用性も高まります。

parameters:
- name: name
  type: string
- name: labelName
  type: string

steps:
- powershell: |
    $result = Invoke-RestMethod `
      -Method GET `
      -Uri $(System.CollectionUri)_apis/git/repositories/$(Build.Repository.ID)/pullRequests/$(System.PullRequest.PullRequestId)/labels?api-version=6.0-preview.1 `
      -Headers @{
        "Authorization" = "Bearer $(System.AccessToken)";
      }
    echo "##vso[task.setvariable variable=exists;isoutput=true]$($result.value.name -contains '${{ parameters.labelName }}')"
  name: ${{ parameters.name }}
  displayName: Check ${{ parameters.labelName }} PR label

Azure Pipelines は変数周りが非常に貧弱で配列は使えないため Logging command を使って、パラメータで指定されたラベルが存在したかどうかを変数として返します。

この Template を実際に使う例が以下の Pipeline 定義になります。GitHub Actions の時とは異なり 1 つ処理が増えてしまうのが残念ですが、これで同じ処理が実現できます。

trigger:
- main

pool:
  vmImage: 'ubuntu-latest'

steps:
- script: dotnet build -c Release
  displayName: Build projects

- template: check-pr-label.yml
  parameters:
    name: full_ci
    labelName: 'Full CI'

- script: dotnet test -c Release
  condition: eq(variables['full_ci.exists'], true)
  displayName: Test projects

単体テストを実行するスクリプトの condition で、ラベルが存在したかどうか条件式を指定しています。冗長さは否めませんが色々な方法を検討した結果、この方法に落ち着きました。

同じように Azure Repos でラベルを付けずに Pull Request を作成すると、単体テストはスキップされます。

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

次に Pull Request にラベルを付けるわけですが、REST API ではラベルと呼ばれているのに Pull Request の画面では Tags という扱いになっているため、若干混乱しそうです。

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

GitHub Actions ではラベルを付けると自動で Workflow が実行されましたが、Azure Pipelines にはラベルを付けたというイベントは用意されていないため、手動で Pipeline を再実行するかコミットを追加するしかありません。Pull Request の画面から Re-queue するのが楽です。

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

これでラベルを付けた後に実行される Pipeline では単体テストが常に実行されるようになりました。

Azure Pipelines の場合は GitHub Actions のように実行時間での課金ではなく、ワーカーの数による課金なので 1 つの Pipeline の実行に時間がかかると、他の処理が止まりがちになります。必要なタイミングにだけ動かすのが早く回すコツになりそうです。

Azure App Service と GitHub Actions を使って Pull Request のプレビュー環境を自動で作成する

Azure Static Web Apps がリリースされた時に羨ましかった機能として、Pull Request を作成する度にプレビュー用の環境を自動で作成してくれる、というものがありました。レビュー中に実際のアプリケーションを確認できるのは重要なので、App Service でも欲しいと思っていました。

App Service にも Deployment slot という機能があるので、これを使えばいい感じに Pull Request が作成されたタイミングでデプロイまで出来るのではと考えていたので、そろそろ試してみることにしました。

Pull Request が作成された時に Deployment slot の作成とデプロイを行い、Pull Request が閉じられた時に Deployment slot を削除すれば、理論上は 20 個まで同時にプレビュー環境を作成出来るはずです。

実のところ、同じ仕組みは App Service Team Blog で既に運用されているので、もはや目新しいことでもないのですが、最近リリースされた GitHub Actions の Environments と組み合わせてみたかったのです。

早速 GitHub Actions の Workflow をいろいろと出していくので、まずはベースとなる部分を載せておきます。Pull Request トリガーの設定が良く使われるものより多いです。

デプロイするアプリケーションは ASP.NET Core 3.1 で適当に作ったもので、App Service はこの例では Windows ですが Linux でも同じような考え方で実現できるはずです。

name: Staging

on:
  pull_request:
    types: [opened, synchronize, reopened, closed]
    branches: [ main ]

env:
  DOTNET_VERSION: 3.1.x
  AZURE_WEB_APP_NAME: webapp-deploytest-1
  AZURE_RESOURCE_GROUP_NAME: deploytest-rg

jobs:
  build:
    if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2

    - name: Use .NET Core ${{ env.DOTNET_VERSION }}
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: ${{ env.DOTNET_VERSION }}

    - name: Publish projects
      run: dotnet publish -c Release

    - name: Upload artifact
      uses: actions/upload-artifact@v2
      with:
        name: webapp
        path: WebApplication1/*.zip

ビルドしたアプリケーションは自動的に zip にするように設定しておくと CI / CD を構築する上で便利です。MSBuild の定義を置いておくだけで実現できます。方法は以下のエントリを参照してください。

作成した zip は一旦 Artifact としてアップロードしておきます。今回のように Job が分かれている時には前に Job のビルド結果は保持されないはずなので、アップロードしておかないと後続 Job で使えません。

PR オープン時に Deployment slot の作成とデプロイ

Pull Request のオープン時にはビルドが必要なので、先に build Job を実行して完了後に Deployment slot の作成とデプロイを行うようにします。

Deployment slot の作成には Azure CLI を使います。このコマンドは同じパラメータで複数回実行してもエラーとならないので、こういうケースの際に使い勝手が良いです。

基本的な構成は元々の App Service と同じにしておきたいので --configuration-source を明示的に指定します。Deployment slot の作成は同期処理なので、コマンドが終了した時点でデプロイが可能な状態です。

従って処理としては通常のデプロイに Deployment slot の作成を挟んだだけです。

deploy:
  runs-on: ubuntu-latest
  environment:
    name: staging
    url: https://${{ env.AZURE_WEB_APP_NAME }}-pr-${{ github.event.number }}.azurewebsites.net
  needs: build
  steps:
  - name: Download artifact
    uses: actions/download-artifact@v2
    with:
      name: webapp

  - name: Login to Azure
    uses: azure/login@v1
    with:
      creds: ${{ secrets.AZURE_CREDENTIALS }}

  - name: Create staging slot
    run: az webapp deployment slot create -n ${{ env.AZURE_WEB_APP_NAME }} -g ${{ env.AZURE_RESOURCE_GROUP_NAME }} -s pr-${{ github.event.number }} --configuration-source ${{ env.AZURE_WEB_APP_NAME }}

  - name: Deploy staging
    uses: azure/webapps-deploy@v2
    with:
      app-name: ${{ env.AZURE_WEB_APP_NAME }}
      slot-name: pr-${{ github.event.number }}
      package: ./WebApplication1.zip

Job には environment を指定して、デプロイの履歴を GitHub 側でも確認出来るようにしています。ここで URL を付けておくと、GitHub から簡単に飛べるようになるので結構便利です。

Environments については以前にまとめたので以下のエントリを参照してください。

ここまでの Job で PR が作成されたタイミングでプレビュー用の Deployment slot が作成された後に、アプリケーションがデプロイされて確認出来るようになります。

PR クローズ時に Deployment slot を削除

プレビュー環境は役目を終えた後には削除される必要があるので、PR が閉じられた時に Deployment slot を削除する Job を追加します。削除も Azure CLI で行うだけなので簡単です。

Job の条件だけが変わっていますが、それ以外は普通に Azure CLI を使っているだけです。

delete-slot:
  if: github.event_name == 'pull_request' && github.event.action == 'closed'
  runs-on: ubuntu-latest
  steps:
  - name: Login to Azure
    uses: azure/login@v1
    with:
      creds: ${{ secrets.AZURE_CREDENTIALS }}

  - name: Delete staging slot
    run: az webapp deployment slot delete -n ${{ env.AZURE_WEB_APP_NAME }} -g ${{ env.AZURE_RESOURCE_GROUP_NAME }} -s pr-${{ github.event.number }}

特に説明は必要ないぐらいのシンプルな Job です。これが PR が閉じられた時に実行されます。

動作確認を行う

適当に Pull Request を作成して動作確認をします。Pull Request を作成すると通常通り GitHub Action によってビルドが行われますが、Environments を使っているのでデプロイステータスも同時に表示されます。

以下は実際の例ですが、ステータスチェック以外にもデプロイの情報も表示されています。

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

Environments で URL を同時に指定したので "View Deployment" からプレビュー環境へ飛ぶことが出来ます。

実際にプレビュー環境を表示してみると、Deployment slot が作成されてデプロイまで行われていることが確認できます。ちゃんと Pull Request で行った変更が確認できています。

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

複数の Pull Request を作成すると、それぞれ別の Deployment slot が作成されていることが確認できます。

少し残念だったのが Environments が複数同時にデプロイ済み状態に出来ないので、PR にデプロイステータスが表示されるのは常に 1 つだけという点でした。このままだとプレビュー環境の URL が失われてしまうので、今はコメントに同時に書き込むしかなさそうです。

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

PR をマージなどして閉じると、Deployment slot を削除する Job が実行されます。

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

これで一通りのプレビュー環境を作成する Workflow の作成と検証が出来ました。Environments だけ少し残念な点が残りましたが、それ以外はかなり実用的だなと感じました。

実際の環境に組み込んでフローを作る場合は、プレビュー環境はどのアプリケーション設定を参照するべきなのか考慮する必要があります。本番とステージングそれぞれとの関係も整理した方が良いです。

よく使う Resource Provider を Azure サブスクリプションに一括登録するスクリプトを書いた

新しく作成した Azure サブスクリプションを使うと、たまに特定の Resource Provider が登録されていなくてリソースの作成時にエラーとなることがあります。

Azure Portal からサブスクリプションへの強い権限を持っていれば勝手に登録されますが、Terraform 向けに権限を与える場合にはまることが多いです。Resource Provider が登録されていないサブスクリプションで Terraform で使うと以下のようなエラーが返ってきます。

Error: Error creating Application Insights "***" (Resource Group "***"): insights.ComponentsClient#CreateOrUpdate: Failure responding to request: StatusCode=409 -- Original Error: autorest/azure: Service returned an error. Status=409 Code="MissingSubscriptionRegistration" Message="The subscription is not registered to use namespace 'microsoft.insights'. See https://aka.ms/rps-not-found for how to register subscriptions."

特に調べることもなくエラーコードの MissingSubscriptionRegistration から原因は分かります。一応ドキュメントも用意されていますが、内容としては Resource Provider を登録しろという話しか書いていません。

Azure Portal / Azure PowerShell / Azure CLI それぞれでの Resource Provider 登録方法が載っています。

一番簡単なのは Azure Portal から実行することですが、Resource Provider は数が結構多いのと Resource Manager への知識がないと、どの Resource Provider が対応するのか分からないという問題があります。

ちなみにサブスクリプション単位で紐付くので、Azure Portal から確認する場合はサブスクリプションを選んでください。検索では出てこないので知らないとはまるポイントです。

一般的な Resource Provider ぐらいはデフォルトで登録しておいてほしい気持ちしかないのですが、どうしようもないので少しでも楽するためにスクリプトを書きました。

よく使われるであろう Resource Provider を一括で登録するので、Azure Portal でポチポチやるよりも圧倒的に楽です。大抵のケースではこれで十分だと思われます。

resourceProviders=(\
  "Microsoft.ApiManagement" \
  "Microsoft.AppPlatform" \
  "Microsoft.Authorization" \
  "Microsoft.Automation" \
  "Microsoft.Blueprint" \
  "Microsoft.BotService" \
  "Microsoft.Cache" \
  "Microsoft.Cdn" \
  "Microsoft.CognitiveServices" \
  "Microsoft.Compute" \
  "Microsoft.ContainerInstance" \
  "Microsoft.ContainerRegistry" \
  "Microsoft.ContainerService" \
  "Microsoft.CostManagement" \
  "Microsoft.CustomProviders" \
  "Microsoft.Databricks" \
  "Microsoft.DataLakeAnalytics" \
  "Microsoft.DataLakeStore" \
  "Microsoft.DataMigration" \
  "Microsoft.DBforMariaDB" \
  "Microsoft.DBforMySQL" \
  "Microsoft.DBforPostgreSQL" \
  "Microsoft.DesktopVirtualization" \
  "Microsoft.Devices" \
  "Microsoft.DevSpaces" \
  "Microsoft.DevTestLab" \
  "Microsoft.DocumentDB" \
  "Microsoft.EventGrid" \
  "Microsoft.EventHub" \
  "Microsoft.HDInsight" \
  "Microsoft.HealthcareApis" \
  "Microsoft.KeyVault" \
  "Microsoft.Kusto" \
  "microsoft.insights" \
  "Microsoft.Logic" \
  "Microsoft.MachineLearningServices" \
  "Microsoft.Maintenance" \
  "Microsoft.ManagedIdentity" \
  "Microsoft.ManagedServices" \
  "Microsoft.Management" \
  "Microsoft.Maps" \
  "Microsoft.MarketplaceOrdering" \
  "Microsoft.Media" \
  "Microsoft.MixedReality" \
  "Microsoft.Network" \
  "Microsoft.NotificationHubs" \
  "Microsoft.OperationalInsights" \
  "Microsoft.OperationsManagement" \
  "Microsoft.PowerBIDedicated" \
  "Microsoft.Relay" \
  "Microsoft.RecoveryServices" \
  "Microsoft.Resources" \
  "Microsoft.Search" \
  "Microsoft.Security" \
  "Microsoft.SecurityInsights" \
  "Microsoft.ServiceBus" \
  "Microsoft.ServiceFabric" \
  "Microsoft.ServiceFabricMesh" \
  "Microsoft.Sql" \
  "Microsoft.Storage" \
  "Microsoft.StreamAnalytics" \
  "Microsoft.TimeSeriesInsights" \
  "Microsoft.Web" \
)

for provider in ${resourceProviders[@]}; do
  az provider register --namespace $provider
done

# Provider の登録状況を確認したい場合に使う
#for provider in ${resourceProviders[@]}; do
#  az provider show --namespace $provider --query "{namespace: namespace, registrationState: registrationState}"
#done

Cloud Shell にコピペして実行すると、現在のサブスクリプションに対して Resource Provider を登録します。もしくは以下の 1 行を Cloud Shell にコピペしても同じように実行できます。

curl -s https://gist.githubusercontent.com/shibayan/e37307ba05a91cac884872505a4625f6/raw/resource-providers.sh | bash

1 つのテナントにサブスクリプションが複数ある場合は az account listaz account set を使って対象を切り替えてください。Azure CLI を使っているので az を使う必要があります。

実際に上のスクリプトを実行すると、未登録のものに関してはメッセージが出力されるので、どの Resource Provider が今回追加されたのか分かるようになっています。

今度は Resource Provider がほぼ登録されているサブスクリプションを使って登録状況の確認側を実行してみると、以下のように Resource Provider 名と登録状況が出力されます。

今回の Resource Provider 一覧は Terraform が暗黙的に登録しているものを拾ってきました。こういう事情があるので Terraform と組み合わせる限りは問題にならないはずです。

サブスクリプションへの強い権限があれば Terraform を実行するだけで、上のスクリプトと同じ効果が得られますが、現実的にはリソースグループへの権限だけ割り当てることも多いかと思います。

しかしリソースグループへの権限だけを割り当てている場合は Resource Provider の登録周りでエラーになってしまうので、利用する場合は skip_provider_registration = true を追加する必要があります。

それなりにはまりどころはありますが、最小限の権限だけを与えるためには必要な作業です。Resource Manager は結構よい仕組みだと思っていますが、いかんせん上手く運用できていない感が若干します。