はじめに
マルチテナントSaaSのバックエンドをEC2からECS Fargateに移行した際、地味だけれど厄介な問題にぶつかりました。Fargateタスクをスケールアウトした瞬間、ユーザーが突然ログアウトされたり、Cookieが無効扱いになったりするという現象です。
原因はASP.NET Coreの「Data Protection」機構でした。この記事では、症状の見え方から根本原因、EFSを使った解決までを整理します。同じ構成(ASP.NET Core + Fargate + マルチタスク構成)で似た壁にぶつかる人の助けになれば幸いです。
起きていたこと
構成としては、以下のようなよくあるパターンでした。
- ASP.NET Core(Cookie認証 / 何らかのトークン保護を利用)
- ALB配下にFargateタスクを複数配置
- タスク数はオートスケールで増減する
この状態で以下のような症状が出ます。
- タスクが1つのときは問題なく動く
- タスク数が2以上になったタイミングで、一部のリクエストだけ認証エラーになる
- 特に再デプロイやスケールアウトの直後に集中してユーザーから「ログインできない」「急にログアウトされた」という報告が来る
再現条件から「特定タスクに固定してアクセスすると問題ない」ことが分かり、タスク間で何かが共有されていないことが疑われました。
根本原因:Data Protectionキーがタスクごとにバラバラ
ASP.NET Coreは、Cookie・CSRFトークン・一部の認証系機能などで「Data Protection」という仕組みを使ってデータを暗号化/復号しています。この仕組みは内部的に暗号鍵(キーリング)を生成・保持しており、同じキーリングを共有していない別インスタンスが復号を試みると失敗します。
EC2で1台構成、あるいはIISでの単一プロセス運用をしている間はこの問題が表面化しません。デフォルトではキーはローカルファイルシステム(またはレジストリ)に保存され、そのまま同一プロセス/同一マシンで使い続けられるからです。
しかしFargateでは事情が変わります。
- タスクごとに独立したファイルシステムを持つ(コンテナが再作成されればキーも消える)
- スケールアウトで複数タスクが同時稼働すると、タスクAが発行したCookieをタスクBが受け取っても復号できない
- デプロイのたびに新しいタスクに置き換わるため、既存キーを引き継げない
ALBは基本的にラウンドロビンやリクエスト単位でタスクに振り分けるため、ユーザーのリクエストが毎回同じタスクに当たる保証はありません。これが「一部のリクエストだけ失敗する」という不安定な症状の正体でした。
解決策:EFSでキーリングを共有する
対処方針はシンプルで、Data Protectionのキーリングをタスク間で共有された永続ストレージに置くことです。今回はEFS(Elastic File System)をFargateタスクにマウントし、そこにキーを保存する形にしました。
1. EFSファイルシステムを用意する
CloudFormation等でEFSを作成し、ECSタスクが所属するVPC・サブネットからアクセスできるようマウントターゲットを設定します。セキュリティグループでNFS(2049番ポート)をタスク側から許可する点を忘れずに。
2. タスク定義にEFSボリュームを追加する
ECSのタスク定義で、EFSをボリュームとしてマウントします(イメージ)。
{
"volumes": [
{
"name": "dataprotection-keys",
"efsVolumeConfiguration": {
"fileSystemId": "fs-xxxxxxxx",
"transitEncryption": "ENABLED"
}
}
],
"containerDefinitions": [
{
"mountPoints": [
{
"sourceVolume": "dataprotection-keys",
"containerPath": "/keys",
"readOnly": false
}
]
}
]
}
3. アプリ側でキーの保存先を明示する
Program.cs(またはStartup.cs)で、Data Protectionの保存先をマウントしたパスに向けます。
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("/keys"))
.SetApplicationName("NextNavinity"); // アプリ名を固定しないと生成されるキーの識別子がズレるので注意
ポイントはSetApplicationNameを明示的に固定することです。指定しない場合、内部的にアプリのパスなどから自動生成される識別子が環境ごとに変わり得るため、キーを共有していても正しく紐付かないことがあります。
4. パーミッションの確認
コンテナ内のプロセスユーザーがEFSマウントパスに書き込み権限を持っているかも要確認ポイントです。非rootユーザーで動かしている場合、マウントオプションやEFSアクセスポイントのUID/GID設定で書き込めずに静かに失敗する、というのもハマりがちなポイントでした。
結果
EFSにキーリングを共有した後は、タスク数を増減させても認証エラーは発生しなくなりました。デプロイでタスクが総入れ替えになっても、既存のCookieが引き続き有効な状態を維持できています。
まとめ・学び
- ASP.NET CoreをEC2の単一構成からコンテナのマルチインスタンス構成に持っていくときは、Data Protectionのキー共有を必ず検討する
- 症状が「一部のリクエストだけ失敗する」「スケール直後に集中する」という形で出るときは、インスタンス間で共有されていない状態を疑う
- EFSでの共有は手軽だが、
SetApplicationNameの固定とパーミッション設定を忘れると解決しない - 同じ悩みは(Redisなどの分散キャッシュにキーを保存する
PersistKeysToStackExchangeRedisという選択肢もあるので、既にRedisを使っている構成ならそちらも検討候補)
似たような「単一サーバー前提で作られたASP.NET Coreアプリをコンテナのマルチインスタンス構成に持っていく」際は、Data Protection以外にも共有すべき状態がないか(セッション、一時ファイル、ローカルキャッシュなど)を洗い出しておくと、同種のハマりを事前に潰せます。