しばやん雑記

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

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

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

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

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

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

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

Linux をデプロイする

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

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

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

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

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

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

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

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

Windows をデプロイする

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

用意されているツール

Diagnose and solve problems を使う

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

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

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

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

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

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

Support Tools を使う

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

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

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

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

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

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

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

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

Kudu を使う

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

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

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

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

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

Application Insights を使う

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

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

https://azure.microsoft.com/en-us/blog/application-insights-profiler/

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

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

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

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

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

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

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

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

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

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

https://azure.microsoft.com/en-us/blog/remote-profiling-support-in-azure-app-service/

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

メモリダンプを取得する

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

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

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

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

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

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

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

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

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

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

Auto Heal を有効にする

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

# escape=`

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

COPY . .

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


FROM microsoft/aspnet:4.7.2

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

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

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

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

ACR Build でイメージを作成

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

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

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

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

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

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

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

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

Web App for Containers にデプロイ

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

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

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

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

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

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

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

Azure App Service の Windows Containers 対応が Public Preview になったので試した

前からちょいちょいと情報が出ていた Web App for Containers の Windows 対応が Public Preview としてリリースされました。これで App Service は Windows / Linux で全て出そろった形になります。

基本的な情報は公式ブログと App Service Team のブログを読めば良いです。GAC や GDI など制限なしに扱えるようになるのがメリットとして挙げられてますが、Server Core というのだけは注意ですね。

https://azure.microsoft.com/en-us/blog/announcing-the-public-preview-of-windows-container-support-in-azure-app-service/

対応しているイメージは LTSC 2016 限定なので、開発環境が 1709 や 1803 の場合は latest ではなく明示的にタグでバージョンを指定する必要があるので注意。

恐らくは本番は Windows Server 2019 がリリースされた時だと思っています。Semi-Annual Channel は使いにくいので、2019 になったタイミングで様々な恩恵を得られるはずです。

とりあえず実際に App Service を作成して試します。Web App for Containers の作成を行うと、新しく Linux と Windows が選べるようになっています。

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

公式ブログにあったように App Service Plan は Windows Containers 用に Premium Container というプランが新しく追加されています。Premium V2 が Dv2 系だったのに対して、Premium Container は Dv3 です。

Premium V2 よりもメモリが倍以上になっています。Windows Containers はメモリを食うので仕方ないですが、裏側が Hyper-V Containers になっている気もします。

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

気になる価格ですが、8 月中は無料になっているみたいなのでまだ公開されてなさそうです。9/13 から 50% 引きのプレビュー価格が始まるようです。

コンテナイメージの設定は Linux と同じです。ACR / Docker Hub / その他が選べます。

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

CI/CD の場合は例によって Docker Image だけ入れ替えれば良いので楽です。

設定時に対応していないベースイメージかどうかの検証を行ってくれるので、間違って 1709 や 1803 のイメージを設定するような事故は起こりにくそうです。

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

設定完了後、デプロイ自体は 20 秒ぐらいで完了しますが、それからコンテナが立ち上がってページが見えるようになるまで数分かかります。地味に時間がかかるのが罠です。

コンテナの起動中は以下のようなページが表示されるので、本番で運用する場合は別スロットを使ってスワップが必要になりそうです。

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

起動処理は Docker Image の pull も行っているので、キャッシュが効くイメージを使うと改善します。

数分待つとアプリケーションが立ち上がります。普通の ASP.NET アプリケーションが動きました。

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

Docker Image を作るのは AppVeyor や VSTS を使って行えば、簡単に CI/CD が組めるのはメリットです。規模が大きくなって AKS に移行する場合も、同じ Docker Image を使えるので楽です。

ちなみに Kudu はちゃんと生きています。Debug Console を使うと Docker のログを簡単に確認できるので便利です。Log stream は開いておかないと見れないですが、こっちはいつでも確認できます。

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

ただし Process Explorer や Site Extension はほぼ無意味なので、使うことは少ないでしょう。

Windows Containers な Web App for Containers は最初から Deployment Slot が使えます。

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

Linux の時と同様に、デプロイに関しては Deployment Slot を使うのはほぼ必須でしょう。

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

スワップを実行すればスロットが入れ替わってくれますが、今の実装ではスワップ実行時に差分が無くても必ずコンテナが再起動してしまうようで、全く持ってメリットがありません。

普通は再起動しないので、今後改善されるとは思いますが、現時点では本番向けには使えませんね。

Linux のように SSH は使えませんが、代わりに Win-RM を使って実行中のコンテナに入って操作することが出来ます。On にすると PowerShell を使ってログインできます。

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

実行中のコンテナにログインする必要をほぼ感じないので、正直使うことはないかなと思っています。むしろ使ったら負けだと思っています。

通常の App Service では実行できなかったアプリケーションも、Windows Containers を使うと制約がほぼ無くなるので移行パスとして良い選択肢になるかと思います。AKS は流石に大げさというケースも多いはず。

Azure Functions と Application Insights を同時に ARM Template でデプロイする

今の Azure Functions は Application Insights が無いとモニタリング周りが成り立たないですが、ARM Template を使うと単独でデプロイされるので同時に Application Insights を作るようにします。

Azure Functions の Consumption Plan で作る場合のサンプルは用意されているので、これをベースに Application Insights のデプロイを追加します。

ARM Template の apiVersion を新しいバージョンに変更すると作成時にエラーになったりするので、非常に扱いにくい項目です。新しいバージョンを選べないのは謎ですね。

まずは Application Insights を作成してからじゃないと Instrumentation Key が取得できないので、リソースを作成する順番も重要です。実際の定義を順に見ていきます。

Application Insights のリソース定義

作成に必要な項目は名前ぐらいなので、非常にシンプルです。大体は App Service と同じ名前でリソースを用意すると思うので、同名で作るようにしておきます。

{
  "type": "Microsoft.Insights/components",
  "name": "[variables('functionAppName')]",
  "apiVersion": "2014-04-01",
  "location": "[resourceGroup().location]",
  "tags": {
    "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', variables('functionAppName'))]": "Resource"
  },
  "properties": {
    "applicationId": "[variables('functionAppName')]"
  }
}

apiVersion によっては項目が変わっているみたいですが、ひとまず動作したものを紹介しておきます。

Azure Functions のリソース定義

上で作成した Application Insights を使う側になる Azure Functions のリソース定義です。Function Runtime のバージョンは v2 を指定してあるので、必要であれば変更してください。

{
  "type": "Microsoft.Web/sites",
  "name": "[variables('functionAppName')]",
  "apiVersion": "2016-03-01",
  "location": "[resourceGroup().location]",
  "kind": "functionapp",
  "dependsOn": [
    "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
    "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]",
    "[resourceId('Microsoft.Insights/components', variables('functionAppName'))]"
  ],
  "properties": {
    "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
    "siteConfig": {
      "appSettings": [
        {
          "name": "APPINSIGHTS_INSTRUMENTATIONKEY",
          "value": "[reference(resourceId('Microsoft.Insights/components', variables('functionAppName')), '2015-05-01').InstrumentationKey]"
        },
        {
          "name": "AzureWebJobsDashboard",
          "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountId'),'2015-05-01-preview').key1)]"
        },
        {
          "name": "AzureWebJobsStorage",
          "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountId'),'2015-05-01-preview').key1)]"
        },
        {
          "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
          "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountId'),'2015-05-01-preview').key1)]"
        },
        {
          "name": "WEBSITE_CONTENTSHARE",
          "value": "[toLower(variables('functionAppName'))]"
        },
        {
          "name": "FUNCTIONS_EXTENSION_VERSION",
          "value": "beta"
        }
      ],
      "clientAffinityEnabled": false
    }
  }
}

重要なポイントは dependsOnAPPINSIGHTS_INSTRUMENTATIONKEY の定義です。dependsOn を使って Application Insights が作成されるまで待つようにしつつ、App Service 作成タイミングで Instrumentation Key を参照するように書きます。

実際に作成した ARM Template は以下で全体を公開しています。

Azure Portal の Template Deployment へのリンクを貼れば、あっという間に必要なリソースを作成できるボタンが完成するので非常に楽です。

Run-From-Zip を組み合わせるとデプロイも簡単なので最高です。

Azure App Service 向けに Let's Encrypt 周りの自動化を行う Azure Functions を作った

タイトルの通りなんですが、App Service 向けに自動で Let's Encrypt の証明書を更新してくれる Azure Functions を作りました。Durable Functions と ACMESharp Core を使っています。

とりあえずベータリリース的な感じで出してみることにしました。

これまでも Site Extensions と WebJob を使って Let's Encrypt の証明書を更新してくれるものはありましたが、1 サイトに 1 つ仕込む必要があり、結構な確率で失敗することが多かったので不便だと思ってました。サービスプリンシパルを作って設定する必要があったのも面倒でした。

なので、今回の azure-appservice-letsencrypt は以下のような問題を解決するために作りました。

  • 1 つの Azure Function で複数の証明書を更新できるように
  • サービスプリンシパルではなく Managed Service Identity を利用
  • とある API 呼び出しが失敗してもリトライで継続できるように
  • モニタリングが容易に行えるように

Let's Encrypt で証明書を発行するために使う ACME は状態を保持したまま、発行フローを書く必要があったので Durable Functions に最適な場面だと考えていました。

Azure Function をデプロイ

適当に Consumption プランで Azure Functions を作成して、GitHub からクローンしたプロジェクトをデプロイしておきます。*1デプロイすると、Functions が大量に表示されるはずです。

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

そして Application settings から必要なキーを追加していきます。以下のキーを登録する必要があります。

  • LetsEncrypt:Contacts
  • LetsEncrypt:ResourceGroupName
  • LetsEncrypt:SubscriptionId

この中でも LetsEncrypt:ResourceGroupName は将来的には必要なくなりますが、今は Azure Management API 側の不具合で仕方なくリソースグループを貰う必要が出ています。

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

これで基本的な設定は完了です。上で指定したメールアドレスは Let's Encrypt のアカウント用なので、証明書の期限が近くなってくるとメールが届いたりします。

Managed Service Identity を有効化

Azure Function の設定が終わったので、Managed Service Identity を有効化していきます。Platform features から選んで、On にするだけなので簡単です。

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

有効化し終わったら、証明書を更新したい App Service が含まれているリソースグループにて Website Contributor 権限を付与します。この辺りに関しては前に書いた記事があるので、そっちを参照してください。

これで Functions が Managed Service Identity を使って Azure Management API を叩けるようになりました。準備も完了したので、実際に適当な App Service を用意して証明書を発行してみます。

新しく証明書を発行する

手持ちのドメインはほぼ HTTPS 化が完了していたので、テストのために利用価値のないドメインを App Service に追加して証明書の新規発行を試します。

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

新規発行のために AddCertificate_HttpStart という Function を用意しているので、この Function にパラメータを与えて実行すれば証明書の発行とバインドまで行います。

パラメータは JSON で以下のように渡します。

{
    "ResourceGroupName": "RESOURCEGROUP_NAME",
    "SiteName": "APPSERVICE_NAME",
    "Domain": "DOMAIN_NAME"
}

存在しない App Service や追加されていないドメインの場合は、ログを出して処理が終了します。

簡単に実行するためのページなどは用意できていないので、適当なクライアントで上の Function をキックしてあげれば、証明書の発行処理が開始されます。

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

実際に上のリクエストを実行して、20 秒ぐらい待つと App Service に新しく SSL バインドが追加されていることが確認できます。新規の場合は SNI SSL 固定になっています。

Azure Portal はキャッシュが結構行われているので、リロードしないと表示されないことが多いです。

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

実際に HTTPS でアクセスしてみると、ちゃんと Let's Encrypt で発行された証明書が確認できます。

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

既に 4 ドメインの証明書更新をこの Function 1 つだけで行っているので、サイト単位で WebJob を使う方法に比べて非常に簡単になりました。

ちなみに ACME Challenge の応答に仮想アプリケーションを使っているので、未検証ですが Azure Functions に対しても適用出来るはずです。

期限が近い証明書を更新する

デフォルトで RenewCertificates_Timer という Function が 1 日に 1 回動作するようになっています。

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

その時に証明書の発行者名が Let's Encrypt Authority になっていて、期限まで 30 日を切っているものに対して更新を行います。まだ更新に関しては検証が足りていないので、バグが残っている可能性があります。

期限切れの証明書を削除する処理はまだ用意できていないので、今は手動で消す必要がありますが、今後は更新と同様にタイマーなどで削除するように対応予定です。

*1:今は Deploy to Azure Button を使ってもデプロイ出来ます。

バックオーダーを使ってドメインを取得してみた

個人的に App Service の現時点でのランタイム情報とか、プラットフォーム依存の情報を知りたかったので App Service Info というサービスを作っていて、ドメインが欲しかったのですが既に取られていたのでバックオーダーを使った話です。

2 年前から試していて、今月ついに取得できた記念にメモとして残します。ちなみにお名前.com です。

メール通知を設定

お名前.com はドメインを検索して既に埋まっている場合には、利用可能になった時にメールで通知するというサービスを設定できます。

まずは、それを設定するところから始めることになります。ちなみにここまでは無料です。

申請可能メールが届く

通知を設定しても、全然メールが届かなかったのでほぼ存在を忘れかけていましたが、急に以下のようなメールが届きました。一度取得されたドメインというのは、中々開かないという現実ですね。

バックオーダーの申し込み時にクレジットカード決済の設定が必要です。

───────────────────────────────────
■事前予約(バックオーダー)のご案内■
───────────────────────────────────
ドメイン名..................:appservice.info
お申込み期限日..............:2018年7月9日 19:00
───────────────────────────────────

お名前.comをご利用いただき、まことにありがとうございます。

メール通知サービスをお申込みいただいたドメイン名【appservice.info】の
事前予約(バックオーダー)のお申込みが可能になりました事をお知らせいたします。
以下の詳細をご確認ください。

ちなみに費用は 11,060 円でした。新規で .info を取ると数百円なのに対して、バックオーダーを使うと数十倍に跳ね上がってしまいましたが、経験してみたかったので支払うことにしました。

取得に失敗した場合は費用は掛からないので、決済が走った時は成功と考えて良さそうです。

決済完了のメールが届く

申し込みから 1 週間後ぐらいに決済完了のメールが届きました。

───────────────────────────────────
■バックオーダー(gTLD)料金ご請求明細/領収書■
───────────────────────────────────
お名前.comをご利用いただき、まことにありがとうございます。

バックオーダー(gTLD)料金につきまして、ご利用のクレジットカードへの課金が
完了いたしました。下記ご請求明細をご確認ください。

課金されたすなわち取得に成功したということになるっぽいので、後はドメインの管理が出来るようになるのを待つだけですが、ここからが地味に時間かかりました。

バックオーダー完了メールが届く

料金を支払ってから 2 週間ぐらいすると、ようやくバックオーダー完了メールが届きました。時系列が分かりにくそうなので、バックオーダー履歴載せておきます。

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

これで正式にドメインが自分のものとなったわけですが、バックオーダーで取得したドメインは自動的にお名前.com で管理できるようになるわけではなく、別のサービス上で初期設定を行わないといけませんでした。

今回は Network Solutions という会社でしたが、最初は社名的にフィッシングかと思いました。

地味に手続きは手間がかかりましたが、要約するとアカウントの所有権を自分に移す作業が必要でした。確認メールからリンクを踏んだり、パスワードを再発効したりしました。

その後、ようやくドメインの管理画面にログイン出来ました。なので、速攻で Azure DNS に変更します。

Azure DNS で管理

適当に Azure DNS で新しいゾーンを作って、NS を変更するだけなので特に書くこともないですね。

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

管理画面が地味に使いにくかったので、さっさと Azure DNS に移しつつ、ドメイン自体の移管も 60 日後には行ってしまおうかと思っています。

Azure DNS では App Service 用のレコードを追加しました。最近の App Service は A レコードの検証用に TXT を使うようになってるんですね。

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

昔は CNAME で awverify というサブドメインを振る必要がありましたが、TXT の方がこういったドメイン所有者を証明するための用途では筋が良い感じがします。

既に作業済みなので http://appservice.info/ でアクセス可能となっています。HTTPS にはしていないですが、それは後日 Let's Encrypt でサクッと対応する予定です。

Azure Functions v2 で .NET Core (netcoreapp2.0) が使いたい

絶賛プレビュー中の Azure Functions v2 では .NET Standard 2.0 を使って開発が出来ますが、最近は .NET Standard 2.0 では API が不足していて困るケースが出てきました。

具体的には TFM として netcoreapp2.0 となる .NET Core App を使いたいわけです。

プロジェクト作成ダイアログでは、昔は .NET Core でしたが今は .NET Standard と表現が変更された点でも、あまり積極的に対応するつもりはないのかなと思っています。

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

GitHub ではそれなりに Issue が上がっていますが、どうやらあまり動きはなさそうです。

ちなみに Visual Studio から Azure Functions v2 を作成して、単純に TargetFramework を netstandard2.0 から netcoreapp2.0 へ変更するとビルド時にエラーが出るようになります。

エラー内容的には Azure Functions の実行に必要なメタデータを生成する部分で失敗しています。

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

Functions 自体のビルドには成功していますが、ビルド後のアセンブリを読み込んで属性から json を作成する部分で、上手くアセンブリを読めていない状態です。

原因は割と単純で、Visual Studio でビルドを行うと .NET Framework 向けの MSBuild が使われるので、netcoreapp2.0 向けのアセンブリが読み込めないという落ちでした。

なので、この場合は .NET Core 向けの MSBuild すなわち dotnet cli を使うとビルド出来ます。

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

残念ながら netcoreapp2.1 向けにすると dotnet cli を使ってもエラーになってしまうので注意。

以下のコマンドで発行用にビルドが行われるので、適当に呼び出してビルドさせました。

dotnet publish -c Release

生成されたファイルは、とりあえず zip にしてから Kudu を使って直接 wwwroot 以下に配置しました。普通なら CI を使うと思うので、その場合は zipdeploy を使ってあげれば良いと思います。

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

折角なので Durable Functions を追加した状態で netcoreapp2.0 としてビルドしました。元々は .NET Standard 2.0 向けに作られているので、問題なく netcoreapp2.0 でもインストールできます。

Azure Functions のランタイムバージョンは予め 2.0 に変更しておかないと詰むので注意。

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

ランタイム側でもちゃんと Functions として認識されています。読み込みに失敗する場合は大体ランタイムバージョンの設定を忘れているはずです。

適当に Durable Functions のエントリポイントを叩いて、実行結果を取得してみました。

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

テンプレ通りの結果ですが、ちゃんと netcoreapp2.0 向けにビルドした Azure Functions が実行出来ることを確認出来ました。問題点としては Visual Studio を使って開発できないことですね。

現状としては Visual Studio Code や Azure Functions CLI などを使うしかなさそうです。

今回の問題は .NET Framework 向け MSBuild 自身が netcoreapp2.0 向けのアセンブリを読み込もうとしているのが原因なので、メタデータの生成部分をアウトプロセスに出来れば解決しそうです。

ちなみに macOS / Linux 上で開発している場合には、最初から特に問題なく netcoreapp2.0 としてビルド出来たかも知れませんね。

追記 - Visual Studio でもビルド可能

アウトプロセスに出来れば解決しそうだと思って Azure Functions SDK のソースを適当に読んでいたら、既に設定を行えばアウトプロセスで動くようになっていました。

なので、Visual Studio でも csproj に UseNETCoreGenerator を追加すればビルド出来ます。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <AzureFunctionsVersion>v2</AzureFunctionsVersion>
    <!-- これを追加 -->
    <UseNETCoreGenerator>true</UseNETCoreGenerator>
  </PropertyGroup>
</Project>

Issue も上がってましたが、完全に見落としていました。まだ解決していない扱いみたいですが。

これで Visual Studio 上でも netcoreapp2.0 が使えて幸せになれそうですが、今のところはデバッグ実行しようとするとエラーになってしまうので、中々厳しいですね。

残念ながら Generator は netcoreapp2.0 としてビルドされているので、例によって netcoreapp2.1 としてビルドしたアセンブリは読み込むことが出来ないです。

Managed Service Identity を利用して Kudu に用意された REST API を実行してみる

あまり弄ってなかったのですが、Managed Service Identity を使うと ARM の API とかいろいろと使いやすくなって素晴らしいという話をぶちぞう RD のブログで読んでました。

そういえば、ちょっと前に GA になったみたいなので安心して使えるようになりました。

Managed Service Identity (Preview) | ブチザッキ

ARM を使うと App Service のいろんな設定を変更したり、新しくリソースを作ったりも出来ますが Kudu に用意されている REST API は直接扱うことは出来ないみたいでした。

Kudu の REST API は結構な数が用意されていて、ARM の API と組み合わせることで何でも出来るようになるので夢が膨らみます。

ARM では実行できないファイルシステム系の API も用意されていて使ってみたかったので、Managed Service Identity で取得した認証情報を使って叩けるようにしました。

とりあえず Azure Functions と Web App を作成して、Azure Functions 側の Managed Service Identity を有効化するところから始めます。

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

有効化すると Access control から権限を割り当てることが出来るようになります。この辺りは UI が MSI 向けに最適化されていて非常に楽でした。

今回は Web App 向けの API を叩きたいので、Website Contributor を割り当てます。

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

これで Azure Functions から MSI を使って ARM の API を叩けるようになりました。

Kudu の API を叩くためには 2 種類の認証情報が用意されていますが、Site Credentials に関しては ARM を使って取得できるので、それを利用して Kudu の API を利用します。

Deployment credentials · projectkudu/kudu Wiki · GitHub

今回はサンプルとして VFS API を利用して、Azure Functions からファイルを作成してみます。

実際に用意した Azure Functions のコードは以下のようになります。やっていることは極めて単純です。

public static class Function1
{
    [FunctionName("Function1")]
    public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequest req, TraceWriter log)
    {
        var subscriptionId = "***";
        var resourceGroupName = "***";

        // ARM 向けに AccessToken を取得する
        var tokenProvider = new AzureServiceTokenProvider();

        var accessToken = await tokenProvider.GetAccessTokenAsync("https://management.azure.com/");

        var websiteClient = new WebSiteManagementClient(new TokenCredentials(accessToken))
        {
            SubscriptionId = subscriptionId
        };

        // Site credentials を取得する
        var credentials = await websiteClient.WebApps.ListPublishingCredentialsAsync(resourceGroupName, "kudu-api-test");

        // Site credentials を使って Kudu REST API を利用する
        var httpClient = new HttpClient();

        httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{credentials.PublishingUserName}:{credentials.PublishingPassword}")));

        // VFS API を使ってファイルを生成してみる
        await httpClient.PutAsync("https://kudu-api-test.scm.azurewebsites.net/api/vfs/site/wwwroot/samplefile.txt", new StringContent(DateTime.Now.ToString()));

        return new OkObjectResult($"OK");
    }
}

AzureServiceTokenProvider を使って AccessToken が取れてしまえば、後は対象の Web App の Site credentials を取得して Kudu REST API を叩くだけです。API に関しては Wiki にまとまっています。

作成した Function を実行すると、Web App に新しくファイルが作成されているはずです。

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

VFS API 以外にも Zip Deploy やコマンド実行など、割と何でも出来るようになるので使い勝手がありそうです。ただし、くれぐれも認証情報の扱いには注意したいところです。

Managed Service Identity のおかげで、ようやく ARM API を使ってみようという気力が湧いてきました。