Terraformをちゃんと使いたいと思い始めて、もう半年くらい経つ。ずっと「気になってるもの」リストに入れたまま手が出せなかったんですが、AWSのリソースをマネコンでポチポチ作るのが限界になったのでついに本腰を入れることにしました。
この記事は全4回のシリーズ「Terraform × AWS インフラ自動化入門」の第1回です。シリーズ全体の流れはざっくりこんな感じを想定しています:
- 第1回(今回):ファイル構成・変数・モジュールの基本
- 第2回:S3リモートステート + DynamoDBロック管理
- 第3回:VPC・EC2を実際に構築してみる
- 第4回:GitHub Actionsで CI/CD パイプライン化
まずは基礎の基礎から。変数とモジュールの概念がぼんやりしたまま進むと後でかなりしんどくなるので、第1回はここを丁寧にやっていきます。
この記事でわかること
- Terraform プロジェクトの実用的なファイル構成
- input variable、local value、output value の役割と使い分け
- 変数の値を渡す3つの方法
- 子モジュールの作り方と呼び出し方
- モジュール設計の考え方(サービス単位 vs 機能単位)
- 最初から意識しておくべき実装パターン
Terraformのファイル構成、何がどこにあるの?
最初に一番迷うのがファイル構成だと思います。公式や記事ごとに微妙に書き方が違ったりして混乱するんですよね。
まずは小〜中規模で実用的なフラットな構成から入るのがおすすめです。
project-root/
├── main.tf # リソース定義のメイン
├── variables.tf # 変数の宣言
├── locals.tf # ローカル値(computed な値)
├── outputs.tf # 出力値
├── terraform.tfvars # 変数の実値を入れるファイル
├── provider.tf # プロバイダー設定
└── backend.tf # ステート管理の設定
それぞれの役割を整理すると:
- main.tf:実際に作るリソースを書く場所。一番触れる
- variables.tf:「このプロジェクトで使う変数はこれです」という宣言だけ書く。値は書かない
- locals.tf:変数から計算した値や、タグの共通定義を置く
- outputs.tf:他のモジュールや外部から参照したい値を出力する
- terraform.tfvars:実際の値を入れるファイル。
.gitignoreに入れることも多い
規模が大きくなると vpc.tf、compute.tf、database.tf のようにリソースごとにファイルを分けていく形になります。Terraformは同じディレクトリ内の .tf ファイルを全部まとめて処理するので、ファイル名は論理的な整理のためだけに存在している感じです。
変数(variable)の使い方
Terraformの変数まわりは3種類あって、最初はここで混乱しました。
- input variable(var.xxx):外からモジュールに渡す値
- local value(local.xxx):モジュール内部だけで使う計算済みの値
- output value(output):モジュールから外に公開する値
input変数は外から注入できる値、local valueは常に元のモジュールのスコープ内にとどまる値、output valueは内部の値を外部と共有するためのものという整理が一番しっくりきます。
input variable の宣言
# variables.tf
variable "environment" {
description = "デプロイ環境(dev / stg / prod)"
type = string
default = "dev"
}
variable "aws_region" {
description = "AWSリージョン"
type = string
}
variable "instance_type" {
description = "EC2インスタンスタイプ"
type = string
default = "t3.micro"
}
default を指定すると任意入力、指定しないと必須入力になります。description は省略しがちですが、書いておくとチームで使うときに助かります(自分一人でもしばらくたつと忘れる)。
変数の値を渡す方法
変数に値を渡す方法はいくつかあります。まず一番シンプルなのは terraform.tfvars ファイルに書いてしまう方法:
# terraform.tfvars
environment = "dev"
aws_region = "ap-northeast-1"
instance_type = "t3.micro"
CLIから直接渡すこともできます:
terraform apply -var="environment=prod" -var="aws_region=ap-northeast-1"
環境変数でも渡せて、TF_VAR_変数名 という形式にするとTerraformが自動で読み取ります:
export TF_VAR_aws_region="ap-northeast-1"
terraform apply
GitHub Actionsで動かすときはこのTF_VAR形式が便利です。Secrets から渡せるので認証情報を直接コードに書かずに済みます。
local value でタグを一元管理する
localsはAWSのタグ管理でよく使われる使い方が特に気に入っています。
# locals.tf
locals {
name_prefix = "${var.project}-${var.environment}"
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
}
}
# main.tf での使い方
resource "aws_s3_bucket" "app_data" {
bucket = "${local.name_prefix}-app-data"
tags = local.common_tags
}
locals ブロック(複数形)の中に1つ以上のローカル値を定義し、local.<name>(単数形)で参照するという仕様です。local.common_tags を全リソースで使い回せるので、タグの修正が一箇所で済みます。これを知る前はリソースごとにタグを書いていて、後から直すのがとても大変でした。
モジュールの考え方
モジュールはTerraformの中で一番「ちゃんと理解したい」部分です。
Terraformの設定は階層的なモジュール構造に従っていて、ルートモジュールは別のモジュールを呼び出す側で、子モジュールへ入力変数の値を提供してその出力を受け取る。terraform apply を実行するのは常にルートモジュールです。
子モジュール(child module)はルートモジュールから呼び出されるもので、リソースを定義して入出力を通じて機能を公開します。子モジュールは独自のスコープを持っているので、基本的には子モジュール内の値や定義を親や他の子モジュールから直接参照できません(必要なら input/output で受け渡しします)。この分離が大事で、変更の影響範囲を局所化できます。
子モジュールのディレクトリ構成
modules/
├── s3/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── ec2/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── vpc/
├── main.tf
├── variables.tf
└── outputs.tf
各モジュールはこの3ファイル構成が最低限の単位…とされることが多いです。
S3バケットを作るモジュールを作ってみる
# modules/s3/variables.tf
variable "bucket_name" {
description = "S3バケット名"
type = string
}
variable "tags" {
description = "タグ"
type = map(string)
default = {}
}
# modules/s3/main.tf
resource "aws_s3_bucket" "this" {
bucket = var.bucket_name
tags = var.tags
}
resource "aws_s3_bucket_versioning" "this" {
bucket = aws_s3_bucket.this.id
versioning_configuration {
status = "Enabled"
}
}
# modules/s3/outputs.tf
output "bucket_id" {
description = "S3バケットID"
value = aws_s3_bucket.this.id
}
output "bucket_arn" {
description = "S3バケットARN"
value = aws_s3_bucket.this.arn
}
子モジュールで定義された output 値はそのままでは外からアクセスできないため、ルートモジュールを通じて公開する必要があります。この例では bucket_id と bucket_arn を outputs.tf で公開しているので、ルートモジュールや他のモジュールから module.s3.bucket_arn のように参照できます。
ルートモジュールからモジュールを呼び出す
# main.tf(ルートモジュール)
module "app_bucket" {
source = "./modules/s3"
bucket_name = "${local.name_prefix}-app-data"
tags = local.common_tags
}
module "logs_bucket" {
source = "./modules/s3"
bucket_name = "${local.name_prefix}-logs"
tags = local.common_tags
}
同じモジュールを2回呼び出しているのがポイントで、source は同じでも別のリソースとして作成されます。共通処理の使い回しができるのがモジュールの強みです。
余談ですが、module "xxx" の xxx 部分(呼び出し名)が後で module.xxx.output名 という参照に使われます。名前の付け方が地味に大事です。
モジュール設計の方針:AWSサービス単位 vs 機能単位
モジュールをどう分けるかは正直まだ正解が見えていないんですが、よく議論されるのが「AWSサービス単位」か「機能(ユースケース)単位」かというパターンです。
サービス単位でモジュールを分けると、S3やCloudFrontなどのリソースをシンプルに管理でき、インフラ設定が直感的でわかりやすく変更がスムーズになります。最初の学習曲線を低く保ち、チーム全体がスムーズに運用を開始できるというメリットがあります。
一方で機能単位(web_server/、data_pipeline/ など)にすると複数AWSサービスをまとめて扱えるため再利用性が上がりますが、設計の難易度も上がります。
個人プロジェクトや学習目的ならサービス単位から入るのが無難だと思います。機能単位はコードが育ってきたときにリファクタリングで移行するのが現実的かもしれません。
最初から意識しておきたいこと
基礎を押さえた上で、最初から意識しておくと後で楽になることをまとめておきます。
descriptionは必ず書く:variable にも output にも。省略したくなるけど書いておくと後の自分が助かる- type 制約を付ける:
type = stringのように書いておくと、間違った値を渡したときに早めにエラーになる - タグは
localsで一元管理:前述の通り、直接書くと後で地獄を見る terraform.tfvarsを.gitignoreに入れる:環境依存の値や認証情報が入ることがあるので- モジュールを追加したら必要に応じて
terraform initを再実行:新しい provider / backend / module の取得が絡むと必要になることが多い
まとめ
今回はファイル構成・変数・モジュールの基本的な考え方を整理しました。コードそのものはシンプルですが、この設計方針を最初に決めておかないと後でリファクタリングがかなり大変になります。
Terraformのステート管理はローカルに置きっぱなしだとチーム開発で詰むので、早めに対処しておきたいポイントです。次回はS3リモートステートとDynamoDBを使ったロック管理について掘り下げていきます。
モジュールのソースにTerraform Registryのパブリックモジュールを指定する方法もあるんですが、そのあたりはまた別の機会に。まずはローカルモジュールを使いこなせるようになってからかなと。

