Pulumi Stack Referencesの深淵:巨大モノリスインフラを爆破し、疎結合なマイクロサービス基盤を構築する極意
インフラストラクチャ・アズ・コード(IaC)の黎明期から、私たちはひとつの呪縛と戦ってきた。
「数千行、あるいは数万行に膨れ上がった単一のHCL/TypeScriptファイル群」――いわゆるモノリシック・インフラである。
VPC、IAM、Kubernetesクラスター、データベース、そしてその上でうごめく無数のマイクロサービス。これらをすべてひとつのStateファイルで管理しようとした瞬間、Terraformのプランニングは数十分の沈黙を生み、`apply`の競合はチームの生産性を殺し、たった一つのセキュリティグループのタイポが本番環境の全ネットワークをデッドロックさせる。
この泥沼から抜け出す唯一の解が、インフラストラクチャのマイクロサービス化、すなわち「関心の分離」である。
そして、TypeScriptやPython、Goといった汎用プログラミング言語の表現力を武器にするPulumiにおいて、このアーキテクチャの成否を握る核心こそが `Stack References`(スタック間参照) だ。
単なる「値の受け渡し」ではない。型安全性を保ったまま、依存関係を完全に分離し、組織のスケーラビリティを極限まで高めるためのスタック間参照の深淵を覗こう。
—
1. なぜモノリスインフラは破綻するのか? 組織とStateの限界
インフラが巨大化すると、以下の3つの悪夢が顕在化する。
1. Blast Radius(影響範囲)の肥大化
アプリケーションのログ基盤をいじるだけの変更が、なぜか基盤VPCのルートテーブル再作成を引き起こし、全サービスの通信が数分間断絶する。
2. Stateのロック競合とコンフリクト
複数のチーム(ネットワークチーム、DBA、アプリ開発者)が同一のStateリポジトリを触らざるを得ず、CI/CDパイプラインでマージ地獄が発生する。
3. 認可制御(RBAC)の崩壊
アプリ開発者に「最低限のデプロイ権限」を渡そうとしたとき、モノリスインフラでは「VPCやIAMといったクリティカルなリソースの破壊権限」まで同時に与えてしまうリスクが生じる。
解決策:レイヤー別スタック分割
インフラを以下の3階層(またはそれ以上)に完全に垂直分割する。
- Layer 0: Core Network Stack(VPC, Subnet, Peering, Transit Gateway)
- Layer 1: Shared Services Stack(K8s Cluster, RDS, Shared Redis, IAM Roles)
- Layer 2: Application Stacks(Microservice A, B, C… 個別のECS/K8s Deployment)
この階層間を安全に、かつ疎結合に結びつけるのが Pulumi Stack References である。
—
2. Stack Referencesのメカニズム:API裏側の挙動と型安全な参照
Pulumiの `StackReference` は、背後でPulumi Service(またはS3/GCSなどのバックエンド)のState APIを叩き、別スタックの `outputs` を安全に取得する機構だ。
Terraformにおける `data “terraform_remote_state”` と同等だが、Pulumiが決定的に優れているのは、取得した値がただの文字列ではなく、TypeScriptの強力な型システム(`Output
実装パターン:Core Network Stackの公開(Outputs)
まずは基盤層(Layer 0)で、後続のスタックが必要とするネットワーク情報を明示的にエクスポートする。
// stack: my-org/core-network/production
import as pulumi from “@pulumi/pulumi”;
import as aws from “@pulumi/aws”;
// 1. VPCの構築
const vpc = new aws.ec2.Vpc(“production-vpc”, {
cidrBlock: “10.100.0.0/16”,
enableDnsHostnames: true,
enableDnsSupport: true,
tags: { Name: “production-vpc” },
});
// 2. パブリックサブネットの作成
const publicSubnet = new aws.ec2.Subnet(“production-public-subnet”, {
vpcId: vpc.id,
cidrBlock: “10.100.1.0/24”,
availabilityZone: “ap-northeast-1a”,
tags: { Name: “production-public-1a” },
});
// 3. 後続スタックへ公開するOutputsの定義
export const vpcId = vpc.id;
export const publicSubnetId = publicSubnet.id;
export const vpcCidrBlock = vpc.cidrBlock;
実装パターン:Application StackでのStack Reference利用
次に、アプリケーション層(Layer 2)のスタックから、上記のLayer 0の出力を参照する。
// stack: my-org/payment-service/production
import as pulumi from “@pulumi/pulumi”;
import as aws from “@pulumi/aws”;
// 別のスタックへの参照を生成(組織名/プロジェクト名/スタック名)
// 注: Pulumi ServiceのBackendを使用している場合の記法
const networkStack = new pulumi.StackReference(“my-org/core-network/production”);
// StackReferenceから型安全に値を取得
// applyメソッドやOutput
const vpcId = networkStack.requireOutput(“vpcId”).apply(id => id as string);
const subnetId = networkStack.requireOutput(“publicSubnetId”).apply(id => id as string);
// 取得したVPCのコンテキスト内でセキュリティグループを作成
const sg = new aws.ec2.SecurityGroup(“payment-service-sg”, {
vpcId: vpcId,
description: “Security group for payment service”,
ingress: [
{ protocol: “tcp”, fromPort: 443, toPort: 443, cidrBlocks: [“0.0.0.0/0”] },
],
});
export const serviceSecurityGroupId = sg.id;
ここで重要なのは、`networkStack.requireOutput(…)` が返す値は `Output
—
3. デプロイ順序の制御とCI/CDパイプラインのアーキテクチャ
スタックを分割した瞬間、開発者が直面するのが「どの順番でデプロイすべきか?」という問題だ。
Layer 2はLayer 0がデプロイされていなければ `requireOutput` で値が取れずに爆発する。
これを人間が手動で順序制御するなど、SREの恥である。完全自動化されたパイプライン設計が不可欠となる。
依存関係のトポロジカルソートと自動化スクリプト
大規模環境では、MakefileやBashスクリプトを捨て、TypeScriptによるオーケストレーショントリガー、あるいはGitHub Actionsの `matrix` / `needs` を駆使した動的パイプラインを組むべきだ。
以下は、独立したスタック群の変更検知と順序制御を行うための独自CLI自動化スクリプト(一部抜粋)のコンセプトである。
// deploy-orchestrator.ts
import { execSync } from “child_process”;
import as fs from “fs”;
interface StackDependency {
stackName: string;
path: string;
dependsOn: string[];
}
// 依存関係グラフの定義
const topology: StackDependency[] = [
{ stackName: “core-network”, path: “./infra/00-network”, dependsOn: [] },
{ stackName: “shared-cluster”, path: “./infra/01-cluster”, dependsOn: [“core-network”] },
{ stackName: “payment-service”, path: “./infra/02-apps/payment”, dependsOn: [“shared-cluster”] },
];
function deployStack(stack: StackDependency) {
console.log(`[INFO] Deploying stack: ${stack.stackName} at ${stack.path}`);
execSync(`pulumi up –stack production –yes`, {
cwd: stack.path,
stdio: “inherit”,
});
}
// トポロジカルソートに基づいた順次実行エンジン
async function runPipeline() {
// 実際にはgit diffなどを検知して差分があるスタックのみを対象にする高度な制御を実装する
for (const node of topology) {
console.log(`[CHECK] Verifying dependencies for ${node.stackName}…`);
// 依存先スタックのステータスチェックなどをここに挟む
deployStack(node);
}
}
runPipeline().catch(err => {
console.error(“[FATAL] Pipeline failed:”, err);
process.exit(1);
});
—
4. 変更影響範囲を最小限に抑える組織的アプローチ(権限委譲とガバナンス)
Stack Referencesの真価は、技術的なモジュール性だけではなく、「組織の権限委譲(Delegation)」を安全に実現できる点にある。
最小権限の原則(Least Privilege)の徹底
モノリスインフラでは、全員が全リソースへのアクセス権を持つか、あるいは過剰に複雑なTerraform Moduleのカタログ運用に苦しむことになる。Pulumi Stack Referencesを使えば、次のような組織的境界線(Boundary)を引くことができる。
[Network Team] ──(Manage)──> Stack: core-network (VPC, Subnet)
│
(Export Outputs)
│
▼
[App Team A] ──(Read via StackRef)──> Stack: payment-service
- Network Team のみが `core-network` スタックの `pulumi up` 権限を持つ。
- App Team A は `core-network` のコードやStateを直接編集することは絶対にできない。彼らに与えられているのは、「指定されたStack ReferenceからVPC IDやSubnet IDを読み取る権限」と「自分たちのアプリリソースを作成する権限」のみである。
この境界線により、アプリケーションエンジニアが誤って基盤のルーティングを破壊する事故は物理的(IAM的)に不可能になる。
—
5. エキスパート向けハック:メモリ消費・パフォーマンス・落とし穴の回避
最後に、現場のトレンチ(泥沼)で戦うエンジニアへ、Stack References運用における深淵の知見を授けよう。
ハック1: 非同期解決(`Promise` と `Output`)のデッドロック回避
Stack Referenceを通じた値の伝搬において、循環参照(Circular Dependency)は絶対のタブーである。
例えば、`core-network` が `payment-service` のセキュリティグループIDを参照し、かつ `payment-service` が `core-network` のVPCを参照するような構成は、PulumiのDAG構築時にデッドロックを引き起こすか、単に循環エラーとして弾かれる。
- 鉄則: 依存の方向は常に `Layer 0 -> Layer 1 -> Layer 2` の単方向(DAG)に限定せよ。逆向きの値が必要な場合は、動的な参照ではなく、共通の設定値(Config)や、独立したパラメーターストア(AWS SSMなど)を経由させること。
ハック2: スタック名変更時のマイグレーション(Stateの破壊を防ぐ)
Stack Referencesは、参照元のスタック名(例: `my-org/core-network/production`)を文字列としてハードコードしがちである。
スタック名をリファクタリング(例: `core-vpc` への改名)する際、これを雑に行うと全依存アプリケーションスタックが参照先を失い、次のデプロイ時に `Output not found` でクラッシュする。
安全なリファクタリング手順:
1. 新しいスタックを作成し、既存と同じ `outputs` を出力する。
2. 依存するAppスタック側の `StackReference` の文字列を新スタック名に書き換え、一度デプロイする(これで新旧どちらの参照も論理的にカバーできる過渡期を作る)。
3. 旧スタックを安全に破棄する。
環境変数やPulumi Configを活用して、スタック名を直接コード内に直書きせず、次のように抽象化するのがプロの作法だ。
// Configからスタック名を動的に組み立てる例
const env = pulumi.require(“environment”); // “production”
const networkStackName = `my-org/core-network/${env}`;
const networkStack = new pulumi.StackReference(networkStackName);
—
結び:インフラのコンポーネント化という名の聖杯
Pulumi Stack Referencesは、単なる「便利な機能」ではない。それは、インフラストラクチャを真の意味で「再利用可能で、疎結合で、組織のスケールに追従するソフトウェアコンポーネント」へと昇華させるための聖杯である。
モノリスという巨大な呪縛を断ち切り、スタックという名の小さな宇宙をいくつも精密に組み上げる。その背後で完璧に型安全性を担保された依存関係のグラフが組み上がったとき、あなたのインフラ基盤は、どんな大規模トラフィックや組織変更をも悠々と受け止める、真の「モダン・クラウドインフラ」へと到達する。
さあ、今すぐそのモノリスのコードを分割し、`StackReference` の扉を開け放て。