Pulumiの深淵:TypeScriptで挑む次世代クラウドインフラストラクチャの完全掌握
世の中の多くのエンジニアは、インフラの宣言的記述といえば「HCL(HashiCorp Configuration Language)のDSL」や「JSON/YAMLの海」を思い浮かべる。だが、大規模なクラウド環境を運用し、数千に及ぶリソースの依存関係、動的な条件分岐、複雑なモジュール化の地獄をくぐり抜けてきた者なら気づいているはずだ。――静的な設定言語の限界に。
本稿では、汎用プログラミング言語(TypeScript)を用いてAWSインフラをコード化する「Pulumi」を取り上げる。単なる「入門ハンズオン」の枠を超え、裏で何が起きているのかという低レイヤのアーキテクチャ、状態管理(State)の真実、そして本番運用の現場で生き残るための極限の知見を叩き込む。
真のDevOpsエンジニアのための、知的で容赦のないインフラ構築の旅を始めよう。
—
1. 内部アーキテクチャの理解:なぜPulumiなのか
TerraformがHCLという独自のDSLを解釈し、リソースグラフを構築するのに対し、Pulumiは「本物のプログラミング言語のランタイム」をそのまま利用する。
[ TypeScript Code ]
↓ (コンパイル & 実行)
[ Pulumi Language Host (Node.js) ]
↓ (gRPC経由でリソースのCRUD要求)
[ Pulumi Engine (状態比較・依存関係グラフ解決) ]
↓
[ AWS Resource Provider (Go製プラグイン) ]
↓
[ AWS APIs ]
TypeScriptで書かれたコードはNode.jsランタイム上で実行され、評価(Evaluation)の過程で「プロビジョニングすべきリソースの抽象構文木(URNやプロパティ)」が生成される。これがgRPCを介してPulumiエンジンに渡り、既存のState(バックエンド)と比較されて実際のAWS APIコールへと変換される。
このアーキテクチャの最大の恩恵は、プログラミング言語の全機能(ループ、関数、型システム、外部ライブラリ)をインフラ定義にそのまま持ち込める点にある。YAMLの無理やりなテンプレート機能や、HCLの複雑な`for_each`ハックとは今日でサヨナラだ。
—
2. 前提条件と環境構築
プロの手元には、常に研ぎ澄されたツールチェーンがある。以下の環境を準備せよ。
- Node.js (v18.x LTS以上推奨)
- AWS CLI (v2)
- Pulumi CLI
Pulumi CLIのインストールと確認
公式インストーラを使用(Linux / macOS)
curl -fsSL https://get.pulumi.com | sh
パスを通し、バージョンを確認
export PATH=$HOME/.pulumi/bin:$PATH
pulumi version
AWS認証情報の設定
環境変数による明示的な注入を推奨する。プロファイル汚染を防ぐためだ。
export AWS_ACCESS_KEY_ID=”AKIAxxxxxxxxxxxxxxxx”
export AWS_SECRET_ACCESS_KEY=”xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx”
export AWS_REGION=”ap-northeast-1″
—
3. プロジェクト初期化と堅牢なディレクトリ構造
安易な`pulumi new`のデフォルト設定に頼るな。本番を見据えた堅牢なプロジェクト構造を構築する。
適当な作業ディレクトリを作成し、TypeScriptプロジェクトを初期化する。
mkdir pulumi-aws-handson && cd pulumi-aws-handson
pulumi new aws-typescript –dir . –name aws-handson-prod –stack production –yes
生成されたファイルを整理し、以下のディレクトリ構成に昇華させよ。
.
├── Pulumi.yaml
├── Pulumi.production.yaml
├── package.json
├── tsconfig.json
└── index.ts # ここにすべてのリソースを美しく集約する(あるいはモジュール分割する)
—
4. VPCとEC2インスタンスをコードで構築する(TypeScript実装)
ここからが本番だ。単に動くだけのコードではなく、型安全性(Type Safety)を最大限に活かし、リソース間の暗黙的・明示的な依存関係を完全に制御したコードを記述する。
以下のコードを `index.ts` に配置せよ。
import as pulumi from “@pulumi/pulumi”;
import as aws from “@pulumi/aws”;
// 1. ネットワーク基盤(VPC)の構築
// 冗長性を考慮し、プライベート/パブリックサブネットを設計する
const vpc = new aws.ec2.Vpc(“core-vpc”, {
cidrBlock: “10.0.0.0/16”,
enableDnsHostnames: true,
enableDnsSupport: true,
tags: {
Name: “core-vpc-production”,
ManagedBy: “Pulumi”,
},
});
// インターネットゲートウェイ
const igw = new aws.ec2.InternetGateway(“core-igw”, {
vpcId: vpc.id,
tags: { Name: “core-igw” },
});
// パブリックサブネット
const publicSubnet = new aws.ec2.Subnet(“core-subnet-public”, {
vpcId: vpc.id,
cidrBlock: “10.0.1.0/24”,
mapPublicIpOnLaunch: true,
availabilityZone: “ap-northeast-1a”,
tags: { Name: “core-subnet-public-1a” },
});
// ルートテーブルとルート設定
const publicRouteTable = new aws.ec2.RouteTable(“core-rt-public”, {
vpcId: vpc.id,
routes: [
{
cidrBlock: “0.0.0.0/0”,
gatewayId: igw.id,
},
],
tags: { Name: “core-rt-public” },
});
new aws.ec2.RouteTableAssociation(“core-rta-public”, {
subnetId: publicSubnet.id,
routeTableId: publicRouteTable.id,
});
// 2. セキュリティグループの定義
// 最小権限の原則(Least Privilege)に基づき、SSH(22)のみを特定のCIDRに限定
const securityGroup = new aws.ec2.SecurityGroup(“core-sg-web”, {
vpcId: vpc.id,
description: “Allow SSH inbound traffic”,
ingress: [
{
protocol: “tcp”,
fromPort: 22,
toPort: 22,
cidrBlocks: [“0.0.0.0/0”], // 運用時は厳格な踏み台IPを指定すること
},
],
egress: [
{
protocol: “-1”,
fromPort: 0,
toPort: 0,
cidrBlocks: [“0.0.0.0/0”],
},
],
tags: { Name: “core-sg-web” },
});
// 3. 最新のAmazon Linux 2023 AMIを動的に取得
// 固定のAMI IDを使わないのは、SREとしての基本中の基本である。
const ami = aws.ec2.getAmi({
filters: [
{
name: “name”,
values: [“al2023-ami–x86_64”],
},
{
name: “virtualization-type”,
values: [“hvm”],
},
],
mostRecent: true,
owners: [“137112412989”], // Amazon
});
// 4. EC2インスタンスのプロパティ定義
const server = new aws.ec2.Instance(“core-ec2-web”, {
instanceType: “t3.micro”,
ami: ami.then(ami => ami.id), // 非同期のAMI取得結果をPromiseで解決
subnetId: publicSubnet.id,
vpcSecurityGroupIds: [securityGroup.id],
associatePublicIpAddress: true,
tags: {
Name: “core-ec2-web-production”,
},
// 万が一の誤削除を防ぐための保護(オプション)
disableApiTermination: false,
});
// 5. スタックの出力(Outputs)
// デプロイ後に必要なエンドポイント情報を外部に露出させる
export const vpcId = vpc.id;
export const instanceId = server.id;
export const publicIp = server.publicIp;
—
5. デプロイ(pulumi up)と破棄(pulumi destroy)の極意
コードが書き上がったら、いよいよ実行フェーズだ。ここでは、現場で役立つ実践的なコマンドオプションと挙動の裏側を解説する。
プレビューとデプロイ (`pulumi up`)
単に実行するだけでなく、必ずプレビューを確認せよ。
pulumi up –stack production
- 何が起きているか?
PulumiエンジンがAWSの現在のリソース状態と、コードから生成された目標状態を比較し、Diff(差分)を計算する。
もし意図しないリソースの再作成(Replacement)が発生している場合(例: セキュリティグループの強制置換など)、このプレビュー画面の `+-` マークで検知できる。本番環境での障害を防ぐ最後の砦である。
状態の確認とエクスポート
PulumiはデフォルトでSaaS型のBackend(app.pulumi.com)を利用するが、企業要件によってはS3やGCSなどのセルフマネージド・バックエンドを使用すべきだ。
バックエンドをローカルファイルシステムに切り替える場合
pulumi login file://~/.pulumi/states
現在のステートをJSONとしてダンプする(トラブルシューティング用)
pulumi stack export –stack production > state-backup.json
迅速な破棄 (`pulumi destroy`)
検証が終わったら、リソースを完全に消し去る。
pulumi destroy –stack production –yes
ここで重要となるのが「依存関係グラフの逆順トポロジカルソート」だ。Pulumiはリソース間の依存関係を完全に把握しているため、サブネットを削除する前にEC2インスタンスを安全にシャットダウン・削除し、最後にVPCやIGWを綺麗に片付ける。手動で順番を気にする必要は一切ない。
—
6. エキスパート向け知見:パフォーマンスと自動化の極限へ
最後に、Pulumiをプロダクションレベルで極めるための高度なハックを授ける。
1. 並行度(Concurrency)の制御
大規模なスタックでは、デフォルトの並行度ではリソース作成に時間がかかる場合がある。また、AWS側のAPIスロットリング(Rate Limit)に引っかかることもある。
環境変数で並行度を制御し、パイプラインのスループットを最適化せよ。
export PULUMI_PARALLEL=20 # 同時に処理するリソース数を制御
pulumi up –yes
2. CI/CDパイプライン(GitHub Actions等)での完全自動化
対話型プロンプト(`–yes`)を外し、プレビュー結果をJSONで保存して静的解析を通すのが、洗練されたDevOpsパイプラインの設計思想である。
計画(Plan)のJSON出力
pulumi preview –stack production –json > plan.json
自動適用
pulumi up –stack production –non-interactive –yes
3. Secret(機密情報)の暗号化
TypeScriptコード内にベタ書きされたDBパスワードやAPIキーは、PulumiのConfigシステムを使って強固に暗号化(Envelope Encryption)される。
機密情報の保存
pulumi config set –secret dbPassword “SuperSecretPassword123!”
これにより、Stateファイル内でも値は完全暗号化され、安全にGit管理(あるいはリモートバックエンド)へ載せることが可能となる。
—
結び
Pulumiを使いこなすということは、インフラを「単なる設定」から「ファーストクラスのソフトウェア」へと昇華させることに他ならない。
型システムによる恩恵、デバッグのしやすさ、そしてプログラミング言語の表現力。一度この領域に足を踏み入れた者にとって、静的設定ファイルだけの世界にはもう戻れなくなるだろう。
コードを書け。インフラを支配しろ。