しばやん雑記

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

ASP.NET (.NET Framework) 向けの各 Session State Provider が大規模アップデート

ASP.NET (.NET Framework) を利用したアプリケーションで必要になることが多い Session State Provider ですが、今年になってから大規模なアップデートが多いので一通り確認しておきました。

現状 ASP.NET 向け Session State Provider は Cosmos DB / SQL Server 向けと Redis 向けで、以下のように別々のリポジトリで開発されています。個人的には AspNetSessionState 側に統一してほしいです。

今回は全ての Session State Provider でアップデートがあったので、InProc と StateServer 以外を使っているアプリケーション全てに関係します。パフォーマンス面に影響のあるアップデートも含まれています。

共通のアップデート

Session State を実装している IIS Module 自体も 2.0.0 にアップデートされて、各 Session State Provider に共通した機能が増えています。ただし Redis に関してはアップデートが未だ行われていません。

アップデート内容を要約すると以下の 2 点になります。両方ともパフォーマンスに影響する更新です。

  • セッションの TTL 更新を行わないオプション skipKeepAliveWhenUnused が追加
    • Session State が必須になっている時だけ TTL を更新する
  • IIS Module の preConditionmanagedHandler が追加
    • これまでは静的ファイルなどでも Session State を通っていた

これまでの ASP.NET の Session State Module 実装では、Session State をページやコントローラー単位で明示的に無効化しても、セッションの TTL 更新のためにリクエストが必ず発行されていたのですが、新しく追加されたオプションではその挙動をオフに出来るようになりました。これにより大半でセッションが不要なアプリケーションで信頼性とパフォーマンスを改善することが可能です。

以前 Azure 上で ASP.NET の Session State を使うベストプラクティスについて書いたエントリで触れた部分が改善されています。設定の有効化前に読んでおいてもらえると参考になるかと思います。

IIS Module については破壊的変更が存在していないので、Redis を使っている場合でも明示的にインストールすると新機能が使えるはずなのですが、手元で試した限りでは確認できませんでした。

SQL Server 向け

主にオンプレ向けで利用されていると思われる SQL Server 向けの Session State Provider は、大幅なアップデートを含む形で 2.0.0 が公開されました。アップデート内容的にも更新は必須です。

アップデート内容は大体以下の通りになります。アップデート内容は多いですが、一番大きな点は使用している SDK が Microsoft.Data.SqlClient に更新されたことです。これにより信頼性とパフォーマンス改善だけではなく、Managed Identity への対応も容易になりました。

注意点としては既に SQL Server を Session State Provider として使っている場合には、互換性を保つために repositoryTypeFrameworkCompat を指定する必要がある点です。デフォルト値が FrameworkCompat にはなっていますが、明示的に指定した方が安全です。

デフォルトで参照されている Microsoft.Data.SqlClient は最新ではないので、必要に応じて明示的にインストールしてバージョンアップしておくと安心です。

Managed Identity を利用する際には接続文字列を変えるだけで問題ないので、以下のエントリを参照してください。古いクライアントと比べると Microsoft.Data.SqlClient では劇的に簡単になっているので、SQL Server での Managed Identity 利用を推し進めていきたいですね。

Microsoft Account では利用できないですが、大体のケースでは組織アカウントを使っているはずなので、ローカル開発時でも問題となることは少ないと思います。

Cosmos DB 向け

Azure 上での利用が多いはずの Cosmos DB を利用した Session State Provider についても、大幅にアップデートされた 2.0.0 が公開されています。こちらも更新は必須と言えます。

今回のアップデートではこれまでの謎設定が綺麗に一掃されて、デフォルトで Cosmos DB のパフォーマンスを最大限利用できるようになりました。特にパーティションキー周りの設定については、これまでの Provider では完全に無駄な実装がありました。

  • パーティションキーが /id 固定に変更
    • partitionKeyPathpartitionNumUsedByProvider の廃止
  • 使用している SDK が Microsoft.Azure.Cosmos に変更
  • プロパティの命名が collectionId => containerId に変更
  • consistencyLevel の追加
  • connectionProtocol の廃止

中でも一番大きいアップデートは SDK が Cosmos DB SDK の v3 ベースになっていることです。これまでの Provider は廃止が予定されている v2 ベースでしたので、来年にはサポートされなくなるところでした。

ただし、こちらも SQL Server の Provider と同様に SDK バージョンが既に古くなっているので、明示的にパッケージをインストールしておくのをお勧めしています。

特に Cosmos DB SDK については推奨される最小バージョンが公開されていて、それが現在 3.33.0 となっているので SDK の明示的なアップデートは必須と言えます。

今回から Session を保持する Container のパーティションキーは /id 固定となっているので、現在使用している Container のパーティションキーをカスタマイズしてしまっている場合には、既存セッションは失われてしまいますが作成しなおしてしまうのがお勧めです。

Redis 向け

Cosmos DB や SQL Server とは開発チーム自体が別のようで、微妙に Session State Module のアップデートには追従できていないようなのですが、今年になってから 5.0.0 が公開されています。

破壊的変更が含まれていますが、アップデート内容としては少なめです。要約すると他の Session State Provider の実装に近付けられたという感じです。

例によって、こちらもベースとなっている StackExchange.Redis のバージョンが古くなっているので、明示的にアップデートしておくのがお勧めです。

Redis 向けについては Azure チームが開発していて、それ以外は ASP.NET チームが開発しているようなのですが、どこかで AspNetSessionState リポジトリ側に開発がマージされて欲しいですね。

App Service Authentication (Easy Auth) と ASP.NET のフォーム認証を共存させつつ利用する

App Service Authentication と ASP.NET を組み合わせて使うと、自動的に Thread.CurrentPrincipal に App Service Authentication で認証したユーザー情報を格納してくれるので、アプリケーション側では Request.IsAuthenticatedUser.Identity でログイン状態やユーザーの情報を確認できて便利です。

ASP.NET 側で以下のようなコードを書くだけで、ログインユーザーの情報を表示できます。

<p>
  @if (Request.IsAuthenticated)
  {
    <text>Login user: </text><b>@User.Identity.Name</b>
  }
</p>

実際に App Service Authentication を有効化した App Service にデプロイしてアクセスすると、以下のように Azure AD でログイン中のユーザー情報が表示されます。

非常に便利なので、もし新規で ASP.NET アプリケーションをデプロイする場合、あるいは既存のアプリケーションの認証を App Service Authentication に移行する場合は、少ない手間で実現できるのでお勧めです。

しかし、既存のアプリケーションが ASP.NET のフォーム認証を利用している場合には少し話が変わってきます。何故なら App Service Authentication は ASP.NET の認証周りをオーバーライドすることで、認証したユーザー情報をセットしているからです。

例えば以下のような簡単な FormsAuthentication を利用するコードと設定を追加します。

public class AccountController : Controller
{
    // GET: Account
    public ActionResult Login()
    {
        FormsAuthentication.SetAuthCookie("shibayan", true);

        return RedirectToAction("Index", "Home");
    }
}
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.8" />
    <httpRuntime targetFramework="4.8" />
    <authentication mode="Forms" />
  </system.web>
</configuration>

この状態でフォーム認証でのログインを行っても、表示されるユーザー情報は以下のように App Service Authentication で認証されたものが表示され続けます。フォーム認証のクッキーは発行されていますが、それが使われているように見えません。

本番環境向けであれば 2 重に認証を有効化することはないので問題ないですが、ステージング環境など開発者のみ確認可能にしたい場合にはログイン画面すら見せたくないので App Service Authentication を使って App Service 全体を守る必要が出てきます。

そのような用途ではアプリケーション側にユーザー情報を渡す必要がそもそもないため、App Settings に WEBSITE_AUTH_DISABLE_IDENTITY_FLOW を追加することで回避可能です。

WEBSITE_AUTH_DISABLE_IDENTITY_FLOW の名前からは挙動が想像付きにくいですが、ASP.NET へのユーザー情報の反映を無効化する設定なので、以下のように App Settings で true を設定すれば良いです。

設定後に再度アクセスすると、App Service Authentication のユーザー情報ではなくフォーム認証のユーザー情報が表示されることが確認出来ます。

これでステージング環境などのアクセス制限をアプリの認証とは別に付けることが出来ました。

実際 App Service Authentication は Twitter / Facebook / Google といった B2C 向けのユーザー認証以外にも、Azure Active Directory を使って IP 制限のように限られたユーザーのみアクセスさせる用途にも使えるので、ステージング環境やスロットでは積極的に使っていきたい機能です。

Azure で ASP.NET の Session State を利用する際のベストプラクティスを確認した

最近は移行絡みで Azure に ASP.NET アプリケーションを Session State 付きでデプロイすることが多いので、Azure 上で Session State を使う際のベストプラクティスを確認しておきました。

この記事で触れるのは .NET Framework の ASP.NET であって、.NET 6 で動作する ASP.NET Core 向けではないことに注意してください。ASP.NET Core 向けなら何も考える必要なく、パッケージをインストールして設定を追加すれば全く問題なく利用できます。

ASP.NET の場合は Session State が IIS と密接に関係しているため若干複雑になっています。

多少長くなったので目次記法を使いました。興味のある部分だけピックアップしてください。

注意したい ASP.NET Session State 特有の動作

まずは ASP.NET の Session State を使う上で注意したい動作をいくつかピックアップしました。最近はこの辺りの情報が少なくなっているので、余計に注意が必要な状況です。

Session に書き込まない限りはクッキーは発行されない

これは既に知っている方も多そうですが、ASP.NET では Session.SessionID を使って現在のセッション ID を取得することが出来ますが、これだけではクッキーが発行されないのでアクセスの度に値が変化します。

クッキーが発行される条件は Session に対して値を書き込む必要があるので、Session.SessionID を参照する場合はダミーでも良いので値を書き込んでおきます。

Session State を無効化してもデータストアへのアクセスは行われる

ASP.NET Web Forms ではページ単位、ASP.NET MVC の場合は Controller 単位などで、簡単に Session State の有効・無効・読み取りのみが設定できます。設定方法は違えど、これらは全て HttpContext に用意された SetSessionStateBehavior を呼び出しているだけです。

ここで SessionStateBehaviorDisabled を設定すると、データストアへのアクセス自体が完全に行われなくなることを期待していると思いますが、実際には既にクッキーが発行されていると有効期間の延長のためにアクセスが行われます。読み込み自体は行われませんが、それなりにコストはかかります。

個人的には Disabled の場合は期間延長自体も欲しくないのですが、この辺りの実装は Session State Module 側で行われているので対応が難しいです。

非同期 Session State Module をインストールした際の設定変更

先ほどの動作と大きく関係するのが非同期 Session State Module をインストールすると、組み込みと若干動作が変わることです。具体的には静的ファイル含むリクエストが Session State Module を通るようになります。

該当モジュールの preConditionmanagedHandler から integratedMode に変更されたことで、ネイティブ実装の StaticFileHandler でも有効化されています。この変更については Issue が上がっています。

正直な話、これは非同期 Session State Module の不具合だと思います。Session State を ASP.NET などのマネージドコード以外で使う必要が、殆ど存在しないからです。

静的ファイルを Blob や CDN から配信している場合には影響しませんが、同じ IIS から配信している場合には直撃するので、設定の変更や CDN でのキャッシュを強く推奨します。

Session State Store に Redis / Cosmos DB を利用する

ASP.NET では Session State Server と SQL Server が組み込みで用意されていますが、Azure 上では Redis と Cosmos DB というより優れた選択肢が用意されているので、基本的にはこのどちらかを使うようにします。

既に Redis をキャッシュ用に使っている場合は、そこに相乗りさせた方がコストは最適化出来ます。Cosmos DB はオートスケールによって 100 RU から開始出来るのと、Serverless プランが用意されているので、開発用から本番利用まで広く使えます。

Redis を使うケース

既に Redis の Session State Provider を使っているケースは多いと思います。Redis のマネージドサービスは多いので Azure 以外でも利用できるのが特徴です。

基本はドキュメント通りに設定しておけば問題ないですが、.NET Framework かつアクセスの多いアプリケーションの場合は、スレッドプールが枯渇してエラーとなることがあります。

Azure Cache for Redis のドキュメントにスレッドプール周りでの重要な情報が載っているので、こちらを参照して予めスレッドプールを拡大させておくのがお勧めです。

.NET 6 などと異なり .NET Framework はデフォルトのスレッドプールが小さいので問題となりやすいです。

Cosmos DB を使うケース

比較的新しい Session State Provider なのが Cosmos DB です。Cosmos DB の低レイテンシでグローバル分散というメリットを出来ますし、TTL も組み込まれているのでキャッシュやセッションに向いています。

こちらもパッケージをインストールして接続に必要な情報を appSettings に追加するだけで利用可能です。

Cosmos DB 固有の設定として接続方式が Direct / Gateway、そしてプロトコルが Https / TCP というように選択可能になっていますが、Direct と TCP の組み合わせを強く推奨しています。

非同期 Session State Module の新機能を有効化する

Cosmos DB の Session State Provider がリリースされたタイミングで、非同期 Session State Module にいくつか新機能が追加されています。詳細は以前書いたのでそちらを参照してください。

非同期 Session State Module に追加された機能で重要なのが aspnet:AllowConcurrentRequestsPerSession になります。デフォルトの Session State Module は同一セッションに対する並列実行を許可していないため、同じセッション ID を持ったリクエストが複数来るとブロッキングするようになっています。

この挙動は現代の SPA など API を多用する場合にはパフォーマンス上の問題になりやすいのと、セッションへのアクセスが排他であることを前提にしたコードが少ないため、並列実行の許可をお勧めしています。

前述した preCondition の設定変更と合わさると、この挙動は大変なパフォーマンス問題に繋がります。

CDN でキャッシュ可能なコンテンツは積極的にキャッシュする

Session State Module の preCondition 設定を修正すると、静的ファイルは Session State Module の影響を受けませんが、IIS から毎回配信するのはコストが高いため可能な限り CDN でキャッシュさせます。

Web.config にある staticContent を設定すると、静的ファイルの Cache-Control ヘッダーなどを一括で設定可能です。ASP.NET で処理されるリクエストとは別扱いなので安全です。

これは ASP.NET 固有ですが、System.Web.Optimization を使った CSS / JavaScript のバンドル化を行っていると、その部分は ASP.NET のパイプラインで実行されるようになるので、どう頑張っても Session State Module を必ず通ります。

デバッグ実行中はバンドルされないので、本番にデプロイしてから発覚しやすい問題です。

バンドル結果はメモリ内にキャッシュされているので高速ですが、無駄に Session State が構築されてしまうのでバンドルされたファイルは、必ず CDN やブラウザにキャッシュさせておきましょう。

そもそも Session State を使う必要があるか検討する

今更感ある話題を最後に持って来ていますが、Azure で動かす際に本当にその Session State は必要なのかという点から考え直すのがお勧めです。

案外 Session State は有効化していたけどアプリケーションでは使っていなかったというパターンも出てくると思います。地味に障害の原因となりやすい部分なので、不要なものは削っていきましょう。

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 が作れるようになれば、もうちょっと楽しくなりそうだと感じました。