0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【GCPリフト&シフト戦記 #1】LB配下でログインが突然切れる!?ASP.NET Coreアプリを「ステートレス」に矯正する泥臭い戦い

0
Posted at

はじめに

こんにちは。オンプレミスのC#資産をクラウド(GCP)へリフト&シフトするプロジェクトに奮闘している皆さん、「とりあえずVMを2台並べてロードバランサ(LB)を置けば、可用性も上がってクラウドっぽい構成になるだろう」と軽く考えていませんか?

かつての私がそうでした。しかし、意気揚々とLB配下にASP.NET CoreのWebアプリを並べた途端、「ログインした直後にログアウトされる」「画面遷移した瞬間にセッションエラーで弾かれる」という謎の現象(通称:インスタンスガチャ)に悩まされることになりました。

本連載では、オンプレミスで動いていた典型的なC#業務システムを、クラウドでオートスケール(または複数台運用)できる「クラウドネイティブ」な姿へ進化させるための泥臭い知見を共有します。
第1回は、アプリケーションのステートレス化(セッションと暗号化キーの外部化)がテーマです。

image.png

【前提環境】

  • C# / .NET 8
  • ASP.NET Core MVC & Identity
  • Google Cloud Platform (GCP)
  • Compute Engine (VM) + Cloud Load Balancing
  • SQL Server

1. インスタンスガチャとセッション消失の罠

【起結】⏱ 10秒まとめ:本章の結論
複数台のVMでWebアプリを動かすと、デフォルトの「インメモリセッション」ではリクエストごとにセッションが消失してしまいます。これを解決するため、セッションの保存先を「SQL Server分散キャッシュ」へ外部化します。

【承】🌩 課題:こんなこと困るよね。難しいよね。
オンプレミス時代、Webサーバーが1台だった頃は何も問題ありませんでした。ユーザーがログインすると、サーバーのメモリ上にセッション情報が保存され、問題なく業務ができていました。
しかしGCPに移行し、可用性を高めるためにVMを2台(VM-A, VM-B)にしてLB配下に置いた瞬間、地獄が始まりました。
1回目のリクエストはVM-Aに飛んでログイン成功(VM-Aのメモリにセッション保存)。しかし次の画面遷移で、2回目のリクエストがVM-Bに振り分けられると、VM-Bは「えっ、君誰?セッション持ってないじゃん」と突き返し、ユーザーは無情にもログイン画面へ強制送還されます。これが恐怖の「インスタンスガチャ」です。

【転】💡 解決策:採用理由と他の選択肢
この問題を解決するためのアーキテクチャ設計(ADR)は以下の通りです。

  • 選択肢A: スティッキーセッション(セッションアフィニティ)を有効にする
    • 見送り理由: LBの設定で「同じユーザーは常に同じVMに飛ばす」機能です。手軽ですが、これでは特定のVMに負荷が偏りやすく、スケールイン(VMの削減)が発生した瞬間にそのVMに繋がっていたユーザー全員が強制ログアウトされるため、クラウドの弾力性を殺してしまいます。
  • 採用案: SQL Serverによる分散セッション(AddDistributedSqlServerCache
    • 採用理由: セッション情報を個々のVMのメモリではなく、全VMからアクセスできるDBに外出し(外部化)します。Redisを使うのが王道ですが、社内システムのリフト&シフト初期段階であり、インフラの管理コンポーネントを増やしたくなかったため、既存のSQL Serverをセッションストアとして活用する方式を採用しました。

【結】💻 具体例・サンプルコード

Program.cs にて、デフォルトの AddDistributedMemoryCache() を削除し、SQL Server キャッシュに置き換えます。
(※事前に dotnet sql-cache create コマンドでDBにテーブルを作っておく必要があります)

// Program.cs

// ❌ アンチパターン (オンプレ・1台構成の設定)
// builder.Services.AddDistributedMemoryCache();

// ✅ クラウドネイティブな設定 (複数台構成対応)
var connectionString = builder.Configuration.GetConnectionString("AppDbContext");

builder.Services.AddDistributedSqlServerCache(options =>
{
    // セッションデータを保存するDBの接続文字列
    options.ConnectionString = connectionString;
    // 作成したセッション管理用テーブルのスキーマと名前を指定
    options.SchemaName = "dbo";
    options.TableName = "AspnetSessionState";
});

// セッションの設定(タイムアウト30分)
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});

var app = builder.Build();

// 注意: UseSessionは必ずUseAuthenticationより前に呼ぶこと!
app.UseSession();
app.UseAuthentication();

🔰 初心者向け解説:ステートレス(Stateless)とは?
役所の窓口をイメージしてください。
オンプレ時代(ステートフル)は「担当者の山田さん(VM-A)の頭の中」にあなたの手続き状況が記憶されていました。翌日、鈴木さん(VM-B)の窓口に行くと「最初から説明してください」と言われます。
クラウド時代(ステートレス)は、「共有の引き出し(DB)」にカルテを保存します。これなら、山田さんでも鈴木さんでも、カルテを取り出せば続きから手続きしてくれます。これがスケーラビリティの基本です。

2. 盲点!Data Protection Key(暗号化キー)の共有漏れ

【起結】⏱ 10秒まとめ:本章の結論
セッションを外出ししても、ASP.NET Core Identityの認証Cookieを復号できなければ結局エラーになります。DataProtection のキー保存先をデータベースに変更し、全サーバーで暗号化キーを共有しなければなりません。

【承】🌩 課題:こんなこと困るよね。難しいよね。
「よし、セッションをDBに移したぞ!これでロードバランサでガンガン振り分けられても大丈夫だ!」
そう思ってテストをすると……まだ稀にログイン画面に戻される事象が発生しました。ログを見ると Error unprotecting the session cookie.(Cookieの復号エラー)と出ています。
ASP.NET Coreはセキュリティが強固で、セッションCookieや認証Cookieをサーバー内で生成されたキーで暗号化してクライアントに返します。デフォルトでは、この暗号化キー(Data Protection Key)は 「サーバーのローカルディスク(またはメモリ)」 に保存されます。
つまり、VM-Aで暗号化されたCookieを持ったユーザーがVM-Bにアクセスすると、VM-Bは「俺が持ってる鍵と違うから、このCookieは偽造だ!破棄!」と判断してしまうのです。

【転】💡 解決策:採用理由と他の選択肢
この「暗号化キー共有問題」を解決するためのアーキテクチャ設計です。

  • 選択肢A: Google Cloud Storage (GCS) にキーファイルを置く
    • 見送り理由: Google.Cloud.Storage.DataProtection のライブラリを使うことでGCSにキーを保存し、全VMで共有できます。非常に優れた選択肢ですが、DBに比べてGCSのアクセス権限(IAM)の設定やローカル開発環境との差異の吸収に手間がかかるため今回は見送りました。
  • 採用案: Entity Framework Core を使ってDBにキーを永続化する
    • 採用理由: すでにASP.NET Core Identity等でEntity Framework Core(AppDbContext)を使用していたため、Microsoft.AspNetCore.DataProtection.EntityFrameworkCore パッケージを追加するだけで、最も簡単かつ安全にキーをDBに保存・共有できたためです。

【結】💻 具体例・サンプルコード

NuGetで Microsoft.AspNetCore.DataProtection.EntityFrameworkCore をインストールし、Program.cs に以下の設定を追加します。

// Program.cs

// Data Protection キーを SQL Server(AppDbContext)に永続化する
// これにより、複数サーバー(LB配下)で同じキーを使ってCookieの暗号化/復号が可能になる
builder.Services.AddDataProtection()
    // 全VMで同じアプリケーションとして認識させるための識別名
    .SetApplicationName("My_Cloud_System")
    // Entity Framework の DbContext を使ってキーをDBの [DataProtectionKeys] テーブルに保存
    .PersistKeysToDbContext<AppDbContext>();

(※ AppDbContext 側に IDataProtectionKeyContext のインターフェース実装と DbSet の定義が必要です)

🔰 初心者向け解説:Data Protection Keyとは?
スパイが使う「日替わりの暗号解読表」のようなものです。
東京支部のスパイ(VM-A)と大阪支部のスパイ(VM-B)が、それぞれ自分勝手に暗号表を作っていると、お互いの暗号文(Cookie)が読めません。だから、本部(DB)の金庫に共通の暗号表を入れておき、全員がそれを使って暗号の作成と解読を行うようにする仕組みです。

終わりに

オンプレミスのC#資産をクラウドに移行する際、コードを1行も書かずにVMに載せ替えるだけでは、ロードバランサやオートスケールの恩恵を受けることはできません。
まずは「ローカルのメモリやディスクを一切信用しない(ステートレスにする)」という矯正ギプスをアプリケーションにはめることが、クラウドネイティブへの第一歩です。

次回(第2回)は、「Windowsタスクスケジューラからの脱却と、バッチ処理のモダナイズ(Worker編)」をお届けします。クラウドで複数台のバッチが同時に起動してデータが破壊される地獄から、いかにして抜け出したかを解説します。お楽しみに!

📖 単語一覧 (Glossary)

  • リフト&シフト (Lift and Shift): システムをオンプレミスからクラウドへ移行する際、まずは環境をそのまま(Lift)移行し、その後クラウドネイティブな形へ改修(Shift)していくアプローチ。
  • ステートレス (Stateless): サーバー自身がユーザーの状態(ログイン情報など)を保持しない設計。どのサーバーにリクエストが飛んでも同じ結果を返せるため、スケールアウト(台数追加)が容易になる。
  • スティッキーセッション: ロードバランサが、同じユーザーからの通信を常に同じサーバーに振り分ける仕組み。「セッションアフィニティ」とも呼ばれる。
  • Data Protection (データ保護): ASP.NET Coreが提供する、Cookieやトークンなどの機密データを暗号化・復号化するための暗号化APIセット。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?