しばやん雑記

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

Azure SignalR Service の GA と ASP.NET SignalR への対応がプレビューになったので試した

これも Ignite 2018 で発表されましたが、予定通り Azure SignalR Service が GA となりました。

分かりやすい変更点としては SLA 99.9% が付いたり、Standard Tier での 100 units までのスケールアウトがサポートされて、仕様上は 10 万コネクションまで扱えるようになりました。

GA 後の Standard Tier の価格は 1 unit 当たり 2800 円 / 月ぐらいなので、Redis Cache のサイズなどで悩んだりするより、SignalR Service に移行した方が全体的なコスト最適化となる可能性が高いですね。

今後は Standard より上の Tier も出るみたいですし、スケーリングに関しては大体のケースで問題となることは無くなるでしょう。ちなみに今回は珍しく Japan East に最初からデプロイされました。

それよりも気になったのは、サポートされているライブラリの紹介の 1 文です。

The Azure SignalR Service supports existing libraries for ASP.NET Core, ASP.NET, Java, and JavaScript clients, opening this service to a broad array of developers.

ASP.NET Core や Java / JavaScript は分かるんですが、ひっそりと ASP.NET も入っています。

気になったので Ignite 2018 の SignalR Service セッションを確認すると、Core じゃない ASP.NET SignalR を使って SignalR Service を利用するデモが行われていました。

ひっそりと ASP.NET SignalR 向けのライブラリが公開されていましたが、ダウンロード数が少ないですね。

ASP.NET Core SignalR への移行は地味に面倒な部分があるので、既に ASP.NET SignalR でアプリケーションを動かしている場合でも SignalR Service が選択肢に入りそうです。

早速なのでサンプルアプリケーションを使って試しておきました。とりあえず SignalR Service を新しく作るところから始めます。

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

何となく Japan East にデプロイしました。とりあえず 2 units を使うようにしてあります。

サンプルアプリケーションは懐かしい Stock Ticker を使いました。空の ASP.NET プロジェクトに NuGet からインストールするだけなので非常に楽です。同時に先ほどのライブラリもインストールします。

SignalR Service を使うための設定は OWIN Startup で呼び出されている MapSignalRMapAzureSignalR に変更するだけです。この辺りは Core SignalR と同じような感じです。

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        // ↓ は要らない
        //app.MapSignalR();

        // applicationName は適当で良い
        app.MapAzureSignalR(GetType().FullName);
    }
}

最後に Azure Portal からコピーしてきた SignalR Service の接続文字列を Web.config に追加します。

名前はデフォルトだと Azure:SignalR:ConnectionString が使われるので、今回はその名前で追加しました。MapAzureSignalR の呼び出し時に明示的に設定も出来るみたいです。

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

これで SignalR Service を使う設定は完了です。Core SignalR と同じぐらい簡単に設定できました。

最後にちゃんと SignalR Service が使われているのか動作を確認するわけですが、分かりやすいのは WebSocket の接続先が SignalR Service になっているかどうかです。

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

無事に接続先が SignalR Service になっていることが確認出来ました。簡単でしたね。

とはいえ、個人的には ASP.NET Core SignalR に移行が可能な場合は、そっちに移行した方が良いと思っています。今回の ASP.NET SignalR への対応のために SignalR 自体もバージョンが上がっていて、地味にプロトコルバージョンが 2.0 に変わっています。

なので、C# / JavaScript 以外のクライアントを使っていると動作しない可能性が高いです。注意しましょう。

Library Manager を使ってクライアントサイドのライブラリをシンプルに利用する

Visual Studio 2017 の 15.7 ぐらいで入ると言われていて入らなかった Library Manager が、こないだリリースされた 15.8 で入るようになりました。

これまで Visual Studio では NuGet や Bower が標準でサポートされてましたが、今回の Library Manager は任意のプロバイダーから必要なライブラリをインストールする機能です。

コメントには厳しい意見もありますが、個人的にはこういうシンプルなアプローチは好きです。私はとりあえず規模は関係なく何でも webpack という最近のフロントエンドの流れには違和感を持っています。

Library Manager はシンプルに特定のライブラリを管理し、インストールする機能しか持っていません。なので、ビルドやバンドルが必要な場合は素直に webpack とかを使うことになります。

ドキュメントには ASP.NET Core とありますが、実は ASP.NET でも使えます。

とりあえず README にある "Reasons to use LibMan" と "Reasons NOT to use LibMan" はしっかりと読んで理解した上で選択しましょう。webpack を置き換えるようなものではなく、そもそも目的は別です。

プロバイダーを追加できる仕組みになっているので、Issue には GitHub を扱えるようにしたいなど、面白そうなものもありました。GitHub の Release を扱えるようになると地味に良さそう。

npm でインストール出来るのは理解してますが、webpack などを使わない場合は node_modules からファイルをコピーしましょうみたいな記事が出てくる現状はおかしいのでは、と思ってます。

実際に使って試すことにします。Visual Studio 2017 の 15.8 がインストールされていれば、右クリックで表示されるメニューに項目が追加されています。

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

翻訳は非常にアレですが、GitHub にあるリソースでは正しい表現になっているので謎です。

追加を選ぶとダイアログが表示されて、そこで名前を入れるとインストールできます。デフォルトでは cdnjs が選択されていますが、unpkg やファイルシステムも選べます。

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

インストールするファイルを選べば、いい感じに wwwroot 以下にインストールしてくれます。

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

簡単ですね。NuGet や Bower を使ってインストールしていた時と同じように扱えます。

続いてよく使いそうな Bootstrap もインストールしてみます。

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

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

これもあっという間に完了しました。外部のメジャーな CDN に乗っかることで、アップデートがリリースされたらすぐに扱えるのは面白い仕組みだと思いました。

インストールしたライブラリの情報は libman.json に保存されています。右クリックするとメニューが表示されるので、ライブラリの復元やクリーニングも行えます。

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

ビルド時の復元を有効にしておけば、他の環境や CI サーバーに持って行った時でもビルド出来るようになるはずです。この辺りは NuGet と同じような動作になるみたいです。

色々と賛否はあると思いますが、こういったシンプルなパッケージマネージャがあっても良いと思います。

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 の進捗を待つという形になりそうです。

ASP.NET Identity から ASP.NET Core Identity へ移行してみた

ASP.NET MVC 5 から Core MVC 2.1 への移行作業を行っていますが、地味に Identity 周りの移行ではまった部分が多かったのでメモとして残します。作業としては DB のマイグレーションがメインです。

移行に関してはドキュメントがありますが、正直これは役に立たないですね。

喜ばしいことに ASP.NET Core Identity になってもパスワードのハッシュ化形式は変更されていないので、ASP.NET Identity のデータをそのままでログインすることは出来ます。

しかし、テーブルのスキーマが変更されているので、マイグレーションを行う必要があります。パッと調べた感じではマイグレーションの方法は公式に用意されていないみたいなので、手動で対応しないといけません。

Identity 設定を移行する

とりあえず ASP.NET Identity と ASP.NET Core Identity のプロジェクトを用意して、DB の差分を確認することから始めます。以下はテンプレートそのままのプロジェクトです。

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

ASP.NET Identity 側にある ApplicationUserManager.Create で設定している情報は、ASP.NET Core Identity では Startup クラスの中で IdentityOptions への設定として行います。

具体的にコードで例を出してみます。以下は ASP.NET Identity でのデフォルト設定です。

public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) 
{
    var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
    // ユーザー名の検証ロジックを設定します
    manager.UserValidator = new UserValidator<ApplicationUser>(manager)
    {
        AllowOnlyAlphanumericUserNames = false,
        RequireUniqueEmail = true
    };

    // パスワードの検証ロジックを設定します
    manager.PasswordValidator = new PasswordValidator
    {
        RequiredLength = 6,
        RequireNonLetterOrDigit = true,
        RequireDigit = true,
        RequireLowercase = true,
        RequireUppercase = true,
    };

    // ユーザー ロックアウトの既定値を設定します。
    manager.UserLockoutEnabledByDefault = true;
    manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
    manager.MaxFailedAccessAttemptsBeforeLockout = 5;

    return manager;
}

上の設定を ASP.NET Core Identity 向けに変更したら、以下のようになります。

大体はデフォルト値のままで問題ないですが、何故か RequireUniqueEmail の値だけは異なっていました。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<IdentityOptions>(options =>
    {
        options.User.RequireUniqueEmail = true;

        options.Password.RequiredLength = 6;
        options.Password.RequireNonAlphanumeric = true;
        options.Password.RequireDigit = true;
        options.Password.RequireLowercase = true;
        options.Password.RequireUppercase = true;

        options.Lockout.AllowedForNewUsers = true;
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
        options.Lockout.MaxFailedAccessAttempts = 5;
    });
}

カスタマイズしている場合には、この設定を合わせておかないと不整合が発生して、下手したらログイン出来なくなるパターンもあるので注意したいですね。

Identity DB を移行する

両方とも実行してユーザーを作成すると、いい感じに LocalDB にデータベースが作られます。

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

Identity と Core Identity でテーブル名は変更されていないのですが、新しくテーブルが追加されたりしているので、この差分を何とかしないと正しく実行できません。

なので Visual Studio の SQL Server オブジェクト エクスプローラーから差分をチェックします。

これはソース DB とターゲット DB を指定すると、分かりやすく差分を表示してくれる上に変更用スクリプトも生成してくれる優れものです。手動で SQL を書く手間が省けました。

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

__MigrationHistory というテーブルは無視して良いですが、何だかんだで全テーブル構造が異なっているという結果になりました。これは結構厳しいです。

当然ながら、この状態で Core Identity 側で Identity が作成した DB を使うようにするとエラーになります。

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

先ほどチェックした差分から変更用スクリプトを作成して実行すれば、データを保持したまま構造を変えてくれますが、今回のケースでは LockoutEndDateUtc というカラムが LockoutEnd にリネームされているので、その部分だけはデータを捨てるなりスクリプトを変更するなりで対応します。

今回はデータを捨てて対応したので、マイグレーション自体はあっさり終わりましたが、これでもログインはまだ行えません。試すとログインエラーになってしまいます。

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

DB のマイグレーションは問題ないのですが、最新の Core Identity は新しく追加された以下のカラムを利用してログイン処理を行うため、データを詰めてあげないと絶対にログインが失敗します。

  • NormalizedUserName
  • NormalizedEmail

このカラムには UserNameEmail を大文字化した値を入れる必要があります。

マイグレーションのスクリプト内で UPPER を行っても良いですし、私みたいに後からこの仕様変更に気が付いた場合は UPDATE を流せば良いです。

UPDATE [AspNetUsers] SET [NormalizedUserName] = UPPER([UserName]), [NormalizedEmail] = UPPER([Email])

この対応後、ようやく Core Identity でのログインが正しく行えるようになりました。長かったですね。

一応は公式で Identity と Core Identity の互換性レイヤーが提供されていますが、今回は完全なる Core Identity への移行が目的だったので使いませんでした。

しかし、このレイヤーのコードを読むことで互換性がない部分を理解できたので助かりました。

ASP.NET / IIS で X-Forwarded-Proto の値を正しく扱いたい

リバースプロキシやロードバランサーで SSL offload を行った場合には、後ろにいる Web サーバーにクライアントの IP などを伝えるために X-Forwarded-* を付けますが、ASP.NET は上手く扱ってくれません。

X-Forwarded-For に関しては ARR Helper を入れることで対応できますが、X-Forwarded-Proto は完全に未対応なので自力で何とかする必要があります。

ARR Helper がインストールされている場合には X-ARR-SSL というヘッダーを渡してあげれば HTTPS = on としてくれますが、非標準なヘッダーなので ARR と組み合わせた場合しか上手く動作しません。

HTTP モジュールで上書きする

ASP.NET の世界だけで扱えれば良いのであれば、専用のマネージ HTTP モジュールを用意して HTTPS / SERVER_PORT サーバー変数を書き換えてあげれば解決します。

適当に書いたコードですが、大体こんな感じになるのではないかと思います。

public class HttpForwardedModule : IHttpModule
{
    public void Init(HttpApplication app)
    {
        app.BeginRequest += OnBeginRequest;
    }

    public void Dispose()
    {
    }

    private void OnBeginRequest(object sender, EventArgs e)
    {
        var app = (HttpApplication)sender;

        if (app.Context.Request.Headers["X-Forwarded-Proto"] == "https")
        {
            app.Context.Request.ServerVariables["HTTPS"] = "on";
            app.Context.Request.ServerVariables["SERVER_PORT"] = "443";
        }
    }
}

このモジュールを PreApplicationStartMethod を使って IIS に登録してあげれば有効になります。

[assembly: PreApplicationStartMethod(typeof(WebApplication17.ModuleInitializer), "Start")]

public class ModuleInitializer
{
    public static void Start()
    {
        HttpApplication.RegisterModule(typeof(HttpForwardedModule));
    }
}

実際のところは BeginRequest のタイミングで上書きの処理が出来ればよいので、Global.asax でも同様に動作します。これで実行して X-Forwarded-Proto 付きのリクエストを投げてあげれば、ASP.NET は HTTPS なリクエストだと判断します。

URL Rewrite で上書きする

ASP.NET の世界で閉じていればマネージ HTTP モジュールで上手くいきますが、ASP.NET 側で変更したサーバー変数は IIS 側には反映されないので、後ろに PHP や HttpPlatformHandler が居る場合には使えません。

要はサーバー変数を書き換えてあげれば良いので、URL Rewrite でも同じことが実現できます。

<system.webServer>
  <rewrite>
    <!-- unlock が必要なので少し面倒 -->
    <allowedServerVariables>
      <add name="HTTPS" />
      <add name="SERVER_PORT" />
    </allowedServerVariables>
    <rules>
      <rule name="HttpForwarded">
        <match url=".*" />
        <conditions>
          <add input="{HTTP_X_FORWARDED_PROTO}" pattern="https" />
        </conditions>
        <serverVariables>
          <set name="HTTPS" value="on" />
          <set name="SERVER_PORT" value="443" />
        </serverVariables>
      </rule>
    </rules>
  </rewrite>
</system.webServer>

やっていることは HTTP モジュールと同じです。allowedServerVariables はデフォルトではロックされているので、書き換えるには applicationHost.config を直接弄るか unlock してから追加する必要があります。

上の例ではアプリケーションの Web.config で書くことを想定してますが、globalRules に入れてマシン全体で使うことも出来るので、考えることが減って少し楽になるかも知れません。

リダイレクト時に https にならない問題

ARR の時には発生しないのですが、それ以外のリバースプロキシや LB を使っている場合には、URL Rewrite でリダイレクトを行うと http として返される問題があります。

何故 ARR の時には発生しないかというと、ARR にはレスポンスヘッダーを自動で書き換える機能があるからです。デフォルトで有効になっているので、ARR / ARR Helper / ASP.NET の場合は何も考えずに済みます。

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

しかし、それ以外の組み合わせになると一気に破綻するので、リダイレクトする際には https 付きの絶対 URL として URL Rewrite を書く必要があります。地味に面倒です。

Outbound Rule を使って一括で Location ヘッダーを書き換える方法も使えると思いますが、事故りやすいのであまりお勧めは出来ないと思ってます。

App Service を使う

例によって App Service を使う場合には ARR と ARR Helper という組み合わせになるので、いい感じに HTTPS = on としてくれます。リダイレクト時の書き換え処理も走るので、ちゃんと https としてクライアントには返ります。

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

App Service で実行する場合には、特に必要な作業はありません。お手軽ですね。ちなみに App Service 以外でも ARR と ARR Helper の組み合わせであれば、同じように動作します。

根本的な対応は IIS が X-Forwarded-Proto に対応するか、専用のネイティブ HTTP モジュールを書くしかないと考えてます。ARR Helper がもっと汎用的になって Proxy Helper とでもなれば最高なのですが。

.NET Framework / .NET Core 向け Docker Image のリポジトリが変更された話

多分 .NET Framework 4.7.2 と .NET Core 2.1 のリリースを狙ったタイミングだと思ってるのですが、この 2 つを利用するための公式 Docker Image のリポジトリが変更されています。

地味に同じようなのにイメージが異なっていた .NET 系の Docker Image が統合されて、シンプルなタグで使えるようになっているので便利です。古い方はもう更新されないので注意。

.NET Framework 向け

これまでは microsoft/dotnet-frameworkmicrosoft/dotnet-framework-build で用途によってイメージ自体が分けられていましたが、現在は microsoft/dotnet-framework 側に統合されています。

  • 4.7.2-sdk
  • 4.7.2-runtime

SDK のイメージには Visual Studio Build Tools がインストールされているので、MSBuild を使ってアプリケーションをコンテナ内でビルドできるようになっています。*1

基本的には最新の .NET バージョンを対象にしているでしょうし、4.7.2 を使っておけばよいと思いますが、上のタグは LTSC 2016 の Server Core イメージが使われているのでサイズが大きいです。

必要に応じて 1709 や 1803 のイメージを使うと半分ぐらいで済むようになります。タグの詳細は Docker Hub を参照すればすぐにわかるはずです。

https://hub.docker.com/r/microsoft/dotnet-framework/

とはいえ、現時点で Hyper-V Containers を使った CI SaaS は無いみたいなので、大人しく LTSC 2016 を使うことになるのではないかと。実際に AWS CodeBuild がそうでした。

ちなみに ASP.NET を使うためのイメージはこれまでと同じ microsoft/aspnet です。

.NET Core 向け

.NET Framework 向けは Windows しかなかったので分かりやすいですが、.NET Core は複数のディストリ向けに公開されてるだけじゃなく、Nano Server のイメージもあるのでちょっと数が多いです。

しかし、ちょっと前から multi-arch なイメージに対応しているので、基本は以下の 3 つだけ知っておけば良いです。後は Docker が勝手に Ubuntu と Nano Server で切り替えてくれます。

  • 2.1-sdk
  • 2.1-aspnetcore-runtime
  • 2.1-runtime

.NET Framework 向けとは異なり、ASP.NET Core 向けのイメージも microsoft/dotnet に統合されるようになったので、今後は microsoft/aspnetcoremicrosoft/aspnetcore-build は更新されません。

例を挙げると Docker を使った CI SaaS では microsoft/dotnet:2.1-sdk を使い、ASP.NET Core を動かすための Dockerfile では microsoft/dotnet:2.1-aspnetcore-runtime を使えば良いです。

特に理由がなければシンプルなタグを使っておけば良いですが、.NET Core に関しては Ubuntu のバージョン違いだけで数種類出ているうえに、Nano Server も 1709 / 1803 / LTSC 2016 が用意されています。

https://hub.docker.com/r/microsoft/dotnet/

更に .NET Core 2.1 からは Alpine Linux のイメージと ARM32 向けイメージも追加されているので、間違わないように注意したいところです。ちなみに Alpine は microsoft/dotnet:2.1-sdk-alpine といった形で指定できます。基本的に OS 名は最後につきます。

Alpine Linux のイメージは今後主流になっていくのではないかと思ってます。

ASP.NET アプリケーションを CodePipeline と CodeBuild を使って Elastic Beanstalk にデプロイする

AWS CodeBuild が Windows にも対応してやりたいことは、やはりビルドしたアプリケーションを何処かにデプロイすることでしょう。少なくとも私はそうだったので、CodePipeline を組み合わせて試しました。

CodeBuild の Windows 対応はこないだ書いたので説明はしません。

ASP.NET アプリケーションをデプロイ用にビルドする際は、やはりビューをプリコンパイルしておきたくなるので、発行プロファイルを作成して管理することにしました。

buildspec.yml で発行プロファイルを使うように書けば、勝手にプリコンパイルされて便利です。

version: 0.2

phases:
  pre_build:
    commands:
      - nuget restore
  build:
    commands:
      - msbuild WebApplication12.sln /p:Configuration=Release /m /v:m /t:Package /p:PublishProfile=CustomProfile
artifacts:
  type: zip
  files:
    - artifacts/*
  discard-paths: yes

今回は artifacts の設定を変更して、指定したディレクトリ以下をアップロードするようにしました。前の方法では CodePipeline を組み合わせた時に 2 重に zip されてしまい、Elastic Beanstalk へのデプロイが上手くいかなかったからです。

artifacts 以下には予め Elastic Beanstalk にデプロイするためのマニフェストを入れておきました。ASP.NET Core 用みたいに紹介されてますが、MSDeploy 向けにも使えます。

{
  "manifestVersion": 1,
  "deployments": {
    "msDeploy": [
      {
        "name": "Web app",
        "parameters": {
          "appBundle": "WebApplication12.zip",
          "iisPath": "/",
          "iisWebSite": "Default Web Site"
        }
      }
    ]
  }
}

実際のリポジトリ内では以下のように配置してあります。msbuild がビルド結果の zip をこのディレクトリに保存してくれるので、良い感じのパッケージが作れるというからくりです。

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

コードの準備は出来たので CodePipeline を新しく作成します。

特に説明することはなく、GitHub からソースを取得して、CodeBuild を使ってアプリケーションをビルド、そして最後は Elastic Beanstalk にデプロイという、シンプルなパイプラインです。

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

パイプラインを実行すると、CodeBuild で ASP.NET アプリケーションがビルドされて、Elastic Beanstalk にアーティファクトがデプロイされます。ビルドログもおまけで載せます。

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

そしてしばらくすると Elastic Beanstalk へのデプロイが完了するので、無事に ASP.NET アプリケーションが見れるようになります。

ちょっとアーティファクト周りの扱いが嫌な挙動でしたが、パイプライン自体は簡単に作れました。

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

デプロイ先としては Windows の場合は Elastic Beanstalk ぐらいしか、簡単に扱えるものが無さそうなのが少し残念ですね。CodeDeploy を組み合わせれば、もうちょっと自由度は上がりそうですが EC2 の管理はあまりしたくないですね。

EKS が Windows に対応したり、CodeBuild が Windows でも Docker Image が作れるようになれば、もうちょっと楽しくなりそうだと感じました。

Visual Studio 2017 に .NET Framework 4.7.2 SDK をインストールする

通常は Visual Studio Installer から .NET Framework の SDK はインストール出来るはずですが、何故か 4.7.2 はまだ落ちてこないので手動でインストールしようとしましたが、何故か入りませんでした。

地味にはまったのでメモとして残しておきます。

.NET に関しては以下のページから .NET Core / Framework を含め SDK や Runtime をダウンロード出来ますが、少なくとも日本語でダウンロードリンクを踏むと言語パックのみ落ちてきます。これが罠でした。

ダウンロードリンクからは Developer Pack が落ちてくるように読み取れますが、実際には言語パックなので何回インストールしても当然ながら 4.7.2 向け開発が出来るようになりません。

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

本物の Developer Pack は以下のページにあるリンクから落としておけば良いです。こっちをインストール後に言語パックをインストールすれば、正しい順番となります。

What's new in the .NET Framework | Microsoft Docs

インストールすれば Visual Studio から .NET Framework 4.7.2 が選べるようになります。

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

地味に 4.7.2 はパフォーマンス周りの取り組みや、ASP.NET 周りでは DI に対応したり SameSite Cookie 対応など機能が多いので、出来れば早めに移行したいと思っています。

ASP.NET の DI はサンプルコードだと動かなかったので、ちょっと検証後にまとめようかと思います。

SignalR のメトリクスを Application Insights に直接送信する

まともに触ったのは数年振りという感じがしますが、前々から SignalR のメトリクスはパフォーマンスカウンターに書き込まれるようになっていて、App Service などから扱いにくいなと思ってました。

最近は ARM 経由で App Service のパフォーマンスカウンターが取れるようになってますが、一部の値だけなので扱いにくいことには変わりないです。

特に SignalR のカウンターはインストールが必要なので、この時点で App Service では無理です。

そもそも Application Insights を使っているなら、パフォーマンスカウンターを経由せずにテレメトリを送信すればよいのではと思ったので、Telemetry Module を書いてみました。

インストールして、ApplicationInsights.config に TelemetryModule を追加すれば良いです。

<?xml version="1.0" encoding="utf-8"?>
<ApplicationInsights xmlns="http://schemas.microsoft.com/ApplicationInsights/2013/Settings">
  <TelemetryModules>
    <Add Type="SignalR.AppInsights.SignalRPerformanceCollectorModule, SignalR.AppInsights" />
  </TelemetryModules>
</ApplicationInsights>

アプリケーションをデプロイすれば、SignalR のメトリクスが Application Insights で確認できるようになります。今は 60 秒間隔でカスタムのパフォーマンスカウンターとして送信しています。

Application Insights で確認できるようになるまで少しかかりますが、しばらくすると選択肢に SignalR のパフォーマンスカウンターが出てくるようになります。

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

実際に Web App を 3 インスタンス用意して、Backplane として Redis を設定したアプリケーションに対して、トランスポートを切り替えて 200 接続ぐらい行った時の値です。

接続毎にメッセージを 100ms 間隔で送信しているので、秒間のメッセージ数がかなり多いことが把握できます。Backplane のメトリクスはパフォーマンスカウンターがないと把握出来ない値でしたが、Application Insights に送信すれば一目で分かりますね。

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

今は SignalR のパフォーマンスカウンターの値を全て送信していますが、この辺り設定可能にしておいても良いのかなと少し思っています。

余力があれば config から変更可能にしようかなという気分です。

ASP.NET でも Cookie Policy への対応を行えるか試した

ASP.NET Core 2.1 で入った Cookie Policy が面白かったのと、ASP.NET というか IIS でも HttpModule を使うことで対応出来そうだったので実装して試すことにしました。

行うことは非常に単純なので、HttpModule を書いたことがあれば大体は想像がつくはずです。

  • トラッキングを許可するクッキーがあるか確認
  • ある場合
    • 何もしない
  • ない場合
    • 確認のメッセージを出す
    • クライアントにクッキーを送信しない

別に Global.asax に書いても良いですが、分けた方が好きなので HttpModule として分けました。ただし DynamicModuleHelper を使って動的に追加するようにしています。

何はともあれ、今回でっち上げたコードは以下のような感じです。難しいことはしていません。

using System;
using System.Web;

using Microsoft.Web.Infrastructure.DynamicModuleHelper;

[assembly: PreApplicationStartMethod(typeof(WebApplication4.CookiePolicyModule), "Register")]

namespace WebApplication4
{
    public class CookiePolicyModule : IHttpModule
    {
        public void Init(HttpApplication context)
        {
            context.BeginRequest += OnBeginRequest;
            context.PreSendRequestHeaders += OnPreSendRequestHeaders;
        }

        private void OnBeginRequest(object sender, EventArgs e)
        {
            var application = (HttpApplication)sender;

            var cookie = application.Request.Cookies[CookiePolicy.CookieName];

            application.Context.Items[CookiePolicy.CanTrackKey] = cookie?.Value == CookiePolicy.CookieValue;
        }

        private void OnPreSendRequestHeaders(object sender, EventArgs e)
        {
            var application = (HttpApplication)sender;

            if (CookiePolicy.CanTrack)
            {
                return;
            }

            // Clear all cookies
            application.Response.Cookies.Clear();
        }

        public void Dispose()
        {
        }

        public static void Register()
        {
            DynamicModuleUtility.RegisterModule(typeof(CookiePolicyModule));
        }
    }
}
using System.Web;

namespace WebApplication4
{
    public static class CookiePolicy
    {
        public static string CookieName { get; set; } = "ASP.NET_CookiePolicy";

        internal const string CookieValue = "yes";

        public const string CanTrackKey = "CanTrack";

        public static bool CanTrack => (bool?)HttpContext.Current.Items[CanTrackKey] ?? false;

        public static string CreateConsentCookie()
        {
            return $"{CookieName}={CookieValue}; path=/";
        }
    }
}

難しいことはしていないですが、Web.config に HttpModule の追加設定を書くのが面倒だったので、DynamicModuleUtility を使って実行時に HttpModule を追加するようにしました。こっちの方が楽ですが使っていいのかわかってません。

とりあえず Web Forms で画面を作ってブラウザで実行してみます。初回なのでクッキーはありません。

f:id:shiba-yan:20180307004527p:plain:w400

Page_Load でセッションに書き込むようにしたので、通常であればセッション用のクッキーが出力されるはずですが、ブラウザで確認しても存在しないです。ちゃんと HttpModule で削除されています。

f:id:shiba-yan:20180307004641p:plain:w550

accept cookie を押して、専用のクッキーを書き込むとセッションクッキーも書き込まれるようになりました。ちなみに専用のクッキーは JavaScript で書き込む必要があるので少し注意が必要ですね。

f:id:shiba-yan:20180307004653p:plain:w550

ASP.NET Core と同じように ASP.NET / IIS でも実現出来ました。試していませんが IIS の HttpModule として実装しているので、原理的には PHP などのアプリケーションでも同様に動作するはずです。

こういうプライバシーの問題は対応が難しいですね。