Pulumi
DevOpsツール
Pulumi
概要
Pulumiは、TypeScript、Python、Go、C#、Javaなどのプログラミング言語を使用してInfrastructure as Code(IaC)を実現するモダンなプラットフォームです。従来のDSL(Domain Specific Language)ではなく、馴染みのあるプログラミング言語でインフラを定義し、150以上のクラウドプロバイダーをサポートしています。
詳細
Pulumiは2017年に設立され、「真のコードベースインフラ管理」を理念として開発されました。TerraformのようなDSLを使用するのではなく、開発者が既に習得している一般的なプログラミング言語(TypeScript、Python、Go、C#、Java)を活用してインフラを定義できます。
このアプローチにより、既存の開発ツール(IDE、デバッガー、パッケージマネージャー、テストフレームワーク)をそのまま活用でき、開発者の生産性が大幅に向上します。Pulumiは14,000以上のコードスニペットと豊富な実装例を提供しており、開発者フレンドリーなアプローチで急成長を続けています。
Pulumi独特の機能として、プログラミング言語の特徴(条件分岐、ループ、関数、クラス、パッケージ管理)を直接インフラ定義に活用できる点があります。また、Automation APIにより、インフラ管理をアプリケーションに組み込むことも可能です。
メリット・デメリット
メリット
- プログラミング言語使用: 既存の開発スキルとツールをそのまま活用可能
- 豊富なプロバイダー: 150以上のクラウドプロバイダーをサポート
- 開発ツール統合: IDE、デバッガー、テストフレームワークを活用
- Automation API: プログラムからインフラ管理を自動化
- 強力な型システム: コンパイル時のエラー検出とIntelliSense
デメリット
- 学習コスト: プログラミング言語の知識が必要(非プログラマーには敷居が高い)
- 複雑性: 過度に複雑なコードを書いてしまう可能性
- ツール依存: 各言語のエコシステムに依存
- パフォーマンス: 大規模環境では実行時間が長くなる可能性
参考ページ
公式リソース
- Pulumi公式サイト - 公式サイト
- Pulumi Documentation - 公式ドキュメント
- Pulumi Registry - プロバイダーとパッケージ検索
- Pulumi GitHub - ソースコード
学習リソース
- Get Started with Pulumi - 公式チュートリアル
- Pulumi Examples - 実装例集
- Pulumi Workshops - ハンズオンワークショップ
書き方の例
基本的なプロジェクト作成(TypeScript)
# 新しいプロジェクト作成
mkdir my-pulumi-app && cd my-pulumi-app
# TypeScriptテンプレートで初期化
pulumi new typescript
# AWSプロジェクトテンプレート
pulumi new aws-typescript
# Azureプロジェクトテンプレート
pulumi new azure-typescript
# GCPプロジェクトテンプレート
pulumi new gcp-typescript
# Kubernetesプロジェクトテンプレート
pulumi new kubernetes-typescript
AWS S3バケット作成(TypeScript)
// index.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// S3バケット作成
const bucket = new aws.s3.Bucket("my-bucket", {
// バケット名は自動生成される
bucket: undefined,
// パブリックアクセスブロック
publicAccessBlock: new aws.s3.BucketPublicAccessBlock("bucket-pab", {
bucket: bucket.id,
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
}),
// サーバーサイド暗号化
serverSideEncryptionConfiguration: {
rule: {
applyServerSideEncryptionByDefault: {
sseAlgorithm: "AES256",
},
},
},
// バージョニング有効化
versioning: {
enabled: true,
},
});
// バケット名をエクスポート
export const bucketName = bucket.id;
export const bucketUrl = pulumi.interpolate`https://${bucket.bucket}.s3.amazonaws.com`;
条件分岐とループ(TypeScript)
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
const environment = config.require("environment"); // dev, staging, prod
// 環境に応じたインスタンスタイプ選択
const instanceType = environment === "prod" ? "t3.large" : "t3.micro";
// 本番環境のみMulti-AZ構成
const subnetIds = environment === "prod"
? ["subnet-12345", "subnet-67890", "subnet-abcdef"]
: ["subnet-12345"];
// 複数のWebサーバーをループで作成
const webServers: aws.ec2.Instance[] = [];
for (let i = 0; i < subnetIds.length; i++) {
const server = new aws.ec2.Instance(`web-server-${i}`, {
ami: "ami-0c94855ba95b798c7", // Amazon Linux 2
instanceType: instanceType,
subnetId: subnetIds[i],
tags: {
Name: `WebServer-${environment}-${i + 1}`,
Environment: environment,
},
});
webServers.push(server);
}
// オブジェクトの配列を使ったループ
const environments = [
{ name: "dev", count: 1 },
{ name: "staging", count: 2 },
{ name: "prod", count: 3 },
];
environments.forEach(env => {
for (let i = 0; i < env.count; i++) {
new aws.ec2.Instance(`${env.name}-instance-${i}`, {
ami: "ami-0c94855ba95b798c7",
instanceType: env.name === "prod" ? "t3.large" : "t3.micro",
tags: {
Name: `${env.name}-instance-${i}`,
Environment: env.name,
},
});
}
});
コンポーネントリソース作成(TypeScript)
// webserver.ts - 再利用可能なコンポーネント
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
export interface WebServerArgs {
vpcId: pulumi.Input<string>;
subnetId: pulumi.Input<string>;
instanceType?: string;
keyName?: string;
}
export class WebServer extends pulumi.ComponentResource {
public readonly instance: aws.ec2.Instance;
public readonly securityGroup: aws.ec2.SecurityGroup;
public readonly publicIp: pulumi.Output<string>;
constructor(name: string, args: WebServerArgs, opts?: pulumi.ComponentResourceOptions) {
super("custom:WebServer", name, {}, opts);
// セキュリティグループ作成
this.securityGroup = new aws.ec2.SecurityGroup(`${name}-sg`, {
vpcId: args.vpcId,
description: "Security group for web server",
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"],
},
{
protocol: "tcp",
fromPort: 22,
toPort: 22,
cidrBlocks: ["0.0.0.0/0"],
},
],
egress: [{
protocol: "-1",
fromPort: 0,
toPort: 0,
cidrBlocks: ["0.0.0.0/0"],
}],
}, { parent: this });
// EC2インスタンス作成
this.instance = new aws.ec2.Instance(`${name}-instance`, {
ami: "ami-0c94855ba95b798c7", // Amazon Linux 2
instanceType: args.instanceType || "t3.micro",
subnetId: args.subnetId,
keyName: args.keyName,
vpcSecurityGroupIds: [this.securityGroup.id],
userData: `#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Hello from ${name}!</h1>" > /var/www/html/index.html
`,
tags: {
Name: name,
},
}, { parent: this });
this.publicIp = this.instance.publicIp;
// このコンポーネントの出力を登録
this.registerOutputs({
instance: this.instance,
securityGroup: this.securityGroup,
publicIp: this.publicIp,
});
}
}
// 使用例(index.ts)
import { WebServer } from "./webserver";
const vpc = new aws.ec2.Vpc("main-vpc", {
cidrBlock: "10.0.0.0/16",
});
const subnet = new aws.ec2.Subnet("public-subnet", {
vpcId: vpc.id,
cidrBlock: "10.0.1.0/24",
mapPublicIpOnLaunch: true,
});
const webServer = new WebServer("my-web-server", {
vpcId: vpc.id,
subnetId: subnet.id,
instanceType: "t3.small",
});
export const serverUrl = pulumi.interpolate`http://${webServer.publicIp}`;
Python実装例
# __main__.py
import pulumi
import pulumi_aws as aws
# 設定値取得
config = pulumi.Config()
environment = config.require("environment")
# VPC作成
vpc = aws.ec2.Vpc("main-vpc",
cidr_block="10.0.0.0/16",
enable_dns_hostnames=True,
enable_dns_support=True,
tags={
"Name": f"{environment}-vpc",
"Environment": environment,
}
)
# インターネットゲートウェイ
igw = aws.ec2.InternetGateway("main-igw",
vpc_id=vpc.id,
tags={
"Name": f"{environment}-igw",
}
)
# パブリックサブネット
public_subnet = aws.ec2.Subnet("public-subnet",
vpc_id=vpc.id,
cidr_block="10.0.1.0/24",
map_public_ip_on_launch=True,
tags={
"Name": f"{environment}-public-subnet",
}
)
# ルートテーブル
route_table = aws.ec2.RouteTable("public-route-table",
vpc_id=vpc.id,
routes=[{
"cidr_block": "0.0.0.0/0",
"gateway_id": igw.id,
}],
tags={
"Name": f"{environment}-public-rt",
}
)
# ルートテーブル関連付け
aws.ec2.RouteTableAssociation("public-route-table-association",
subnet_id=public_subnet.id,
route_table_id=route_table.id
)
# セキュリティグループ
security_group = aws.ec2.SecurityGroup("web-sg",
vpc_id=vpc.id,
description="Security group for web server",
ingress=[
{
"protocol": "tcp",
"from_port": 80,
"to_port": 80,
"cidr_blocks": ["0.0.0.0/0"],
},
{
"protocol": "tcp",
"from_port": 443,
"to_port": 443,
"cidr_blocks": ["0.0.0.0/0"],
},
],
egress=[{
"protocol": "-1",
"from_port": 0,
"to_port": 0,
"cidr_blocks": ["0.0.0.0/0"],
}],
tags={
"Name": f"{environment}-web-sg",
}
)
# EC2インスタンス
instance = aws.ec2.Instance("web-server",
ami="ami-0c94855ba95b798c7", # Amazon Linux 2
instance_type="t3.micro" if environment != "prod" else "t3.small",
subnet_id=public_subnet.id,
vpc_security_group_ids=[security_group.id],
user_data="""#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Hello from Pulumi Python!</h1>" > /var/www/html/index.html
""",
tags={
"Name": f"{environment}-web-server",
"Environment": environment,
}
)
# 出力
pulumi.export("vpc_id", vpc.id)
pulumi.export("instance_ip", instance.public_ip)
pulumi.export("website_url", instance.public_ip.apply(lambda ip: f"http://{ip}"))
Automation API(プログラムからの実行)
// automation.ts - プログラムからPulumiを実行
import * as pulumi from "@pulumi/pulumi";
import { LocalWorkspace } from "@pulumi/pulumi/automation";
import * as aws from "@pulumi/aws";
// プログラムとして定義するインフラ
const pulumiProgram = async () => {
const bucket = new aws.s3.Bucket("auto-bucket", {
serverSideEncryptionConfiguration: {
rule: {
applyServerSideEncryptionByDefault: {
sseAlgorithm: "AES256",
},
},
},
});
return {
bucketName: bucket.id,
};
};
async function main() {
const stackName = "dev";
const projectName = "automation-example";
// Pulumiワークスペース作成
const stack = await LocalWorkspace.createOrSelectStack({
stackName,
projectName,
program: pulumiProgram,
});
console.log("Refreshing stack...");
await stack.refresh({ onOutput: console.info });
console.log("Setting up configuration...");
await stack.setConfig("aws:region", { value: "us-west-2" });
console.log("Updating stack...");
const upRes = await stack.up({ onOutput: console.info });
console.log(`Update summary: ${upRes.summary.resourceChanges}`);
console.log(`Bucket name: ${upRes.outputs.bucketName.value}`);
}
main().catch(console.error);
Kubernetesリソース管理
// k8s.ts
import * as k8s from "@pulumi/kubernetes";
import * as pulumi from "@pulumi/pulumi";
// Namespace作成
const namespace = new k8s.core.v1.Namespace("app-namespace", {
metadata: {
name: "my-app",
},
});
// ConfigMap
const configMap = new k8s.core.v1.ConfigMap("app-config", {
metadata: {
namespace: namespace.metadata.name,
name: "app-config",
},
data: {
"config.json": JSON.stringify({
database: {
host: "localhost",
port: 5432,
},
features: {
logging: true,
monitoring: true,
},
}),
},
});
// Secret
const secret = new k8s.core.v1.Secret("app-secret", {
metadata: {
namespace: namespace.metadata.name,
name: "app-secret",
},
type: "Opaque",
stringData: {
"db-password": "super-secret-password",
"api-key": "abcd1234-5678-90ef",
},
});
// Deployment
const deployment = new k8s.apps.v1.Deployment("app-deployment", {
metadata: {
namespace: namespace.metadata.name,
name: "my-app",
},
spec: {
replicas: 3,
selector: {
matchLabels: {
app: "my-app",
},
},
template: {
metadata: {
labels: {
app: "my-app",
},
},
spec: {
containers: [{
name: "app",
image: "nginx:1.21",
ports: [{
containerPort: 80,
}],
env: [
{
name: "DB_PASSWORD",
valueFrom: {
secretKeyRef: {
name: secret.metadata.name,
key: "db-password",
},
},
},
],
volumeMounts: [{
name: "config-volume",
mountPath: "/etc/config",
}],
}],
volumes: [{
name: "config-volume",
configMap: {
name: configMap.metadata.name,
},
}],
},
},
},
});
// Service
const service = new k8s.core.v1.Service("app-service", {
metadata: {
namespace: namespace.metadata.name,
name: "my-app-service",
},
spec: {
selector: {
app: "my-app",
},
ports: [{
port: 80,
targetPort: 80,
}],
type: "LoadBalancer",
},
});
export const serviceEndpoint = service.status.loadBalancer.ingress[0].ip;
Pulumi基本コマンド
# プロジェクト初期化
pulumi new typescript
pulumi new aws-typescript
pulumi new kubernetes-typescript
# スタック管理
pulumi stack init dev
pulumi stack select dev
pulumi stack ls
# 設定管理
pulumi config set aws:region us-west-2
pulumi config set myapp:environment dev
pulumi config set --secret myapp:apiKey abc123
# デプロイメント
pulumi preview # プレビュー(terraform planに相当)
pulumi up # デプロイ実行
pulumi up --yes # 確認スキップ
# 状態確認
pulumi stack output
pulumi stack output bucketName
pulumi logs
# リソース管理
pulumi state delete <resource-urn>
pulumi import <resource-type> <name> <id>
# 破棄
pulumi destroy
pulumi destroy --yes
# その他
pulumi console # ブラウザでコンソール表示
pulumi convert --from terraform --language typescript # Terraform変換
開発ワークフロー
1. プロジェクト設定
# 新規プロジェクト作成
mkdir my-infrastructure && cd my-infrastructure
pulumi new aws-typescript
# 依存関係インストール
npm install
# 設定
pulumi config set aws:region ap-northeast-1
2. 開発サイクル
# 1. コード作成・編集
code index.ts
# 2. プレビュー(変更確認)
pulumi preview
# 3. デプロイ
pulumi up
# 4. 動作確認
pulumi stack output
curl $(pulumi stack output endpointUrl)
# 5. ログ確認
pulumi logs
3. CI/CD統合
# .github/workflows/pulumi.yml
name: Pulumi Infrastructure
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
pulumi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm install
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Pulumi Preview
uses: pulumi/actions@v3
with:
command: preview
stack-name: dev
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
- name: Pulumi Deploy
if: github.ref == 'refs/heads/main'
uses: pulumi/actions@v3
with:
command: up
stack-name: production
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}