初学者がTerraformの構造を理解するために

これはフェンリル デザインとテクノロジー Advent Calendar2025 18日目の記事です。

こんにちは。インフラ担当の小林匠です。 みなさん、Terraformを使用してリソースを作成していますか。 私はTerraform歴3ヶ月で、現在はAWSクラウドのリソースをデプロイしています。当初モジュール構造についてきちんと理解できず何度もエラーを起こしては解消してを繰り返してきました。 何度も何度も”Unsupported attribute”や”required argument”などのエラー出力を見ました。

そんな苦労もありましたが、初学者である私が実際にモジュール構造を理解した上で、 そのモジュール化のメリットを最大限に活かしてリソース作成までできるようになったところを、解像度を細かくしてお伝えしていきます。

今回は、あくまで実体験ベースでの記事となるため、私なりの理解の仕方を文字列や図解で整理しています。 もちろん、理解の仕方は人それぞれですし、Terraformのソースコードの書き方にルールはありますが、リソースをどのように作成するかは書く人によってバラバラです。 私自身は視覚的に図解で理解できるようになったので、初学者の方へ理解のきっかけになってくれたらと思います。

※ 本記事で記載している内容は、Terraformの推奨とは異なる部分があります。あくまでもTerraformの構造を早く理解するために私が実際に行ったことを表現しております。読者のみなさまにはその点ご留意いただけますと幸いです。

そもそも「Terraform」「Terraformのモジュール構造」とは

ここでは、Terraformに関する概要については深く触れることはないですが、簡単に紹介します。

Terraformとは

Terraformは、インフラストラクチャを安全かつ効率的に構築、変更、バージョン管理できるコードツールです。 コンピューティングインスタンス、ストレージ、ネットワークといった低レベルコンポーネントから、DNSエントリやSaaS機能といった高レベルコンポーネントまで、あらゆるインフラストラクチャの構築、変更、バージョン管理をサポートします。

上記の説明はTerraformの公式にある記述ですが、簡単に言えばAWSでいうとVPCやサブネットのネットワークリソースや、ECSやEC2などのコンピュートサービスを作成することができるツールです。 このTerraformはAWSだけでなく、AzureやGoogle Cloudといった他クラウドサービスもサポートしており、マルチクラウドのシステム構築にも対応しています。

developer.hashicorp.com

モジュール構造とは

再利用可能なモジュールは、ルートモジュールで使用されるのと同じ設定言語の概念をすべて使用して定義されます。

例えば開発、検証、本番環境に対して同じシステム構成でリソースを作成したい!となった場合、モジュール化することで、同じ定義を何度も書く必要がなくなります。 モジュールに定義したリソースを各環境から参照するだけで、同一リソース構成のデプロイ可能を実現させることができるわけです。

developer.hashicorp.com

モジュールとリソースのイメージ

ソースコードをモジュール化することの最大のメリット

上記モジュール構造で軽く触れてはいますが、ソースコードをモジュール化することの最大のメリットはモジュール側に定義したソースコードを再利用することです。 仮に、複数環境に同じリソースを作成する必要があるときに、それぞれの環境分同じリソースを同じ分だけ書くのは時間がかかりますし、ソースコードを後から修正する際に同リソースの数だけ同じ変更をする必要があります。 また、人がソースコードを読んだときに確認する箇所が少なくなればそれに越したことはないですよね。

モジュール利用によりトータルのソースコード量は削減することができ、同時に定義している箇所を見比べる必要もなくなります。

開発、検証、本番の3環境を想定して、envディレクトリにはdev=開発、stg=検証、prd=本番を置いています。 一方、(今回はネットワークリソースを例に)リソースを定義するモジュール側にはリソースを定義するファイルのほかに、Terraform自体がバージョン確認の際に使用する管理ファイルを配置しています。

ルートモジュールとリソースモジュールのイメージ

それぞれのtfファイルはどこをどのように参照しているのか

そしてここからが本題です。 私がTerraformを学び始めた頃に理解するのに時間を要した部分になります。 それでは具体的にtf各ファイルがどのように他のファイルを参照しているのか、対象環境はdev=開発で、作成するリソースはネットワークを例にして、各ファイルの役割とその相関関係がわかる図解に書き出してみました(今回は説明が成立する最低限のみのソースコードとします)。

各tfファイルの役割
aws/env/dev/

env/dev/なら開発環境、env/stg/なら検証環境というように環境ごとにディレクトリを使い分ける

  • module.tf:リソースブロックで対象モジュールを呼び出す(今回で言うとネットワークリソース)

  • version.tf:terraformやproviderブロックを用意して、このTerraformバージョンやプロバイダー(例えば"aws")を指定する(Terraformを提供しているhashicorp社が定義している対象クラウドごとで定義する決めごとのようなもの)

aws/modules/network/

modules/network/ならネットワークのリソースを、modules/backend/ならバックエンド側のリソースファイルを格納する

  • vpc.tf:作成したいリソースを定義する(Terraform内でのリソース名があり、VPCを作成したい場合は"aws_vpc"を定義して任意のリソース名("main"など)を指定する)

  • version.tf:terraformやproviderブロックを用意して、このTerraformバージョンやプロバイダー(例えば"aws")を指定する

  • variables.tf:aws/env/dev/module.tfのモジュールブロックで渡された引数を受ける

  • output.tf:このネットワークモジュールで定義したリソースを他のリソース(例えばECSやRDSなど)でも使用したい場合に外部モジュールに参照させる

私がTerraformで難しいと感じた点

これまでソースコードを使用してデプロイした経験がほとんどなかった私が、Terraform、モジュール構造をある程度理解した上でリソースを作成することができたことについてです。 実際のところ、私自身インターネットで検索してTerraformに関する記事を読み、参考になるなぁと思ったソースコード部分を抜き出して書く、というようなやり方をしていました。 もちろんそのやり方は決して悪いことではないと思ってはいますが、正直初学者としては見よう見まねで着手したに過ぎず、自分の中できちんと理解した上でTerraformを書く、ということにはなっていませんでした。 今でこそ反省ではありますが、3ヶ月前の私について以下の3つです(今となっては少し恥ずかしい・・・)。

  • Terraform公式リファレンスを参照せずに書き始める
  • ソースコード内の1行ごとの意味を把握していない
  • 変数の参照方法が全然理解できない

今となってはそれぞれ改善できてはいますが、特に理解するのにひと工夫が必要だったのが変数の参照方法です。 結論から言うと、その辺にあったペンと紙に以下のような構成を書いて、どこがどこに関連していて何を参照しようとしているのかを明確にしようとしました。

ファイル相関関係のイメージ

さらに、公式リファレンスを活かしていなかったのもあり、ドキュメントの使い方もままならないことが分かり、これでは良くないと痛感しました。 例えば、作成したVPCリソースを他のモジュール(ECSやRDSなど)で利用したい場合、公式リファレンスの最後の方にある「Attributes Reference(属性参照)」のセクションを参照すると良いです。 そこには、そのリソースを作成した後に「外側へ出力できる値」が定義されています。 今回はVPCのIDが必要なので、一覧の中から"id"を探し、"aws_vpc.リソース名.id"という形で記述すれば良いことがわかります。

# aws/modules/network/output.tf
output "vpc_id" {
  value       = aws_vpc.main.id
  description = "作成したVPCのIDを他モジュールでネットワーク指定に利用する"
}

registry.terraform.io

上記の参照方法について詳細に分解すると、以下のようになります。

  • output "vpc_id":「vpc_id」という名前で外部へ公開する出口を作る
  • value:出力する中身を指定する
  • aws_vpc.main.id:「aws_vpc」というリソースの、「main」という名前の、「id」という属性を参照する

Terraformは参照先のパスを「.(ドット)」で繋ぎます(「module.(モジュール名).(output名)」という感じ)。 つまり"module.network.vpc_id"と記述することで、他のファイルからこの値を呼び出すことができます。 "id" 変数の参照先であるパスを図式に書き起こして整理すると、私は次のようなイメージで理解できました。 この参照先のパスの対応関係の理解を他のリソースでも何度も確認しました。

リソース変数参照のイメージ

VPC情報を他のモジュールで参照できるようにする場合は、ネットワークモジュールにある用意したoutputを活用することで出力できます。

実際に、VPCの情報が必要な他モジュールでは、以下のようにモジュールを呼び出すことで引数(aws/modules/network/output.tf)を渡すことができます。 例えば、バックエンドモジュールで定義するECSを配置するネットワーク環境として、VPCの情報を参照すると以下のような書き方になります。

module "backend" {
  source = "../../modules/backend" # ECSを定義しているディレクトリ
  vpc_id = module.network.vpc_id   # VPCのIDを参照する
}

上記の参照を図式に書き起こして整理すると、私は次のようなイメージで理解できました。

モジュール間参照のイメージ

まとめ

ここまでTerraformのモジュール構造について、私が実際に理解するために行ったことを図解を用いながらお伝えしてきました。

Terraformのメリットを最大限に活かすためには、ただリソースがデプロイできるだけではいけません。 ソースコードを書いている自分はもちろんのこと、自分以外の人が実際のソースコードを読んだときに可読性があり、運用時にオペレーターがメンテナンスをいかにしやすくするかなど考慮すべきことはたくさんあります。

また、Terraformにはモジュール化の他にもより少ないコード量や汎用性の高い書き方を実現することができます。

私自身、これからもさらにTerraformのことを深掘り、よりベストプラクティスを意識したソースコードを書けるように精進していきます。 それではまた。