Infrastructure as Code(IaC)と言えばTerraformが事実上の標準だが、近年Pulumiが急速に注目を集めています。PulumiはTypeScript、Python、Go、C#などの汎用プログラミング言語でインフラを定義できるツールです。筆者はTerraformを5年以上使い続けてきたが、最近の複数プロジェクトでPulumiに切り替えた経験から、両者の実践的な比較を行う。
Pulumiのアーキテクチャを理解するために、まずは基本的な概念を押さえておこう。
TerraformのHCLに相当する部分を、慣れ親しんだプログラミング言語で記述できる点が最大の差別化要因だ。
実際のコードを見るのが最も理解が早い。以下はAWS上にS3バケットとLambda関数を作成する例だ。
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// 設定の読み込み
const config = new pulumi.Config();
const environment = config.require("environment");
// S3バケットの作成
const bucket = new aws.s3.Bucket("data-bucket", {
bucket: \`myapp-data-\${environment}\`,
versioning: {
enabled: true,
},
tags: {
Environment: environment,
ManagedBy: "pulumi",
},
});
// Lambda関数の作成
const lambdaRole = new aws.iam.Role("lambda-role", {
assumeRolePolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [{
Action: "sts:AssumeRole",
Effect: "Allow",
Principal: { Service: "lambda.amazonaws.com" },
}],
}),
});
const lambdaFunction = new aws.lambda.Function("processor", {
runtime: "nodejs18.x",
handler: "index.handler",
role: lambdaRole.arn,
code: new pulumi.asset.FileArchive("./lambda"),
environment: {
variables: {
BUCKET_NAME: bucket.id,
},
},
});
// 出力のエクスポート
export const bucketName = bucket.id;
export const functionArn = lambdaFunction.arn;
TypeScriptの型システムにより、リソースのプロパティに対する補完やバリデーションがエディタ上でリアルタイムに効く。これはHCLでは得られない体験だ。
Terraformが採用するHCLは宣言的で読みやすいが、複雑なロジックの表現に限界がある。条件分岐やループが必要な場面では、しばしば可読性が低下します。
# Terraformでの条件分岐(やや読みにくい)
resource "aws_instance" "example" {
count = var.create_instance ? 1 : 0
ami = var.environment == "prod" ? var.prod_ami : var.dev_ami
}
# Pulumiなら自然なプログラミング(TypeScript)
if (shouldCreateInstance) {
const instance = new aws.ec2.Instance("example", {
ami: environment === "prod" ? prodAmi : devAmi,
});
}
Pulumiではforループ、map、filterなどの言語機能をそのまま活用できるため、複雑なインフラパターンも自然に記述できます。
Pulumiの大きなアドバンテージがテスタビリティです。汎用言語のテストフレームワークがそのまま使える。
import * as pulumi from "@pulumi/pulumi";
import { describe, it, expect } from "vitest";
describe("Infrastructure", () => {
it("S3バケットにバージョニングが有効であること", async () => {
const bucket = new aws.s3.Bucket("test-bucket", {
versioning: { enabled: true },
});
const versioning = await new Promise((resolve) =>
bucket.versioning.apply((v) => resolve(v?.enabled ?? false))
);
expect(versioning).toBe(true);
});
it("タグにEnvironmentが設定されていること", async () => {
// ユニットテストでリソースのプロパティを検証
pulumi.runtime.setMocks(new MyMocks());
const infra = await import("./index");
// アサーションロジック
});
});
Terraformにもterratestやtftest等のテストツールはあるが、言語の壁により一体感のあるテスト体験を実現するのは難しい。
PulumiはデフォルトでPulumi Cloudにステートを保存します。S3やGCSなどのセルフホストバックエンドも選択可能だ。Terraformのリモートステート管理と基本的な概念は同じですが、Pulumi Cloudを使う場合はステート管理のセットアップが不要で、チーム間のステートロックも自動的に処理されます。
Pulumiでは複数のリソースをまとめたカスタムコンポーネントを作成できます。これはTerraformのモジュールに相当するが、クラスベースのため継承やコンポジションが使える。
class WebApplication extends pulumi.ComponentResource {
public readonly url: pulumi.Output;
constructor(name: string, args: WebAppArgs, opts?: pulumi.ComponentResourceOptions) {
super("custom:WebApplication", name, {}, opts);
const bucket = new aws.s3.Bucket(\`\${name}-assets\`, {
website: { indexDocument: "index.html" },
}, { parent: this });
const cdn = new aws.cloudfront.Distribution(\`\${name}-cdn\`, {
// CloudFront設定
}, { parent: this });
this.url = cdn.domainName;
}
}
// 使用側はシンプル
const app = new WebApplication("myapp", { domain: "example.com" });
Pulumiはシークレットをステート内で自動的に暗号化します。pulumi.ConfigのrequireSecretメソッドで取得した値は、ログやステートファイルに平文で露出しない。Terraformではこの部分をVaultなどの外部ツールで補完する必要がある場合が多いです。
TerraformからPulumiへの移行を検討する際、以下の点に留意すべきです。
pulumi importコマンドで既存リソースの取り込みが可能。Terraformのステートからの移行ツールも提供されていますPulumiは「プログラマのためのIaC」として、Terraformとは異なる価値を提供しています。型安全性、テスタビリティ、表現力の高さは、ソフトウェアエンジニアリングの文化が根付いたチームにとって強力な武器となります。一方で、Terraformの巨大なエコシステムと実績は依然として大きなアドバンテージだ。筆者としては、新規プロジェクトでTypeScript/Pythonに強いチームであればPulumiを積極的に検討すべきだと考えています。既存のTerraform資産がある場合は、段階的な移行を視野に入れつつ、まずは小さなプロジェクトでPulumiを試すことをお勧めします。