しばやん雑記

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

Azure Web Apps 上で動作するアプリケーションのタイムゾーンを設定できるようになっていた

エラー調査のために Kudu 周りを調べていたら、Wiki に Web Apps のタイムゾーン設定が追加されていたのに気が付いたので、実際に設定して試してみました。

Kudu の Wiki はちょいちょい更新されているので、ちゃんと確認しておかないといけないですね。

Configurable settings · projectkudu/kudu Wiki · GitHub

基本的に Azure のサービスは UTC がデフォルトの設定になっていて、Cloud Services では Startup Task などを使ったタイムゾーンの変更が非推奨となっていました。

But I would strongly advice against it. The Fabric Controller maintains operating system time synchronization for the system when roles are first booted. If you utilize administrative access startup scripts to change the localization settings (including local server time), but it is not recommended you do so. Doing so will introduce instability and the fabric controller may determine your role is out of sync. You will likely end up cycling your roles as fabric attempts to bring them to goal state in such a case. A better strategy is to write your applications to be aware that they actually run with UTC and default US CultureInfo settings.

http://blogs.msdn.com/b/cie/archive/2013/07/29/manage-timezone-for-applications-on-windows-azure.aspx

Azure 界の抱かれたい男 No.1 ことぶちぞうさんも、止めといた方がいいと言ってます。

しかし、Web Apps だけは WEBSITE_TIME_ZONE というキーを追加することで、任意のタイムゾーンで動作させることが出来るようになったみたいです。

WEBSITE_TIME_ZONE に設定する値は、レジストリや TimeZoneInfo から取得出来る文字列です。

日本時間は JST ではなくて Tokyo Standard Time なので、少し注意が必要な感じです。

確認のために DateTime.Now と UtcNow を出力するだけのページを用意しました。

デフォルトは UTC なので、両方とも同じ時間となっています。これが Azure では普通です。

ここでポータルから WEBSITE_TIME_ZONE = Tokyo Standard Time という設定を追加してみます。

設定を保存後、ページを読み込みなおすと DateTime.Now が日本時間に変化していました。

App Service Plan 単位でタイムゾーンが設定されるのかと思ったので、同一ホスティングプラン上で動作しているアプリケーションでも試してみました。

マシン名を表示させているので、同一マシンで動作していることが分かると思います。予想に反してサイト単位でタイムゾーンが設定可能になっているようです。

Kudu のソースコードに出てこないので、カスタム IIS モジュールで変更するようになっているのだと思いますが、特定のプロセスだけタイムゾーンを変更可能なのか、正直よくわかっていません。謎技術です。

古いアプリのマイグレーション用途ぐらいで、乱用はしない方がいい気がします。公式ドキュメントの FAQ にも記載されるようになっているので、普通に使っていっても良いと思います(実際使ってます

何だかんだでタイムゾーンを設定した方が楽なことが多いです。確認した感じでは Consumption での Azure Functions でも問題なく動作しているので、特に不安もないと思っています。

Azure Web Apps へのデプロイ時に Git のコミットハッシュをアセンブリに埋め込む

Web アプリケーションのフッターに、バージョン情報や Git のコミットハッシュが埋め込まれているのを見ます。ちょっと違いますが、Kudu もコミットハッシュが表示されてます。

ASP.NET アプリケーションでも、同じようにバージョンとコミットハッシュを表示させてみたいので、コミットハッシュを埋め込む 2 つの方法を試してみました。

MSBuildTasks を使う

Web Deploy を使って Web Apps へデプロイする場合には、MSBuildTasks を使って埋め込むしか方法がありません。GitVersion タスクでコミットハッシュを取得して、AssemblyInfo.cs を丸ごと作成します。

実際の手順は以下のブログにとても分かりやすく書いてありました。MSBuild 初心者でも分かりやすい。

MSBuild: add git commit hash to AssemblyInfo · now from home

NuGet で MSBuildTasks をインストールし、csproj を編集してビルドすると埋め込まれます。

全く新しい AssemblyInfo.cs が生成されていることが確認できます。

実際に使う場合には .gitignore に AssemblyInfo.cs を入れておかないと、毎回コミット対象になってしまってちょっとめんどくさい感じでした。

コミット後に新しいコミットハッシュが埋め込まれるのでしょうがない感じはします。知らずに放置しておくと、無限にコミット出来てしまいそうですね。

カスタムデプロイスクリプトを使う

カスタムデプロイスクリプト内では SCM_COMMIT_ID という環境変数で、デプロイしようとしているコミットのハッシュが取れるので、これを利用して AssemblyInfo.cs にコミットハッシュを埋め込みます。

デフォルトのバッチファイルだと処理が大変すぎるので、PowerShell で書き直したバージョンを使います。

既に設定されている AssemblyVersion を読み取って、そのバージョンにコミットハッシュを追加したいので、正規表現で AssemblyInfo.cs からバージョンを抽出します。

実際に書いたスクリプトは以下のような感じです。イマイチな感がありますが、動くのでまあ良しとします。

$ASSEMBLY_INFO_PATH = "$DEPLOYMENT_SOURCE\WebApplication7\Properties\AssemblyInfo.cs"
$ASSEMBLY_INFO_CONTENT = Get-Content $ASSEMBLY_INFO_PATH

$ASSEMBLY_VERSION = [regex]::Match($ASSEMBLY_INFO_CONTENT, "AssemblyVersion\(`"(.+?)`"\)").Groups[1].Value
$ASSEMBLY_INFORMATIONAL_VERSION = [string]::Format("[assembly: AssemblyInformationalVersion(`"{0}-{1}`")]", $ASSEMBLY_VERSION, $env:SCM_COMMIT_ID.Substring(0, 7))

Add-Content -Path $ASSEMBLY_INFO_PATH -Value $ASSEMBLY_INFORMATIONAL_VERSION

これを deploy.ps1 の MSBuild 呼び出し前に追加して、Web Apps にプッシュすると埋め込まれます。

Kudu を使って Web Apps 上のリポジトリを確認すると、AssemblyInfo.cs に属性が埋め込まれているのが確認できます。リポジトリ内のファイルを変更しても、Kudu が更新時に面倒みてくれるので大丈夫です。

コミットハッシュ込みのバージョンを取得する

昔はカスタム属性を取得するのがめんどくさかった記憶があるのですが、.NET 4.5 からはジェネリックに対応した拡張メソッドが用意されているみたいで、とてもシンプルに書けるようになっていました。

CustomAttributeExtensions.GetCustomAttribute メソッド (System.Reflection) | Microsoft Learn

この拡張メソッドを使って AssemblyInformationalVersion 属性を取得して値を取り出します。

// 実行中のアセンブリを取得
var assembly = Assembly.GetExecutingAssembly();

// AssemblyInformationalVersion 属性を取得
var assemblyInformationalVersion = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();

// ex. 1.0.0.c33eac3
var informationalVersion = assemblyInformationalVersion.InformationalVersion;

カスタム属性は実行中に変化することがないので、アプリケーションの初期化時に取得しておきましょう。

それっぽくフッターに表示してみました。どのバージョンが Web Apps にデプロイされているか調べるためにはポータルを確認するしかなかったので、さっと確認出来るようになり少し便利になりました。

今回は Git のコミットハッシュでしたが、日付を付けるなど割と自由に出来ます。

Kudu のデプロイスクリプトを PowerShell で書き直してみた

Azure Web Apps に Git でデプロイする場合に作られるデプロイスクリプトは、バッチファイルと bash の 2 種類を選択できますが、正直なところ機能不足感が否めません。

タイトル通りですが deploy.cmd を PowerShell で書き直しました。これで柔軟に処理が書けるはずです。

# ----------------------
# KUDU Deployment Script
# Version: 0.2.2
# ----------------------

# Helpers
# -------

function exitWithMessageOnError($1) {
  if ($? -eq $false) {
    echo "An error has occurred during web site deployment."
    echo $1
    exit 1
  }
}

# Prerequisites
# -------------

# Verify node.js installed
where.exe node 2> $null > $null
exitWithMessageOnError "Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment."

# Setup
# -----

$SCRIPT_DIR = $PSScriptRoot
$ARTIFACTS = "$SCRIPT_DIR\..\artifacts"

$KUDU_SYNC_CMD = $env:KUDU_SYNC_CMD

$DEPLOYMENT_SOURCE = $env:DEPLOYMENT_SOURCE
$DEPLOYMENT_TARGET = $env:DEPLOYMENT_TARGET

$NEXT_MANIFEST_PATH = $env:NEXT_MANIFEST_PATH
$PREVIOUS_MANIFEST_PATH = $env:PREVIOUS_MANIFEST_PATH

if ($DEPLOYMENT_SOURCE -eq $null) {
  $DEPLOYMENT_SOURCE = $SCRIPT_DIR
}

if ($DEPLOYMENT_TARGET -eq $null) {
  $DEPLOYMENT_TARGET = "$ARTIFACTS\wwwroot"
}

if ($NEXT_MANIFEST_PATH -eq $null) {
  $NEXT_MANIFEST_PATH = "$ARTIFACTS\manifest"

  if ($PREVIOUS_MANIFEST_PATH -eq $null) {
    $PREVIOUS_MANIFEST_PATH = $NEXT_MANIFEST_PATH
  }
}

if ($KUDU_SYNC_CMD -eq $null) {
  # Install kudu sync
  echo "Installing Kudu Sync"
  npm install kudusync -g --silent
  exitWithMessageOnError "npm failed"

  # Locally just running "kuduSync" would also work
  $KUDU_SYNC_CMD = "$env:APPDATA\npm\kuduSync.cmd"
}

##################################################################################################################################
# Deployment
# ----------

echo "Handling Basic Web Site deployment."

# 1. KuduSync
if ($env:IN_PLACE_DEPLOYMENT -ne "1") {
  & $KUDU_SYNC_CMD -v 50 -f "$DEPLOYMENT_SOURCE" -t "$DEPLOYMENT_TARGET" -n "$NEXT_MANIFEST_PATH" -p "$PREVIOUS_MANIFEST_PATH" -i ".git;.hg;.deployment;deploy.ps1"
  exitWithMessageOnError "Kudu Sync failed"
}

##################################################################################################################################

# Post deployment stub
if ($env:POST_DEPLOYMENT_ACTION -ne $null) {
  & $env:POST_DEPLOYMENT_ACTION
  exitWithMessageOnError "post deployment action failed"
}

echo "Finished successfully."

相変わらず PowerShell 力は低い感じですが、ひとまず動作するものを作ることが出来ました。基本的な方針として処理の流れをバッチファイル版に合わせてあります。

.deployment ファイルを作成し、PowerShell を使ってスクリプトを実行するコマンドを追加しておきます。

[config]
command = powershell -NoProfile -NoLogo -ExecutionPolicy Unrestricted -File deploy.ps1

これを Web Apps にデプロイすると、これまで通りにデプロイされるのが確認できます。

たまに "Window title cannot be longer than 1023 characters" というエラーが表示されて失敗することがありますが、ポータルから再デプロイを行うと問題なく動きます。

同様のエラーが WebJob を PowerShell で書いた時にも発生しているようです。何となく Web Apps 側の問題なのではないかと思っていますが、原因はよくわかっていません。

上に載せたスクリプトはシンプルな Web サイトをデプロイするものでしたが、ちょっと修正すると ASP.NET アプリケーションのデプロイも行えるようになります。

いちいち手書きするのはとてもめんどくさいので、デプロイスクリプトのジェネレータである KuduScript を弄って PowerShell 版のスクリプトを生成できるようにしました。

https://github.com/shibayan/KuduScript/tree/powershell

git clone してから NPM を使ってインストールすると、ローカルで使えるようになります。具体的なインストール手順を念のために書いておきます。

npm install

npm -g install "kuduscript へのフルパス"

kuduscript -y -t posh -aspWAP WebApplication\WebApplication.csproj -s WebApplication.sln

これで ASP.NET アプリケーションのデプロイ用スクリプトが PowerShell で生成されます。

Azure で静的コンテンツを効率よく配信する手順をまとめた

Azure を使って静的なコンテンツを効率よく配信するために、実行するべき手順を 3 つにまとめました。

普段は高速な回線で確認しているケースが多いと思うので問題ないように見えますが、油断していると携帯回線で悲惨なことになります。shibayan.jp を PageSpeed Insights で調べると以下のようになります。

shibayan.jp は Web Apps 上で動かしているため多少ましですが、スマートフォン向けでキャッシュが有効に使われていない点は見過ごすわけにはいきません。

行うべき作業は以下のような感じです。ぶっちゃけ当たり前という感じですが、忘れがちです。

  1. Blob からファイルを配信する
  2. Cache-Control でファイルの有効期限を設定する
  3. Azure CDN でのファイル圧縮を有効にする

既に何回か書いたかもしれませんが、完全版という感じで書くことにします。

Blob からファイルを配信する

静的なファイルを Blob から配信するのは、可用性とパフォーマンスを考えた場合の基本的な方法だと思います。Azure Cloud Design Pattern で言うところの Static Content Hosting Pattern です。

Static Content Hosting Pattern | Microsoft Learn

Blob は高い可用性とパフォーマンスを持ち、そして安いのでファイルストレージとして最適です。

画像などが大量に含まれるとデプロイに余計な時間がかかるので、小規模なアプリケーション以外では Blob から配信するべきです。特に Web Apps から大量に静的なコンテンツを配信するのは避けるべきです。

Cache-Control でファイルの有効期限を設定する

Azure Blob は明示的に指定しない限り Cache-Control ヘッダーが書き出されないため、変更がない場合には基本的には ETag と Last-Modified を使った 304 レスポンスになるはずです。

304 ではコンテンツが返されないので高速ですが、無駄な通信が発生してしまいます。

Azure Blob Storage の有効期限を管理する - Azure Content Delivery Network | Microsoft Learn

ドキュメント中のサンプルでは、プロパティの値をアップロード後に弄っているので SetProperties を明示的に呼び出す必要がありますが、アップロード前に設定しておけば不要になります。

// 保存するファイル名
var filename = "sample.txt";

var blob = directory.GetBlockBlobReference(filename);

// Cache-Control ヘッダーで有効期限を 1 年に設定する
blob.Properties.CacheControl = "public, max-age=31536000";

// Content-Type を設定しておく
blob.Properties.ContentType = MimeMapping.GetMimeMapping(filename);

blob.UploadText("sample text");

アップロード後に設定されたプロパティを確認すると、Cache-Control が設定されています。

ブラウザでアクセスすると、2 回目以降はキャッシュが使われていることも確認できました。

これで Blob に配置したファイルが、ブラウザで正しくキャッシュされるようになりました。

Azure CDN でのファイル圧縮を有効にする

Blob から配信する場合は圧縮が行われないので、そのままだと CSS や JavaScript といった圧縮が効きやすいファイルで無駄が発生してしまいます。なので Azure CDN を使って解決します。

これまでも Azure CDN を Blob の先に噛ませ、配信のパフォーマンスを改善するといった方法は多く実行されてきたと思いますが、少し前のアップデートで自動的にファイルの圧縮が行えるようになりました。

Azure CDN でのファイル圧縮によるパフォーマンスの向上 | Microsoft Learn

Azure CDN のアップデートについては Azure 界の抱かれたい男 No.1 こと @kosmosebi のブログでも紹介されています。ファイル圧縮以外にも新機能があるので、活用していってください。

Azure Update (2015.08.12) | ブチザッキ

実際にファイル圧縮の設定を行うわけですが、クラシックポータルに CDN 補助ポータルというものが追加されているので、そこからサクッと設定を有効にしてしまいます。

デフォルトで圧縮が有効に働く MIME タイプのみが選択されています。MIME ベースで圧縮の有無を切り替えるので、Blob に正しい Content-Type を設定しておかないと、圧縮が無効になるので注意が必要です。

Minify を行うともう少しサイズを縮小可能ですが、最近は Application Insights などで JavaScript のエラーを収集することもあるので、シグネチャが変わってしまうとめんどくさいことになります。

キャッシュを無視して読み込ませる方法

Azure CDN はキャッシュされたコンテンツのパージが行えないため、配信するファイルの URL を変更したりクエリ文字列を追加して、新しいファイルが読み込まれるように対応する必要があります。

クエリ文字列を使う場合には CDN の設定から有効にしておかないと、クエリ文字列が無視されてキャッシュされたコンテンツが返ってくるので注意が必要です。

おまけ:通信環境をエミュレートする

Chrome の開発者ツールに備わっているネットワークスロットリング機能を使うと、様々な環境でどのくらい読み込みに時間がかかっているのか簡単に確認することができます。

通信速度を絞るだけではなく RTT についてもエミュレートしてくれるので、通信環境が良くない場合にどうなるのかを確認できて便利です。積極的に使っていきたい機能ですね。

Azure Web Apps が Go 1.5 に対応していた件と最近のアップデートについて

たまたま Kudu を見たら、Azure Web Apps のインスタンスがリイメージされていたので調べてみると、リリースされたばかりの Go 1.5 がインストールされていることに気が付きました。

インストールパスの関係でそのままではエラーになりますが、GOROOT を設定するとちゃんと動作します。

ということで、何時も通りデプロイまで行ってみます。使ったコードは以前の記事と同じです。

go ファイルが存在していると Kudu が自動的にデプロイスクリプトを生成してくれますが、デフォルトだと 1.4.2 が自動的に選択されるので、ポータルから GOROOT を予め設定しておきます。

バージョンの選択は将来的には Node.js と同じような形になるのだと思います。アップデートに期待します。

これで Web Apps にデプロイを行うと、Go 1.5 を使ってビルドが行われます。

デプロイが完了すると、Go 1.5 でアプリケーションが動作しました。正直かなり簡単です。

今回はリイメージのタイミングが良かったのか、ランタイムのインストールが早かったですね。

セッションアフィニティの無効化

ARMExplorer を使って調べている時に clientAffinityEnabled という設定を見つけました。名前からして ARR のアフィニティクッキーを書き出すかどうかだと思ったので、実際に確認してみました。

デフォルトでは true になっているので false にして保存しておきます。

この状態でブラウザを使ってアクセスしてみると、ARRAffinity クッキーが書き出されませんでした。

これまでは Arr-Disable-Session-Affinity という HTTP レスポンスヘッダーを書き出す必要がありましたが、設定を切り替えるだけで対応できるようになりました。ちょっと隠し機能っぽいですが。

関係ないですが Kudu だけではなく ARMExplorer にもアイコンが付いてました。

プレビューポータルのアップデート

Web Apps のプレビューポータルは頻繁にアップデートされていますが、ネットワークに関係する設定がわかりやすくなっていたので紹介しておきます。

ネットワーク関係の機能として VNET / Hybrid Connections / ASE VNET の 3 つが Web Apps に備わっていますが、どういったものなのか簡単な説明付きで設定できるようになってました。

現在の状態がタイルで確認できるのもなかなか良いと思いました。

日本でも Azure Web Apps の SSL/TLS 暗号スイートから RC4 が削除されました

6 月に発表されていた Azure Web Apps の SSL/TLS 暗号スイートのアップデートですが、やっと日本でも実施されたようです。US のデータセンターでは少し前にアップデートが実施されていました。

事前に発表された予定より遅れた感はありますが、今日確認すると東日本でも RC4 が暗号スイートから削除されて、SSL Server Test で A 評価が出るようになっていました。

問題のある RC4 が削除されたことで、より安全かつ安心して Web Apps を使うことができますね。

暗号スイートの一覧を確認すると、削除予定だったはずの 3DES は生き残っていました。内部でのテスト中に、何処かで互換性に問題が見つかったのかもしれません。

これまでは優先度に問題がありましたが、アップデートで ECDHE が優先的に使われるようになりました。実はこの対応だけは RC4 の無効とは別で、比較的早いうちに行われていたようです。

Forward Secrecy もモダンブラウザでは対応するようになりました。ECDHE のおかげですね。

残念ながら TLS_FALLBACK_SCSV への対応は Windows 側に依存するため、A+ 評価を出すことは出来ないでしょう。TLS_FALLBACK_SCSV に対応せずに A+ 評価を出すには TLS 1.2 のみにする必要があります。

したがって HTTP Strict Transport Security に対応させても A+ までは行きません。しかし、対応しているブラウザが増えているので、SSL に限定させたい場合には設定しておくのがいいと思います。

JRuby 9.0.0.0 がリリースされたので Azure Web Apps にインストールして Ruby on Rails を動かしてみた

ちょっと前に JRuby 9.0.0.0 がリリースされていました。Ruby 2.2 との互換性があるようです。

http://jruby.org/2015/07/22/jruby-9-0-0-0.html

JRuby を使うと Azure Web Apps でも Ruby on Rails アプリを CRuby な環境よりも安定して動かすことができるので、これまでにも何回か扱ってきました。

そして偶然にも Web Apps もポータル周りがアップデートされて Java 8 を簡単に使うことができるようになったので、前のように JRuby 9.0.0.0 と Java 8 の組み合わせを Web Apps で動かしてみます。

最近のアップデートでポータルから Java のバージョンと Web コンテナの種類も設定可能になりました。

http://azure.microsoft.com/blog/2015/07/30/july-update-for-web-apps-and-app-service-environments-in-the-azure-preview-portal/

これまでは JAVA_HOME を自分でオーバーライドする必要がありましたが、これからはポータルから必要な Java バージョンに切り替えるだけです。

Web コンテナを必ず指定しないといけないのはいまいちなので、使わない設定が欲しいですね。

Web Apps で動かすためのサンプルを以前に GitHub で公開しましたので、今回はそれを修正して JRuby 9.0.0.0 への対応を行いました。Java 8 はポータルからの設定だけで問題ありません。

主な修正点としては JRuby のバージョン指定とパスを修正したぐらいです。

以前は bundle exec を使って実行していたのですが、JRuby の Wiki を見ていると bundle exec と同じことを行ってくれる -G オプションを知ったので、こちらを使うように Procfile を修正しました。

bundle exec
Bundler's "exec" command causes a second JRuby instance to be launched for the sole purpose of booting only your Gemfile gems. You can avoid the second process by passing -G to JRuby, which will do the Bundler pre-booting before starting JRuby and loading RubyGems.

Improving startup time · jruby/jruby Wiki · GitHub

PATH の問題なのか puma を相対パスで指定する必要があったので、その対応だけ行いました。

jruby.exe -G vendor/bundle/bin/puma --env development -p %PORT%

これまでは bundle exec の実行のために JRuby を起動した後、さらに JRuby が起動される形だったため起動パフォーマンスとメモリ消費が激しかったのですが、これで多少改善できました。

やはり初回起動には時間がかかりますが、実際にブラウザでアクセスすると Rails のいつものページが表示されます。ちゃんと JRuby 上で Ruby on Rails が動作することも確認できました。

相変わらずの話になりますが、JRuby をインストールするプロセスに地味に時間がかかるので、プリインストールされるようになればいいなと思います。

実行後に Process Explorer を見てみると、JRuby が 1 つだけ動いていることが確認できます。

JRuby が 1 つだけ起動するようになったので、起動時間も 20 秒ほど短縮できたと思います。

かなり JRuby 9.0.0.0 で互換性が改善されたらしく、Web Apps 上でも実用出来そうな気がしています。

ASP.NET アプリケーションを Azure Web Apps にソース管理からデプロイする時にプリコンパイルさせる方法

Azure Web Apps に ASP.NET アプリケーションを GitHub などからデプロイする場合に、ビューのプリコンパイルを行いたいと思っていましたが、カスタムデプロイスクリプトを書くことで対応できたので紹介します。

やはり同じことを考える人は居たようですが、スクリプト例が見つかりませんでした。

aspnet_compiler.exe について色々書かれていましたが、現行の Web Apps にインストールされているもので問題なくプリコンパイル出来ました。

スクリプトの重要な部分というか、修正が必要な部分を順に解説していきます。

プリコンパイルを行う準備

最初にプリコンパイル前のビルド結果を一時的に格納させるためのディレクトリと、aspnet_compiler.exe のパスを用意しておきます。基本的に存在しないはずなので常に新しく作ります。

SET PRECOMPILE_TEMP=%temp%\___precompileTemp%random%
mkdir "%PRECOMPILE_TEMP%"

SET ASPNET_COMPILER_PATH=%WINDIR%\Microsoft.NET\Framework\v4.0.30319\aspnet_compiler.exe

これでランダムなディレクトリがデプロイごとに作成されます。

MSBuild の出力ディレクトリを変更

通常のデプロイスクリプトでは MSBuild の実行結果を DEPLOYMENT_TEMP に書き出すのですが、プリコンパイルするために今回作成した PRECOMPILE_TEMP に書き出すように変更します。

call :ExecuteCmd "%MSBUILD_PATH%" /p:_PackageTempDir="%PRECOMPILE_TEMP%" %SCM_BUILD_ARGS%

このコマンドは長いの不要な部分は削除しました。オリジナルのスクリプトから _PackageTempDir を指定してる部分を探して、そこを PRECOMPILE_TEMP に設定します。

aspnet_compiler でプリコンパイル

PRECOMPILE_TEMP に MSBuild でビルドされたアプリケーションのファイル一式が入っているので、それをソースにして DEPLOYMENT_TEMP にプリコンパイルした結果を出力します。

call :ExecuteCmd "%ASPNET_COMPILER_PATH%" -v / -p "%PRECOMPILE_TEMP%" -c "%DEPLOYMENT_TEMP%"

仮想ディレクトリを表す -v オプションの値は / のままで基本は良いと思います。DEPLOYMENT_TEMP に結果を出力すると、後は KuduSync が wwwroot に差分をコピーしてくれます。

プリコンパイルに使ったディレクトリを削除

追加したプリコンパイル用のディレクトリは Kudu によって自動的に削除されないので、スクリプトの最後の方で削除するようにしておきます。

IF EXIST "%PRECOMPILE_TEMP%" rd /s /q "%PRECOMPILE_TEMP%"

一応存在チェックを行ってから削除するようにします。

完成したスクリプト例

ここまで紹介したスクリプトを全て組み込んだ例を最後に張り付けておきます。

当然ですが、そのままコピペで使えるというわけではないです。これで処理の全体像を掴んでください。

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Deployment
:: ----------

SET PRECOMPILE_TEMP=%temp%\___precompileTemp%random%
mkdir "%PRECOMPILE_TEMP%"

SET ASPNET_COMPILER_PATH=%WINDIR%\Microsoft.NET\Framework\v4.0.30319\aspnet_compiler.exe

echo Handling .NET Web Application deployment.

:: 1. Restore NuGet packages
IF /I "AspNet.sln" NEQ "" (
  call :ExecuteCmd nuget restore "%DEPLOYMENT_SOURCE%\AspNet.sln"
  IF !ERRORLEVEL! NEQ 0 goto error
)

:: 2. Build to the temporary path
IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" (
  call :ExecuteCmd "%MSBUILD_PATH%" "%DEPLOYMENT_SOURCE%\AspNet\AspNet.csproj" /nologo /verbosity:m /t:Build /t:pipelinePreDeployCopyAllFilesToOneFolder /p:_PackageTempDir="%PRECOMPILE_TEMP%";AutoParameterizationWebConfigConnectionStrings=false;Configuration=Release /p:SolutionDir="%DEPLOYMENT_SOURCE%\.\\" %SCM_BUILD_ARGS%
) ELSE (
  call :ExecuteCmd "%MSBUILD_PATH%" "%DEPLOYMENT_SOURCE%\AspNet\AspNet.csproj" /nologo /verbosity:m /t:Build /p:AutoParameterizationWebConfigConnectionStrings=false;Configuration=Release /p:SolutionDir="%DEPLOYMENT_SOURCE%\.\\" %SCM_BUILD_ARGS%
)

IF !ERRORLEVEL! NEQ 0 goto error

:: 3. Precompile Razor
IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" (
  call :ExecuteCmd "%ASPNET_COMPILER_PATH%" -v / -p "%PRECOMPILE_TEMP%" -c "%DEPLOYMENT_TEMP%"
  IF !ERRORLEVEL! NEQ 0 goto error
)

:: 4. KuduSync
IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" (
  call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_TEMP%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd"
  IF !ERRORLEVEL! NEQ 0 goto error
)

:: 5. Clean Precompile Dir
IF EXIST "%PRECOMPILE_TEMP%" rd /s /q "%PRECOMPILE_TEMP%"

デプロイスクリプトのひな形は Azure CLI を使うと簡単に作成できます。

Azure Web Apps (Websites) - Custom Deployment Scripts Generator

実際にこのスクリプトを組み込んだアプリケーションを Web Apps へ GitHub からデプロイすると、プリコンパイルが行われていることが確認できます。

ビューをプリコンパイルしておくことで、アプリケーションの起動パフォーマンスを改善出来ます。

特に Azure Web Apps はアクセスが無いとインスタンスが落とされる関係上、仮想マシンなどに置いてあるアプリケーションよりも初回起動が多くなる傾向にあるので、プリコンパイルは有効な手段になります。

Visual Studio 2015 リリース記念イベントで使ったプレゼント応募アプリを支える技術

先日、Visual Studio 2015 リリース記念イベントがニコ生で放送されました。

既に Azure 界の抱かれたい男 No.1 かつ、ブログにまとめる速度 No.1 がブログにまとめてくれてます。

Visual Studio 2015 RTM & リリース記念イベント | ブチザッキ

イベントでは Surface 3 LTE や MADOSMA が当たるという、かなり太っ腹なプレゼント企画がありました。

ニコ生中にチャックさんが何回か以下のような画面を出していたと思います。

今回、このプレゼント応募アプリを作ったので、それを支える技術ということで中身の解説を簡単に行っておこうと思います。大雑把な仕組みは以下のような流れです。

  1. ASP.NET 5 アプリから Web Apps 上の Web API を叩く
  2. Web API から Event Hubs へデータを JSON で送信
  3. Stream Analytics が Event Hubs からデータを受信し、1 秒のタンブリングウィンドウで集計
  4. 集計結果を Power BI へリアルタイム出力

重要な部分は Event Hubs / Stream Analytics / Power BI になるので、個別に見ておきます。

Event Hubs

Stream Analytics でリアルタイムな処理を行う場合には Event Hubs 一択になります。使い方は NuGet から WindowsAzure.ServiceBus をインストールして、EventHubClient を作成し Send メソッドを呼ぶだけです。

var client = EventHubClient.CreateFromConnectionString(ConnectionString, EventHubPath);

var data = JsonConvert.SerializeObject(entry);

await client.SendAsync(new EventData(Encoding.UTF8.GetBytes(data)));

送信するデータの形式は Stream Analytics で設定可能ですが、大体は UTF-8 な JSON でいいと思います。

Stream Analytics

Stream Analytics は SQL ライクな言語でストリームデータに対して処理が書けるんですが、ちょっと慣れるまでは不思議な感覚だと思います。

考え方としては Rx 的なので、SQL ライクな言語より Rx で書きたい感じがします。

SELECT
    Location,
    DATEADD(hour, 9, System.TimeStamp) AS Time,
    COUNT(*) AS Count
INTO
    PowerBI
FROM
    Input
GROUP BY
    Location,
    TUMBLINGWINDOW(second, 1)

今回のアプリで実際に使ったクエリは以上になります。基本的には日付は全て UTC で扱って、出力段階で UTC+9 にするのが良いと思いました。

タンブリングウィンドウを使うと指定した時間単位で集計を行えるようになるので、この場合は 1 秒間隔での応募数を集計するようにしてあります。RPS や DAU などを簡単に集計出来そうです。

Power BI

最後に Stream Analytics の出力として Power BI を使って、グラフィカルな表示を行います。Power BI は組織アカウントが必要になるので、Azure Active Directory でユーザーを作るなど手間が少しかかりました。

極々稀に更新される「Azureの小ネタ(改)」というブログで組織アカウントの作り方が紹介されているので、悔し涙を流しながら参考にして作りました。

Power BI のデータセットは Stream Analytics が最低でも 1 件のデータを処理しないと作られないので、その点だけ注意しておきたいですね。

そして最終的に出来上がった画面が上のようなものになります。

元々は Excel の Power Map で作っていたのを Stream Analytics と Power BI に作り替えましたが、作業時間は 1 時間ほどでした。このお手軽感がたまりません。

Azure Web Apps にソース管理からデプロイするとスロット名を環境変数から取れる

何時からか分かりませんが、Azure Web Apps に GitHub などのソース管理からアプリケーションのデプロイした場合、環境変数から現在のデプロイスロットを取得できるようになっていました。

App Service Plan を標準にしなくても Production スロットが 1 つ存在するという扱いになるようです。

そして App Service Plan を標準へとアップグレードを行い、ステージングスロットを作成してみた様子です。ちゃんと WEBSITE_SLOT_NAME 環境変数の値は Staging となっています。

ちなみに手動でデプロイした場合や、Visual Studio からデプロイした場合には環境変数が存在しません。

これまでデプロイスロットを取得するには URL を見るしかなかったのが、スロット名を環境変数で取れるようになったのは嬉しいですね。ただし、URL Rewrite を使ったアクセス制限は実現できません。

Azure Web Sites - block web access to non-production deployment slots - RuslanY Blog

URL Rewrite はそのままだと環境変数の値を見ることが出来ないので、WEBSITE_SLOT_NAME 環境変数を条件にルールを作成できないのが理由です。

仮想マシンであれば以下のように URL Rewrite にプロバイダーを追加することで対応は可能です。

実際には Web Apps の場合 GAC にインストールとか不可能なので、諦めて Global.asax や HTTP Module などで処理しましょう。Production 以外の場合には 403 を返すだけです。

protected void Application_BeginRequest(object sender, EventArgs e)
{
    var slotName = Environment.GetEnvironmentVariable("WEBSITE_SLOT_NAME") ?? "Production";

    if (slotName != "Production")
    {
        Response.StatusCode = 403;
        Response.End();
    }
}

今回は Global.asax に書きましたが、基本的に HTTP Module の場合も同じです。

これを Web Apps にデプロイしてステージングサイトにアクセスすると、403 で見れなくなりました。

実際には表示可能な IP アドレスを設定可能にするべきなんでしょうけど、サンプルということで。

やはり理想としては URL Rewrite で環境変数の値を見れるようにしたいですね。