Pulumiインフラ as Code実戦:5つのパターンでTypeScriptでクラウドリソースを管理
DevOps
Pulumi:本物のプログラミング言語でインフラを管理
TerraformはHCLで設定を書くが、ループ/条件/再利用が必要な時はワークアラウンドの山。PulumiはTypeScript/Python/Goなどの本物のプログラミング言語でインフラを定義、ループ、条件、関数、クラス——すべてのプログラミング能力が利用可能。2026年、Pulumiは80以上のクラウドプロバイダーをサポート、Pulumi AIがインフラコードを自動生成、Pulumi DeploymentsでGitOps自動デプロイを実現。
本記事では5つのコアパターンから、プロジェクト構築→コンポーネント抽象→マルチクラウドデプロイ→状態管理→GitOpsのフルパイプライン実戦を解説します。
コア概念
| 概念 | 説明 |
|---|---|
| Pulumi | プログラミング言語でインフラを定義するIaCツール |
| Stack | 同一プロジェクトの異なる環境(dev/staging/prod) |
| Resource | クラウドリソースの抽象表現 |
| ComponentResource | カスタムコンポーネント、複数のResourceをカプセル化 |
| Provider | クラウドプロバイダープラグイン(AWS/GCP/Azure/K8s) |
| State | インフラ状態の永続化ストレージ |
| Output | 非同期値、リソース作成後のプロパティを表す |
| Config | Stackレベルの設定キーバリューペア |
問題分析:Pulumi IaCの5つの課題
- 学習曲線:HCLからTypeScriptへの移行には思考の転換が必要
- Output処理:Outputは直接文字列として使えない、非同期チェーンの理解が必要
- 状態管理:状態ファイルの破損や競合からの復旧
- コンポーネント設計:再利用可能なインフラコンポーネントの抽象化方法
- マルチクラウドオーケストレーション:異なるクラウドプロバイダーリソース間の依存と調整
ステップバイステップ:5つのPulumi IaCパターン
パターン1:Pulumiプロジェクトと基本リソース
// index.ts - Pulumiプロジェクトエントリポイント
import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';
import * as awsx from '@pulumi/awsx';
const config = new pulumi.Config();
const environment = config.get('environment') || 'dev';
const vpcCidr = config.get('vpcCidr') || '10.0.0.0/16';
const vpc = new aws.ec2.Vpc('main-vpc', {
cidrBlock: vpcCidr,
enableDnsHostnames: true,
enableDnsSupport: true,
tags: {
Name: `${environment}-vpc`,
Environment: environment,
},
});
const publicSubnets = [0, 1, 2].map((i) => {
return new aws.ec2.Subnet(`public-${i}`, {
vpcId: vpc.id,
cidrBlock: `10.0.${i}.0/24`,
availabilityZone: aws.getAvailabilityZonesOutput().names[i],
mapPublicIpOnLaunch: true,
tags: { Name: `${environment}-public-${i}`, Environment: environment },
});
});
const privateSubnets = [0, 1, 2].map((i) => {
return new aws.ec2.Subnet(`private-${i}`, {
vpcId: vpc.id,
cidrBlock: `10.0.${i + 10}.0/24`,
availabilityZone: aws.getAvailabilityZonesOutput().names[i],
tags: { Name: `${environment}-private-${i}`, Environment: environment },
});
});
const cluster = new aws.ecs.Cluster('main-cluster', {
tags: { Name: `${environment}-cluster`, Environment: environment },
});
const sg = new aws.ec2.SecurityGroup('app-sg', {
vpcId: vpc.id,
ingress: [
{ protocol: 'tcp', fromPort: 80, toPort: 80, cidrBlocks: ['0.0.0.0/0'] },
{ protocol: 'tcp', fromPort: 443, toPort: 443, cidrBlocks: ['0.0.0.0/0'] },
],
egress: [
{ protocol: '-1', fromPort: 0, toPort: 0, cidrBlocks: ['0.0.0.0/0'] },
],
tags: { Name: `${environment}-app-sg`, Environment: environment },
});
export const vpcId = vpc.id;
export const clusterArn = cluster.arn;
export const securityGroupId = sg.id;
パターン2:ComponentResourceコンポーネント抽象
// components/ecs-service.ts
import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';
interface EcsServiceArgs {
clusterArn: pulumi.Input<string>;
subnets: pulumi.Input<string>[];
securityGroups: pulumi.Input<string>[];
imageName: pulumi.Input<string>;
cpu: number;
memory: number;
port: number;
desiredCount: number;
environment: string;
}
export class EcsService extends pulumi.ComponentResource {
public readonly serviceArn: pulumi.Output<string>;
public readonly albDns: pulumi.Output<string>;
constructor(name: string, args: EcsServiceArgs, opts?: pulumi.ComponentResourceOptions) {
super('custom:ecs:Service', name, {}, opts);
const alb = new aws.lb.LoadBalancer(`${name}-alb`, {
subnets: args.subnets,
securityGroups: args.securityGroups,
internal: false,
loadBalancerType: 'application',
tags: { Name: `${args.environment}-${name}-alb`, Environment: args.environment },
}, { parent: this });
const targetGroup = new aws.lb.TargetGroup(`${name}-tg`, {
port: args.port,
protocol: 'HTTP',
vpcId: args.subnets[0],
targetType: 'ip',
healthCheck: {
path: '/health',
interval: 30,
timeout: 5,
healthyThreshold: 2,
unhealthyThreshold: 3,
},
tags: { Name: `${args.environment}-${name}-tg`, Environment: args.environment },
}, { parent: this });
const listener = new aws.lb.Listener(`${name}-listener`, {
loadBalancerArn: alb.arn,
port: 80,
protocol: 'HTTP',
defaultActions: [{
type: 'forward',
targetGroupArn: targetGroup.arn,
}],
}, { parent: this });
const taskDefinition = new aws.ecs.TaskDefinition(`${name}-task`, {
family: `${args.environment}-${name}`,
networkMode: 'awsvpc',
requiresCompatibilities: ['FARGATE'],
cpu: args.cpu.toString(),
memory: args.memory.toString(),
containerDefinitions: pulumi.jsonStringify([{
name: name,
image: args.imageName,
essential: true,
portMappings: [{ containerPort: args.port, protocol: 'tcp' }],
logConfiguration: {
logDriver: 'awslogs',
options: {
'awslogs-group': `/ecs/${args.environment}-${name}`,
'awslogs-region': 'us-east-1',
'awslogs-stream-prefix': 'ecs',
},
},
}]),
}, { parent: this });
const service = new aws.ecs.Service(`${name}-svc`, {
cluster: args.clusterArn,
desiredCount: args.desiredCount,
launchType: 'FARGATE',
taskDefinition: taskDefinition.arn,
networkConfiguration: {
awsvpcConfiguration: {
subnets: args.subnets,
securityGroups: args.securityGroups,
assignPublicIp: true,
},
},
loadBalancers: [{
targetGroupArn: targetGroup.arn,
containerName: name,
containerPort: args.port,
}],
}, { parent: this });
this.serviceArn = service.id;
this.albDns = alb.dnsName;
this.registerOutputs({
serviceArn: this.serviceArn,
albDns: this.albDns,
});
}
}
パターン3:マルチクラウドデプロイ
// multi-cloud.ts
import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';
import * as gcp from '@pulumi/gcp';
const config = new pulumi.Config();
const cloud = config.get('cloud') || 'aws';
function createAwsInfrastructure() {
const bucket = new aws.s3.Bucket('data-bucket', {
forceDestroy: true,
tags: { Name: 'data-bucket' },
});
const db = new aws.rds.Instance('database', {
engine: 'postgres',
engineVersion: '16.1',
instanceClass: 'db.t3.micro',
allocatedStorage: 20,
username: 'admin',
password: config.requireSecret('dbPassword'),
skipFinalSnapshot: true,
});
return { bucketArn: bucket.arn, dbEndpoint: db.endpoint };
}
function createGcpInfrastructure() {
const bucket = new gcp.storage.Bucket('data-bucket', {
location: 'US',
forceDestroy: true,
});
const db = new gcp.sql.DatabaseInstance('database', {
databaseVersion: 'POSTGRES_16',
settings: { tier: 'db-f1-micro' },
deletionProtection: false,
});
return { bucketName: bucket.name, dbConnection: db.connectionName };
}
const infra = cloud === 'aws' ? createAwsInfrastructure() : createGcpInfrastructure();
export const bucketOutput = 'bucketArn' in infra ? infra.bucketArn : infra.bucketName;
export const dbOutput = 'dbEndpoint' in infra ? infra.dbEndpoint : infra.dbConnection;
パターン4:状態管理とディザスタリカバリ
// pulumi.state-config.ts
// S3バックエンドで状態を保存
const backendConfig = {
url: 's3://pulumi-state-bucket/my-project',
};
// pulumi stack init my-project-dev
// pulumi config set aws:region us-east-1
// pulumi up
# 状態ロック競合の解決
pulumi stack import --file backup-state.json
# 状態ファイルのバックアップとリカバリ
pulumi stack export --file state-backup-$(date +%Y%m%d).json
# 強制アンロック(注意して使用)
pulumi stack unlock
# 状態を実際のリソースと同期
pulumi refresh
# 全リソースの破棄
pulumi destroy
// 状態ドリフト検出
import * as pulumi from '@pulumi/pulumi';
pulumi.runtime.setOptions({
stackTransform: (args) => {
args.resource.options.protect = true;
return Promise.resolve(args);
},
});
パターン5:Pulumi GitOpsと自動化
# .github/workflows/pulumi-deploy.yml
name: Pulumi Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
preview:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: pulumi/actions@v5
with:
command: preview
stack-name: my-project-staging
comment-on-pr: true
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY }}
deploy:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
needs: []
steps:
- uses: actions/checkout@v4
- uses: pulumi/actions@v5
with:
command: up
stack-name: my-project-prod
skip-install: false
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY }}
// Policy as Code - コンプライアンスチェック
import * as pulumi from '@pulumi/pulumi';
import { PolicyPack, validateResourceOfType } from '@pulumi/policy';
new PolicyPack('infra-policies', {
policies: [
{
name: 'no-public-egress',
description: 'Security groups must not allow unrestricted egress',
enforcementLevel: 'mandatory',
validateResource: validateResourceOfType(aws.ec2.SecurityGroup, (sg, args, reportViolation) => {
for (const rule of sg.egress || []) {
if (rule.cidrBlocks?.includes('0.0.0.0/0')) {
reportViolation('Security group allows unrestricted egress');
}
}
}),
},
{
name: 'encryption-required',
description: 'S3 buckets must have encryption enabled',
enforcementLevel: 'mandatory',
validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args, reportViolation) => {
if (!bucket.serverSideEncryptionConfiguration) {
reportViolation('S3 bucket must have server-side encryption enabled');
}
}),
},
{
name: 'tag-compliance',
description: 'All resources must have required tags',
enforcementLevel: 'advisory',
validateResource: (args, reportViolation) => {
const tags = args.props.tags || {};
if (!tags.Environment) {
reportViolation('Resource missing required tag: Environment');
}
},
},
],
});
よくある落とし穴
落とし穴1:Outputを文字列として使えない
// ❌ 間違い:Outputを直接連結
const url = `https://${alb.dnsName}/api`; // 型エラー
// ✅ 正しい:pulumi.interpolateまたはapplyを使用
const url = pulumi.interpolate`https://${alb.dnsName}/api`;
// または
const url = alb.dnsName.apply(dns => `https://${dns}/api`);
落とし穴2:リソース依存の未宣言
// ❌ 間違い:暗黙的依存、作成順序が間違う可能性
const db = new aws.rds.Instance('db', { ... });
const app = new aws.ecs.Service('app', {
environment: [{ name: 'DB_HOST', value: db.endpoint }], // 暗黙的依存
});
// ✅ 正しい:依存を明示的に宣言
const app = new aws.ecs.Service('app', {
environment: [{ name: 'DB_HOST', value: db.endpoint }],
}, { dependsOn: [db] });
落とし穴3:Secretの平文保存
// ❌ 間違い:パスワードが平文
const db = new aws.rds.Instance('db', {
password: 'my-password-123',
});
// ✅ 正しい:Config Secretを使用
const password = config.requireSecret('dbPassword');
const db = new aws.rds.Instance('db', {
password: password,
});
落とし穴4:リソース保護が未設定
// ❌ 間違い:本番データベースが無保護、誤削除の可能性
const db = new aws.rds.Instance('prod-db', {
skipFinalSnapshot: true,
});
// ✅ 正しい:保護を有効化
const db = new aws.rds.Instance('prod-db', {
skipFinalSnapshot: false,
finalSnapshotIdentifier: 'prod-db-final',
}, { protect: true });
落とし穴5:状態ファイルが暗号化されていない
# ❌ 間違い:ローカルファイルバックエンド、暗号化なし
pulumi login --local
# ✅ 正しい:Pulumi Cloudまたは暗号化S3バックエンドを使用
pulumi login
# または
pulumi login s3://pulumi-state-bucket?region=us-east-1
エラートラブルシューティング
| # | エラーメッセージ | 原因 | 解決方法 |
|---|---|---|---|
| 1 | Cannot read property of Output |
Outputでapply未使用 | pulumi.interpolateまたはapplyを使用 |
| 2 | Resource already exists |
状態と実際が不一致 | pulumi importで既存リソースをインポート |
| 3 | Conflict: stack is locked |
状態ロック競合 | pulumi stack unlock |
| 4 | 403 Access Denied |
AWS認証情報が無効 | AWS_ACCESS_KEY_ID設定を確認 |
| 5 | Provider credential failed |
クラウドプロバイダー認証失敗 | Provider設定を検証 |
| 6 | Destroy failed: protected resource |
リソースが保護されている | pulumi state unprotect |
| 7 | Stack not found |
Stackが存在しない | pulumi stack initで作成 |
| 8 | Plugin not found |
Providerプラグインが不足 | pulumi plugin install |
| 9 | Drift detected |
状態と実際のドリフト | pulumi refreshで同期 |
| 10 | Secret exposed in state |
Secretが暗号化されていない | requireSecretと暗号化バックエンドを使用 |
高度な最適化
- Pulumi AI:自然言語記述からインフラコードを自動生成
- Automation API:アプリケーションにPulumiを組み込み、セルフサービスプラットフォームを実現
- Pulumi Deployments:Gitプッシュで自動デプロイをトリガー
- CrossGuard:Policy as Codeコンプライアンスチェック
- Pulumi ESC:環境シークレットと設定の一元管理
比較分析
| 次元 | Pulumi | Terraform | CDK for Terraform | AWS CDK |
|---|---|---|---|---|
| 言語 | TS/Python/Go/C# | HCL | TS/Python/Java | TS/Python/Java |
| 状態管理 | Pulumi Cloud/S3 | S3/Consul/Cloud | Terraform Cloud | CloudFormation |
| テスト | ユニット+インテグレーション | terraform test | CDKテスト | CDKテスト |
| マルチクラウド | 80+プロバイダー | 3000+プロバイダー | 3000+プロバイダー | AWSのみ |
| コンポーネント再利用 | プログラミング言語ネイティブ | Module | CDK Construct | Construct |
| 学習曲線 | 中 | 低 | 中高 | 中 |
まとめ:Pulumiは本物のプログラミング言語でインフラを定義し、IaCにループ、条件、型チェック、ユニットテストの力をもたらします。ComponentResourceコンポーネント抽象+Output非同期チェーン+Policy as Codeコンプライアンスチェックの三位一体により、インフラコードが「設定ファイル」から「エンジニアリングコード」へ進化。2026年のPulumi AIとAutomation APIの成熟により、インフラ管理がよりインテリジェントで自動化されています。
オンラインツール推奨
- JSONフォーマッター:/ja/json/format
- Hash計算:/ja/encode/hash
- cURL→コード変換:/ja/dev/curl-to-code
ブラウザローカルツールを無料で試す →
#基础设施即代码#Pulumi#IaC#TypeScript IaC#2026#DevOps