はじめに
こんにちは。オンプレミスのC#資産をクラウドへリフト&シフトする連載の第3回です。
過去2回を通して、Webサーバーのセッションを外部化し、バッチ処理をジョブキュー方式へと変更することで、安全に複数台構成(スケールアウト)ができる状態を作りました。
いよいよ今回は、「クラウドネイティブサービス(マネージドサービス)への歩み寄り」がテーマです。
オンプレミス時代、私たちは無意識のうちに「ローカルディスク」と「設定ファイル」に大きく依存していました。C:\temp\uploads にユーザーのアップロードファイルを保存し、appsettings.json にデータベースの接続パスワードを堂々と書き込んでいませんでしたか?
クラウド環境において、これらの「物理的な場所に依存する」運用は、セキュリティリスクとスケーラビリティの阻害要因になります。本記事では、GCPのマネージドサービスを活用して、アプリケーションから「ディスク依存」と「パスワード直書き」を排除した泥臭いプロセスを共有します。
【前提環境】
- C# / .NET 8
- ASP.NET Core
- Google Cloud Platform (GCP)
- Secret Manager
- Cloud Storage (GCS)
1. パスワード直書きの恐怖と Secret Manager の導入
【起結】⏱ 10秒まとめ:本章の結論appsettings.json にDB接続文字列やAPIキーを平文で書くのはセキュリティ上危険です。Google Cloud Secret Manager を導入し、アプリケーションの起動時に動的に機密情報を取得・構成(Configuration)に統合する仕組みを構築します。
【承】🌩 課題:こんなこと困るよね。難しいよね。
オンプレ時代は「サーバーの中に入れるのは限られた管理者だけだから」という理由で、平気でDBのパスワードや外部連携API(SendGridなど)のキーを appsettings.json に書き込んでいました。
しかし、開発メンバーが増え、ソースコードがGitで管理されるようになると話が変わります。「誰かが誤って本番のパスワードをコミットしてしまう」というインシデントのリスクが跳ね上がります。さらに、パスワードを変更するたびにアプリケーションの再デプロイが必要になるという運用上の負債も抱えていました。
【転】💡 解決策:採用理由と他の選択肢
機密情報をソースコードから完全に分離するためのアーキテクチャ設計(ADR)です。
-
選択肢A: サーバーの「環境変数」にセットする
- 見送り理由: 手軽な方法ですが、VM(Compute Engine)を作り直すたびに環境変数を設定し直す必要があり、MIG(マネージドインスタンスグループ)によるオートスケール運用と相性が悪いため見送りました。
-
採用案: Google Cloud Secret Manager の採用
- 採用理由: クラウド標準の機密情報管理サービスです。GCP上で一元管理でき、アクセス権限(IAM)も厳密に制御できます。.NETの
Google.Cloud.SecretManager.V1ライブラリを使えば、起動時にAPI経由でシークレットを取得し、ASP.NET CoreのIConfigurationに自然に統合できるため、アプリケーションの既存コード(_config["ConnectionStrings:AdminContext"]など)をほとんど書き換えずに移行できる点が決め手でした。
- 採用理由: クラウド標準の機密情報管理サービスです。GCP上で一元管理でき、アクセス権限(IAM)も厳密に制御できます。.NETの
【結】💻 具体例・サンプルコード
appsettings.json には「どのシークレットを取得するか」の名前だけを書き、実際のパスワードは空(またはダミー)にしておきます。Program.cs の起動時に Secret Manager から値を取得して設定を上書きします。
// appsettings.json
{
"GoogleCloud": {
"ProjectId": "sample-project-prd",
"SecretNames": [
"sample-prd-secret-db-connection",
"sample-prd-secret-api-sendgrid"
]
},
"ConnectionStrings": {
// パスワードは絶対に書かない。実行時にSecret Managerの値で上書きされる
"AdminContext": "***"
}
}
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// ★ローカル開発環境以外(GCP環境)の場合のみ、Secret Managerから機密情報を読み込む
if (!builder.Environment.IsDevelopment())
{
// 内部で Google.Cloud.SecretManager.V1 APIを呼び出し、
// 指定されたSecretNamesの値を一括でConfigurationにマージする拡張メソッド
builder.Configuration.AddGcpSecretManager();
}
// 取得した接続文字列は、通常通りIConfigurationから安全に読み出せる
var connectionString = builder.Configuration.GetConnectionString("AdminContext");
builder.Services.AddDbContext<AdminContext>(options =>
{
options.UseSqlServer(connectionString);
});
🔰 初心者向け解説:Secret Managerとは?
銀行の「貸金庫」のようなものです。
アプリケーションは現金(パスワード)を財布(appsettings.json)に入れて持ち歩くのではなく、「貸金庫の鍵(GCPのサービスアカウント権限)」だけを持ち歩きます。アプリが起動するたびに銀行(Secret Manager)に行って現金を引き出し、メモリ上だけで使う仕組みです。これなら、もし財布を落としても被害はありません。
2. 消えるアップロードファイルと Cloud Storage (GCS) への移行
【起結】⏱ 10秒まとめ:本章の結論
複数台のVMでシステムを稼働させる場合、ローカルディスクへのファイル保存は厳禁です。Google Cloud Storage (GCS) などのオブジェクトストレージを採用し、ファイルの読み書きをネットワーク越しのAPI操作に置き換える必要があります。
【承】🌩 課題:こんなこと困るよね。難しいよね。
「ユーザーがアップロードした顔写真が表示されないぞ!」
VMを2台構成(VM-A, VM-B)にした直後、カスタマーサポートからクレームが入りました。調べてみると原因は単純。VM-Aに繋がったユーザーが画像を C:\inetpub\wwwroot\uploads\photo.jpg に保存したのに、画像を表示しようとした次のリクエストはロードバランサによってVM-Bに振り分けられたため、VM-Bのディスクにはその画像が存在しなかったのです。
オンプレミス時代なら「共有ファイルサーバー(NAS)」をマウントすれば解決しましたが、クラウドでSMBプロトコルのファイルサーバーを維持するのはコストも管理の手間もかかります。
【転】💡 解決策:採用理由と他の選択肢
複数サーバー間でファイルを共有するためのアーキテクチャ設計です。
-
選択肢A: Cloud Filestore(マネージドNFS)をVMにマウントする
- 見送り理由: 既存の「ファイルパス指定のコード」を変更せずに済むため移行は楽ですが、最低容量が大きく、ランニングコストが月額数万円〜と高額になるため、要件に見合わないと判断しました。
-
採用案: Google Cloud Storage (GCS) と専用インターフェースの導入
- 採用理由: クラウドネイティブなオブジェクトストレージです。容量単価が非常に安く、HTTPベースのAPIでどこからでもアクセス可能です。アプリケーション側に
IBusinessStorageServiceというインターフェースを新設し、「ファイルの保存先がローカルなのかGCSなのか」を意識せずにビジネスロジックを書けるようにリファクタリングを行いました。
- 採用理由: クラウドネイティブなオブジェクトストレージです。容量単価が非常に安く、HTTPベースのAPIでどこからでもアクセス可能です。アプリケーション側に
【結】💻 具体例・サンプルコード
ローカルのファイルI/Oクラス(File.WriteAllBytesなど)を直接使うのをやめ、設定ファイルからGCSのバケット名を読み込んで操作する専用サービスをDIコンテナに登録します。
// appsettings.json
{
"GcsSettings": {
"TempBucketName": "sample-prd-storage-temp",
"AssetsBucketName": "sample-prd-storage-assets"
}
}
// Program.cs
// GCSの設定を読み込み、業務ロジック用のストレージサービスをDI登録する
builder.Services.Configure<GcsSettings>(builder.Configuration.GetSection("GcsSettings"));
builder.Services.AddScoped<IBusinessStorageService, BusinessStorageService>();
// 業務ロジック側 (Controller や Service)
public class UserProfileService
{
private readonly IBusinessStorageService _storageService;
// 依存性の注入(DI)でストレージサービスを受け取る
public UserProfileService(IBusinessStorageService storageService)
{
_storageService = storageService;
}
public async Task UploadProfileImageAsync(string userId, byte[] imageData)
{
// ❌ アンチパターン: ローカルディスクへの直接書き込み
// File.WriteAllBytes($"C:\\temp\\{userId}.jpg", imageData);
// ✅ クラウドネイティブ: インターフェース越しのGCS書き込み
var objectName = $"profiles/{userId}.jpg";
await _storageService.SaveToAssetsAsync(objectName, imageData);
}
}
🔰 初心者向け解説:Cloud Storage (オブジェクトストレージ) とは?
「無限に入る、ネットワーク越しの巨大な貸し倉庫」です。
今までは自分のデスクの引き出し(ローカルディスク)に書類を入れていたので、隣の席の人(別のVM)からは見えませんでした。GCSはインターネット上にある共有の貸し倉庫なので、どの席からでも「ユーザーAの書類ちょうだい」とリクエストすれば、全く同じ書類を取り出すことができます。
終わりに
クラウドへのリフト&シフトにおいて、「IaaS(VMの仮想化)」から一歩踏み出し、「PaaS/SaaS(マネージドサービス)」の恩恵を受け始めるフェーズがこの「Shift編」です。
「ディスク(ファイル)と設定ファイル(パスワード)」という、オンプレミス時代の物理的な束縛からアプリケーションを解放することで、システムは真の意味で弾力性(スケーラビリティ)とセキュリティを獲得します。インターフェースを切って外部依存を抽象化するという、C#らしいオブジェクト指向のアプローチが活きる場面でもありました。
次回は、クラウド化で最後まで立ちはだかる強敵、「レガシーライブラリとの終わらない戦い(泥臭いハック編)」をお届けします。クラウド上で帳票をPDF出力するために、我々がどのように AssemblyResolve と格闘したかをご紹介します。お楽しみに!
📖 単語一覧 (Glossary)
- Secret Manager: APIキー、パスワード、証明書などの機密データを安全に保存し、アクセスを管理できるGCPのマネージドサービス。
- オブジェクトストレージ (Cloud Storage / Amazon S3): データを階層的なディレクトリ(フォルダ)ではなく、フラットな「オブジェクト」としてバケットという空間に保存する仕組み。Web API経由で操作する。
- DI (Dependency Injection / 依存性の注入): クラスが直接他のクラス(今回であればGCS接続クラス)を生成するのではなく、外部からインターフェース越しに渡してもらう設計手法。これにより「ローカル保存」と「GCS保存」の切り替えが容易になる。
- SMB / NFS: オンプレミスでよく使われる、ネットワーク越しにファイル共有を行うためのプロトコル(ファイルサーバーの規格)。
