AIの時代だからこそ、強い型付けのIaCツールをつくっている
AIがあれば型は要らない、と主張する人や、IaCは要らない、と主張する人がいる。多数派ではないと思うけど。自分はむしろ、強い型付けをコンセプトにしたIaCツール、 Carina をつくっている。
AIがあればIaCは要らない、という話を最初に見たときは、正直こう思っただけだった。
その後、ymotongpooさんが、IaCがなくならない理由について筋道立てた記事を書いた。また、tadsanさんとnishioさんの型に関するツイートも流れてきたので、それらに絡めて、自分がなぜAIの時代に型付きのIaCツールをつくっているのかを書いておく。
ついでに、nishioさんが提案していた「LLMが生成した最初のコードが型エラーになる確率」を、Carinaの開発ログから実際に測ってみた。
ymotongpooさんの記事
発端は、James Ward氏が2026年10月31日にXに投稿した「IaC is dead」。AIがシステムのソースコードと実際のインフラを読めば、変更の内容も承認の可否も判断できるので、望ましい構成をコードとして書いておく必要はない、という主張とのこと。
ymotongpooさんの記事は、これに対して、IaCがこれまで何を担ってきたのかを整理し直している。TL;DRから引用する。
IaCは、望ましい状態をコードとして残す、意図を記録する役割と、状態を保持して差分を計算し、適用する、実行基盤としての役割を担ってきました。
そして、AIに変更を任せるには少なくとも4つの条件が要る、としている。
- 現在の状態の把握
- 部分的な失敗に耐える収束
- 検証できる目標の状態
- 適用前の差分と承認
このうち最初の2つはプラットフォームが担えますが、残りの2つは望ましい状態を記録したものがなければ満たせません。
Kubernetesはクラスタの内側で、実行基盤としての役割をプラットフォームに組み込んだけど、利用者は今もマニフェストを書いてGitで管理している。AIが記述を書くようになっても、承認し、再現し、元に戻すための記述は残る。だいたいそういう話で、自分もこの整理には全面的に同意する。
Serverspecが出てきた
記事の中で、Serverspecが、サーバーのあるべき状態をテストとして書いて、実際の状態を確かめる試みとして紹介されていた。それだけでなく、脚注には自分のことも書かれていて、
執筆時点ではフリーランスのソフトウェアエンジニアで、余暇に、Rustで書かれた型付きのIaCツールであるCarinaを開発しています。
と、Carinaまで紹介してもらっていた。ありがとうございます。
自分の中では、ServerspecとCarinaはつながっている。Serverspecは、あるべき状態をテストとして書いて、実際の状態がそれと合っているかを、適用した後に確かめるもの。Carinaの型は、望ましい状態の記述そのものに誤りがないかを、適用する前に確かめるもの。どちらも「意図を書いて、機械に確かめさせる」という点では同じで、確かめる対象とタイミングが違う。
もう1つの役割:クラウドのバッドノウハウを吸収する
ymotongpooさんの整理は「意図の記録」と「実行基盤」の2つだけど、IaCツールにはもう1つ、クラウドのバッドノウハウを吸収する、という面もあると思っている。
バッドノウハウは高林哲さんの言葉で、「ソフトウェアを使いこなすためには必要だが、本来は知りたくもない、ソフトウェアの設計次第では不要なはずのノウハウのこと。」( gihyo.jpのインタビュー記事 の注より)。クラウドのAPIを正しく叩くには、こういう知識がいろいろ要る。たとえば、
-
作ったばかりのIAMロールを指定してVPC Flow Logを作ろうとすると、IAMの結果整合性のせいで、まだロールを引き受けられずに失敗することがある。Carinaのawsプロバイダーでは、
Unable to assume IAM roleなどのエラーメッセージを見分けて、5秒おきに最大12回まで試している -
作成に成功したはずのリソースが、直後に読みにいくと見つからないことがある。awsccプロバイダーで、S3バケットの作成が成功したのに、直後の読み込みでは
not_foundが返ってきた( carina#4168 )。今は、読めるようになるまで、デフォルトで最大2分待つようにしている -
IAMの信頼ポリシーのprincipalにロールのARNを書くと、AWSは保存時にそれをロールの一意ID(
AROA...)に変換する(表示するときはARNに戻す)。権限昇格を防ぐための 仕様 だけど、参照先のロールを削除して作り直すと、ポリシーは新しいロールには効かなくなり、ARNに戻せない古い一意IDがそのまま見えるようになる。planにはARNとAROA...の差分として出てくるけど、それだけでは何が起きているのか分からない( carina-provider-aws#381 、これはまだ吸収しきれていない) -
Cloud ControlのS3バケットのスキーマでは、
ObjectLockEnabledは作成後も変更できる属性になっていて、S3のAPIを直接使えば既存のバケットでも有効にできる。なのに、Cloud ControlのUpdateResourceで変更すると、成功が返ってくるのに、実際には何も変わらない。以前のCarinaは、これをそのまま適用済みとして記録していた。今は、更新後に読み直して、変更した属性が反映されていなければ部分的な成功として報告するようにしている( carina-provider-awscc#419 )
IaCツールがなければ、こういったバッドノウハウは、AIが毎回その場で踏み抜いて学び直すことになる。AIが自力で気づいて対処できることもあるだろうけど、毎回そうできる保証はない。
IaCツールがバッドノウハウを吸収しておけば、人間もAIも、それを知らずに済む。一度直せば全員が同じ挙動を得られるし、受け入れテストで検証もできる。
本来こういうことは、バッドノウハウの定義どおり、クラウドの側の設計で不要にできるはずのもの。ymotongpooさんの4条件でいうと、2つ目の「部分的な失敗に耐える収束」に近く、記事でもプラットフォームが担えるものとされている。ただ、今のクラウドについては
現在のクラウドは、最初の2つの条件からも離れています。
とも書かれていて、今のところその役割はIaCツールが担っている。
記述が残っても、人間の検証が追いつかない
ymotongpooさんは、AIが記述を書く場合について、こう書いている。
生成が速くなるほど、人がレビューする差分も増え、記述を残しておく必要も大きくなります。
ここは本当にそうで、自分もCarinaの開発や、Carinaで管理しているインフラの変更を全部AIにやらせているけど、AIが書く量に対して、人間が全部読んでレビューするのはもう無理になっている。
なので、記述が残るだけでは足りなくて、その記述を、人間が全部読まなくても検証できる必要がある。機械が確かめられる部分は機械に任せて、人間はplanを見て「これは意図した変更か」だけを判断すれば良い、という状態にしたい。その「機械が確かめられる部分」をできるだけ広げるのが、Carinaの型の役割だと考えている。
汎用言語の型と、ドメインの型
ymotongpooさんの記事では、AWS CDKやPulumiのような汎用言語で書くIaCについて、こう書かれている。
汎用の言語で書く利点の1つは型です。
TypeScriptで書けば、存在しない項目名や型の合わない値、必須の項目の書き忘れを、デプロイする前にコンパイラーが検出します。
これはそのとおりなんだけど、ここで挙げられているのは、項目名、値の型、必須の項目といった、構造についてのチェック。インフラの意味までは、型がいつも見てくれるわけではない。
CDKでも、L2やL3のコンストラクトの多くは、VPCを
IVpc
というオブジェクトとして受け取るので、同じアプリの中で作ったリソースを参照する限り、取り違えはコンパイル時に分かる。ただ、L1の
CfnSubnetProps
の
vpcId
は
string | IVPCRef
だし、既存のVPCを取り込む
Vpc.fromVpcAttributes
では、
vpcId
は
string
、サブネットIDは
string[]
。IDを文字列として扱う所では、VPC IDを入れるべき所にサブネットIDを入れても、コンパイルは通る。
AWSのAPIの上では、VPC IDもサブネットIDも、VPCのテナンシー(
instance_tenancy
)も、ただの文字列。なので、IDを取り違えたり、取れない値を書いたりしても、実際に適用するまで分からない。
Carinaでは、IDやARNはリソースごとに別の型にし、テナンシーのような値は取れる値だけを持つ型にしている。試しに、2つ目のサブネットの
vpc_id
に、わざと1つ目のサブネットのIDを渡してみる。
let vpc = aws.ec2.Vpc {
cidr_block = '10.0.0.0/16'
}
let public = aws.ec2.Subnet {
vpc_id = vpc.vpc_id
cidr_block = '10.0.1.0/24'
}
let private = aws.ec2.Subnet {
vpc_id = public.subnet_id
cidr_block = '10.0.2.0/24'
}
carina validate
を実行すると、こうなる。
$ carina validate .
Validating...
Error: aws.ec2.Subnet.private: cannot assign aws.ec2.Subnet.Id to 'vpc_id': expected aws.ec2.Vpc.Id, got aws.ec2.Subnet.Id (from public.subnet_id)
AWSのAPIを1回も呼ばずに、
plan
より前の段階で間違いが分かる。
同じ構成をTerraform(AWS provider 6.71.0)で書くと、
terraform validate
も
terraform plan
も通る。
aws_subnet
の
vpc_id
はただの文字列で、しかもplanの時点では1つ目のサブネットのIDがまだ決まっていないので、検証しようがない。間違いに気づくのは、applyしたときになる。
言語の型システムが強いか弱いかではなく、インフラのドメイン知識をどこまで型にしているか、の違い。そして、Carinaが汎用言語ではなくDSLなのは、汎用言語の自由度を持ち込まずに、ドメインの型だけを持ち込みたいから。何でも書ける言語は、AIにとっても何でも書けてしまう言語なので。
型はAIにとってもテストになる
型についてはXでも議論になっていた。tadsanさんのツイート。
それを引用したnishioさんのツイート。
Carinaの開発は、全部Claude CodeとCodexにやらせていて、ログは手元にある。なので、nishioさんの言う統計を実際にとってみた。
ここで測るのは、Carinaの実装(Rust)の方で、.crnの型ではない。ただ、AIが書いたものを、実行する前に型で機械的に止める、という構図は同じ。
測り方
- 対象は、Carina関連のリポジトリでのClaude Codeのログ(サブエージェントも含む)と、Carina関連のリポジトリで動かしたCodexのセッション
-
AIが
.rsファイルを編集した後、最初に実行したcargo check、build、test、clippy、nextestを1回の「試行」とする - 同じセッションで直前のコンパイルが失敗していた試行は、エラーを直した後の「再試行」として分け、それ以外を新しく書いたコードの1回目とする
-
出力に
error[E....](rustcのエラーコード)が含まれていれば失敗とする。テストが落ちただけの場合や、clippyの警告、構文エラー、ビルドスクリプトの失敗は数えていない
結果
新しく書いたコードの1回目の結果。
| 試行 | エラーコードあり | 割合 | |
|---|---|---|---|
| Claude Code | 6,019 | 461 | 7.7% |
| Codex | 11,051 | 1,325 | 12.0% |
| 合計 | 17,070 | 1,786 | 10.5% |
AIが新しく書いたRustのコードは、だいたい10回に1回、最初のコンパイルで落ちて、直されている。また、一度エラーになった後の再試行では、27%くらいがまたエラーになっていた。nishioさんの言う「実行前におかしなコードを検出するテスト」に、少なくとも10回に1回は引っかかっていることになる。
月ごとに見ると、Claude Codeでの試行は5月まででほぼ終わっていて、6月以降はCodexがほとんど。これは実装をCodexに任せるようにした時期と一致する。試行が1,000回を超える月で見ると、割合は7〜13%くらいで、月によって幅があった。一番高かったのは10月のCodex(13.2%)。
1回目で失敗したときに出ていたエラーの上位はこんな感じ。
| エラー | 内容 | Claude Code | Codex |
|---|---|---|---|
| E0308 | 型の不一致 | 93 | 328 |
| E0599 | 存在しないメソッド | 94 | 328 |
| E0425 | 未解決の名前 | 56 | 243 |
| E0277 | traitを満たしていない | 23 | 182 |
| E0433 | 未解決のパス | 54 | 159 |
| E0061 | 引数の数の誤り | 27 | 117 |
| E0063 | 構造体のフィールドの書き忘れ | 57 | 63 |
| E0004 | matchの網羅漏れ | 16 | 55 |
E0004は、典型的には、enumにバリアントを足したのに、それを使っている
match
を直していない、というケース。
注意点
- 判定は雑なヒューリスティック。複数の編集をまとめて1回の試行として数えているし、テストコードだけを編集した場合も含まれている。1回目と再試行の分け方も、同じセッションで直前のコンパイルが失敗したかどうかを見ているだけ
- エラーコードには、E0425(未解決の名前)のように、厳密には型エラーと呼ばないものも含めている
-
出力を
tailで切ったり、ファイルに書き出したり、バックグラウンドで実行したりした試行は、エラーコードが見えないので成功に数えている。なので、実際の割合はもう少し高い可能性がある - 型のない言語と比べたわけではないので、「型がなかったら、この10%がどこで見つかっていたか」までは分からない
.crnの型の場合
ちなみに、Carinaの方の型(.crnファイル)でも同じことを測ろうとしたけど、carina-rs/infraなどでの
carina validate
の実行約790回のうち、失敗は38回で、そのほとんどは型エラーではなく、providerが入っていなかった、一緒に実行した別のコマンドが失敗した、といった別の理由だった。Rustのときのように「編集した後の最初の1回」で数えたわけでもないので、割合は出していない。型エラーとして引っかかっていたのは、たとえば次のようなもの(5月時点のメッセージ)。
Error: ../../../modules/github-oidc: attribute 'oidc_provider_arn': type mismatch: expected iam_oidc_provider_arn, got arn (from oidc_provider.arn)
OIDCプロバイダーのARNを期待している所に、ただのARNを渡していて、型で弾かれている。
なお、tadsanさんのツイートにある、型を書くことはAIへの文脈になるのか、トークンの浪費なのか、という話は、コードに型を書く前提の話だと思う。Carinaでは、リソースの属性の型はプロバイダーのスキーマの側にあって、.crnには書かない(書くのはモジュールの引数の型くらい)。なので、.crnを書くAIにとって、型は自分で書くものではなく、間違えたときに
carina validate
が返してくれるもの、という位置づけになる。
まとめ
- ymotongpooさんの整理のとおり、AIが記述を書くようになっても、意図の記録としてのIaCは残る
- それに加えて、IaCツールには、クラウドのバッドノウハウを吸収する役割もある
- AIが書く量が増えるほど、人間のレビューは追いつかなくなるので、人間が全部読まなくても記述を検証できる必要がある
- CDKのような、汎用言語で書くIaCツールでも、リソースの関係はある程度型で表せるけど、IDそのものは文字列のまま。インフラの意味まで型にするには、ドメインの型が要る
- AIが新しく書いたRustのコードは、10%くらいが最初のコンパイルで落ちていて、型チェックが実行前に止めている
というわけで、AIの時代だからこそ、強い型付けのIaCツールが要る、と思ってCarinaをつくっている。