Pulumi型安全性を極める:TypeScript/Pythonでインフラの「事故」をデプロイ前に根絶する設計パターン
こんにちは。テックリードの私だ。
日々のインフラ運用で、こんな絶望を味わったことはないだろうか?
- `pulumi up` を叩いて数分待った挙句、AWS APIのバリデーションエラー(例: 「VPCのCIDRブロックが不正です」「環境名に大文字が含まれています」)でデプロイが弾かれた。
- レビュー時に、ジュニアエンジニアが `instance_type` に存在しないインスタンスファミリを指定しているのを見落とし、ステージング環境を盛大に爆破した。
- 動的言語(TypeScript/Python)の自由度の高さゆえに、設定オブジェクトのタイポが実行時(しかもCI/CDパイプラインの途中)まで発覚しない。
HCL(Terraform)やYAMLの静的な記述力にフラストレーションを感じ、プログラミング言語の表現力を求めてPulumiを採用したはずだ。しかし、型安全性を放棄して `any` や生のプリミティブ型でリソースを定義していれば、それは「プログラミング言語の皮を被った単なる複雑なスクリプト」に成り下がる。
今回は、TypeScript(Zod)とPython(Pydantic)を駆使し、「不適切なインフラパラメータの入力をデプロイ前に100%防ぐ」ための厳格なスキーマ検証と、カスタムバリデーションの実装パターンを深淵まで解説する。
—
1. 開発スピードを爆発させる環境設定(VSCode 秘伝の作法)
型安全性を極める第一歩は、IDEとの対話を極限まで高速化することだ。無駄なキーボードストロークを削ぎ落とし、思考を途切れさせないための環境を構築する。
絶対入れるべき神プラグイン(VSCode)
1. Error Lens: バリデーションエラーや型の不一致を、コード行の末尾にインラインで赤字表示する。エラーを探すためにマウスを動かす時間は人生の無駄だ。
2. Zod Viewer / Pydantic Workspace: 複雑なスキーマのネスト構造を視覚化する。
3. Todo Tree: インフラのTODOや一時的なハックを視覚化し、負債の放置を防ぐ。
隠れたキーボードショートカット(Mac / Win)
- クイックフィックス (`Cmd + .` / `Ctrl + .`): 型エラーやインポート漏れを瞬時に修正する。Zod/Pydanticのスキーマ定義から自動的に型を推論させるときに多用する。
- シンボルへ移動 (`Cmd + Shift + O` / `Ctrl + Shift + O`): 巨大化したPulumiプログラム群から、特定のコンポーネントリソースへ一瞬でジャンプする。
—
2. TypeScript × Zod で実現する「要塞型」インフラ構成
TypeScriptでPulumiを書く場合、`Config` クラスをそのまま使うのは素人だ。実行時バリデーションのデファクトスタンダードである Zod を用い、環境変数や `Pulumi.
実践:プロダクションレベルのZodスキーマ定義
以下のコードは、本番環境向けのVPCおよびEKSクラスター設定を受け取るための、妥協なきバリデーション層である。
import as z from “zod5”; // または zod
import as pulumi from “@pulumi/pulumi”;
// 1. 厳格なバリデーションルールの定義
const EnvironmentSchema = z.enum([“dev”, “stg”, “prd”]);
const CidrSchema = z.string().refine(
(val) => {
// 簡易的なCIDRバリデーション正規表現
const cidrRegex = /^([0-9]{1,3}\.){3}[0-9]{1,3}\/([0-9]|[1-2][0-9]|3[0-2])$/;
return cidrRegex.test(val);
},
{ message: “無効なCIDRブロック形式です。” }
);
const TagSchema = z.object({
Owner: z.string().min(1, “Ownerは必須です。”),
CostCenter: z.string().regex(/^CC-[0-9]{4}$/, “CostCenterは ‘CC-XXXX’ 形式である必要があります。”),
Environment: EnvironmentSchema,
});
// インフラ設定全体のスキーマ
export const InfraConfigSchema = z.object({
awsRegion: z.enum([“ap-northeast-1”, “us-east-1”]),
environment: EnvironmentSchema,
vpc: z.object({
cidrBlock: CidrSchema,
maxAzs: z.number().int().min(2).max(3),
}),
eks: z.object({
nodeInstanceType: z.string().refine(
(val) => [“t3.medium”, “t3.large”, “m5.large”, “m5.xlarge”].includes(val),
{ message: “許可されていないインスタンスタイプです。コスト管理ポリシーを確認してください。” }
),
minSize: z.number().int().min(1),
maxSize: z.number().int().max(10),
}).refine((data) => data.minSize <= data.maxSize, {
message: "minSizeはmaxSize以下である必要があります。",
path: ["minSize"], // エラーを紐付けるパス
}),
tags: TagSchema,
});
// TypeScriptの型をスキーマから自動導出(Type Assertion)
export type InfraConfig = z.infer
/
- PulumiのConfigから安全に設定をロードし、Zodでパースする関数
/
export function loadValidatedConfig(): InfraConfig {
const config = new pulumi.Config();
// 生のJSONやオブジェクトとしてPulumiConfigに保存されていると仮定
const rawConfig = {
awsRegion: config.require(“awsRegion”),
environment: config.require(“environment”),
vpc: config.requireObject(“vpc”),
eks: config.requireObject(“eks”),
tags: config.requireObject(“tags”),
};
try {
//ここでパース失敗時は即座にプロセスが落ち、詳細なエラーが出力される
return InfraConfigSchema.parse(rawConfig);
} catch (error) {
if (error instanceof z.ZodError) {
console.error(“🔥 【致命的】インフラ設定のバリデーションエラー:”);
error.errors.forEach((err) => {
console.error(` – path: [${err.path.join(” -> “)}] => ${err.message}`);
});
process.exit(1);
}
throw error;
}
}
このアプローチの美しさは、「インフラのビジネスルール(例: コストセンターの命名規則、インスタンスタイプのホワイトリスト、min/maxの整合性)」をコードとして宣言的に記述できる点にある。`pulumi up` を叩いた瞬間に、不完全な設定は実行を拒絶される。
—
3. Python × Pydantic で実現する堅牢なデータモデリング
PythonエコシステムでPulumiを駆動する場合、Pydantic (v2) がその真価を発揮する。型ヒントとランタイムバリデーションの融合により、IDEの補完を完全に効かせながら、不正なパラメータをシャットアウトする。
実践:Pydanticを用いたカスタムバリデーション
from typing import Literal, List
import re
import sys
from pydantic import BaseModel, Field, field_validator, model_validator
import pulumi
EnvironmentType = Literal[“dev”, “stg”, “prd”]
RegionType = Literal[“ap-northeast-1”, “us-east-1″]
class VpcConfig(BaseModel):
cidr_block: str = Field(…, description=”VPCのCIDRブロック”)
max_azs: int = Field(…, ge=2, le=3, description=”使用するAZの数 (2または3)”)
@field_validator(“cidr_block”)
@classmethod
def validate_cidr(cls, v: str) -> str:
pattern = r”^([0-9]{1,3}\.){3}[0-9]{1,3}\/([0-9]|[1-2][0-9]|3[0-2])$”
if not re.match(pattern, v):
raise ValueError(f”無効なCIDRフォーマットです: {v}”)
return v
class EksConfig(BaseModel):
node_instance_type: str
min_size: int = Field(…, ge=1)
max_size: int = Field(…, ge=1)
@field_validator(“node_instance_type”)
@classmethod
def validate_instance_type(cls, v: str) -> str:
allowed = [“t3.medium”, “t3.large”, “m5.large”, “m5.xlarge”]
if v not in allowed:
raise ValueError(f”許可されていないインスタンスタイプです: {v}. 許可リスト: {allowed}”)
return v
@model_validator(mode=”after”)
def validate_node_sizes(self) -> “EksConfig”:
if self.min_size > self.max_size:
raise ValueError(“min_sizeはmax_size以下でなければなりません。”)
return self
class TagConfig(BaseModel):
owner: str = Field(…, min_length=1)
cost_center: str
@field_validator(“cost_center”)
@classmethod
def validate_cost_center(cls, v: str) -> str:
if not re.match(r”^CC-[0-9]{4}$”, v):
raise ValueError(“CostCenterは ‘CC-XXXX’ 形式である必要があります。”)
return v
class RootInfraConfig(BaseModel):
aws_region: RegionType
environment: EnvironmentType
vpc: VpcConfig
eks: EksConfig
tags: TagConfig
def load_and_validate_config() -> RootInfraConfig:
config = pulumi.Config()
try:
raw_data = {
“aws_region”: config.require(“awsRegion”),
“environment”: config.require(“environment”),
“vpc”: config.require_object(“vpc”),
“eks”: config.require_object(“eks”),
“tags”: config.require_object(“tags”),
}
return RootInfraConfig(raw_data)
except Exception as e:
print(f”🔥 【致命的】Pydanticバリデーションエラー:\n{e}”, file=sys.stderr)
sys.exit(1)
Pythonのダイナミズムを担保しつつ、静的解析ツール(MypyやPyright)とも完璧に統合されるため、リファクタリング時の安心感が桁違いに向上する。
—
4. チーム開発で役立つ設定の共有化ルールとベストプラクティス
型安全なバリデーション層を用意しても、設定ファイル(YAML)自体がバラバラであれば意味がない。チーム開発において破綻しないための設計ルールを提示する。
1. `Pulumi.yaml` と環境別設定の分離
スタックごとの設定(`Pulumi.dev.yaml` など)には、機密情報や環境差分のみを置く。構造化されたオブジェクト(VPCやEKSの設定など)は、極力JSON形式で表現し、Zod/Pydanticでパースする。
推奨する `Pulumi.dev.yaml` の構成例:
config:
# プリミティブな環境差分のみを明記
my-project:awsRegion: ap-northeast-1
my-project:environment: dev
# 複雑なオブジェクトはJSON文字列、または pulumi config set-object で管理
my-project:vpc:
cidrBlock: “10.100.0.0/16”
maxAzs: 2
my-project:eks:
nodeInstanceType: “t3.medium”
minSize: 1
maxSize: 3
my-project:tags:
Owner: “platform-team@example.com”
CostCenter: “CC-1004”
Environment: “dev”
2. CI/CDパイプラインでの「ドライラン」義務化
プルリクエスト作成時やマージ前に、必ず以下のプロセスをGitHub Actions等のCIに組み込む。
1. `pulumi stack select`
2. `pulumi preview` (ここで初めてZod/Pydanticによるバリデーションが走る)
仮に誰かが不正なパラメータをコミットした場合、PRのチェックフェーズで即座にエラーログが出力され、レビュー待ちの時間を無駄にすることがなくなる。
—
5. 伝説のSREからのメッセージ
「動くからいいや」で書かれたインフラコードは、数ヶ月後の障害対応時に自分(あるいは後任のエンジニア)の首を絞める凶器に変わる。
Pulumiという強力なエンジンを手に入れたのなら、そのプログラミング能力を単なるリソースの羅列に使うのではなく、「不正な状態を表現不可能な型システム(Make illegal states unrepresentable)」 の構築に全力を注いでほしい。
型安全性と厳格なスキーマ検証は、エンジニアの自由を奪う鎖ではない。
「安心して高速にデプロイを繰り返すための、最強の盾」 である。
さあ、今日のデプロイから `any` 型を排除し、コードベースを要塞化しよう。