Terraformのコーディング規約は、HashiCorpが公開しているStyle Guideを土台に決めるのが基本です。整形はterraform fmtで自動化できますが、公式のfmtコマンドのリファレンス自身が「スタイル規約の一部(subset)を適用する」と書いているとおり、fmtだけでは公式スタイルガイドのすべての規約を検査・修正できません。本記事はTerraform 1.17.0とTFLint 0.65.0で実際に動かし、fmtが直すもの・直さないもの・TFLintで拾えるものを切り分けたうえで、命名・コメント・ファイル構成の規約と、CIで規約を強制する設定までをまとめます(2026年10月26日時点)。
まとめ:fmt・TFLint・レビューによるTerraform規約の分担
- 整形はfmtに任せる:インデント2スペースと等号の揃えは
terraform fmtが直す。CIではterraform fmt -check -recursive -diffを使い、未整形なら終了コード3で落とす。 - fmtが直さない規約がある:
//コメント、トップレベルのブロック間の空行、countの位置、リソース名の大文字、"${var.x}"だけの補間は、fmtを通してもそのまま残る。 - 残りはTFLintで拾う:設定ファイルなしのTFLintは推奨ルールだけを使う。命名(snake_case)・
#コメント・説明の有無・ファイル配置まで検査するにはpreset = "all"にする。 - 命名は名詞+アンダースコア:
web_apiのようにsnake_caseで書き、リソースタイプ名を識別子に重ねない。型名の重複はTFLintでも検出できないため、レビューで見る。 - コメントは
#:コメントアウトも#を行頭に付ける。//と/* */は旧HCLとの互換のために動くだけで、慣用的な書き方ではない。 - 変数と出力には型と説明:すべての変数に
typeとdescriptionを付ける。Terraform 1.15以降は出力にもtypeを書ける。 - 強制はpre-commitとCIの2段:ローカルはpre-commit-terraformのフック、最終防衛線はプルリクエストのCI。CIのTerraformは版を固定し、手元とfmtの結果をそろえる。
公式スタイルガイドと規約を決める範囲
HashiCorpのStyle Guideは、前半がコードのスタイル(整形・命名・ファイル構成・変数・コメント)、後半が運用のスタイル(版の固定・リポジトリ構成・ブランチ戦略・シークレット管理・テスト)という2部構成です。冒頭では、コミット前にterraform fmtとterraform validateを実行すること、TFLintのようなリンターで組織独自のルールを強制することを、規約の最初の2項目として挙げています。
チームの規約を作るときは、公式ガイドの項目を「機械で強制できるもの」と「人が判断するもの」に分けるところから始めます。機械で強制する項目は、採用したルールと設定ファイルの場所を文書に書くだけにして、判断基準の説明は人が判断する項目(命名の語彙、ファイルの分け方、変数にする範囲、コメントの粒度)に絞ります。こうすると、レビューのコメントが整形の指摘で埋まらなくなります。
Google Cloudもスタイルと構造に関するベストプラクティスを公開しています。HashiCorpのガイドと矛盾はしませんが、命名についてはGoogle Cloudの方が細かく決めています(後述の命名規則の章で比較します)。
terraform fmtの使い方:-check・-recursive・-diffと終了コード
terraform fmtは、設定ファイルを公式の正規フォーマット(canonical format)に書き換えるコマンドです。公式リファレンスによると、出力の形式を変えるオプションはありません。好みが分かれても全コードベースで同じ形にそろえることが目的なので、意図的にカスタマイズできない設計です。
基本の構文と整形対象のファイル
# カレントディレクトリの .tf などを整形して上書き
terraform fmt
# ファイルやディレクトリを指定
terraform fmt main.tf
terraform fmt modules/network
# 標準入力を整形して標準出力へ(ファイルは書き換えない)
cat main.tf | terraform fmt -
Terraform 1.17.0のterraform fmt -helpには、整形対象は.tf・.tfvars・.tftest.hclで、JSON形式の.tf.jsonなどは変更しないと書かれています。実際に各種ファイルを置いて-checkを実行すると、ヘルプに載っていない.tfquery.hcl(terraform queryで使うクエリファイル)も整形対象として列挙され、.tf.jsonは対象外でした。
-recursiveによるサブディレクトリの一括整形
引数なしのterraform fmtはカレントディレクトリだけを処理し、modules/配下のような子ディレクトリには入りません。リポジトリ全体をそろえるには-recursiveを付けます。
terraform fmt -recursive
initでダウンロードされた外部モジュールが入る.terraform/ディレクトリは、-recursiveでも整形されませんでした。.terraform/modules/に未整形のファイルを置いて実行しても、一覧に出ず内容も書き換わりません。自分のコードを整形するときに、取得したモジュールの差分まで出る心配はありません。
-checkと-diffによるCIでの整形差分の検出
CIでは、ファイルを書き換えずに整形が必要かどうかだけを判定します。
# 書き換えずに判定。未整形のファイル名と差分を表示する
terraform fmt -check -recursive -diff
echo $?
公式リファレンスは-checkの終了コードを「非ゼロ」としか書いていないため、1.17.0で値を確かめました。
| 状態 | 終了コード | 出力 |
|---|---|---|
| すべて整形済み | 0 | なし |
| -check で未整形あり | 3 | 未整形のファイル名 |
| 構文エラー | 2 | Unclosed configuration block など |
| -write=false -diff(-checkなし) | 0 | ファイル名と差分 |
CIのジョブは非ゼロで落ちるので、通常は値を区別しなくて構いません。「未整形(3)」と「壊れた設定(2)」を分けて通知したいときだけ、この値で分岐します。-write=falseだけでは終了コードが0のままなので、CIの判定には必ず-checkを使います。
VS Codeの保存時自動整形設定
手元では、保存時にfmtを実行すると、エディターで編集したファイルの整形漏れを減らせます。HashiCorpのVS Code拡張(HashiCorp Terraform)のREADMEは、settings.jsonに次の設定を書くよう案内しています。
"[terraform]": {
"editor.defaultFormatter": "hashicorp.terraform",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file"
},
"[terraform-vars]": {
"editor.defaultFormatter": "hashicorp.terraform",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file"
}
editor.formatOnSaveModeはfileだけが動作対象です。READMEは、terraform fmtがファイル全体の整形を前提に作られているため、変更行だけを整形するmodificationsは使えないと説明しています。.tfvarsは言語IDがterraform-varsなので、こちらにも同じ設定を入れます。
terraform fmtが直さない規約
fmtの挙動を確かめるため、公式ガイドに反する書き方をわざと詰め込んだファイルを用意し、Terraform 1.17.0のterraform fmtを通しました。
// legacy comment
/* block
comment */
variable "env" {
type = "string"
default = "dev"
}
resource "aws_instance" "webAPI" {
ami = "ami-123"
instance_type="t3.micro"
tags = {
Name = "${var.env}"
}
count = 2
}
結果を公式ガイドの項目ごとに並べると、fmtが直したのは表の上4行だけでした(ラベルのクォートは、命名規則の章の悪い例で確かめています)。
| 公式ガイドの規約 | fmt 1.17.0 | 拾う手段 |
|---|---|---|
| ネストごとに2スペース | 直す | fmt |
| 連続する引数の = を揃える | 直す | fmt |
type = "string" の旧記法 |
string に直す | fmt |
| 型と名前をダブルクォートで囲む | 補う | fmt |
| コメントは # | 直さない | TFLint(all) |
"${var.env}" だけの補間 |
直さない | TFLint(推奨) |
| 名前は snake_case | 直さない | TFLint(all) |
| トップレベルの間に空行 | 直さない | レビュー |
| count を先頭に置く | 直さない | レビュー |
| 名前に型名を含めない | 直さない | レビュー |
variableとresourceの間に空行が無くても、countが末尾にあっても、fmtはそのまま通します。公式リファレンスは、fmtに新しい整形規則が入るのは主に既存のスタイル規約を取り込むときで、fmtがまだ自動適用しない規約にも従うよう勧めています。「fmtが通ったから規約どおり」とは言えないので、残りの項目をTFLintとレビューで分担します。
また公式リファレンスは、Terraformの版によって正規フォーマットがわずかに変わる場合があるとしています。手元とCIでTerraformの版が違うと、片方では整形済みのファイルがもう片方で差分になることがあります。CIで使うTerraformの版は固定してください。
terraform validateとの違いと実行順
terraform fmtは見た目だけを扱い、terraform validateは構文と内部の整合(参照先の有無、型の一致)を検証します。公式ガイドによると、validateはプロバイダー固有の値が正しいかまでは確かめず、既存のstateも評価しません。クラウドのAPIを呼ばないので、保存時やpre-commit、CIで何度実行しても副作用はありません。
注意点は、validateの前にinitが要ることです。プロバイダーを使う設定でinitをせずに実行すると、1.17.0ではError: Missing required providerで止まりました。CIではbackendに接続しない-backend=falseでinitしてからvalidateします。
terraform fmt -check -recursive -diff
terraform init -backend=false
terraform validate
順番はfmt→validateにします。fmtはinit不要で一瞬で終わるため、整形の問題を先に返したほうが開発者の待ち時間が短くなります。
TFLintによる命名・コメント・説明漏れの検出
公式ガイドは「Terraformには組み込みのリンターが無い」とし、TFLintのようなサードパーティのツールで規約を強制するよう勧めています。TFLintには、Terraform言語の規約を検査するルールセット(tflint-ruleset-terraform)が同梱されています。
注意すべきは既定の範囲です。設定ファイル(.tflint.hcl)を置かずにTFLint 0.65.0(同梱ルールセット0.15.0)を実行すると、検査は推奨ルールだけに絞られます。前章のような違反を含むファイルで比べると、検出件数は次のとおりでした。
| ルール | 推奨(既定) | preset = "all" |
|---|---|---|
| terraform_deprecated_interpolation | 検出 | 検出 |
| terraform_typed_variables | 検出 | 検出 |
| terraform_required_version | 検出 | 検出 |
| terraform_required_providers | 検出 | 検出 |
| terraform_unused_declarations | 検出 | 検出 |
| terraform_comment_syntax | 対象外 | 検出 |
| terraform_naming_convention | 対象外 | 検出 |
| terraform_documented_variables | 対象外 | 検出 |
| terraform_documented_outputs | 対象外 | 検出 |
| terraform_standard_module_structure | 対象外 | 検出 |
同じファイルで、推奨のみは5件、preset = "all"は13件でした。命名・#コメント・説明の有無という、スタイルガイドの中心にあたる規約は推奨に入っていません。規約を機械で守らせるなら、次の設定を置きます。
# .tflint.hcl
plugin "terraform" {
enabled = true
preset = "all"
}
実測で分かった限界もあります。terraform_comment_syntaxが検出するのは//だけで、/* */は検出しませんでした。resource "terraform_data" "web_terraform_data"のように型名を名前に重ねても、命名ルールはsnake_caseなら通します。またterraform_standard_module_structureは、変数や出力が無いモジュールにも空のvariables.tfとoutputs.tfを置くよう警告します。
既存のリポジトリにいきなりpreset = "all"を入れると警告が大量に出て、CIが落ち続けるだけになりがちです。最初は推奨のままCIに組み込み、命名と説明の違反を片付けてからallへ上げる段階導入が現実的です。インストール方法やプラグインの追加はTFLintとは?Terraform静的解析の導入・設定・CI組み込みで扱っています。
命名規則:snake_case・名詞の使用と型名の重複回避
公式ガイドの命名規則は、説明的な名詞を使う、単語はアンダースコアで区切る、識別子にリソースタイプを含めない(リソースアドレスに型が既に入っているため)、型と名前をダブルクォートで囲む、の4点です。変数・出力・ローカル値の名前にも同じ規則を当てます。
# 悪い例:クォートなし・キャメルケース・ハイフン・型名の重複
resource aws_instance webAPI-aws-instance {}
# 良い例
resource "aws_instance" "web_api" {}
4点のうちクォートだけはfmtが補います。上の悪い例をfmtに通すとresource "aws_instance" "webAPI-aws-instance" {}になり、キャメルケースとハイフン、型名の重複は残りました。
ハイフンを使ってよいのは、クラウド側に渡す値(name引数やタグ)とモジュールのリポジトリ名だけです。Google Cloudのガイドも、識別子はweb_server、name引数は"web-server"という使い分けを推奨例に挙げています。
HashiCorpのガイドが決めていない部分は、Google Cloudのガイドが補えます。規約に取り込む候補は次の4つです。
- モジュール内にその型のリソースが1つだけなら、名前を
mainにする(google_compute_global_address.main)。 - リソース名は単数形にする。
- サイズや容量を表す変数・出力には単位を付ける(
ram_size_gb)。 - ブール値の変数は肯定形にする(
enable_external_access)。
どちらのガイドも、略語の大文字・小文字やリソース名の語彙までは決めていません。たとえばdbとdatabaseのどちらを使うかは、チームの規約文書で1つに決めます。他言語の命名規則との比較はプログラミングの命名規則まとめにまとめています。
コメントの書き方とコメントアウト
公式ガイドは、単一行・複数行とも#を使うよう定めています。//と/* */は慣用的ではないものの、以前のHCLとの後方互換のためにサポートされている、という位置付けです。
# 各トンネルは、対応するゲートウェイから出入りする通信を
# 暗号化・復号する。
resource "google_compute_vpn_tunnel" "tunnel1" {
# 一時的に無効化(コメントアウト)するときも行頭に #
# shared_secret = var.old_secret
shared_secret = var.vpn_shared_secret
}
コメントアウトも同じく、行頭に#を付けます。/* */で複数行をまとめて囲むことも文法上はできます。ただ、前章のとおりTFLintのterraform_comment_syntaxは/* */を検出しないため、消し忘れたブロックコメントが規約チェックをすり抜けます。動作確認が済んだら、コメントアウトした行は削除してGitの履歴に任せます。
Google Cloudのガイドは、countの例のなかで//のコメントを使っています。二つのガイドを併用する場合も、コメント記号はHashiCorpの#に統一し、TFLintのallで//を弾くのが確実です。
コメントの中身について、公式ガイドは「コードを分かりやすく書き、必要なときだけコメントで複雑さを補う」としています。for_eachやメタ引数のように効果がすぐ読み取れない箇所に、なぜそうしたかを1行添えます。
ファイル構成とディレクトリの規約
公式ガイドが推奨するファイル名は次のとおりです。変数と出力は、ファイル内でアルファベット順に並べます。
| ファイル | 中身 |
|---|---|
| terraform.tf | required_version・required_providers |
| backend.tf | backend の設定 |
| providers.tf | provider ブロック |
| main.tf | resource と data |
| variables.tf | variable(アルファベット順) |
| outputs.tf | output(アルファベット順) |
| locals.tf | 複数ファイルで使う locals |
| override.tf | オーバーライド(最小限) |
main.tfが大きくなったら、network.tf・storage.tf・compute.tfのように論理グループで分けます。公式ガイドの基準は「どこに何が定義されているかが保守担当者にすぐ分かること」です。ローカルモジュールは./modules/<module_name>に置き、Registryに公開するモジュールのリポジトリ名はterraform-<PROVIDER>-<NAME>の3部構成にします。
terraform.tfによるTerraformとプロバイダーのバージョン制約
公式ガイドは、プロバイダーとモジュールの版を固定し、Terraform本体の最低版をrequired_versionで指定するよう勧めています。
terraform {
required_version = ">= 1.15"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
TFLintの推奨ルールでも、required_versionとrequired_providersの版指定が無いと警告になります。制約の書き方やロックファイルの扱いはterraform providerのバージョン制約とalias設計を参照してください。
.gitignoreでコミットしないファイル
公式ガイドは、コミットしないものとしてterraform.tfstateとそのバックアップ、.terraform.tfstate.lock.info、.terraform/ディレクトリ、-outで保存したplanファイル、機密値を含む.tfvarsを挙げています。逆に、依存ロックファイル.terraform.lock.hclとREADME.mdは必ずコミットします。ひな形はGitHubのTerraform.gitignoreが使えます。
変数・出力・localsの書き方
公式ガイドは、すべての変数にtypeとdescriptionを付け、任意の変数には妥当なdefaultを置くよう求めています。引数の並びはtype→description→default→sensitive→validationの順です。
variable "db_password" {
type = string
description = "APIデータベースの管理者パスワード"
sensitive = true
}
output "web_public_ip" {
type = string
description = "Webインスタンスのパブリック IP"
value = aws_instance.web.public_ip
}
出力のtypeは、Terraformの変更履歴では1.15で追加された機能です。公式ガイドも1.15版から、出力の引数順をtype→description→value→sensitiveに改めています。1.17.0で試すと、type = list(string)の出力にマップを渡した設定はterraform validateの時点でInvalid output valueになりました。一方、type = stringの出力に数値を渡したケースはエラーにならず、文字列へ変換されます。型の宣言で防げるのは変換できない不一致だけです。1.14以前のTerraformも使うチームでは、出力のtypeは書かず、required_versionを1.15以上に上げてから規約に入れます。
sensitive = trueはplanやapplyの表示を隠すだけで、stateには平文で保存されると公式ガイドは注意しています。stateを置くbackendの暗号化とアクセス制御は別に必要です。
ローカル値は、複数ファイルから参照するならlocals.tfに、1ファイルでしか使わないならそのファイルの先頭に置きます。公式ガイドは変数とローカル値の使いすぎを避けるよう繰り返し書いています。デプロイ先ごとに値が変わらない設定まで変数にすると、値を追う手間が増えるだけです。変数とlocalsの使い分けはterraform variableとlocals・tfvarsの使い分けで詳しく扱っています。
リソースの並び順とメタ引数の位置
リソースを書く順番は、Terraformが作成する順番に影響しません。作成順は依存関係から決まります。そのため公式ガイドは、読みやすさを基準に「参照されるものを先に、参照するものを後に」並べるよう勧めています。データソースは、それを参照するリソースの直前に置きます。
リソースブロック内の引数は、次の順にそろえます。
countまたはfor_each- ブロックでない引数
- ネストしたブロック
lifecycle(必要な場合)depends_on(必要な場合)
resource "aws_instance" "web" {
count = 2
ami = data.aws_ami.web.id
instance_type = "t3.micro"
network_interface {
# ...
}
lifecycle {
create_before_destroy = true
}
}
この並びはfmtでは直らないため、レビューで見ます。providerブロックについては、エイリアスの無い既定の設定を必ず用意して先に書き、エイリアス付きのブロックではaliasを最初の引数にします。
countとfor_eachは控えめに使うのが公式の方針です。ほぼ同じリソースを並べるならcount、値が少しずつ違うならfor_eachを選び、効果が読み取りにくい箇所にはコメントを添えます。
pre-commitとCIによる規約チェックの自動化
公式ガイドは、Gitのpre-commitフックでコミットのたびにterraform fmtを実行する方法を挙げています。pre-commit-terraform(2026年10月20日公開のv1.109.3)ではfmt・validate・TFLintのフックを設定できます。使用するツールとpre-commitを用意し、次の設定を保存したうえで、対象リポジトリでpre-commit installを実行してフックを有効にします。
# .pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.109.3
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_tflint
ローカルのフックは、入れていない人やスキップした人の分が抜けます。マージ前の最終チェックはCIに置きます。以下はリポジトリ直下にルートモジュールと.tflint.hclがある場合の例です。独立したルートモジュールを複数置く構成では、それぞれのディレクトリでinitとvalidateを実行します。
name: terraform-lint
on: pull_request
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: hashicorp/setup-terraform@v4
with:
terraform_version: 1.17.0
- run: terraform fmt -check -recursive -diff
- run: terraform init -backend=false
- run: terraform validate
- uses: terraform-linters/setup-tflint@v6
with:
tflint_version: v0.65.0
- run: tflint --init
- run: tflint --recursive --config "$(pwd)/.tflint.hcl"
terraform_versionを固定しているのは、前述のとおりTerraformの版でfmtの正規フォーマットが変わりうるためです。手元の版もこれに合わせます(版の確認と更新はTerraformの最新バージョンとアップグレード手順)。TFLintを--recursiveで実行すると、設定ファイルのパスがモジュールのディレクトリ基準で解決されるため、TFLintの公式ドキュメントにあるとおり絶対パスで--configを渡します。TFLintの終了コードは問題検出時に2です。
planやapplyまで含めたパイプラインの設計はTerraformのCI導入ガイド、AWSへの認証とapplyの手順はTerraformをGitHub Actionsで自動化しAWSへ反映する手順で解説しています。チームへの導入を外部に任せたい場合は、一創のDevOps・CI/CD導入支援でも、IaCの規約づくりからCIへの組み込みまでを支援しています。
よくある質問
terraform fmtとterraform validateの違いは?
fmtはインデントや等号の揃えなど見た目だけを整えるコマンドで、initは不要です。validateは構文と参照・型の整合を検証し、事前にinitが要ります。CIではfmt→init→validateの順に実行します。
Terraformでコメントアウトするにはどう書く?
無効にしたい行の先頭に#を付けます。//や/* */も動きますが、公式ガイドでは慣用的でない書き方とされています。/* */はTFLintでも検出されないため、使わないほうが安全です。
terraform fmt -recursiveは.terraformディレクトリも書き換える?
書き換えません。Terraform 1.17.0で.terraform/modules/に未整形のファイルを置いて-recursiveを実行しても、対象に含まれませんでした。整形対象は指定ディレクトリ以下の対象ファイルで、Gitの追跡状況やコードの作成者では区別されません。
terraform fmtの整形ルールは設定で変えられる?
変えられません。公式リファレンスは、fmtは意図的に主張の強い(opinionated)ツールで、カスタマイズのオプションを持たないと説明しています。インデント幅などを変えたい場合はサードパーティのフォーマッターを使うことになり、その場合はTerraformが自動生成するファイルにも同じツールをかけるよう公式は勧めています。
リソース名はハイフンとアンダースコアのどちらを使う?
アンダースコアです(例:web_api)。リソース名・変数名・出力名などTerraformの識別子はsnake_caseにします。ハイフンを使うのは、name引数やタグなどクラウド側に渡す値と、terraform-aws-vpcのようなモジュールのリポジトリ名だけです。