しばやん雑記

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

Docker Compose を使って DB 付きの ASP.NET アプリケーションを実行する

ASP.NET MVC アプリケーションは microsoft/aspnet イメージを使うことで簡単に Windows Containers で動作しましたが、実際のアプリケーションには大抵 DB がセットなので、DB を含めた形でアプリケーションを Docker で動かしてみました。

SQL Server 2016 も Docker Hub でイメージが公開されているので、これを使っていきます。

アプリケーションと DB のコンテナ両方を立ち上げる必要があるので、管理には Docker Compose を使います。依存関係を定義できるので、管理が非常に簡単になります。

Docker Compose の新しいフォーマットで書いてみたところ、コンテナの起動時にエラーになってしまいましたが、以下のサンプルに含まれる定義を参考にして解消できました。

networks の設定を追加しておかないと、現在の Windows Containers では問題になるようです。これもまた WinNAT の制約っぽいです。

作成した docker-compose.yml は以下になります。web と db の 2 つを定義してあります。

version: '2'

services:
  web:
    build: ./app
    depends_on:
      - "db"
    ports:
      - "8080:80"

  db:
    image: microsoft/mssql-server-windows-express
    ports:
      - "1433:1433"
    volumes:
      - "C:/temp/:C:/temp/"
    environment:
      - sa_password=P@ssw0rd
      - ACCEPT_EULA=Y
      - attach_dbs="[{'dbName':'SampleDB','dbFiles':['C:\\temp\\AdventureWorksLT2012_Data.mdf']}]"

networks:
  default:
    external:
      name: nat

web は Dockerfile に基づいてイメージを作成するようにしてあります。DB は attach_dbs でアタッチしたい DB ファイルを指定しておき、コンテナ起動後に SampleDB という名前で使えるようにしてあります。

この docker-compose.yml があるディレクトリで docker-compose up -d を実行すると、ビルドが行われた後にコンテナが依存関係に従って起動されます。

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

用意した ASP.NET アプリケーションは DB の内容を一覧表示するだけの、とても単純なものです。

毎回 IP アドレスを docker inspect で取得するのが面倒になったので、別マシンから確認してみました。

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

上の例ではプリコンパイル済みの ASP.NET アプリケーションを使いましたが、Dockerfile を工夫してプロジェクトフォルダをマウントし、サイトの物理パスを書き換えると開発しながら実行することもできます。

FROM microsoft/aspnet:4.6.2

RUN %windir%\system32\inetsrv\appcmd set vdir "Default Web Site/" -physicalPath:"C:\app"

appcmd を使って Default Web Site の物理パスを C:\app に変更しています。

このままだと IIS に設定したパスにはディレクトリすら存在しないので、docker-compose.yml 側で ASP.NET プロジェクトのディレクトリをマウントするように設定を追加します。

services:
  web:
    build: .
    depends_on:
      - "db"
    ports:
      - "8080:80"
    volumes:
      - "C:/app/WebApplication2/:C:/app/"

これでコンテナを起動すると ASP.NET プロジェクトのパスをサイトのルートとして IIS が起動するので、ビューを変更してリロードすると再コンパイルが行われて画面に反映されるようになります。

過去プロジェクトの環境を維持するのが面倒でしたが、Docker Compose を使えば DB のファイルを保持しておくだけで、簡単に維持が出来そうです。

Windows Containers を使って既存の ASP.NET MVC アプリケーションを動かす

Azure App Service の Linux 版が Docker に対応したり、Azure Container Registry がプレビューになったりと、Docker と Container に対応しておくと色々幸せになれそうなので、既存の ASP.NET アプリケーションに絞って調べることにしました。

Azure Container Service は Windows Containers 対応がプライベートプレビューになっていますし、ASP.NET 4.6 アプリケーションを Windows Containers 対応にしておくと有利になりそうです。

今後は App Service でも Windows Containers 対応が来る可能性もありますし、対応しておいて損はないと思います。対応させるための手間もほとんどかかりません。

既に Docker Hub で公開されている IIS イメージを使って ASP.NET アプリケーションは動かしてますが、もう少し掘り下げていくことにします。

Docker Hub に公開されている ASP.NET 向けのイメージは .NET Framework 3.5 と 4.6.2 向けの 2 つが公開されてます。分かりにくいことに ASP.NET Core も混ざっているので、注意が必要です。

ビルドに使われている Dockerfile のリポジトリは公開されています。

ありがたいことに ASP.NET MVC アプリケーションを Docker で動かす方法もドキュメントとして公開されています。関係ないですが docs.microsoft.com はかなり見やすくて良いですね。

このドキュメント通りで問題ないですが、少し冗長な部分があったので Dockerfile をもっとシンプルに書き換えて、Windows Containers で動かしてみました。

適当に ASP.NET MVC アプリケーションを作成し、Dockerfile を追加します。

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

Dockerfile には以下のように記述します。microsoft/aspnet イメージは IIS に ASP.NET 4.5 の追加や ENTRYPOINT の指定を行ってくれているので、アプリケーションを wwwroot にコピーするだけで動きます。

FROM microsoft/aspnet:4.6.2

COPY . /inetpub/wwwroot

COPY コマンドで現在のディレクトリにあるファイルを全て wwwroot にコピーするようにします。

プロジェクトに追加した Dockerfile は、プロパティからビルドアクションをコンテンツに変えておきます。こうしないと発行時にファイルがコピーされません。

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

アプリケーション側の準備はたったこれだけで終わりました、非常に簡単ですね。URL Rewrite や ARR が必要な場合には Web PI 経由でインストールしたり、MSI を実行したりと追加しないといけませんが、そのイメージを作ってしまえば済む話です。

このアプリケーションの Docker イメージを作成するために、発行プロファイルを作成していきます。

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

イメージを作るためにはファイルシステムに必要なファイルを書き出す必要があるので、発行方法としてファイルシステムを選んで出力先を適当に選びます。

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

Razor ビューもすべてプリコンパイルするようにして、起動時のパフォーマンスを改善します。

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

これで発行を行うと Dockerfile を含む全てのファイルが出力されます。あとはこのファイルを元に Docker コマンドを使ってイメージを作成するだけです。

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

イメージの作成は docker build コマンドを使います。あっという間に完了します。

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

本来なら作成したイメージを Docker Hub や Azure Container Registry に追加して、ACS や ECS などのオーケストレーションツールを使う形になると思いますが、両方とも対応していないのでローカルで行います。

イメージを実行するために docker run コマンドを使います。

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

イメージの起動後に IP アドレスを取得して、ブラウザからアクセスするとページが表示されます。

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

Visual Studio Tools for Docker の Windows Containers 対応が来ると簡単に対応できるようになりそうですが、Visual Studio 2017 待ちになりそうです。あと、いい加減に WinNAT の対応を行ってほしい気持ちです。

Azure App Service や AWS Elastic Beanstalk で Windows Containers が使えるようになると、かなり利用が簡単になると思うので対応を期待してます。

Windows Server 2016 の Windows Containers と Docker を使って IIS と ASP.NET を動かしてみた

Windows Server 2016 が MSDN からダウンロード出来るようになっていたので、Service Fabric Cluster 用に買った Intel NUC にインストールしてみました。

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

Hyper-V 上に入れた 2016 で Windows Containers を試したとき、1 日経ってもコンテナが起動しなかったのでホスト OS に Windows Container をインストールして再度試しました。

MSDN のドキュメントも更新されているようなので、Docker を含んだセットアップは手順通り行いました。

ドキュメントにある .NET Core のサンプルはあっさり動作したので、前に Hyper-V 上だと実行に失敗した microsoft/iis イメージを実行してみます。

既に Windows Container と IIS を使って ASP.NET を動作させる方法が色んなところで紹介されてます。

ASP.NET アプリケーションを用意するのは正直面倒だったので、とりあえず IIS のデフォルトページが表示されるところまでやります。

イメージをそのまま pull して、実行するだけという簡単なコマンドです。

docker pull microsoft/iis

docker run -d -p 80:80 microsoft/iis

pull には物凄く時間がかかりますが、一度イメージを作成してしまえば起動は高速です。docker run を実行してから数秒でコンテナが起動しました。

docker ps で実行中のコンテナを確認できます。ポートの設定も同時に見れるので便利です。

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

これでコンテナと同時に IIS も起動しているので、ブラウザで見るといつものページが表示されます。

ここまで時間は 30 分ぐらいかかりましたが、9 割がイメージのダウンロードと作成でした。

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

理由はよくわかりませんでしたが、コンテナホストから localhost は見れませんでした。別のマシンからは表示されたので、実際にはあまり問題にならない気はします。

実行中のコンテナには docker exec を使えば入ることが出来ます。コンテナの ID が必要なので、予め docker ps を実行してコピーしておきます。

docker exec -i -t CONTAINER_ID cmd

powershell を指定してもいいのですが、背景色が狂ってしまって見にくかったのでとりあえず cmd で行きます。これを実行すると、コンテナに入ることが出来ます。

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

コンテナホストは日本語 OS ですが、コンテナの中は英語になっていることが分かります。IIS がインストールされたコンテナなので inetpub がありますが、コンテナホストには存在していません。抜け出すには exit と入力するだけです。

機嫌よくなってきたので ASP.NET も簡単なアプリを用意して動かしてみることにしました。こっちも先人の知恵を頼りに Dockerfile を用意してイメージを作成します。

SHELL で powershell を使うようにすると、コマンドレットがそのまま使えるので便利ですね。ASP.NET の実行に必要な機能をインストールしておきます。

FROM microsoft/iis

SHELL ["powershell"]

RUN Install-WindowsFeature NET-Framework-45-ASPNET ; \
    Install-WindowsFeature Web-Asp-Net45

COPY . C:\\inetpub\\wwwroot

CMD ["ping", "-t", "localhost"]

何もしないとコンテナがすぐ終わってしまうので、終わらないような処理を最後に書いておきます。これは他にもっと良い手があるのではないかと思います。

Dockerfile をビルドした ASP.NET アプリケーションと同じディレクトリに置いて、docker build を実行してイメージを作成します。

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

これも 2 回目以降はキャッシュが使われるので非常に高速です。

後はこれまで通り docker run に作成したイメージ ID を渡せば、そのイメージが起動します。

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

ブラウザでアクセスすると、ASP.NET アプリケーションが実行されているのを確認できます。マシン名は Windows Containers によって適当に付けられているみたいです。

普通の Windows Server と IIS が動いているので、Dockerfile に必要な IIS 拡張をインストールする処理を書けば、URL Rewrite や ARR も問題なく使えると思います。Docker の理解が深まりました。

ASP.NET MVC で reCAPTCHA を使ってみた

今更感が半端ないですが、ASP.NET MVC で開発したアプリケーションに reCAPTCHA の version 2 を組み込む方法を調べました。何番煎じか分かりませんが、小ネタ程度にまとめておきます。

reCAPTCHA を使うためには Google のサイトから URL を登録して、トークンを貰う必要があります。

reCAPTCHA

ドキュメントも数ページしかないので、これを読めば誰でも実装は出来るはずです。

Developer's Guide  |  reCAPTCHA  |  Google for Developers

流れとしてはビューにタグを追加しておき、POST されたデータを API に投げて正しいかチェックするだけです。この流れは ValidateAntiForgery と似てるので、例によって Action Filter として実装します。

とりあえず作成した ValidateReCaptcha 属性のコードを丸ごと載せておきます。

public class ValidateReCaptchaAttribute : ActionFilterAttribute
{
    private const string SiteVerifyEndpoint = "https://www.google.com/recaptcha/api/siteverify";
    private const string ResponseFieldKey = "g-recaptcha-response";

    private static readonly string SecretKey = ConfigurationManager.AppSettings["ReCaptcha:SecretKey"];

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var data = new NameValueCollection
        {
            { "secret", SecretKey },
            { "response", filterContext.HttpContext.Request.Form[ResponseFieldKey] },
            { "remoteip", filterContext.HttpContext.Request.UserHostAddress }
        };

        var json = new WebClient().UploadValues(SiteVerifyEndpoint, data);

        var result = JsonConvert.DeserializeObject<ReCaptchaVerifyResponse>(Encoding.UTF8.GetString(json));

        if (!result.Success)
        {
            throw new ReCaptchaValidationException(result.ErrorCodes[0]);
        }
    }
}

public class ReCaptchaVerifyResponse
{
    public bool Success { get; set; }

    [JsonProperty("error-codes")]
    public string[] ErrorCodes { get; set; }
}

使い方の説明は要らない気がしますが、検証したいアクションに属性を付けるだけです。

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    [HttpPost]
    [ValidateReCaptcha]
    public ActionResult Index(FormCollection collection)
    {
        return View("Success");
    }
}

用意したビューは以下のような感じです。特に言うことはありません。

<script src="https://www.google.com/recaptcha/api.js"></script>

<h2>Index</h2>

@using (Html.BeginForm())
{
    <div class="g-recaptcha" data-sitekey="..."></div>
    <br/>
    <button type="submit">Submit</button>
}

これで準備が出来たので、実際に実行して動作を確認しておきます。ブラウザでページを表示すると、誰もが 1 回は見たことあると思われる CAPTCHA が表示されます。

チェックを入れて Submit を行うと、検証が成功するのでページが表示されます。

チェックを入れずに Submit を行うと、検証に失敗して例外が投げられるので、そこで処理が中断されます。

思ったより簡単な仕組みだったので、昼休みの暇潰し程度で試せました。ASP.NET MVC 6 だと Action Filter が Task ベースになるので、HttpClient を使いたいところです。

FatAntelope を使って XML Document Transform (XDT) を自動で生成する

主に ASP.NET アプリケーションの Web.config 変換で使われている XML Document Transform (XDT) ですが、個人的にはあれを手書きするのはかなり嫌いでした。

XML の差分を持っているだけなので、自動生成できるのではないかと長い間思ってましたが、最近 FatAntelope というまさに待ち望んでいたライブラリが公開されました。

2 つの XML を指定すると、自動的に差分を取って XDT として書き出してくれます。最高ですね。コマンドラインツールやライブラリの形で公開されているので、簡単に使い方の紹介をしておきます。

XDT を生成する

FatAntelope のコマンドラインツールは GitHub で公開されているので最新版をダウンロードしてきます。

今回はサンプルとして Web.config っぽいものを適当に用意しました。開発時に使うものになります。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <add name="DefaultConnection"
         connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=DevelopmentDB;Integrated Security=True"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.web>
    <compilation debug="true" targetFramework="4.6" />
    <httpRuntime targetFramework="4.6" />
  </system.web>
</configuration>

そしてこっちが本番用の Web.config になります。内容としては接続文字列を本番向けにして、リリースビルドを行うというものです。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <add name="DefaultConnection"
         connectionString="Server=tcp:***.database.windows.net,1433;Database=ProductionDB;User ID=***;Password=***;Trusted_Connection=False;Encrypt=True;Connection Timeout=30;"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.web>
    <compilation targetFramework="4.6" />
    <httpRuntime targetFramework="4.6" />
  </system.web>
</configuration>

この 2 つのファイルを FatAntelope に与えると、XDT が生成されます。生成された XDT はひとまず Web.Release.config とでも名前を付けて保存するようにします。

FatAntelope Web.config Production.config Web.Release.config

コマンドを実行すると、コンソールにメッセージが出るのでエラーにならなければ成功です。

そして出力された XDT が以下のようになります。ちゃんと差がある部分だけを抽出してくれています。

<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <connectionStrings>
    <add connectionString="Server=tcp:***.database.windows.net,1433;Database=ProductionDB;User ID=***;Password=***;Trusted_Connection=False;Encrypt=True;Connection Timeout=30;" xdt:Transform="SetAttributes(connectionString)" />
  </connectionStrings>
  <system.web>
    <compilation xdt:Transform="RemoveAttributes(debug)" />
  </system.web>
</configuration>

手書きの場合と比較すると xdt:Locator が無かったりと、少し違和感があるかもしれませんが、XDT の仕様としてはこれで問題はありません。XML 宣言は何故か付かないので、後で追加しましょう。

C# から XDT を生成する

FatAntelope は NuGet でライブラリが公開されているので、API には多少癖がありますが、自分のアプリケーションなどに簡単に組み込むことが可能です。

当然ながら NuGet を使って FatAntelope をインストールする必要があります。

Install-Package FatAntelope

FatAntelope の API は多少癖があるので、最低限の XDT 生成を行うコードを紹介しておきます。

using System;
using System.Xml;

using FatAntelope;
using FatAntelope.Writers;

namespace ConsoleApplication17
{
    class Program
    {
        static void Main(string[] args)
        {
            // 元になる XML
            var baseXml = new XmlDocument();

            baseXml.Load(@"C:\Users\shibayan\Downloads\FatAntelope.v0.2.3\Web.config");

            // 新しい XML
            var newXml = new XmlDocument();

            newXml.Load(@"C:\Users\shibayan\Downloads\FatAntelope.v0.2.3\Production.config");

            // XmlDocument から XTree を作成しておく
            var baseTree = new XTree(baseXml);
            var newTree = new XTree(newXml);

            // XDiff を使って差分を計算し、newTree に格納
            XDiff.Diff(baseTree, newTree);

            // 差分が格納された XTree を元に XDT として書き出し
            var patch = new XdtDiffWriter().GetDiff(newTree);

            patch.Save(Console.Out);

            Console.WriteLine();
        }
    }
}

XDiff.Diff を呼び出した結果がイマイチな感じですね。これを実行すると XDT がコンソールに出力されます。

生成された XDT をどのように使うかはお任せします。MSBuildTask などにしてしまうのも手だと思います。

おまけ:XDT を XML に反映させる

完全におまけですが、元になる XML と XDT を使って、差分が反映された XML を生成する方法も紹介しておきます。ちなみにこちらは Microsoft 純正のライブラリが提供されています。

http://blogs.msdn.com/b/webdev/archive/2013/04/23/xdt-xml-document-transform-released-on-codeplex-com.aspx

ライブラリは NuGet で公開されているので、FatAntelope と同じようにサクッと使えます。

基本的に XmlDocument を継承したクラスを使って XML に対して XDT を適用する形になります。こちらもサンプルコードを紹介しておきます。

using System;

using Microsoft.Web.XmlTransform;

namespace ConsoleApplication17
{
    class Program
    {
        static void Main(string[] args)
        {
            // 変換元の XML を読み込む
            var source = new XmlTransformableDocument();

            source.Load(@"C:\Users\shibayan\Downloads\FatAntelope.v0.2.3\Web.config");

            // XDT を読み込んで Apply で反映させる
            var xdt = new XmlTransformation(@"C:\Users\shibayan\Downloads\FatAntelope.v0.2.3\Web.Release.config");

            xdt.Apply(source);

            source.Save(Console.Out);

            Console.WriteLine();
        }
    }
}

実行すると XDT の内容が反映された XML がコンソールに出力されます。

これまでは XDT を適用するという一方向のみの対応でしたが、FatAntelope のリリースによって XML から XDT を作成するという、双方向の変換が可能になりました。

個人的には Web.config 変換を書きやすくするための Visual Studio 拡張を作りたいですね。

ASP.NET MVC 5 アプリケーションを .NET Framework 4.6 と C# 6.0 にアップデートしてみた

リリースされたばかりの .NET Framework 4.6 が Azure Web Apps で使えるようになったらしいので、動かしていた ASP.NET MVC 5 なアプリケーションを ASP.NET 4.6 と C# 6.0 にアップデートしてみました。

8 月中にリリースという話でしたが、思ったよりも早かったです。

ポータルの表示はまだ 4.5 なので注意が必要です。Azure 公式ブログもアップデートされています。

http://azure.microsoft.com/blog/2015/08/11/update-on-net-framework-4-6-and-azure/

Azure 界の抱かれたい男 No.1 こと @kosmosebi、通称ぶちぞうさんも既にブログで Web Apps の .NET 4.6 対応について書いてます。要チェックです。

Azure App Service に.NET Framework 4.6がロールアウトされました | ブチザッキ

現時点では RyuJIT は無効化されているのと、Web Apps は Windows Server 2012 で動いていて HTTP/2 は使えないので、Web Forms を使っている人ぐらいにしか大きな恩恵は無さそうです。

これだけだとアレなので、C# 6.0 を使う方法を紹介することにします。

ASP.NET と C# 6.0 の関係

C# 6.0 の機能はコンパイラが提供するので .NET Framework 4.6 とは別扱いです。このあたりの関係がどうにもわかりにくく感じたので、表にしてみました。

ASP.NET 4.5 ASP.NET 4.6 ASP.NET 5
cs ファイル △ (Roslyn が必要) ○ (MSBuild のパスを変更)
ASPX / Razor △ (CodeDOM Provider が必要) △ (CodeDOM Provider が必要)

.NET 4.6 をインストールすると Roslyn 込みの MSBuild がインストールされているので、C# のコードは MSBuild のパスを変えるだけでビルド可能になりますが、実際には CodeDOM Provider が Roslyn もインストールするので、特に設定しなくても使えるようになります。

結局のところ CodeDOM Provider をインストールすれば C# 6.0 が使えるようになるってことです。

例外的存在の ASP.NET 5 は Roslyn が組み込まれているので、デフォルトで C# 6.0 に対応しています。ASP.NET 5 に関しては RC1 が出てから本気を出すつもりでいます。

ASP.NET MVC 5 で C# 6.0 を有効にする

ASPX や Razor で C# 6.0 の機能を使うためには CodeDOM Provider をインストールする必要があります。

http://blogs.msdn.com/b/webdev/archive/2015/07/20/announcing-asp-net-4-6-and-asp-net-5-beta-5-in-visual-studio-2015-release.aspx

NuGet パッケージを調べてみると、このパッケージは .NET 4.6 だけではなく .NET 4.5 でも動くみたいなので、C# 6.0 の機能はすぐに使え始めることが出来そうです。

ちなみに Visual Studio 2015 を使って ASP.NET MVC プロジェクトを作成すると、最初から CodeDOM Provider がインストールされています。なので、特に何も考えることなく C# 6.0 を使って開発が行えます。

一緒にインストールされている Microsoft.Net.Compiler は Roslyn の本体になります。

展開されたパッケージを見ると csc や vbc が存在しているのが確認できます。

MSBuild 用の targets ファイルも含まれているので、ASP.NET アプリケーション以外に WebJobs などのコンソールアプリケーションでも C# 6.0 の機能が利用可能です。

話が逸れましたが、この CodeDOM Provider をインストールすると Web.config には system.codedom 要素が追加されているので、この設定を変えることで C# のバージョンを変更したりできます。

実際に行ったことは CodeDOM Provider のインストールだけですが、これだけで GitHub などのソース管理からのデプロイも問題なくできるようになります。もちろん Web Deploy も利用可能です。

以前に紹介した aspnet_compiler.exe を使ってプリコンパイルを行う方法もそのまま使えました。

コンパイル時間が前よりも大幅に遅くなった気がしますが、C# 6.0 が使えるので我慢しようかと思います。

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

インストール時に MachineKey を自動生成する NuGet パッケージを作った

大昔に紹介した MachineKey をオンラインで生成できるサイトが、いつの間にかドメインごと消滅していてとても不便だったので、代わりになるものとして MachineKey を自動的に生成し、Web.config に設定まで行う NuGet パッケージを作りました。

このパッケージをインストールすると、自動的に Web.config に machineKey 要素が追加されます。今のところ AES256 と HMACSHA256 で固定にしています。

MachineKey に関してもう少し知りたいという場合には、下の記事を参考にしてください。


1 つのアプリケーションを複数の仮想マシンで動作させる場合、問題となることが多いです。

例としてパッケージマネージャーコンソールから、MachineKeyGenerator をインストールしてみました。

インストールするとテキストファイルも追加されますが、これは NuGet の仕様でパッケージに content か lib が含まれていないと install.ps1 / uninstall.ps1 が実行されないので、仕方なく入れています。

Web.config を開くと machineKey 要素が追加されているのが確認できます。本当は 1 行で追加されますが、分かりにくいので改行してあります。

パッケージをアンインストールすると、MachineKey も同時に削除するようにしています。*1

既に MachineKey を設定済み、もしくは NuGet パッケージの復元を行った場合には処理をスキップするようにしています。一度設定した MachineKey が変わることはありません。

実際に NuGet パッケージの復元を行いましたが、MachineKey は変わっていません。

PowerShell 力は相変わらずですが、少しコードを整理した後に GitHub にでも置いておこうかと思います。

追記

GitHub にスクリプト一式を置いておきました。build.cmd を叩けば nupkg が生成されます。

*1:この仕様は後で変更する可能性ありそう

ASP.NET MVC の Display Mode を使っている場合には Vary HTTP ヘッダーを出力するべきだった

ASP.NET MVC の Display Mode を使えば、ビューを用意するだけで PC 版とスマートフォン版のページを同じ URL で公開することができます。

既に何回か紹介しているので、Display Mode については以下の記事を参考にしてください。

機能としては上の記事の通りにすれば問題ないですが、Google のドキュメントに動的な配信を行う場合には Vary HTTP ヘッダーを使って、User-Agent で異なるビューが返されることを伝えるべきとありました。

動的な配信の場合、ページをリクエストするユーザー エージェントに応じて、同じ URL で異なる HTML(および CSS)が配信されます。

この設定では、PC 用ユーザー エージェントのクロール時にはモバイル コンテンツが隠されているため、モバイル用ユーザー エージェント向けにサイトの HTML が変更されることはすぐにはわからない状態になっています。サーバーからヒント送信し、スマートフォン用 Googlebot がページをクロールしてモバイル コンテンツを検出するようリクエストすることをおすすめします。このヒントは Vary HTTP ヘッダーを使用して実装します。

モバイルファースト インデックスに関するおすすめの方法 | Google 検索セントラル  |  ドキュメント  |  Google for Developers

適切にヒントをクローラーに与えていない場合には、検出されるまでに時間がかかることがありそうです。

Vary HTTP ヘッダーを出力する

結局のところ Vary HTTP ヘッダーで User-Agent を返せば良いだけなんですが、IIS の圧縮モジュールを使っている場合には簡単にはいきません。モジュールに既知の不具合があるからです。

IIS で gzip 圧縮が有効になっている場合、強制的に Vary HTTP ヘッダーの値が Accept-Encoding に上書きされてしまいます。残念ながら直る気配がありません。

この時は gzip 圧縮をオフにして対応しましたが、それだと効率が悪化するので URL Rewrite を使いました。

<system.webServer>
  <rewrite>
    <outboundRules>
      <rule name="Append Vary" preCondition="IsHTML">
        <match serverVariable="RESPONSE_Vary" pattern="^.*$" />
        <action type="Rewrite" value="{R:0}, User-Agent" />
      </rule>
      <preConditions>
        <preCondition name="IsHTML">
          <add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" />
        </preCondition>
      </preConditions>
    </outboundRules>
  </rewrite>
</system.webServer>

URL Rewrite Module はサーバー変数を書き換えると、レスポンスヘッダーにも反映されます。

この定義を Web.config に張り付けるだけで、HTML を返した時だけ Vary HTTP ヘッダーに User-Agent が追加されます。実際にブラウザからアクセスして確認してみました。

Content-Encoding は gzip のままで、Vary には Accept-Encoding と User-Agent が追加されています。

URL Rewrite の Outbound Rule 処理は HTTP 圧縮が行われた後に実行されるので、影響を受けずに HTTP ヘッダーを操作することが出来る、というからくりでした。

おまけ:Vary HTTP ヘッダーが存在しない場合

ついでに圧縮を無効にした場合の挙動も一緒に確認しておきました。

HTTP 圧縮モジュールが Vary を出力しないため、User-Agent のみ出力されています。理想的な挙動です。

ASP.NET MVC 5 アプリケーションの開発を始める時のテンプレート的なものを作った話

昔 ASP.NET MVC 5 で開発を始める上で、自分が定型的に追加している設定をまとめた記事を書きました。

完全に自分用として書いた記事なので、頻繁に読むようになってしまいました。

ぶっちゃけ、自分で MVC 5 アプリケーションを作り始める時に毎回参照しているのも馬鹿らしくなってきたので、GitHub にて色々と設定を追加したテンプレート的な MVC 5 プロジェクトを公開しました。

テンプレートと言いつつ、VSIX 形式にまでは出来ていないので参考にするとかコピペ元にするとか、そういった使い方をしようかと思ってます。

標準のテンプレートとの違い

具体的にこのテンプレートで、どのような設定を追加で行っているかまとめておきます。

  • 最新の ASP.NET MVC 5.2.3 にアップデート
  • RazorViewEngine のみを有効化
  • 認証クッキー名を .ASPXAUTH から auth に変更
  • セッションクッキー名を ASP.NET_SessionId から session に変更
  • CSRF トークンのクッキー名を __RequestVerificationToken から token に変更
  • Attribute Routing のみを使うように
  • URL ルーティングで生成される URL を小文字に
  • レスポンスヘッダに ASP.NET / ASP.NET MVC のバージョン、X-Powered-By を出力しない
  • クライアントサイド検証を無効化
  • エラー画面の表示を IIS 側の設定に統一
  • HTML ヘルパーを少し追加

カスタム HTTP レスポンスヘッダは地味に非表示にしろと言われるので、最初から非表示にしました。

追加した HTML ヘルパー

バリデーションエラーを表示する ValidationSummary ヘルパーの拡張性が全くなく、実際にデザインを当てる場合に不便しかなかったので、部分ビューを使って自由に表示できるようにしています。

ヘルパー名が微妙ですけど、良いのが思いついたらたぶん変更します。

@using (Html.BeginForm())
{
    @Html.ValidationSummaryView("_ErrorView")
    
    @Html.TextBoxFor(m => m.Name)

    <button type="submit">Submit</button>
}

引数にレンダリングする部分ビュー名を指定しておけば、エラーがある場合にはそのビューを使って表示してくれます。エラーがない場合はレンダリング自体を行いません。

エラー表示用のビューはモデルとして ModelError を取ります。

@model IEnumerable<ModelError>

<p>エラーがあります</p>
<ul>
    @foreach (var error in Model)
    {
        <li>@error.ErrorMessage</li>
    }
</ul>

このヘルパーを使ってエラー表示を行った場合には、以下のようになります。

最近行っている開発では、HTML ヘルパー自体をカスタマイズすることが多すぎるので、ある程度の自由度を持たせて楽できるようにしたいですね。

テンプレートは暇なときに更新していきたいと思います。