Pulumi Component Resourcesの深淵:組織のインフラを「モジュール化」の呪縛から解放する設計パターン
こんにちは。大規模クラウドインフラの設計・運用を統括するテックリードの私から、日々のIaC(Infrastructure as Code)の泥臭いメンテナンスに疲れ果てているあなたへ、決定的な処方箋をお渡ししよう。
「TerraformのModulesの複雑さに身悶えしたことがある」「CloudFormationのネストされたスタックで精神を病みかけた」「TypeScriptでIaCを書いているのに、毎回似たようなVPCやECSのボイラープレートをコピペしている」……。
もし心当たりがあるなら、あなたはPulumi Component Resources(カスタムコンポーネント)という名の聖杯にまだ触れていないだけだ。
今日は、単なるAPIのラッパーではない、「組織のベストプラクティスを強制し、開発スピードを劇的に高める」ためのComponent Resourcesの設計思想と実装の極意を、一切の妥協なく解説する。
—
1. なぜ「コンポーネントリソース」なのか?(Terraform Modulesとの決定的な違い)
多くのエンジニアが勘違いしている。TerraformのModulesやAWS CDKのConstructと、PulumiのComponent Resourcesは同じものだと。
だが、その思想の根底には決定的な違いがある。
- Terraform Modules: 単なる「HCLのマクロ展開」に過ぎない。状態管理やプロバイダーの受け渡しで複雑化しやすく、モジュール内部の抽象化が漏れ出すと地獄を見る。
- Pulumi Component Resources: 「独自のクラウド・リソース(Custom Resource)」をプログラム上で定義する仕組みである。
Component Resourcesの本質は、「複数の低水準リソース(VPC, Subnet, RouteTable等)をカプセル化し、単一の論理的リソースとしてPulumiエンジンに認識させる」ことにある。これにより、ステートファイル上では1つの「カスタムコンポーネント」として扱われ、差分検出や削除のライフサイクルが美しく制御される。
—
2. 実践:セキュアなVPCをカプセル化するカスタムコンポーネントの実装
口を動かす前に、コードを書こう。ここでは、AWS上に「パブリック/プライベートサブネット、NATゲートウェイ、フローログ」を標準装備した、組織標準のセキュアVPCをTypeScriptで実装する。
ディレクトリ構成のベストプラクティス
インフラストラクチャのリポジトリは、次のようにコンポーネントを独立したパッケージ(またはローカルモジュール)として切り出すのがプロの作法だ。
.
├── package.json
├── tsconfig.json
├── index.ts # メインのエントリポイント
└── components
└── secure-vpc.ts # カスタムコンポーネント定義
実装コード:`components/secure-vpc.ts`
import as aws from “@pulumi/aws”;
import as pulumi from “@pulumi/pulumi”;
// コンポーネントに入力するプロパティの型定義(厳格に縛る)
export interface SecureVpcArgs {
cidrBlock: string;
environment: “dev” | “stg” | “prod”;
enableFlowLogs?: boolean;
}
// コンポーネントリソース本体
export class SecureVpc extends pulumi.ComponentResource {
// 外部に公開するプロパティ(出力値)
public readonly vpcId: pulumi.Output
public readonly publicSubnetIds: pulumi.Output
public readonly privateSubnetIds: pulumi.Output
constructor(name: string, args: SecureVpcArgs, opts?: pulumi.ComponentResourceOptions) {
// 第1引数にコンポーネントの型名(namespace:module:type)を指定する
super(“custom:infra:SecureVpc”, name, {}, opts);
// 親コンテキスト(this)を明示的に渡すことで、リソースの親子関係がPulumi上で綺麗にツリー化される
const childOpts = { parent: this };
// 1. VPCの作成
const vpc = new aws.ec2.Vpc(`${name}-vpc`, {
cidrBlock: args.cidrBlock,
enableDnsHostnames: true,
enableDnsSupport: true,
tags: {
Name: `${name}-vpc`,
Environment: args.environment,
},
}, childOpts);
this.vpcId = vpc.id;
// 2. パブリック・プライベートサブネットの作成(簡略化のためAZを2つに固定)
const azs = [“ap-northeast-1a”, “ap-northeast-1c”];
const publicSubnets = azs.map((az, i) =>
new aws.ec2.Subnet(`${name}-public-${i}`, {
vpcId: vpc.id,
cidrBlock: `10.0.${i}.0/24`,
availabilityZone: az,
mapPublicIpOnLaunch: true,
tags: { Name: `${name}-public-${az}` },
}, childOpts)
);
const privateSubnets = azs.map((az, i) =>
new aws.ec2.Subnet(`${name}-private-${i}`, {
vpcId: vpc.id,
cidrBlock: `10.0.${10 + i}.0/24`,
availabilityZone: az,
tags: { Name: `${name}-private-${az}` },
}, childOpts)
);
this.publicSubnetIds = pulumi.output(publicSubnets.map(s => s.id));
this.privateSubnetIds = pulumi.output(privateSubnets.map(s => s.id));
// 3. インターネットゲートウェイ & ルートテーブル
const igw = new aws.ec2.InternetGateway(`${name}-igw`, {
vpcId: vpc.id,
}, childOpts);
const publicRt = new aws.ec2.RouteTable(`${name}-public-rt`, {
vpcId: vpc.id,
routes: [{ cidrBlock: “0.0.0.0/0”, gatewayId: igw.id }],
}, childOpts);
publicSubnets.forEach((subnet, i) => {
new aws.ec2.RouteTableAssociation(`${name}-pub-rta-${i}`, {
subnetId: subnet.id,
routeTableId: publicRt.id,
}, childOpts);
});
// 4. 条件付きフローログ設定(ベストプラクティスの強制)
if (args.enableFlowLogs ?? true) {
const logGroup = new aws.cloudwatch.LogGroup(`${name}-flow-log-group`, {
retentionInDays: args.environment === “prod” ? 365 : 30,
}, childOpts);
const iamRole = new aws.iam.Role(`${name}-flow-log-role`, {
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ Service: “vpc-flow-logs.amazonaws.com” }),
}, childOpts);
// (※ポリシーアタッチ等のボイラープレートは省略)
new aws.ec2.FlowLog(`${name}-flow-log`, {
vpcId: vpc.id,
trafficType: “ALL”,
iamRoleArn: iamRole.arn,
logDestination: logGroup.arn,
}, childOpts);
}
// 最後に必ずregisterOutputsを呼ぶことで、出力値が確定する
this.registerOutputs({
vpcId: this.vpcId,
publicSubnetIds: this.publicSubnetIds,
privateSubnetIds: this.privateSubnetIds,
});
}
}
この実装の美しさは、「呼び出し側にAWSの複雑なルーティングやIAMの構築ロジックを一切意識させない」点にある。開発者は「セキュアなVPCが欲しい」という意図だけをコードにすればいい。
—
3. 組織内でインフラのベストプラクティスをモジュール化するコツ
コードを書くだけなら誰でもできる。問題は「組織全体でどう徹底させるか」だ。テックリードとして、以下の3つのルールをチームに強制してほしい。
① 命名規則とタグ付けの強制(ポリシー・エンフォースメント)
コンポーネント内部でタグや命名規則をカプセル化し、開発者が自由にいじれないようにする。これにより、コスト配分タグの付け忘れや、リソース迷子が劇的に減る。
② TypeScriptの型システムを「ガードレール」として使う
「便宜上、適当な文字列を渡す」をTypeScriptのコンパイルエラーで弾く。
例えば、環境変数(`dev`, `stg`, `prod`)に応じて、プロダクション環境では自動的にログの保持期間を延長させたり、暗号化を強制したりするロジックをコンポーネントの内部に隠蔽する。
③ チーム開発で役立つ設定の共有化ルール(Pulumi Config)
環境ごとのパラメータは、コードにハードコードせず `Pulumi.
Pulumi.production.yaml
config:
aws:region: ap-northeast-1
my-project:environment: prod
my-project:vpcCidr: 10.100.0.0/16
これをTypeScript側で型安全に読み込む。
const config = new pulumi.Config();
const environment = config.require(“environment”) as “dev” | “stg” | “prod”;
const vpcCidr = config.require(“vpcCidr”);
const vpc = new SecureVpc(“core-vpc”, {
cidrBlock: vpcCidr,
environment: environment,
enableFlowLogs: true,
});
—
4. インフラのユニットテスト:pulumi/pulumi-testを用いた検証
「インフラのテストは実環境にデプロイしないと分からない」と思っているなら、それは古い。Pulumiには強力なMocks(モック機能)が存在する。実リソースを作らずに、コンポーネントが意図した通りにリソースを生成しているかをユニットテストで検証できる。
テストコードの実装例 (`components/secure-vpc.test.ts`)
import as pulumi from “@pulumi/pulumi”;
import { SecureVpc } from “./secure-vpc”;
// Pulumiのランタイムをモック化
pulumi.runtime.setMocks({
newResource:-async (args: pulumi.runtime.MockResourceArgs) => {
// リソース作成時の入力をキャプチャして検証できる
return {
id: `${args.name}-id`,
state: args.inputs,
};
},
call: async (args: pulumi.runtime.MockCallArgs) => {
return {};
},
});
describe(“SecureVpc Component”, () => {
it(“VPCと必要なサブネットが正しく生成されること”, async () => {
// テスト対象のコンポーネントをインスタンス化
const vpcComp = new SecureVpc(“test-vpc”, {
cidrBlock: “10.0.0.0/16”,
environment: “dev”,
enableFlowLogs: false,
});
// 出力値の検証
const vpcId = await pulumi.all(vpcComp.vpcId).apply(id => id);
expect(vpcId).toBeDefined();
const publicSubnets = await pulumi.all(vpcComp.publicSubnetIds).apply(ids => ids);
expect(publicSubnets.length).toBe(2);
});
});
このテストをCI/CDパイプライン(GitHub Actionsなど)のPRチェックに組み込む。これにより、「インフラコードの構文エラーやロジックミスを、AWSに1円も課金することなく数秒で検出する」究極のフィードバックループが完成する。
—
プロの実践テクニック:開発スピードを最大化する環境構築
最後に、私が日常の開発で手放せない「隠し味」を共有しよう。
1. VSCodeの神プラグイン「Pulugi」系は入れるな、公式拡張機能だけで十分だ:
余計なサードパーティ製拡張機能を入れると補完が重くなる。VSCodeの公式「Pulumi」拡張機能と、TypeScriptの強烈な型推論を信じろ。
2. キーボードショートカットの極み:
`pulumi up –refresh` を毎回手打ちするな。`package.json` の `scripts` に以下を定義し、IDEのタスクランナーにバインドしろ。
“scripts”: {
“up”: “pulumi up –suppress-outputs”,
“preview”: “pulumi preview”
}
これで `Ctrl + Shift + B` (またはカスタムキー)一発で変更差分のプレビューが走る環境を作る。
結びにかえて
インフラストラクチャを「コードとして書く」時代から、「ソフトウェアとして設計する」時代へ。
Pulumi Component Resourcesを使いこなせるようになると、あなたやあなたのチームは、もはやクラウドの細かいAPI仕様に悩まされることはなくなる。
組織のベストプラクティスをコードという名のカプセルに閉じ込め、開発チーム全員をインフラの呪縛から解放してほしい。
健闘を祈る。