Terraform moduleの分割粒度とoutput設計|自作とRegistryの判断
Terraformのコードが1,000行を超えたあたりで、必ず「モジュールに切るべきか」という議論が始まります。切り方を決めずにmodulesディレクトリを作ると、変数を素通しするだけの薄い層が増え、planの差分が読めなくなる。この記事では、source・versionの制約とinput/outputによる境界設計を押さえたうえで、サービス単位・機能単位・環境単位の3方式の選び分けと、Terraform Registryの公開モジュールを採るか自作するかの損益分岐を扱います。既存構成を後からモジュールへ畳むとplanがdestroyとcreateになる問題も、movedブロックで解く手順として整理しました。バージョン依存の記述は2026年8月13日に公式ドキュメント・GitHub Releases・Registry APIで確認した内容です。
まとめ:モジュール化は3環境以上か分岐10個超で踏み切る
踏み切る条件は「同じ構成を3環境以上へ展開する」か「呼び出し側の条件分岐が10個を超える」のどちらかです。満たさないうちは、ルートモジュールにベタ書きしたままのほうがplanが読めて速く回る。どちらかを満たしたら、機能単位で切るのが変更の波及を最小にします。
公開モジュールと自作の線引きも数値で決まります。VPCやEKSのようにクラウド側の仕様が複雑な基盤リソースは公開モジュールへ預け、命名規約やタグ規約を強制したい層だけ自作する。そして既存構成を畳むときは、コードを移す前にmovedブロックを書く。これを飛ばすとplanが破棄と再作成を提案します。
ルートモジュールと子モジュールの構造とsource指定の書式
モジュールの議論が噛み合わない原因は、たいてい用語の指す範囲がずれていることにあります。まず構造を固定します。
作業ディレクトリ=ルートモジュールという前提と呼び出しの向き
Terraformを実行するディレクトリそのものが「ルートモジュール」で、そこに置いたmoduleブロックから呼ばれる側が「子モジュール」になります。モジュールを作らずmain.tfを1枚書いた状態でも、すでにルートモジュールを1つ使っている。この非対称を押さえると、「モジュール化」が実際には「ルートモジュールから一部の記述を子モジュールへ切り出す」作業だと分かります。
呼び出しの向きは常に親から子への一方向で、子から親を参照する手段はありません。値を親へ返す唯一の経路がoutputです。前提となる宣言的な構成管理の考え方はIaCとは?Infrastructure as Codeの仕組み・メリットと導入判断を解説で扱っています。
source引数がリテラル文字列に限られる制約と実務での回避策
公式のmoduleブロック仕様はsourceを「a literal string」と定義しています。テンプレート構文や任意の式は受け付けず、参照できるのは定数として宣言された入力変数とローカル値だけ。環境名を変数にしてsourceを組み立てる書き方は通りません。
回避策は2つに絞られます。呼び出し側のディレクトリを環境ごとに分け、それぞれのmain.tfへ固定文字列で書くか、sourceを1つに固定して環境差分をすべて変数側で吸収するか。前者は記述が重複する代わりに差分が目で追え、後者は重複が消える代わりに変数の分岐が増えると読みにくくなります。
version引数がレジストリ由来のモジュールにしか効かない件
version引数について公式は「This argument only applies when installing modules from a registry」と明記しており、ローカルファイルパス由来のモジュールでは機能しません。適用先はTerraform Registry、HCP Terraform、Terraform Enterpriseのプライベートレジストリに限られます。
Gitリポジトリを直接指す場合は、sourceのURLにリビジョン指定を付けて固定します。固定できていない状態は、参照先の既定ブランチが更新された瞬間に別の構成へ切り替わる事故につながる。レジストリ経由か、リビジョン固定か。どちらかを必ず選んでください。
標準モジュール構造で置くファイルとmodules・examples
公式が定める標準モジュール構造で必須なのはルートモジュールだけです。推奨はREADME、LICENSE、main.tf、variables.tf、outputs.tfの5点で、すべての変数と出力に1〜2文の説明を付けることも推奨に含まれます。
modules/サブディレクトリ:ネストした子モジュールを置き相対パスで参照する。README.mdを持つものは外部利用想定、持たないものは内部用途と見なされるexamples/:リポジトリ直下に置く。利用者がコピーする前提のため、内部のmoduleブロックは相対パスではなく外部参照先を指定する
この構造に沿っておけば、Registryへ公開したときにドキュメントが自動生成されます。命名規則やfmtの当て方といった記法そのもののルールは、この記事では扱いません。
input変数とoutput値でモジュール境界を決める設計手順
分割粒度より先に決めるべきは、モジュールの入口と出口です。境界の設計が甘いと、粒度をいくら調整しても保守は楽になりません。
variableの型制約とvalidationで呼び出し側の誤りを止める
モジュールのvariableは型を明示したうえでvalidationを付けます。CIDRブロックを受け取る変数に文字列型だけを指定すると、誤った値がapplyまで到達してクラウド側のエラーで初めて落ちる。validationで受け付ける形式を宣言しておけば、planの段階で止まります。
変数の数には目安を置きます。1つのモジュールでvariableが20を超えたら、責務を持ちすぎている兆候です。呼び出し側から見て「何を渡せばいいのか」が1文で言えなくなった時点で、分割の検討に入ってください。
outputの粒度とsensitive・ephemeralの使い分けの基準
outputは親からmodule.CHILD_MODULE_NAME.OUTPUT_NAMEの形で参照します。ここで機密値の扱いを誤ると、後から取り返しがつきません。公式はsensitiveについて「Terraform stores the values of sensitive outputs in your state」と明記しており、CLI出力の表示を抑えるだけでstateには平文で残る仕様です。
さらにterraform outputへ-jsonや-rawを付ければ、その値は平文で取り出せます。stateに残したくない値にはephemeralを使う。stateとplanファイルから値を除外できる代わりに、代入できる値に制約が付きます。state自体の保存先とロックの設計はTerraform stateとは?tfstateの構造とS3バックエンド・移動削除の安全手順にまとめてあります。
モジュール同士で値を受け渡すときに結合度を上げない引数の設計
やりがちな失敗は、モジュールAのoutputをモジュールBのvariableへ次々と流し込む構成です。一見きれいに繋がりますが、Aの内部実装を変えるとBのplanまで動き、変更の影響範囲が読めなくなる。渡すのはIDやARNのような安定した識別子だけに絞ります。
オブジェクト全体を丸ごと渡す設計は、結合度を上げる代わりに得るものがほとんどありません。判断の目安はこうです。モジュール間で受け渡す値が5個を超えたら、その2つは分けるべきではなく1つのモジュールだったと考えてください。
分割粒度の3方式(サービス単位・機能単位・環境単位)の比較と選び方
切り方の候補は実質3つに収束します。どれが優れているかではなく、チーム規模と環境数で答えが変わります。
AWSサービス単位で切る構成の利点と横断的な変更で崩れる境界
VPC、EC2、RDS、S3といったサービス種別ごとにモジュールを作る方式です。導入初期には強い。AWSのドキュメントとディレクトリ名が1対1で対応するため、初めて触る人でも目的のファイルへ到達できます。
崩れるのは横断的な変更が来たときです。「バッチ処理機能を1つ追加する」という要求に対して、VPCモジュールにサブネット、SGモジュールに規則、ECSモジュールにタスク定義、IAMモジュールにロールを足す作業が発生する。変更が4つのディレクトリへ散り、レビュー時に全体像を掴めなくなります。
機能単位へ切り直したときの変数受け渡しとPRレビューの負荷量
機能単位はこの散在を解きます。「バッチ処理」というモジュールの中にサブネット、SG、タスク定義、ロールをまとめて置くため、1つの機能追加が1つのディレクトリで完結する。PRの差分が機能の単位と一致するので、レビューでの見落としも減ります。
代わりに払うコストが2つあります。1つは共通リソースの置き場所が曖昧になること。VPC本体のように複数機能から参照される層は、機能モジュールの外へ出す判断が必要です。もう1つは機能をまたぐリソースの重複で、同じSGを2つの機能から使う場面ではどちらに置くかを都度決めることになります。
3方式の比較表とチーム規模・環境数から採用を決める判断の目安
3方式を並べると、選ぶ基準がはっきりします。
| 方式 | 環境差分の持ち方 | 変更の波及範囲 | 向く条件 |
|---|---|---|---|
| サービス単位 | 変数で切り替え | 横断変更で複数箇所 | 導入初期・1〜2環境 |
| 機能単位 | 機能ごとに変数集約 | おおむね機能内に収まる | 3環境超・複数チーム |
| ディレクトリ分割のみ | 環境ごとに全記述 | その環境内に閉じる | 環境が2つまで |
判断はこう決めます。環境が2つまではディレクトリ分割だけで足ります。3環境目が視野に入ったらサービス単位で切り始め、横断変更のPRが3ディレクトリ以上に及ぶようになったら機能単位へ切り直す。この順序を飛ばして最初から機能単位を狙うと、機能の輪郭が固まっていないため境界を何度も引き直すことになります。なお、環境そのものをどう分けるか(workspace・ディレクトリ分割・backend分割)の選び分けはterraform workspaceの使い方と環境分離の判断基準|分割方式の比較で扱っています。
Registry公開モジュールの採用条件と自作との損益分岐点
ここが競合記事で最も空いている論点です。自作前提の解説は多いものの、公開モジュールを取り込む判断基準はほとんど示されていません。数値で言い切ります。
terraform-aws-modules/vpcの実測ダウンロード数と要求版
2026年8月13日にTerraform Registry APIで実測したところ、terraform-aws-modules/vpc/awsの最新版は6.6.1、累計ダウンロード数は204,678,371回でした。要件はTerraform 1.0以上、AWSプロバイダ6.28以上。この規模で使われているモジュールは、公式ドキュメントに書かれていない組み合わせの不具合が先に踏まれ、Issueとして潰されています。
もう1つ読み取れるのが非推奨化の進め方です。v6系ではルートモジュール内でのVPC Flow Log作成が非推奨扱いになり、独立したflow logモジュールへ誘導されています。破壊的変更を段階的に告知する運用が回っているかどうかも、採用判断の材料です。
公開モジュールを採用する条件と自社で書くべきリソースの線引き
採るべきなのは、クラウド側の仕様が複雑でオプション分岐が多く、しかも自社固有の要件が薄い基盤リソースです。VPC、EKS、RDSがここに当たる。自分で書くとサブネットの割り当てやルートテーブルの分岐で数百行を費やしたうえ、AZ追加のたびに手直しが発生します。
- 採用する:オプション分岐が10個を超える基盤リソースで、累計ダウンロードが1,000万回超・CHANGELOGで破壊的変更が追える公開モジュール
- 自作する:命名やタグの規約を強制する層、ラップ対象が3リソース以下のまとまり、社内固有の承認や監査要件が絡む設定
- 二層にする:公開モジュールを取り込んだうえで自社モジュールで包み、規約だけを自作側で当てる
実務では3つ目に落ち着く場面が多くなります。基盤の複雑さは公開モジュールへ預け、規約の強制だけを自社側に残す形です。
Registryへ自作モジュールを公開する要件と社内配布の代替
自作モジュールをRegistryへ出す場合、公式が示す要件は明確です。GitHub上のpublicリポジトリであること、terraform-PROVIDER-NAMEの3部構成で命名すること(例はterraform-google-vaultやterraform-aws-ec2-instance)、標準モジュール構造に従うこと、リリースタグ名がセマンティックバージョンであること。タグはvプレフィックスの有無を問わず、v1.0.4と0.9.2のどちらも有効で、初回公開時に最低1つ必要です。GitHubのリポジトリ説明がそのまま短い説明として使われる点も覚えておくとよいでしょう。
社内利用に留めるなら公開せず、プライベートレジストリかGitリポジトリのリビジョン固定で配布します。入力と出力の一覧はTerraform-docsとは? ドキュメント自動生成ツールの概要・機能・特徴で扱っているツールに生成させるのが手間の少ない方法です。
公開モジュールの破壊的変更へ追随するバージョン固定の運用ルール
公開モジュールを取り込むなら、更新をどう追うかまで決めておきます。バージョン制約を~> 6.0のように書けばマイナー更新は自動で入り、メジャー更新は止まる。問題になるのは、止めたまま放置される運用です。
ルールはこう置きます。メジャーを固定してマイナーは開ける。そのうえで四半期に一度、メジャー更新のCHANGELOGを読む枠を作る。2年以上メジャーを上げない状態が続くなら、その公開モジュールを使う利点は失われており、必要な機能だけを抜き出して自作へ切り替えたほうが保守しやすくなります。
既存構成をモジュールへ畳むときのstate移行とmovedブロック
ここが実務で最も痛い工程です。動いている構成をモジュールへ切り出す作業は、コードの移動だけでは終わりません。
モジュールへ移すとplanがdestroyとcreateに見える理由
ルートモジュールにあったaws_subnet.privateを子モジュールへ移すと、そのリソースのアドレスはmodule.network.aws_subnet.privateへ変わります。Terraformはstate内のアドレスで実体を照合するため、旧アドレスの記録は「設定から消えた」、新アドレスは「設定に増えた」と解釈される。結果としてplanは破棄と新規作成を提案します。
サブネットやSGなら復旧の余地がありますが、RDSインスタンスやEBSボリュームで同じことが起きればデータが消えます。モジュール化のPRでplanにdestroyが並んでいたら、そこで手を止めてください。
movedブロックで移動を宣言する書式とplanでの確認手順
解決策はmovedブロックです。v1.1系以降で使えるようになった機能で、fromに旧アドレス、toに新アドレスを書きます。モジュール呼び出し、リソース、子モジュール内のリソースのいずれも指定できる。Terraformは移動元の既存オブジェクトを確認し、名前を変えてからplanを作るため、リソースは破棄されません。
- コードを子モジュールへ移す前に、移動後のアドレスを確定させる
- movedブロックを
fromとtoのペアで書き足す - コードを子モジュールへ移動し、呼び出し側に
moduleブロックを置く terraform planでdestroyが0件、変更が移動のみになっていることを確認する- applyしたあと、次のリファクタリングまでmovedブロックを残しておく
state側の記録を直接書き換える手段もありますが、planで事前に内容を確認できるmovedブロックを既定にしたほうが安全です。planの差分でdestroyが0件かどうかを機械的に確かめる手順はterraform planの差分の読み方|init・apply・destroyの安全手順にまとめました。state外にあるリソースを新しくモジュール配下へ取り込む場合は、terraform importで既存リソースをコード化する手順の側の工程になります。
for_each付きモジュールへ畳むときのインスタンスキー指定
モジュールでもcount、for_each、depends_on、providersのメタ引数が使えます。3環境へ同じ構成を展開するならfor_eachで呼び出すのが素直な形です。
畳むときの注意はインスタンスキーです。countで呼ぶとアドレスに[0]のような数値添字が付き、リストの並び替えで別のリソースを指すようになる。環境名のようにキーが自然に決まる場面ではfor_eachを選び、movedブロックのtoにもキー付きのアドレスを正確に書いてください。数値添字のままでは、環境を1つ追加した拍子に無関係な環境が作り直されます。
モジュール化を見送る条件と、薄いラッパーがplan差分を隠す失敗
モジュール化は無条件に善ではありません。見送るべき条件を先に決めておくと、不要な層を作らずに済みます。
リソース2個以下・再利用2箇所以下ならモジュール化は見送る判断
数値で線を引きます。ラップ対象のリソースが2個以下で、かつ呼び出される箇所が2箇所以下なら、モジュールにしない。この条件下では、モジュール化で削れる行数よりも、variableとoutputの宣言で増える行数のほうが多くなります。
「将来増えるかもしれないから今から切っておく」という判断は採りません。増えてからmovedブロックで畳めば、データを失わずに移行できます。
変数を素通しするだけのラッパーがplan差分を読めなくする害
最も避けたいのが、受け取ったvariableをそのままリソース属性へ渡すだけのモジュールです。行数は減らず、呼び出し側からリソースの実際の設定値も見えなくなる。planの差分にはモジュール経由のアドレスが並ぶため、どの設定が変わったのかを追うのにコードを2枚開くことになります。
判定は簡単です。variableとリソース属性が1対1で対応し、モジュール内に条件分岐も加工も存在しない。この状態なら削ってよい層です。逆に、複数のリソースにまたがる整合性をモジュール内で保証しているなら、その層には価値があります。
ネストが2階層を超えたらTerragruntへ切り替える境界条件
モジュールのネストを深くしてDRY化を進めると、どこかで限界が来ます。境界は2階層です。ルートモジュールから子、子から孫までは追えますが、その先は変数がどこから来ているのかを人が追跡できなくなる。
3階層目が必要になった時点で、Terraform単体でのDRY化は諦めます。バックエンド設定やプロバイダ設定の重複を外側から解くTerraformの課題を解決するTerragruntの基本概念と導入メリットのようなツールへ移すか、分割方式そのものを機能単位へ見直すか。この2択です。自社の構成でどちらを採るべきか判断がつかない場面では、インフラ構築(AWS・Google Cloud・Azure)の設計支援として、既存のstateとディレクトリ構成を見たうえで移行手順まで引くこともできます。
よくある質問
モジュール設計で実際に問い合わせが多い5点をまとめました。
terraform moduleはどのくらいの粒度で分割すべきですか?
環境が2つまでならモジュールを作らずディレクトリ分割で足ります。3環境以上へ展開するか、呼び出し側の条件分岐が10個を超えたら踏み切ってください。1つのモジュールでvariableが20を超えたら責務を持ちすぎている兆候なので、さらに分ける検討に入ります。
terraform outputで子モジュールの値を親から参照する書式は?
子モジュール側でoutputブロックを宣言し、親からはmodule.CHILD_MODULE_NAME.OUTPUT_NAMEの形で参照します。受け渡すのはIDやARNのような安定した識別子に絞ってください。オブジェクトを丸ごと渡すと、子の内部実装を変えたときに親のplanまで動きます。
Terraform Registryの公開モジュールは業務で使って問題ないですか?
オプション分岐が多い基盤リソースであれば、むしろ自作より安全です。2026年8月13日にRegistry APIで実測したterraform-aws-modules/vpc/awsは累計204,678,371回ダウンロードされており、この規模なら組み合わせ不具合が先に潰されています。判断材料はダウンロード数とCHANGELOGの整備状況です。
既存のリソースを後からモジュールへ移すと作り直されますか?
movedブロックを書かずに移せば、planは破棄と新規作成を提案します。アドレスがaws_subnet.privateからmodule.network.aws_subnet.privateへ変わり、Terraformが別物と判断するためです。v1.1系以降のmovedブロックにfromとtoを書いておけば破棄されません。
モジュールのバージョンはどう固定すればよいですか?
version引数はレジストリ経由でインストールするモジュールにしか効きません。Gitリポジトリを直接指す場合はsourceのURLにリビジョン指定を付けます。運用ルールとしては、メジャーを固定してマイナーは開け、四半期ごとにメジャー更新のCHANGELOGを読む枠を作るのが現実的な落としどころです。
関連記事
- IaCとは?Infrastructure as Codeの仕組み・メリットと導入判断を解説:モジュール設計の前提になる宣言的な構成管理の考え方と導入判断を扱います。
- Terraformコーディング規約(スタイルガイド):分割の前段になるfmt・命名規則・ディレクトリ構成の統一ルールです。
- Terraform stateとは?tfstateの構造とS3バックエンド・移動削除の安全手順:モジュール移行時に触るstateの保存先とロックの設計をまとめています。
- terraform importで既存リソースをコード化する手順:state外のリソースをモジュール配下へ取り込む工程の解説です。
- TFLintでTerraformコードを静的解析する:モジュールに切り出したコードへ機械的な検査を当てる手段です。