混沌を制する者へ:Pulumi Automation APIによるマルチテナントSaaS動的プロビジョニングの深淵
世の多くのインフラストラクチャ・エンジニアは、TerraformのWorkspaceや、静的に定義されたPulumiのStackファイル(`Pulumi.yaml`, `Pulumi.prod.yaml`など)の手動管理という呪縛に囚われている。
「テナントが1社増えるたびに、Gitリポジトリに設定ファイルをPRで生やし、CI/CDパイプラインを走らせる」
このアプローチは、テナント数が数十社程度であれば微笑ましく機能する。しかし、数千、数万のテナントを抱える真のマルチテナントSaaSにおいて、この静的構成管理は確実に破綻する。Gitのメタデータ肥大化、コンフリクトの嵐、そしてパイプラインのスケジューリング遅延。これらはすべて、設計の怠慢が生んだ技術的負債に他ならない。
本稿では、Pulumiの真髄である Automation API を用い、GitOpsの静的な枠組みすらも超越した「テナントのオンデマンド動的プロビジョニング・アーキテクチャ」の全貌を解き明かす。
—
1. 静的Stack管理の限界と、Automation APIという福音
静的Stackが抱える構造的欠陥
PulumiのCLIとStackファイル群をベースにした従来の手法は、Declarative(宣言的)なインフラ管理においては王道である。しかし、SaaSのライフサイクル(テナントのサインアップ、プラン変更、テナント破棄)は本質的にイベント駆動型の動的(Imperative / Event-driven)なドメインである。
ここに静的なStackを持ち込むと、以下の致命的なボトルネックに直面する。
1. 状態の肥大化とロック競合: 全テナントのインフラを少数の巨大なStackに詰め込むと、Blast Radius(影響範囲)が広がりすぎて障害時の被害が甚大になる。かといってテナントごとにStackを静的ファイルで数千個作れば、Pulumi Engineのバックエンド(Service / S3など)のインデックス検索が重くなり、CLIの応答速度が劣化する。
2. CI/CDパイプラインの限界: GitHub ActionsやGitLab CIは、動的に生成される数千の独立したジョブを制御するようには最適化されていない。
Automation API:インフラストラクチャを「コードとして埋め込む」
Pulumi Automation APIは、CLIをラップして外部プロセスとして実行するようなお遊戯的なツールではない。Pulumi Engineそのものをプログラムのライブラリ(SDK)としてインポートし、インプットから実行、ステート管理までを完全にプログラムから制御するための神聖なインターフェースである。
これを用いれば、「テナント登録API」のエンドポイント内で、インフラストラクチャの定義、プレビュー、デプロイ、そして破棄までをトランザクショナルに完結させることができる。
—
2. 障害ドメイン分離とコスト最適化の設計パターン
真にスケーラブルなマルチテナントSaaS基盤では、すべてのテナントを均一に扱ってはならない。テナントのTier(Free, Standard, Enterprise)に応じて、インフラストラクチャの分離レベルを動的に変化させる必要がある。
[テナント層] (Free Tier: 共有基盤) (Enterprise Tier: 完全分離基盤)
│ │
[プロキシ層] ▼ ▼
API Gateway / ALB Dedicated ALB
│ │
[コンテナ層] Shared EKS Cluster Dedicated Namespace
(Namespace分離) / Dedicated Cluster
│ │
[データ層] Multi-tenant RDS Dedicated RDS / Aurora
(Schema/Row分離) (Encryption Key別)
Automation APIを用いることで、テナントのメタデータ(契約プラン、リージョン、コンプライアンス要件)をトリガーとして、コードベース側でインフラストラクチャのトポロジー(Topology)を動的に切り替えることが可能になる。
—
3. 実装:Automation APIによる動的Stackプロビジョニング・エンジン
ここからは、実際にTypeScript(Node.js)を用いて、テナント追加時に動的に独立したStackを生成・デプロイするプロビジョニング・エンジンのコア実装を解説する。
このコードは、APIサーバーやバックグラウンドワーカー(TemporalやAWS SQSのコンシューマーなど)から呼び出されることを想定している。
インフラプロビジョニングのコアロジック (`tenantProviser.ts`)
import as pulumi from “@pulumi/pulumi”;
import as aws from “@pulumi/aws”;
import { LocalWorkspace, Stack } from “@pulumi/pulumi/automation”;
interface TenantConfig {
tenantId: string;
tier: “free” | “enterprise”;
region: string;
}
/
- 指定されたテナントのための独立したPulumi Stackを動的に構成・デプロイする
/
export async function provisionTenantInfrastructure(config: TenantConfig) {
const stackName = `tenant-${config.tier}-${config.tenantId}`;
const projectName = “saas-core-infra”;
// 1. Automation APIのワークスペースをインメモリ/一時ディレクトリで初期化
// プログラム自体がPulumiのインフラ定義(Inline Program)を持つため、
// 別途Pulumiのプロジェクトファイルをディスクに静置する必要がない。
const workdir = await LocalWorkspace.createOrSelectStack({
stackName: stackName,
projectName: projectName,
program: async () => {
// — テナントごとのインフラストラクチャ定義 —
// 障害ドメインの完全分離: Enterpriseの場合は専用のVPCとデータベースを構築
if (config.tier === “enterprise”) {
const vpc = new aws.ec2.Vpc(`${config.tenantId}-vpc`, {
cidrBlock: “10.100.0.0/16”,
enableDnsHostnames: true,
enableDnsSupport: true,
tags: { TenantId: config.tenantId, Tier: “enterprise” },
});
const subnet = new aws.ec2.Subnet(`${config.tenantId}-subnet`, {
vpcId: vpc.id,
cidrBlock: “10.100.1.0/24”,
tags: { TenantId: config.tenantId },
});
const dbInstance = new aws.rds.Instance(`${config.tenantId}-db`, {
engine: “postgres”,
instanceClass: “db.t4g.medium”,
allocatedStorage: 50,
dbName: `tenant_${config.tenantId}`,
username: “saas_admin”,
// 本番ではKMSやSecretsManagerから安全に取得すること
password: “SuperSecretPassword123!”,
skipFinalSnapshot: true,
dbSubnetGroupName: new aws.rds.SubnetGroup(`${config.tenantId}-dbsg`, {
subnetIds: [subnet.id],
}).name,
});
// 出力値の登録(API層から取得可能になる)
pulumi.export(“dbEndpoint”, dbInstance.endpoint);
pulumi.export(“vpcId”, vpc.id);
} else {
// Free Tierの場合: 共有データベース内の論理パーティション(Schema)設定のみを作成
// コスト最適化のため、リソースを新規作成せず設定パラメータのみを返却
pulumi.export(“sharedClusterSchema”, `schema_${config.tenantId}`);
}
},
});
// 2. クラウドプロバイダーの設定(AWSの認証情報やリージョンを動的に注入)
await workdir.setAllConfig(“aws:region”, { value: config.region });
// 3. デプロイメントの実行(CI/CDパイプラインの代行)
console.log(`[Automation API] Starting deployment for stack: ${stackName}`);
const upResult = await workdir.up({
onOutput: (msg) => console.log(`[Pulumi Engine] ${msg}`),
});
console.log(`[Automation API] Successfully deployed stack: ${stackName}`);
return {
stackName,
summary: upResult.summary,
outputs: upResult.outputs,
};
}
このアーキテクチャの圧倒的な優位性
1. コードの自己完結性 (Inline Program): `program`プロパティにインラインでTypeScriptの関数を渡しているため、テナント追加のたびに別リポジトリのコードをクローンしてビルドする必要がない。プロビジョニング・ロジックの変更は、プロビジョニング・サーバーのデプロイだけで全テナントに即座に反映される。
2. ステートの完全な独立: テナントごとにStackが分かれているため(例: `tenant-enterprise-tenant_abc123`)、万が一あるテナントのインフラ削除や破損が発生しても、Blast RadiusはそのテナントのStackおよびバックエンドステートに完全に封じ込められる。
3. 並列処理の安全性: Pulumi Backend(Pulumi CloudやS3/DynamoDB)のロック機構により、複数のテナントが同時にサインアップしプロビジョニングが走った場合でも、競合やステートの破損が数理的に防止される。
—
4. 運用・スケール時の深い知見と最適化ハック
このアーキテクチャをプロダクション環境に投入し、数千スケールに到達したときに直面する「現実」と、その処方箋を授けよう。
ハック1: Pulumi Engineのメモリ消費とコンカレンシー制御
Node.jsプロセス内でPulumi Engine(Go製バイナリのラップ)を動かす場合、同時実行数(Concurrency)が無制限に上がると、V8エンジンのメモリ消費量が急増し、OOM (Out Of Memory) キラースカベンジャーの餌食になる。
- 対策: テナントプロビジョニング・キュー(AWS SQS + Workerなど)を挟み、ワーカーごとの並列度(Concurrency)を厳格に制限すること。さらに、大規模なStackのデプロイ時には子プロセスが大量のメモリを消費するため、Kubernetes上で実行する場合はPodのメモリリクエスト/リミットを適切にチューニング(最低2GiB以上推奨)すること。
ハック2: ステートバックエンドのシャード戦略
標準的なS3バックエンドを使用する場合、同一S3バケット内に数万個の`.json`(Stateファイル)が蓄積される。これ自体はS3のスケーラビリティにより耐えられるが、Pulumi CLIがステートのリストやロック確認を行う際にS3のAPIスロットリング(HTTP 503 / 429)にヒットしやすくなる。
- 対策: テナントIDのハッシュプレフィックスに基づいてS3バケットを動的にシャーディングするか、スケーラビリティに優れた Pulumi Service (SaaS) バックエンド、あるいはSelf-HostedなPulumictl + データベースバックエンドを検討せよ。
ハック3: 失敗時のロールバックと「孤児リソース」の防衛
動的プロビジョニング中にネットワーク切断やAWS側のAPI制限(Rate Limit)で `up()` が失敗した場合、中途半端にリソースが作成された状態でステートが保存される。
- 対策: Automation APIの `up()` が失敗したキャッチブロック内で、自動的に `destroy()` を非同期でトリガーするか、あるいはステートに残ったリソースを安全にクリーンアップするサーキットブレーカーパターンを実装せよ。
try {
await workdir.up();
} catch (error) {
console.error(`Provisioning failed for ${stackName}. Initiating rollback/destroy…`, error);
await workdir.destroy();
throw error;
}
—
5. 結び:インフラを「データ」として扱え
インフラストラクチャをコードとして扱う(Infrastructure as Code)時代から、インフラストラクチャをデータおよびAPIのペイロードとして完全に従属させる時代へ。
Pulumi Automation APIを使いこなすことは、単にツールを使い分けることではない。インフラストラクチャのライフサイクルそのものをアプリケーションのドメインロジックの直下に組み込み、人間の手作業や、静的なGitOpsの限界からシステムを解放することを意味する。
この境地に到達したエンジニアにとって、テナントの増加はもはや恐怖ではなく、単なるデータベースのレコード数の増加にすぎない。さあ、静的な世界を捨て、動的プロビジョニングの深淵へ踏み出すがいい。