Terraform / CDK / Pulumi / Carina比較マトリクス
4ツール(Terraform / CDK / Pulumi / Carina)を軸ごとに横断比較した概観表を集約したもの。(要確認)と書かれたセルは埋まっていないまま残してある — 何が確認できていないかも情報である。ここでの「CDK」はAWS CDKを指す(cdktf / cdk8sは別engineへsynthする派生で対象外)。
記述言語と実行モデル(§1-1)
| ツール | 記述言語 | ユーザワークフロー | 内部実行モデル |
|---|---|---|---|
| Terraform | HCL (専用DSL) | plan → apply | DSLを解析してresource graph構築 → 全体の差分を一括計算 → 順次実行 |
| CDK | TS / Python / Java / C# / Go (汎用言語経由) | synth → diff → deploy | コード実行でCFNテンプレート生成(synth) → CFN engineがapply |
| Pulumi | TS / Python / Go / .NET / Java (各言語ネイティブSDK) | preview → up | コード実行中にengineへgRPCストリーミングでRegisterResourceを逐次送信 → engineが随時差分判断・実行(ランタイム経路に静的中間テンプレートなし) |
| Carina | Carina DSL (.crn) | plan → apply | DSLを解析してresource graph構築 → 全体の差分を一括計算 → 順次実行 |
Pulumiはランタイム実行経路には静的中間テンプレートを持たないが、エコシステム全体には中間表現的なものが存在する: pulumi convertのPCL (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)
| ツール | 単位(複数リソースを束ねる抽象化) | 読み込み + インスタンス化の構文 | 実体 |
|---|---|---|---|
| Terraform | module (子moduleは再利用、root moduleはデプロイ単位) | module "network" { source = "./modules/network"; ... } (DSLレベルで読み込みとインスタンス化が一体) | ディレクトリ(HCLファイル群) |
| CDK | Stack (デプロイ単位) / ユーザ定義Construct (再利用単位) / L3 Construct (既製パターン) | ホスト言語のimport + new MyStack(app, 'main') (DSLレベルの読み込み構文はなくホスト言語に委譲) | TypeScriptクラス(jsiiで多言語化) |
| Pulumi | Stack (デプロイ単位、設定単位) / ユーザ定義ComponentResource / 既製Component (Crosswalk等) | ホスト言語のimport + new NetworkComponent('network', args, opts) | 各言語ネイティブのクラス |
| Carina | module (子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もある。
層の対応:
| 層 | Terraform | CDK | Pulumi | Carina |
|---|---|---|---|---|
| 複数リソース束ね層 | module "..." { ... } | Stack / ユーザ定義Construct / L3 | Stack / ユーザ定義ComponentResource / 既製Component | use { source = "..." } + name { ... } |
| 個別リソース層 | resource "aws_s3_bucket" "foo" { ... } | L1 (new CfnBucket(...))、L2は中間層 | new aws.s3.Bucket(...) (CustomResource) | aws.s3.Bucket { ... } |
3自由度の偏り(§1-3-2)
各セルはgraph形状側と属性値側の両方を含む評価。
| 対象 | Terraform | CDK | Pulumi | Carina |
|---|---|---|---|---|
| 抽象化の追加 | 言語: 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)
| ツール | 評価モデル | 中間表現 | 実行境界 |
|---|---|---|---|
| Terraform | DSL → resource graph → 全体差分一括計算 → 順次実行 | tfplan (binary、不可侵) | provider pluginのgRPC、state backend |
| CDK | TS code実行 → cloud assembly (CFNテンプレート + assets)生成 → CFN deploy | CFNテンプレート(静的、JSON/YAML) | CFNサービスへのChangeSet経由deploy |
| Pulumi | コード実行中にengineへgRPC streaming RegisterResource → engine随時差分判断・実行 | ランタイム経路に中間テンプレートなし。ただしPCL、Update Plans (--save-plan)は存在 | engine ↔ language host gRPC、provider gRPC |
| Carina | DSL → resource graph → 全体差分一括計算 → 順次実行(Terraformと同じ宣言型) | Plan<Effect>型付きRust値、同一プロセス内加工可 | provider plugin (WASM等)、state backend |
schema / 型 / unknown概観(章3)
| ツール | schema表現 | ユーザ側の型 | unknown値の表現 | 特徴 |
|---|---|---|---|---|
| Terraform | cty (string/number/list/map/object/tuple) | variable type system (limited) | cty.Value + unknownフラグ、refinements (1.6+)で部分情報保持 | provider pluginからGetSchema RPCで取得 |
| CDK | CFN schema (CloudFormationの型情報、TSの型に変換) | TSの型(BucketProps等のinterface) | Token (stringに偽装、synth時にFn::GetAtt等に解決) | 型システムはunknown値を区別しない |
| Pulumi | Pulumi schema (providerが定義、各言語の型にcodegen) | TS/Python/Goの型 + Output<T> | Output<T>独立型、依存リソース・unknownフラグ・secretフラグをメタ情報として運ぶ | 型システムでunknown値の扱いを強制 |
| Carina | AttributeType (Union/StringEnum/Custom (refinement)/Struct) | DSLの型(namespaced enum含む) | 式木保持(ResourceRef / Interpolation / FunctionCall)、forでdeferred退避 | partial evaluation / symbolic executionに近い |
CDK Token vs Pulumi Output<T>の対比:
| 観点 | CDK Token | Pulumi Output<T> |
|---|---|---|
| 型 | string (普通の文字列に偽装) | Output<string> (独立した型) |
| 文字列補間 | ${bucket.arn}がそのまま書ける | pulumi.interpolateを経由する必要あり |
| stringメソッド | 呼べてしまうが意図通り動かないことがある | 型エラーで呼べない |
| 条件分岐 | synth時のToken値で分岐になる(実値で分岐できない) | apply()で実値が確定してから分岐できる |
| 「うっかり間違い」 | 起きる(型システムが助けない) | 防げる(型システムが強制) |
| 書く時の負担 | 軽い(普通の文字列として書ける) | 重い(apply / interpolateを経由) |
state概観(章4)
| ツール | state所有者 | state形式 | refresh | drift detection | lock | 暗号化 |
|---|---|---|---|---|---|---|
| Terraform | 自前 | tfstate (JSON) | terraform refresh (applyに統合) | terraform planで検出 | backendが提供(S3 + DynamoDB等) | backend依存 |
| CDK | CFNサービスに委譲 | CFN stack state (AWS側) | CFN drift detection | CFN drift detection機能 | CFNサービス側 | CFNサービス側 |
| Pulumi | 自前(Pulumi service / S3 / local) | checkpoint (JSON) | pulumi refresh | pulumi 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)
| ツール | 識別子 | 生成方式 | リファクタ耐性 |
|---|---|---|---|
| Terraform | resource address (aws_s3_bucket.foo) | ユーザ制御(<type>.<ラベル>) | movedブロックでリネーム/移動を吸収 |
| CDK | CFN論理ID (例: MyBucketF68F3FF0) | construct pathのMD5ハッシュ先頭8桁(大文字16進)。L2は内部でid ResourceのCfnBucketを生成するためpath長2で必ずハッシュが付く。stack直下のL1のみ例外的にハッシュなし | overrideLogicalId()で明示制御可、cdk refactor (新機能?) |
| Pulumi | URN (urn:pulumi:<stack>::<project>::<parent$type>::<name>) | type/name/parent等をengineが連結、Component$Resource形式で親typeが$区切りで連結 | aliasesで旧URNを保持して移動を吸収 |
| Carina | named: binding名(ラベルそのまま) / anonymous: content-based hash (SimHash 16 hex、create-only attrsからstandard hash 8 hexも) | namedはユーザ制御、anonymousはcompute_anonymous_identifiersで自動生成 | (要確認: rename機構、moved相当の有無) |
物理リソース名の自動生成(確認済み):
| 観点 | CDK | Pulumi (デフォルト) |
|---|---|---|
| 形式 | <stack>-<logicalId>-<random> | <logicalName>-<7hex> (terraform-bridgeベース) |
例(new ... ("mybucket")) | mystack-mybucketf68f3ff0-abc123xyz | mybucket-d7c2fa0 |
| stack名を含む | はい | いいえ |
| 論理IDのhashを含む | はい | いいえ |