はじめに
【対象読者】 本ドキュメントはTerraformの基本構文を理解しており、複数人でIaCを管理する環境で保守性向上を目指している方を対象としています。
現在、私はとあるプロジェクトの技術支援を行っています。そのプロジェクトでは複数人でTerraformやK8sマニフェストを作成しており、ルールもなく各々好きにコードを書いている状況です。そのためPMから「IaCを綺麗にしたい...」という要望を受けました。
気持ちは良く分かります。私も早速「IaCを綺麗にしよう!」と思いました。が、具体的に何をすれば綺麗なのか?と疑問に感じました。そこで「綺麗なIaCとは何か?」を自分なりに整理し、どうすれば綺麗になるか考えた内容を共有します。
綺麗なIaCとは?
「綺麗なIaC」とは「整理整頓されていて誰が見てもわかりやすいIaC」、つまり可読性の高いIaCかと真っ先に考えました。しかし、IaCは単にコードを読むだけでなく、実際にインフラを構築・変更・削除するためのものです。したがって、保守性の高いIaCが「綺麗なIaC」と言えるのではと考えました。さらに保守性の高いIaCには以下の3つの観点が重要だと考えました。
可読性
新規参画者など、初見の人への理解のしやすさ。この観点が考慮されていないと、コードを読むのに時間がかかり、ミスや誤解が生じやすくなります。
- 命名規則が一貫している
- コードの書き方が一貫している
- ディレクトリ構造が整理されている
- README、コメントが適切に書かれている
安全性
発生しうるリスク(誤操作・漏えい・脆弱性) への対処。この観点が考慮されていないと、システムの安定性やセキュリティが損なわれる可能性もあります。
- 誤操作
- 変更が容易に行える
- 変更による影響範囲が最小限に抑えられている
- 漏洩
- 機微な情報が含まれていない
- 脆弱性
- セキュリティベストプラクティスが守られている
- クラウドのベストプラクティスが守られている
再利用性
コードの使い回しやすさ。この観点が考慮されていないと、同じようなコードが複数箇所に散在します。
- 変数が適切に抽象化されている
- 関連したリソース群が一つにまとめられている
⚠️ 注意:優先順位を間違えないこと
再利用性を意識しすぎると可読性や安全性が低下する場合があります。初心者や小規模プロジェクトでは、再利用性より可読性と安全性を優先し、プロジェクトが成熟してから徐々に取り入れましょう。
再利用性が重要になる場面
- 複数プロジェクトで同じリソースセットを使い回す場合
- 同一プロジェクト内で全く同じリソースセットを複数作る場合(マルチテナント等)
❌ 個人的に避けた方がいい再利用
開発・本番環境間でのコード共有は避けること。多くの場合、環境によって微妙に構成が異なるため環境差異により複雑化し、誤適用のリスクが高まります。
開発プロセスも重要
また、コード自体だけでなく、開発プロセスも重要です。IaCは作って終わりではなく、継続的に変更・改善していくものだからです。そのため、綺麗なIaCを維持するためには、適切な開発プロセスが必要です。
- Gitで管理されている
- レビュープロセスが確立されている
- 環境への適用方法が確立されている
綺麗なIaCを実現するためにすること
現在のプロジェクトで「綺麗なIaC」を実現するためには以下順番で取り組むのが良さそうです。Terraformを例に説明します。
- ディレクトリ・ファイル構成を決める
- コーディング規約を決める
- プロセスを決める
1. ディレクトリ・ファイル構成を決める
ぱっと見で綺麗でないIaCの多くは、ディレクトリ・ファイル構成が整理されていないことが多いです。まずは、適切なディレクトリ・ファイル構成を設計し、コードを整理します。これだけでかなり綺麗になります。
ディレクトリ構成
まずディレクトリですが、階層や関連性を考慮して整理します。例えば以下のポイントを考慮します。
- 階層を合わせる
- 関連するリソースをまとめる
- 一度に作成・削除・変更する関連リソースでまとめる
- 影響範囲が大きくなりすぎないようにする
- あまり細かく分けすぎない(1リソースしかないディレクトリなどは避ける)
# 悪い例 階層がバラバラで、関連性が不明確な構成
.
├── network/
│ ├── dev/
│ └── stg/
├── ec2/
├── alb/
│ └── production/
└── security/
# 良い例 階層を揃え、関連するリソースを適切にまとめた構成
.
├── dev/
│ ├── network/
│ ├── compute/
├── stg/
│ ├── network/
│ ├── compute/
└── prod/
├── network/
└── compute/
上記の良い例では環境ごとにディレクトリを分けています。また、環境の分け方は関連リソースを軸に分ける方法もあります。
.
├── network/
│ ├── dev/
│ ├── stg/
│ └── prod/
└── compute/
├── dev/
├── stg/
└── prod/
環境軸、関連リソース軸ではそれぞれ以下の特徴があります。プロジェクトに応じてどちらが適切か判断します。
- 環境軸
- 各環境の全体像が把握しやすい
- 新しく環境増やす場合、ディレクトリを丸ごとコピーすれば良いので簡単
- 関連リソース軸
- 関連リソースごとの環境差異が把握しやすい
- 環境にまたがる横ぐしの変更がしやすい
ファイル構成
ファイルもディレクトリと同じく書く内容や関連性を考慮して整理します。
- 役割(Terraformのブロック)ごとに分割する
- 関連するリソース群をまとめる
# 悪い例① 役割や関連性が不明確なファイル構成
compute/
└── main.tf (すべてのリソースが1ファイルに混在)
├── terraformブロック
├── providerブロック
├── variableブロック
├── outputブロック
├── ALB
├── ALB リスナー
├── ALB ターゲットグループ
├── EC2インスタンス
├── セキュリティグループ (ALB用)
├── セキュリティグループ (EC2用)
└── IAMロール
# 悪い例② 過剰に分割されたファイル構成
compute/
├── alb.tf
├── alb-listener-http.tf
├── alb-listener-https.tf
├── alb-target-group-app.tf
├── alb-target-group-api.tf
├── ec2-instance.tf
├── ec2-security-group.tf
└── ...
# 良い例 役割ごとに適切に分割されたファイル構成
compute/
├── alb.tf (ALB関連リソースをまとめる)
│ ├── ALB
│ ├── ALB リスナー群
│ ├── ALB ターゲットグループ群
│ └── ALB 用セキュリティグループ
├── ec2.tf (EC2関連リソースをまとめる)
│ ├── EC2 インスタンス
│ ├── EC2 用セキュリティグループ
│ ├── EC2 用IAMロール
│ └── ALB ターゲットグループへの登録
├── versions.tf (Terraformバージョンやプロバイダ定義)
├── variables.tf (変数定義)
└── outputs.tf (出力値定義)
上記の良い例では、主となるリソースごとにファイルを分割し、主リソースに付随するサブリソースは同じファイルにまとめています。また、複数のEC2インスタンスを作成する場合は、ec2-frontend.tf、ec2-backend.tfのようにファイルを分割するのが良いでしょう。
具体的な設計例
小規模なプロジェクトにおける設計例です。プロジェクト規模が大きくなると階層も深くなると思います。
- ディレクトリ
- 運用時に環境間の差異を把握しやすくするため、以下の方針でディレクトリを分割する
- 第一階層: デプロイ単位ごとにディレクトリを分割する
- 第二階層: 環境ごとにディレクトリを分割する
- デプロイ単位とは、一度にapply/destroyする関連リソースのまとまりのこと(例: EC2を入れ替えたらALBのターゲットも入れ替えるため一緒にまとめる)
- Terraform初心者が多く、可読性と安全性を高めるため、すべてのモジュールはルートモジュールとし、子モジュール化は行わない
- メンバーの成熟度およびプロジェクト規模に応じて、徐々に子モジュール化を検討する
- 運用時に環境間の差異を把握しやすくするため、以下の方針でディレクトリを分割する
- ファイル
- 判別しやすいように
output、variable等のresource以外のブロックはブロックごとに専用ファイルを作成する - さらに判別しやすくするため、
resource以外のブロックファイルはファイル名の先頭にアンダースコア(_)を付ける -
resourceブロックはmain.tfにまとめるか、ある程度の塊でファイルを分割する-
main.tfが長くなる場合(目安100行以上)はファイルを分割する - 分ける場合は以下の方針で分ける
- 主要リソースでファイルを分ける。主要リソースに付随するリソースは同じファイルにまとめる
- 例:
ec2.tfにEC2関連のinstanceやIAM、SGを記述し、alb.tfにALB関連のリスナーやSGを記述する
- 例:
- 同じリソースタイプで複数のリソースを作成する場合は、ファイルを分割しファイル名を一意にする
- 例: 複数のEC2インスタンスを作成する場合、
ec2-frontend.tf、ec2-backend.tfのように分割する
- 主要リソースでファイルを分ける。主要リソースに付随するリソースは同じファイルにまとめる
-
- 判別しやすいように
# 具体例
terraform/
├── network/ # 第一階層: デプロイ単位 (ネットワーク関連)
│ ├── dev/ # 第二階層: 環境
│ │ ├── main.tf # VPC、サブネット、ルートテーブルなど (100行未満のため1ファイル)
│ │ ├── _variables.tf # 変数定義 (先頭に _ をつける)
│ │ ├── _outputs.tf # 出力値定義 (先頭に _ をつける)
│ │ └── _versions.tf # プロバイダとバージョン定義
│ ├── stg/ # devと同じ構成
│ └── prod/ # devと同じ構成
└── compute/ # 第一階層: デプロイ単位 (サービス関連)
├── dev/ # 第二階層: 環境
│ ├── alb.tf # ALB、リスナー、ターゲットグループ、SG
│ ├── ec2-frontend.tf # Frontend EC2、IAM、SG
│ ├── ec2-backend.tf # Backend EC2、IAM、SG
│ ├── _variables.tf
│ ├── _outputs.tf
│ └── _versions.tf
├── stg/ # devと同じ構成
└── prod/ # devと同じ構成
2. コーディング規約を決める
命名規則やコードスタイルをプロジェクトで定め、全員が従うようにします。これにより、コードの一貫性が保たれ、可読性が向上します。以下のようなポイントを考慮すると良いでしょう。
- 命名規則
- 単語の区切り(例: snake_case、camelCaseなど)
- 名前の付け方(リソース、変数、出力など)
- 実装方針
- 変数の使い方
- ループの仕方
- 値の連携方法
- 変数と出力
- どういう時に変数を使うか、出力を使うか
- 書式
- フォーマット
- ブロックを書く順番
Terraformの場合、公式のスタイルガイドが参考になります。Style Guide - Configuration Language | Terraform | HashiCorp Developer
具体的な設計例
- Terraformリソース名
- Terraformリソース名は小文字英数字とアンダースコア(_)のみを使う
- 単語の区切りはアンダースコア(_)を使う
- Terraformリソース名はリソースタイプを繰り返さない(NG:resource "aws_vpc" "vpc" OK:resource "aws_vpc" "this")
- モジュール内に複数同じタイプのリソースを作成する場合、Terraformリソース名に
this等は使わず、用途が判別しやすい名前にする(例:computeモジュール内で2つのSGを作成する場合、"aws_security_group" "frontend"、"aws_security_group" "backend"のように命名する)
- 実装方針
- 入力変数は
localsではなくvariableを使う - 環境差異の発生しない共通パラメーターはハードコードする(variableの数を抑えて分かりやすさを向上。後から可変にしたくなったらvariableを定義する)
- ループ処理はなるべく
for_eachを使う(countはインデックス管理が煩雑になるため極力使わない) - すべてのリソースに共通して付与するタグはAWSプロバイダーの
default_tagsを使う - モジュール間で値を参照する場合はtfstateのリモート参照を使う
- 他モジュールで参照する値を
outputで出力する
- 入力変数は
- 変数名
- 変数名は用途が判別しやすい名前にする(NG:id OK:instance_id)
- リストやマップなど複数の要素を持つ変数名は複数形にする(NG:id OK:ids)
- descriptionを書く
- typeを指定する
- 書式
-
terraform fmtを実行してフォーマットを整える - リソース内の表記は以下の順番で書く
-
count、for_each - 非ブロックのパラメータ
- ブロックのパラメータ
-
tagsブロック -
lifecycleブロック -
depends_onブロック
-
- 上記ブロックごとに行間を空ける
- 以下例
resource "aws_example" "this" { count = var.example_count name = "example-name" type = "example-type" settings { setting_key = "setting_value" } tags = { Environment = "production" } lifecycle { prevent_destroy = true } depends_on = [aws_other_resource.this] } -
- セキュリティ
- パスワードやアクセスキーなどの機微な情報はコードに含めない
3. プロセスを決める
最後に綺麗なIaCを維持するためのプロセスを確立します。せっかく綺麗にしても、開発プロセスが確立されていないと、すぐに汚れてしまいます。
プロセスは以下の3つの観点で整理すると良いでしょう。
- レビュープロセス
- コードの品質を保つ仕組み
- 環境への適用プロセス
レビュープロセス
コードレビューは、可読性や安全性を保つために非常に重要です。以下のようなプロセスを確立します。
-
ブランチ戦略を決める
- 例:
mainブランチを保護し、Pull Request経由でのみマージ可能にする - 具体的にはGitHub Flow、GitLab Flowなどの戦略がある
- シンプルで運用負荷の小さいGitHub Flowがおすすめ
- 例:
-
レビュー観点を明確にする
- 多くの観点がありますが、最低限以下を確認するようにします
- 影響範囲は適切か
- コーディング規約に準拠しているか
- 修正内容が要件に沿っているか
- 多くの観点がありますが、最低限以下を確認するようにします
-
レビュアーを決める
- 最低1名以上のレビューを必須にする
- インフラの変更は影響が大きいため、経験者のレビューが望ましい
コードの品質を保つ仕組み
コーディング規約の確認など、レビュアーによる確認だけだと負担が大きくミスも発生する可能性があります。そのため、仕組みとしてチェックできると良いです。具体的には以下のようなツールを活用します。
-
terraform fmt: コードフォーマットを自動修正 -
terraform validate: 構文チェック -
tflint: より詳細な静的解析(命名規則、非推奨リソース、セキュリティリスクなどをチェック) -
tfsec/checkov/trivy: セキュリティスキャン(脆弱性の検出)
これらのツールを手動で実行しても良いですが、CI/CDパイプライン(GitHub Actions、GitLab CI、など)に組み込み、Pull Request作成時に自動実行できると便利です。自動化することで、レビュー担当者の負担を軽減し、ミスを減らすことができます。
環境への適用プロセス
IaCの変更を環境に適用する際のプロセスを確立します。コードだけ修正して環境への適用を忘れてしまったり、誤った変更が本番環境に適用されないようにします。
-
事前確認
-
terraform planで変更内容を確認 - 影響範囲を把握し、必要に応じて関係者に共有
-
-
適用とマージ
-
terraform applyを実行し、環境に適用- applyで失敗することもあるのでmainマージ前にapply
- 問題なく適用できたら、mainブランチにマージ
-
初めは手動でプロセスを進めても良いですが、慣れてきたらterraformの実行をCI/CDパイプラインに組み込んでも良いでしょう。自動化することで作業の証跡を残すことができます。
具体的な設計例
最低限レビューするプロセスが確立されていれば、静的解析や適用の自動化は後から徐々に導入していっても良いでしょう。ただし、自動化は早ければ早いほど恩恵が大きいです。
- インフラのコードは環境ごとにディレクトリで分けている。ブランチ戦略はシンプルで運用負荷の小さいGitHub Flowを採用する
- コードの変更は最新のmainブランチから派生したfeatureブランチで行う
- Pull Request作成時に以下の静的解析をGitHub Actionsで自動実行する
terraform fmt -checkterraform validatetflint-
trivy config(HIGH,CRITICAL)
- 静的解析に問題がなければ、
terraform planをGitHub Actionsで実行し、結果をPull Requestにコメントとして表示する - レビュアーは以下の観点でレビューを行う
- 修正内容が要件に沿っているか
- コーディング規約に準拠しているか
- 新規でディレクトリ・ファイルを作成している場合、設計に沿った構成か
- レビュアーは問題がなければPull Requestを承認する
- Pull Request承認後、
terraform applyをGitHub Actionsで実行し、環境に適用する - 問題なく適用できたらPull Requestをmainブランチにマージする
- issueをクローズする