しばやん雑記

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

Azure App Service の Private Link サポートを一通り試した

ちょっと前に SQL Database や Cosmos DB などの Private Link サポートが一気に GA になったタイミングで、新たに App Service の Private Link サポートが Preview になりました。

発表の数日前から App Service の Networking 設定に Private Link が増えていたので、先取りして試してましたが 500 エラーが出て上手くいかず、そのまま放置していたのでした。

Preview 発表後も上手くいかないので怪しんでいましたが、今のところは East US と West US 2 でのみ試せるようです。他のリージョンでも使えるようになるにはしばらくかかりそうです。

ドキュメントが追加になっているので読んでおくと良いです。特に難しいことは書いていなくて、VNET にデプロイした VM から Private Link 経由で App Service にアクセスしているだけです。

実際にデプロイして試してみると、ちょいちょい課題が出てきたので都度潰しつつまとめました。詳しくは後述しますが、App Service から Private Link 経由で他のサービスを使えないのが割と致命的です。

Private Link (Private Endpoints) を作成する

とりあえず適当に VNET と App Service を作成して、Private Link を追加してみました。App Service の Networking 設定から追加できますが、ここから追加すると Private DNS Zone が作成されなかったので、Private Link Center から追加した方が楽です。

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

Private Link Center から Private Endpoints を選択して項目を入力していくと、Private DNS Zone を作成する設定が出てくるので On にして進めます。On にしておくと必要な A レコードも作成してくれます。

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

後から手動で Private DNS Zone を作成して、必要な A レコードを追加して、VNET にリンクすれば同じですが地味に手間です。ただし ARM Template や Terraform を使う場合には手動で書く必要がありそうでした。

Private Endpoints のデプロイが完了すると以下のような状態になります。

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

他の Private Link 対応サービスと基本的には同じですが VNET 内には Network Interface が追加されて、そいつに Private IP が割り振られています。そこから先は Private Link でいい感じに各 Azure Resources に直接繋がるようになっているみたいです。

App Service の場合は公開側と SCM 側で 2 つホスト名が振られるようになっているので、Private Endpoints でもそれぞれのホスト名が同じ Private IP を指すように設定されます。

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

ここまで完了すれば、VNET に別途 VM をデプロイすれば簡単に動作が確認できます。

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

App Service のホスト名ですが Private IP でアクセスされていることが確認できます。もちろん VNET 外から同じアドレスにアクセスすると 403 エラーが返ってきます。

Service Endpoints との違いが判らなくなりそうですが、Service Endpoints は Public IP を使ってアクセスされるのに対して、Private Endpoints では Private IP でアクセスされているというのが特徴です。これまでも Azure Services へのアクセスに関しては、ルーティングによってインターネットには出ないと言われてましたが、Private Link ではその辺りの挙動が明確になっています。

Private IP でのアクセスになるので、NSG などで Global IP へのアクセスを全て遮断も出来ますね。

Regional VNET Integration は Private DNS Zone に非対応

今回確認用に VM を使いましたが、本来なら App Service / Azure Functions から利用したい人が多いはずです。しかし現在 Regional VNET Integration では Private DNS Zone を利用した解決が行えないため、Private Link を使ったアクセスが実質不可能です。

試しに VNET に入れてある App Service からホスト名の解決を行いましたが、Global IP が返ってきます。

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

GA で新しく追加された WEBSITE_VNET_ROUTE_ALL オプションを使っても結果は同じです。古い Gateway が必要な VNET Integration の場合は動作するようですが、そっちは使いたくありません。

もちろんこの問題は認識されているので対応されるはずですが、試してみたかったので裏技っぽい方法で動作を確認してみることにしました。Private DNS Zone が見えないだけでネットワーク的には問題ないので、確認自体は比較的簡単です。まずはサクッと curl で試しました。

curl https://privatelink-webapp.azurewebsites.net/ -s --resolv privatelink-webapp.azurewebsites.net:443:10.0.0.4 --dump-header -

--resolv オプションを使うと hosts ファイルに追加したのと同じ効果が得られるようです。

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

問題なく HTTPS で App Service にアクセス出来ています。単純に Host ヘッダーを追加する方法の場合は、SNI を使っている大半の App Service では TLS 接続を確立できないためエラーになります。

ただし .NET Core の HttpClient の場合は TLS 接続の確立時に Host ヘッダーの値を見る実装になっているので、以下のようなコードでアクセスが可能です。

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

        var request = new HttpRequestMessage(HttpMethod.Get, "https://10.0.0.4/");

        request.Headers.Host = "privatelink-webapp.azurewebsites.net";

        var response = await httpClient.SendAsync(request);
        var content = await response.Content.ReadAsStringAsync();

        Console.WriteLine(content);
    }
}

なのでちょっとコードを書けば対応は出来ますが、Regional VNET Integration が Private DNS Zone に対応すれば済む話なので今回のように動作確認ぐらいで留めておくべきでしょう。

Private Link 追加後のデプロイ方法

今回 App Service の Private Link サポートを試して気になったのが、SCM 側も Private Link の対象になるので Visual Studio や Azure Pipelines がデプロイに使っている API に VNET 外からアクセス出来ないことです。

試しに Visual Studio からデプロイしようとすると、403 エラーが返ってきて失敗します。

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

恐らくは FTP / FTPS や古いエンドポイントを使った MS Deploy といった古典的な方法なら動作する可能性はありますが、2020 年に使うようなものではないので、もっとモダンな方法で解決する必要があります。

同一 VNET 内に Self-hosted Agent をデプロイすれば解決する話でしょうが、それをメンテナンスするといった作業をしたくないのでもっと良い方法が必要です。

Run From Package で URL を指定してデプロイ

最初に思いついたのは Run From Package でも URL を指定してデプロイする方法です。

WEBSITE_RUN_FROM_PACKAGE に Service Endpoints で保護した Blob Storage への URL をセットすることで、外部からのパッケージ保存とデプロイを実現します。

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

良くサンプルやドキュメントで見る Run From Package では SAS を使ってエンドポイントを保護していましたが、Regional VNET Integration と Service Endpoints を組み合わせているので SAS は不要です。

設定を保存して App Service が再起動されると、指定した URL のアプリケーションがデプロイされます。

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

Run From Package の処理は Kudu が担っているので、Regional VNET Integration 経由で保護された Blob Storage にアクセスできるという仕組みでした。

この方法はこれまでのように Zip をプッシュするのではなく、App Service が Zip をプルしに行くのが特徴です。再起動の度に Zip をダウンロードしに行くので、サイズが大きい場合にはオーバーヘッドが乗っかってくるのと、コールドスタートのパフォーマンスに影響が出ます。

Azure Resource Manager ベースでのデプロイ

もう一つは Azure Resource Manager に用意されている Zip Deploy の API を使ってデプロイする方法です。

以前に ARM Template で Zip Deploy を実現する時に使った API と同じものを使いますが、これもまたドキュメントには書いていない API になります。Private Link の GA 前には提供してほしいところです。

リクエストは非常にシンプルで、以下のようなペイロードを用意して PUT で投げるだけです。

{
  "properties": {
    "packageUri": "https://appservicezipdeploy.blob.core.windows.net/deployment/WebApplication60.zip"
  }
}

Kudu の Zip Deploy API とは異なり URL で Zip を指定しないといけないので、Service Endpoints で保護された Blob Storage にアップロードしておくのが無難です。

Azure CLI の REST コマンドがこういった非公開 API を試すときに便利です。以下のようなコマンドで ARM API 経由での Zip Deploy が実行できます。

az rest -m PUT -u "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/PrivateLink-RG/providers/Microsoft.Web/sites/privatelink-webapp/extensions/ZipDeploy?api-version=2019-08-01" -b @body.json

実行に成功するとほぼ空っぽのレスポンスが返ってきますが、裏では非同期でデプロイが行われています。

デプロイのログは ARM Explorer などで確認できるようになっています。

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

デプロイが完了すると、ちゃんとアプリケーションが動作していることが確認できます。

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

この ARM の Zip Deploy API は裏で Kudu の API を呼んでいるだけなので、挙動自体は全く同じです。なので WEBSITE_RUN_FROM_PACKAGE1 を設定しておくと、Run From Package として動作します。

現状、このような動作をする Azure Pipelines の Task は存在しないので、コマンドで処理をガリガリ書く必要がありそうです。実際のところ、どのようにデプロイされるのを想定していたのか気になります。