インフラ

terraform testの書き方|unit/integrationの分け方とCIコスト

planは通ったのに、applyでプロバイダのバリデーションに引っかかって止まる。モジュールの変数にデフォルト値を足したつもりが、別の環境だけ想定外の値で組み上がる。Terraformのコードは静的解析だけでは守り切れない領域があり、そこを埋めるために公式のテストフレームワークが用意されています。この記事では.tftest.hclの最小構成から、command = planapplyで分かれるunitテストとintegrationテストの境界、mock providerによる外部依存の切り離し、CIへの組み込みと実費の抑え方までを扱います。版に依存する記述は2026年8月14日にHashiCorp公式ドキュメントとGitHub Releasesで確認した内容です。

まとめ:planで足りる検証とapplyが要る検証の分界点とCI配置

結論を先に置きます。terraform testで最初に決めるのは、テストの書き方ではなくそのテストが実インフラを作るかどうかです。command = planなら何も作らずに変数と条件分岐の論理だけを検証でき、command = apply(既定値)は本物のリソースを作って壊します。前者は無料で数秒、後者は課金と数分から数十分。この差が、そのままCIのどこに置くかを決めます。

実務での配置はこうなります。プルリクエストのたびに回すのはplanベースのunitテストとモックだけ。applyを伴うintegrationテストは、マージ後の使い捨て環境か夜間の定期実行に寄せる。両方をPRごとに回す構成にすると、レビュー待ち時間とクラウド費用の両方が跳ね上がり、やがて誰もテストを追加しなくなります。

もうひとつ、導入前に確かめてほしい前提があります。terraform testが本領を発揮するのは、入力変数によって出力が変わるモジュールを自分たちで書いている場合です。既製モジュールを並べているだけの構成では、テストが検証しているのは自分のコードではなくプロバイダの仕様になり、労力に見合いません。その線引きは記事の後半で条件付きで示します。

terraform testが埋める検証の空白とTFLint・planとの役割分担

テストフレームワークを入れる前に、既にある道具が何を見ていて何を見ていないかを整理しておくと、書くべきテストの量がかなり減ります。

planの成功がapplyの成功を保証しない典型パターン3つ

terraform planが通ってもapplyで落ちる状況は珍しくありません。ひとつ目は、プロバイダ側のバリデーションがapply時にしか走らないケース。S3バケット名の大域一意性や、命名規則の文字数制限がこれにあたります。ふたつ目は、作成順序に依存する参照で、planでは「(known after apply)」として素通りした値が、実際には空文字やnullで返るケース。

3つ目が最も厄介で、モジュールの変数に条件分岐がある場合です。ある環境の変数値では意図した分岐に入るのに、別の値ではリソース自体が生成されない。planは与えた変数1組ぶんしか検証しません。terraform testは複数のrunブロックで変数の組み合わせを変えながら検証できるため、この空白を埋められます。差分そのものの読み方はterraform planの差分の読み方で扱っています。

TFLintの静的解析とterraform testの動的検証の守備範囲の差

両者は競合しません。守備範囲が重ならないためです。TFLintはコードを実行せずに構文・規約・非推奨の書き方を検出し、terraform testはplanかapplyを実際に走らせて出来上がる構成を検証します。

観点 TFLint(静的解析) terraform test(動的検証)
実行方式 コードを読むだけ planまたはapplyを実行
検出できるもの 命名規約違反・非推奨引数・未使用変数 変数の組み合わせで変わる出力・条件分岐の結果
プロバイダ認証 不要 planでも provider の初期化が必要
実行時間の目安 秒単位 planで秒単位・applyで分単位
クラウド費用 発生しない applyを伴う場合に発生

順序としては、TFLintを先に通してからテストを回す構成が効率的です。規約違反はテストを書かずに落とせるため、テストの本数を増やさずに済みます。導入と設定はTFLintによるTerraform静的解析の導入と設定にまとめてあります。

v1.6.0で標準化されたテスト機能と外部ツールを使う判断の変化

公式ドキュメントには「This testing framework is available in Terraform v1.6.0 and later.」と明記されています。それ以前はGo製のTerratestやKitchen-Terraformといった外部ツールが選択肢で、テストを書くためにGoの実行環境とテストコードの保守が必要でした。

標準化されたことで、HCLだけでテストが書けるようになった意味は小さくありません。Terraformを書く人がそのままテストも書けるからです。外部ツールが今も優位なのは、HTTPリクエストを投げて応答内容を検証するといった、Terraformの外側の振る舞いを確かめたい場合。逆に「この変数を渡したらこのタグが付くか」程度の検証で外部ツールを持ち出すのは過剰な構成です。

.tftest.hclの最小構成とrun・assert・variablesの書き分け

テストファイルの拡張子は.tftest.hclまたは.tftest.jsonです。使えるブロックは4種類しかありません。runが1個以上(必須)、variablesがルートレベルに0個か1個、providerが0個以上、testが0個か1個。

run・assert・error_messageで書く最小のテストファイル例

最小構成はrunブロック1つとassertブロック1つです。runにテスト名を付け、commandで実行方式を指定し、assertのconditionに真偽を返す式、error_messageに失敗時のメッセージを書きます。

conditionの式では、対象モジュールのリソース属性をaws_s3_bucket.this.bucketのようにアドレスで直接参照できます。出力値ならoutput.bucket_arn、入力変数ならvar.env。ここで書き慣れないうちに躓きやすいのが、error_messageが必須である点です。省略すると構文エラーになるため、条件だけ書いて済ませられません。メッセージには「何が期待値で、何が実際の値か」を入れておくと、CIのログだけで原因が特定できます。

variablesブロックの階層とrunのoutputを次のrunへ渡す書式

variablesは2階層で書けます。ファイルの最上位に置けばそのファイルの全runに適用され、run内に置けばそのrunだけを上書きする。共通の環境名やリージョンを最上位に、検証したい差分だけを各runに置く形にすると、runブロックが短く保てます。

runの結果を次のrunで使う書式がrun.先行runの名前.output.出力名です。たとえばVPCを作るrunの出力したサブネットIDを、次のrunでECSサービスの入力変数に渡す。この参照関係があるrunどうしは並列実行されません。参照した時点で順序が確定するためです。1.13.0(2025-08-20公開)以降は、ファイルレベルのvariablesブロックからもrunの出力を参照できます。

expect_failuresでvalidationの失敗自体を検証する場面と書式

assertが「成功すること」を確かめるのに対し、expect_failuresは「想定どおり失敗すること」を確かめます。書式はrunブロック内に検証対象のアドレスをリスト形式で並べるだけ。

使いどころは、変数のvalidationブロックやリソースのpreconditionを自分で書いた場合です。「環境名にprod以外を渡したら弾かれる」というガードを書いたなら、そのガードが実際に働くかを検証しないと、ガード自体が壊れていても気付けません。expect_failuresを書かずにvalidationだけ足す運用は、警報装置を設置して電源を入れないのと同じ状態。ガードを1本書いたら、それを踏み抜くrunを1本書く、という対で覚えておくと漏れません。

command=planとapplyで分かれるunit/integrationの設計境界

この章が記事の中心です。commandの値ひとつで、テストの速度・費用・検証できる範囲がすべて変わります。既定値がapplyである点には注意が必要で、書き忘れると実リソースが作られます。

command=planで検証できる範囲とcomputed属性の限界

planを指定すると、Terraformはインフラを作らずに論理的な検証だけを行います。検証できるのは、入力変数から確定的に決まる値です。命名規則の組み立て、タグの付与、countやfor_eachによるリソース個数、条件分岐でリソースが作られるか否か。ここまでは実インフラなしで確認できます。

限界はcomputed属性です。ARN、ID、エンドポイントのURLなど、プロバイダが作成後に埋める値はplan段階では確定していません。これらをassertのconditionに書くと、値が未確定であることを理由にテストが失敗します。planベースのunitテストで参照してよいのは、自分のコードが決めている値だけ。この線を守ると、テストが「プロバイダの都合で落ちる」現象がなくなります。

command=applyでしか確認できない実リソースの検証項目

applyが要るのは、作ってみないと分からない検証です。プロバイダ側のバリデーション、リソース間の依存関係が実際に解決されるか、IAMポリシーの構文がAWS側に受理されるか。加えて、複数モジュールを組み合わせたときに参照が繋がるかどうかも、applyでしか確かめられません。

applyを使うrunを書くときは、範囲を絞ることが費用に直結します。VPCとRDSとECSを丸ごと作るintegrationテストを1本置くより、依存の少ないモジュール単位に割ってrunを分ける方が、失敗時の切り分けも速い。モジュールの分割粒度そのものはTerraform moduleの分割粒度とoutput設計で扱っています。

state_keyとmoduleブロックで分かれるstateファイルの単位

公式ドキュメントは「There is always at least one state file that maintains the state of the main configuration under test.」と説明しています。テスト対象の主構成に対してstateが必ず1つあり、moduleブロックで別モジュールを読み込むrunには既定でそれぞれ別のstateが割り当てられる、という構造です。

state_keyを明示すると、どのstateを使うかを自分で制御できます。同じモジュールを2つのrunで別々の変数値で作りたいとき、state_keyを分ければ互いに干渉しません。逆に、先行runが作ったリソースを後続runで参照したいなら、同じstate_keyにそろえる。テスト用のstateは実行中だけメモリ上で扱われ、通常のバックエンドには書かれないため、本番のtfstateとは別物です。バックエンドに置くstateの構造はTerraform stateとtfstateの構造を参照してください。

mock_providerとoverride系ブロックで外部依存を切る手順

モック機能は公式ドキュメントに「Terraform v1.7.0 introduced the ability to mock data returned by the providers during a terraform test」と記されています。認証情報なしでplanベースのテストを回せるようになるため、CIの構成が一段簡単になります。

mock_providerが自動生成する値の規則と数値0・文字列8文字

mock_provider "aws" {} と書くだけで、そのプロバイダのリソースとデータソースは設定から読める値をそのまま返し、computed属性には偽のデータが自動生成されます。生成規則は型ごとに決まっています。

  • 数値:0
  • 真偽値:false
  • 文字列:ランダムな8文字の英数字
  • コレクション:空

ここを知らずにassertを書くと、テストが素通りします。「ARNが空でないこと」を条件にしても、8文字のランダム文字列が入るので必ず真になるからです。モックを使うunitテストでは、生成値そのものではなく自分のコードが組み立てた部分を検証対象にしてください。

mock_resourceのdefaultsとtfmock.hclへの切り出し基準

ランダム値では困る場合に使うのがmock_resourcemock_dataです。mock_provider内にネストし、defaultsで属性ごとの固定値を指定します。ARNのように形式が決まっている値をassertで比較したいときは、defaultsで実在しそうな形式を与えておく。

モック定義が増えてきたら、source属性で外部ファイルに切り出せます。拡張子は.tfmock.hclまたは.tfmock.json。切り出す基準は単純で、同じmock定義を2つ以上のテストファイルで使い始めたときです。1ファイルでしか使わないうちは、テストファイル内にインラインで書いた方が読む側の負担が少なくなります。

override_resourceでモックを部分適用する場面と適用できない構造

プロバイダ全体をモックにすると検証したい部分まで偽物になります。そこで使うのがoverride_resourceoverride_dataoverride_moduleで、これらはファイルレベルにもrunブロックレベルにも置けます。targetで対象のアドレスを指定し、その値だけを差し替える形です。override_moduleはoutputs属性でモジュールの出力を丸ごと差し替えます。

適用できない構造も公式が明記しています。繰り返しブロックとネスト属性については、複数インスタンスを区別できず、コレクションの全要素に同じ値が入る仕様です。for_eachで3つのサブネットを作り、それぞれ異なるCIDRを返させたい、という使い方はできません。この制約に当たったら、モックをやめてapplyベースのintegrationテストに切り替える方が早い。データソースの参照設計はterraform dataの使い方とdata sourceの参照設計で整理しています。

tests配下のファイル構成と-filter・-junit-xmlでのCI実行

テストが書けたら、次はどこに置いてどう回すかです。ここの設計を先に決めておくと、テストが増えても実行時間が線形に伸びません。

testsディレクトリの既定と-test-directoryを変える判断

既定のテストディレクトリはtestsです。加えて、ルート構成ディレクトリ直下に置いたテストファイルは-test-directoryの指定に関わらず常に読み込まれます。この「常に読まれる」挙動が、意図しない実行の原因になることがあります。

ディレクトリを変えるべき場面は限られます。モノレポで複数のTerraform構成を抱えていて、構成ごとにテスト置き場を分けたいときくらい。単一構成なら既定のtestsのままにして、ファイル名で用途を分ける方が管理しやすくなります。unit_naming.tftest.hclintegration_vpc.tftest.hclのように接頭辞を揃えておくと、次に述べるfilterがそのまま効きます。

-filterと-parallelism既定10で分けるPR時と定期実行の設定

-filter=testfileで実行対象のテストファイルを限定できます。ファイル名に接頭辞を付けておく理由がここにあり、PRのCIではunit系だけ、定期実行ではintegration系も含める、という切り替えがフラグ1つで済みます。-parallelismは同時に走らせるplan/apply操作の数で、既定値は10です。

実行タイミング 対象 command プロバイダ 所要時間の目安
プルリクエストごと unit系のみ plan mock_provider 数秒から数十秒
マージ後・夜間定期 integration系を含む全件 apply 実プロバイダ 数分から数十分

この2段構えにすると、レビュー中の待ち時間を伸ばさずに済みます。integration系までPRごとに回す構成は、テストが10本を超えたあたりで現実的でなくなる、というのが実務上の分かれ目です。

-junit-xmlのレポート出力とGitHub Actionsへの組み込み手順

-junit-xml=にパスを渡すとJUnit XML形式でレポートが保存され、CIサービスのテスト結果表示にそのまま載ります。-jsonは機械可読な出力、-verboseは各runブロックのplanかstateを表示するフラグで、失敗の調査時に使います。

  1. ワークフローでTerraformをセットアップし、対象ディレクトリでterraform initを実行する
  2. PR用のジョブで、unit系に絞ったテストをJUnit XML出力付きで実行する
  3. レポートファイルをテスト結果表示アクションに渡し、失敗したrun名をPR上で見えるようにする
  4. マージ後のジョブでは絞り込みを外し、実プロバイダの認証情報を渡して全件を実行する

PR時のジョブはモックだけで完結するため、クラウドの認証情報を渡す必要がありません。権限を持つ認証情報の露出面が減る構成でもあります。ワークフロー側の認証やロック制御はGitHub ActionsとTerraformでAWSのCI/CDを構築する手順にまとめてあります。テスト実行そのものをプルリクエストのコメント側へ寄せる別方式は、AtlantisでTerraformをPR運用する構成と権限設計で扱っています。

integrationテストが積み上げる実費・実行時間と並列化の限界

ここからが独自の論点です。terraform testの解説記事は書式で終わりがちですが、運用に載せたあと問題になるのは請求額と実行時間の方でした。

applyとdestroyの往復で増える課金対象と削除漏れの報告

公式ドキュメントは、テストファイルの実行が終わるとTerraformが残存インフラのdestroyを試み、破棄できなかった場合は「作成したが削除できなかったリソースの一覧」を報告すると説明しています。裏を返せば、削除に失敗する状況が実際にあるということです。

費用の観点で警戒すべきは、時間課金ではなく作成・削除自体に費用や制約が伴うリソースです。NATゲートウェイ、RDSインスタンス、ロードバランサあたりが代表格。テストのたびに作って壊す構成にすると、テスト1回あたりの単価が跳ね上がります。公式が「定期的に安全に一掃できる専用のテストアカウントを用意する」ことを推奨しているのは、この削除漏れが積み上がる前提に立っているからです。本番アカウントでintegrationテストを回す構成は採用しないでください。削除漏れが本番リソースと混ざった時点で、一掃という手段が使えなくなります。

parallelがtrueでも並列化されない3条件と実行時間の見積り

paralleltrueにすれば速くなる、とは限りません。公式ドキュメントは並列実行の成立条件を3つ挙げています。

  • 互いのoutputを参照していないこと
  • 同じstateファイルを共有していないこと(stateファイルはstate_key、未設定ならモジュールソースで決まる)
  • 双方がparallel = trueであること

2番目が見落とされやすい条件です。同じモジュールを対象にした複数のrunは、state_keyを明示しない限り同じstateを共有するため、parallelを付けても直列に走ります。並列化したいなら、runごとにstate_keyを分けておく設計こそ前提です。なお1.13.0ではteardownの並列実行が入ったため、後片付けに要する時間は以前より短縮されています。実行時間を見積もるときは、runの本数ではなくstate_keyの種類数を基準にすると実測に近づきます。

terraform testを導入しない方がよい条件と別手段への切り替え

導入を勧める記事ばかりですが、割に合わない構成は確かに存在します。ここは条件を付けて言い切ります。

モジュールを自作しない構成でterraform testが過剰になる条件

次の3つがすべて当てはまるなら、terraform testは見送ってかまいません。自作モジュールが無くRegistryの既製モジュールを呼ぶだけ、変数による条件分岐が無い、環境が単一。この構成でテストを書いても、検証しているのは自分のコードではなくプロバイダとRegistryモジュールの仕様です。

逆に、1つでも自作モジュールがあり、それを2つ以上の環境や案件で使い回しているなら導入する価値があります。使い回すコードは変更の影響範囲が読みにくく、変数の組み合わせを変えたときの壊れ方が事前に分からないためです。判定の軸はリポジトリの規模ではなく、同じモジュールを異なる変数値で2回以上呼んでいるかに置いてください。受託でインフラを構築して運用まで引き継ぐ場合は、引き継ぎ後に触る人が変数を変えても壊れないことを示す手段としても働きます。構築から運用設計までの相談はインフラ構築(AWS・Google Cloud・Azure)で受け付けています。

テストを書く前にTFLintとplan差分レビューで足りる場面の判定

テスト導入より先に効く手があります。TFLintによる静的解析と、PRでのplan差分レビューです。この2つで防げる事故が、実際の障害の相当な割合を占めます。命名規約の違反、非推奨引数の残存、意図しないリソースの削除。いずれもテストを1本も書かずに検出できます。

テストに進むべき合図は、plan差分を目で追っても安全だと断言できなくなったときです。具体的には、モジュールの入力変数が10個を超えたあたり、あるいは条件分岐で作られたり作られなかったりするリソースが出てきたとき。その手前でテストフレームワークを持ち込むと、保守対象のコードが増えるだけで防げる事故は増えません。terraform providerのバージョン制約とalias設計のように、lockファイルで固定して差分を減らす対策の方が先に効く場面もあります。

よくある質問

terraform testの導入検討でよく挙がる質問を、公式ドキュメントの記述に沿って整理しました。

terraform testはどのバージョンから使えますか?

公式ドキュメントに「This testing framework is available in Terraform v1.6.0 and later.」と明記されており、v1.6.0以降で利用できます。プロバイダのモック機能(mock_provider・override系ブロック)はv1.7.0以降で追加されました。テストのteardownの並列実行やテストファイル内での外部変数定義は1.13.0(2025-08-20公開)以降です。本体の最新安定版は2026年8月14日時点でv1.15.8(2026-07-08公開)で、いずれの機能も利用できます。導入手順はTerraformのインストール手順にまとめています。

.tftest.hclファイルはどこに置けばよいですか?

既定のテストディレクトリはtestsで、ここに置いたファイルが自動的に読み込まれます。加えて、ルート構成ディレクトリ直下のテストファイルは-test-directoryの指定に関わらず常に読み込まれる仕様です。置き場所を変えたい場合は-test-directory=に相対ディレクトリを渡します。単一構成なら既定のtestsのままにして、ファイル名の接頭辞で用途を分ける方が、-filterによる絞り込みがそのまま効きます。

mock_providerを使えば課金は完全にゼロになりますか?

mock_providerを使い、なおかつcommand = planで実行するrunだけなら、リソースは作られないためクラウド側の課金は発生しません。ただしcommand = applyのままモックを使うと、モックが返す偽データで論理は通っても実リソースは作られない一方、テスト対象外の実プロバイダが混在していれば、その部分は作成されます。課金をゼロにしたいなら、モックの有無ではなくcommandの値を必ずplanにそろえてください。既定値がapplyである点に注意が必要です。

terraform testとTerratestはどちらを選ぶべきですか?

Terraformの構成そのものを検証するならterraform testで足ります。HCLだけで書けるため、Terraformを書く人がそのままテストも保守できます。Terratestを選ぶ理由が残るのは、作成後のエンドポイントにHTTPリクエストを投げて応答内容を確かめる、といったTerraformの外側の振る舞いを検証したい場合です。Goの実行環境とテストコードの保守コストを負担してでも確かめたい対象があるか、で判断してください。両方を並行運用すると検証範囲が重複します。

テストで作ったリソースが消えずに残った場合はどうしますか?

Terraformはテストファイルの実行後に残存インフラのdestroyを試み、破棄できなかった場合は作成したが削除できなかったリソースの一覧を出力します。まずCIのログでこの一覧を確認し、該当リソースをコンソールかCLIで手動削除してください。公式は、定期的に安全に一掃できる専用のテストアカウントをプロバイダ側に用意することを推奨しています。本番アカウントでintegrationテストを回していると、残骸と本番リソースの区別が付かなくなり、この一掃という手段が使えなくなります。

関連記事

資料請求

RELATED POSTS 関連記事