notes mizzy.org

Terraform / CDK / Pulumi / Carina比較マトリクス

確認済み 出典: explorations/2026-04-26-cdk-pulumi-terraform-carina-comparison/chapter-1〜5 作成

4ツール(Terraform / CDK / Pulumi / Carina)を軸ごとに横断比較した概観表を集約したもの。(要確認)と書かれたセルは埋まっていないまま残してある — 何が確認できていないかも情報である。ここでの「CDK」はAWS CDKを指す(cdktf / cdk8sは別engineへsynthする派生で対象外)。

記述言語と実行モデル(§1-1)

ツール記述言語ユーザワークフロー内部実行モデル
TerraformHCL (専用DSL)plan → applyDSLを解析してresource graph構築 → 全体の差分を一括計算 → 順次実行
CDKTS / Python / Java / C# / Go (汎用言語経由)synth → diff → deployコード実行でCFNテンプレート生成(synth) → CFN engineがapply
PulumiTS / Python / Go / .NET / Java (各言語ネイティブSDK)preview → upコード実行中にengineへgRPCストリーミングでRegisterResourceを逐次送信 → engineが随時差分判断・実行(ランタイム経路に静的中間テンプレートなし)
CarinaCarina DSL (.crn)plan → applyDSLを解析してresource graph構築 → 全体の差分を一括計算 → 順次実行

Pulumiはランタイム実行経路には静的中間テンプレートを持たないが、エコシステム全体には中間表現的なものが存在する: pulumi convertPCL (Pulumi Configuration Language) は公式に "intermediate representation" と呼ばれ、pulumi preview --save-plan静的planファイル(Update Plans) を生成して後でpulumi up --planで適用できる。「中間IRが一切ない」とは言えない。

CDKはjsii(実装はTypeScriptで、jsiiがPython/Java/C#/Goへbridging、APIのセマンティクスはTSが原本)。Pulumiは各言語ネイティブ実装で、多言語Component (MLC)は別建てのschema-driven codegenで実現される。

抽象化機構(§1-2)

ツール単位(複数リソースを束ねる抽象化)読み込み + インスタンス化の構文実体
Terraformmodule (子moduleは再利用、root moduleはデプロイ単位)module "network" { source = "./modules/network"; ... } (DSLレベルで読み込みとインスタンス化が一体)ディレクトリ(HCLファイル群)
CDKStack (デプロイ単位) / ユーザ定義Construct (再利用単位) / L3 Construct (既製パターン)ホスト言語のimport + new MyStack(app, 'main') (DSLレベルの読み込み構文はなくホスト言語に委譲)TypeScriptクラス(jsiiで多言語化)
PulumiStack (デプロイ単位、設定単位) / ユーザ定義ComponentResource / 既製Component (Crosswalk等)ホスト言語のimport + new NetworkComponent('network', args, opts)各言語ネイティブのクラス
Carinamodule (子moduleは再利用、root moduleはデプロイ単位)読み込み: let network = use { source = './modules/network' } / インスタンス化: network { ... } (DSLレベルで分離)ディレクトリ(.crnファイル群)

CDKのL2 Constructはサービスにより性格が大きく異なる中間層である。s3.Bucketのような単一リソース寄りのL2もあれば、ec2.Vpc (VPC + サブネット6個 + IGW + NAT Gateway)、rds.DatabaseCluster (Cluster + Instance + SubnetGroup + SG)、lambda.Function (Function + IAM Role + Log Group)のように複数リソース束ね寄りのL2もある。

層の対応:

TerraformCDKPulumiCarina
複数リソース束ね層module "..." { ... }Stack / ユーザ定義Construct / L3Stack / ユーザ定義ComponentResource / 既製Componentuse { source = "..." } + name { ... }
個別リソース層resource "aws_s3_bucket" "foo" { ... }L1 (new CfnBucket(...))、L2は中間層new aws.s3.Bucket(...) (CustomResource)aws.s3.Bucket { ... }

3自由度の偏り(§1-3-2)

各セルはgraph形状側と属性値側の両方を含む評価。

対象TerraformCDKPulumiCarina
抽象化の追加 言語: graph側はmodule切り出しのみ、属性値側もユーザ定義関数不可 (組込み関数 + provider functionのみ) 規約: graph側はConstructクラス、属性値側はTSの関数 / クラス / 高階関数 / 外部libで自由、組織が縛る 規約:同左(ComponentとTS/Python/Goの関数機構) 言語: graph側はmodule、属性値側はfn (型注釈・再帰検出) + 関数合成>> + パイプ|> + closure。高階関数と一級関数値は未実装
複数量の制御 言語: graph側はfor_each / count / dynamicの専用構文、属性値側はfor式 / [for ...]の専用構文のみ 規約: graph側 / 属性値側ともTSのfor / map / filter等で自由 規約:同左 言語: graph側 / 属性値側ともfor式 / if式の専用構文のみ(count相当は未実装)
実装の切替 言語: graph側はsourceリテラルのみ、属性値側も組込み関数のみで動的計算ロジック差し替え不可 規約: graph側は普通のクラス選択で自由、属性値側もホスト言語の関数選択で自由 規約:同左 言語: graph側はsourceリテラルのみ + use式はlet束縛RHSのみ、属性値側もfn値が一級でないので動的差し替え不可

Terraformはprovider function (1.8+)をprovider開発者が定義できるが、ユーザのconfig内で任意のヘルパー関数を定義する構文はない。属性値計算の自由度はTerraform (組込み関数のみ) < Bicep (funcあり) < Carina (fn + 関数合成 + closure) < CDK / Pulumi (高階関数 + クラス継承 + 外部lib)の順で表現力が上がる。

結果予測性(§1-3-4)

ツールコードからの結果予測性(記述自由度に起因する範囲)
Terraform高い(graph形状・属性値ともfor_each / dynamic使用時、およびmodule経由で低下)
CDK書き方依存 (graph形状・属性値ともL1ベタ書きなら高、ループ/条件/継承/高階関数を駆使すると低)
Pulumi書き方依存 (同様)
Carina高い(graph形状: forのdeferredはplan表示で確認可能。属性値: fnで値計算が抽象化されるが高階関数なしのため動的差し替えは不可。いずれもmodule経由で低下)

実行モデル概観(章2)

ツール評価モデル中間表現実行境界
TerraformDSL → resource graph → 全体差分一括計算 → 順次実行tfplan (binary、不可侵)provider pluginのgRPC、state backend
CDKTS code実行 → cloud assembly (CFNテンプレート + assets)生成 → CFN deployCFNテンプレート(静的、JSON/YAML)CFNサービスへのChangeSet経由deploy
Pulumiコード実行中にengineへgRPC streaming RegisterResource → engine随時差分判断・実行ランタイム経路に中間テンプレートなし。ただしPCL、Update Plans (--save-plan)は存在engine ↔ language host gRPC、provider gRPC
CarinaDSL → resource graph → 全体差分一括計算 → 順次実行(Terraformと同じ宣言型)Plan<Effect>型付きRust値、同一プロセス内加工可provider plugin (WASM等)、state backend

schema / 型 / unknown概観(章3)

ツールschema表現ユーザ側の型unknown値の表現特徴
Terraformcty (string/number/list/map/object/tuple)variable type system (limited)cty.Value + unknownフラグ、refinements (1.6+)で部分情報保持provider pluginからGetSchema RPCで取得
CDKCFN schema (CloudFormationの型情報、TSの型に変換)TSの型(BucketProps等のinterface)Token (stringに偽装、synth時にFn::GetAtt等に解決)型システムはunknown値を区別しない
PulumiPulumi schema (providerが定義、各言語の型にcodegen)TS/Python/Goの型 + Output<T>Output<T>独立型、依存リソース・unknownフラグ・secretフラグをメタ情報として運ぶ型システムでunknown値の扱いを強制
CarinaAttributeType (Union/StringEnum/Custom (refinement)/Struct)DSLの型(namespaced enum含む)式木保持(ResourceRef / Interpolation / FunctionCall)、forでdeferred退避partial evaluation / symbolic executionに近い

CDK Token vs Pulumi Output<T>の対比:

観点CDK TokenPulumi Output<T>
string (普通の文字列に偽装)Output<string> (独立した型)
文字列補間${bucket.arn}がそのまま書けるpulumi.interpolateを経由する必要あり
stringメソッド呼べてしまうが意図通り動かないことがある型エラーで呼べない
条件分岐synth時のToken値で分岐になる(実値で分岐できない)apply()で実値が確定してから分岐できる
「うっかり間違い」起きる(型システムが助けない)防げる(型システムが強制)
書く時の負担軽い(普通の文字列として書ける)重い(apply / interpolateを経由)

state概観(章4)

ツールstate所有者state形式refreshdrift detectionlock暗号化
Terraform自前tfstate (JSON)terraform refresh (applyに統合)terraform planで検出backendが提供(S3 + DynamoDB等)backend依存
CDKCFNサービスに委譲CFN stack state (AWS側)CFN drift detectionCFN drift detection機能CFNサービス側CFNサービス側
Pulumi自前(Pulumi service / S3 / local)checkpoint (JSON)pulumi refreshpulumi previewで検出backend側Pulumi serviceの場合は組込、自前backendは要設定
Carina自前carina.state.json (state v3、binding/dependency_bindings含む)(要確認: refresh機構があるか)plan時に差分計算(要確認)(要確認)

state操作の手段: Terraformはstate mv / state rm / import / 手動JSON編集(非推奨)、Pulumiはpulumi state delete / pulumi stack export / pulumi stack import、CDKは該当なし(CFNコンソール / CLI経由)、Carinaは(要確認)

identity概観(章5)

ツール識別子生成方式リファクタ耐性
Terraformresource address (aws_s3_bucket.foo)ユーザ制御(<type>.<ラベル>)movedブロックでリネーム/移動を吸収
CDKCFN論理ID (例: MyBucketF68F3FF0)construct pathのMD5ハッシュ先頭8桁(大文字16進)。L2は内部でid ResourceのCfnBucketを生成するためpath長2で必ずハッシュが付く。stack直下のL1のみ例外的にハッシュなしoverrideLogicalId()で明示制御可、cdk refactor (新機能?)
PulumiURN (urn:pulumi:<stack>::<project>::<parent$type>::<name>)type/name/parent等をengineが連結、Component$Resource形式で親typeが$区切りで連結aliasesで旧URNを保持して移動を吸収
Carinanamed: binding名(ラベルそのまま) / anonymous: content-based hash (SimHash 16 hex、create-only attrsからstandard hash 8 hexも)namedはユーザ制御、anonymousはcompute_anonymous_identifiersで自動生成(要確認: rename機構、moved相当の有無)

物理リソース名の自動生成(確認済み):

観点CDKPulumi (デフォルト)
形式<stack>-<logicalId>-<random><logicalName>-<7hex> (terraform-bridgeベース)
例(new ... ("mybucket"))mystack-mybucketf68f3ff0-abc123xyzmybucket-d7c2fa0
stack名を含むはいいいえ
論理IDのhashを含むはいいいえ

#参照 #比較表 #iac