Pulumi

DevOpsInfrastructure as CodeIaCTypeScriptPythonGoC#Javaプログラミング言語マルチクラウド

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

デメリット

  • 学習コスト: プログラミング言語の知識が必要(非プログラマーには敷居が高い)
  • 複雑性: 過度に複雑なコードを書いてしまう可能性
  • ツール依存: 各言語のエコシステムに依存
  • パフォーマンス: 大規模環境では実行時間が長くなる可能性

参考ページ

公式リソース

学習リソース

書き方の例

基本的なプロジェクト作成(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 }}