エンタープライズなプロジェクトでTerraformを使うことも増えてきました。いくつかのプロジェクトでTerraformを使ってきた経験から、Terraformを使う上で頻繁に考える設計項目とガイドラインをまとめます。このガイド以外にも設計項目はあると思いますが、最低限必要な内容をまとめています。(一部検証が足りない部分あり。後日更新します。 更新しました。)
前提
- HCP Terraformを使わない想定。HCP Terraformを使う場合は異なる可能性がある。
- Terraformの導入方法やCI/CDの構築方法など、Terraformの設計以外の内容については触れない。あくまでTerraformの設計に関する内容に限定する。
- クラウドはAWSを想定して書いている。権限などの内容はAWSのIAMを前提にしている。GCPやAzureなど他のクラウドを使う場合は異なる可能性がある。
- GitはGitHubを想定して書いている。GitLabやBitbucketなど他のGitサービスを使う場合は異なる可能性がある。
Terraformの基本
Terraformは、インフラの構成をコードで記述し、そのコードに従ってクラウドリソースを機械的に作成・変更・削除できるツールです(Infrastructure as Code)。AWS・GCP・Azureなど多様なクラウドプロバイダーに対応しています。
このセクションは設計項目ではなく、Terraformを使い始めるにあたって押さえておくべき概念と用語を概説します。
全体像
Terraformを使った作業の基本的な流れは以下の通りです。
tfファイルを書く → terraform init → terraform plan → terraform apply
| ステップ | 内容 |
|---|---|
| tfファイルを書く | 作りたいインフラをコードで記述する |
terraform init |
プロバイダーをダウンロードし、tfstateのバックエンドを初期化する |
terraform plan |
変更内容のプレビューを確認する(実際には変更しない) |
terraform apply |
変更を実際のクラウド環境に適用する |
主要な構成要素
tfファイル(.tf)
作成・管理したいリソースをコードで記述するファイルです。resourceブロックでAWSのVPCやEC2を定義したり、providerブロックでプロバイダーの設定を記述したりします。Terraformはコマンド実行時にディレクトリ内の.tfファイルをすべて読み込みます。
# tfファイルの記述例(AWSのVPCを作成する)
resource "aws_vpc" "this" {
cidr_block = "10.0.0.0/16"
}
Terraformコマンド
terraform initやterraform plan、terraform applyなどのコマンドの総称です。作業端末やCI/CD環境にインストールして使用します。主なコマンドは以下の通りです。
| コマンド | 内容 |
|---|---|
terraform init |
プロバイダーのダウンロード、tfstateのバックエンド初期化 |
terraform plan |
変更内容のプレビュー(実際には何も変更しない) |
terraform apply |
リソースの作成・変更・削除を実行 |
terraform destroy |
Terraformが管理する全リソースを削除 |
terraform fmt |
コードのフォーマットを自動整形 |
terraform import |
既存リソースをTerraform管理下に取り込む |
プロバイダー
対象のクラウドやサービスに対してAPIを通じてリソースを操作するためのプラグインです。AWSならAWSプロバイダー、GCPならGCPプロバイダーといったように、クラウドごとに対応するプロバイダーがあります。
tfstate(.tfstate)
Terraformが管理しているリソースの状態を記録するファイルです。terraform applyを実行するたびに更新されます。Terraformはこのtfstateと実際のクラウド環境の差分を比較することで、何を変更すべきか判断します。複数人で開発する場合はtfstateをS3などの共有場所(バックエンド)に保存します。tfstateを誤って削除するとTerraformがリソースを管理できなくなるため、取り扱いには細心の注意が必要です。
モジュール
Terraformコードをディレクトリ単位でまとめたものです。terraformコマンドを実行するディレクトリをルートモジュールと呼び、ルートモジュールごとにtfstateが作成されます。ルートモジュールとは別に共通化したいリソース群をまとめた別ディレクトリを作り切り出すこともできます(子モジュール)。
Terraformコマンドおよびプロバイダーのバージョン
Terraformコマンドおよびプロバイダーにはそれぞれバージョンがあります。
Terraformコマンドはv1.x系で後方互換がサポートされています。Terraformのバージョンによって追加される機能としては、例えばv1.5ではterraform importに変わるimportブロックが追加されたり、v1.6ではterraform testコマンドが追加されたりしました。Terraformの各バージョンで追加された機能の詳細はTerraformのCHANGELOGを確認します。
参考
Terraformコマンドとは別にプロバイダーにもバージョンがあります。プロバイダーはバージョンによって扱えるリソースが増えたり、リソース定義の書き方が変わったりします。例えばS3バケットのリソース定義はv3系のAWSプロバイダーまでは多くの設定がaws_s3_bucketリソースで行いましたが、v4系以降ではaws_s3_bucket_loggingリソースなどに分割されました。クラウドの新サービスや新機能を使うには対応したバージョンのプロバイダーを使う必要があります。プロバイダーのリリースサイクルはプロバイダーごとに異なります。例えばAWSプロバイダーだと7日ほどの間隔で最新バージョンがリリースされます。
設計アドバイス
基本的には開発開始時点の最新バージョンを使用し、必要に応じてバージョンアップしていくのが良いでしょう。
バージョン指定
Terraformコマンドとプロバイダーのバージョンは以下のようにコード内で指定します。
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
バージョン指定はバージョンを固定(=)にするか、範囲指定(>=, <=, ~>)にするか選べます。各演算子の意味は以下の通りです。
-
=: バージョンを固定。例:= 5.0.0は5.0.0のみ許可 -
>=: 指定バージョン以上。例:>= 5.0.0は5.0.0以上すべて許可 -
<=: 指定バージョン以下。例:<= 5.0.0は5.0.0以下すべて許可 -
~>: 悲観的制約演算子。最後のバージョン番号のみ上昇を許可する。例:~> 5.0.0は5.0.x(5.0.0以上かつ5.1.0未満)のみ許可、~> 5.0は5.x(5.0以上かつ6.0未満)のみ許可
>= と < を組み合わせて上限・下限を同時に指定することもできます(例: >= 5.0.0, < 6.0.0)。~> はメジャーバージョンやマイナーバージョンを固定しつつ細かいバージョンアップを許容したい場合に便利です。
バージョンを固定にすると、全員が同じバージョンを使うことが保証されるため、バージョンの差異による問題が減ります。範囲指定にすると、ある程度のバージョン差異は許容されるため、最新機能を使いやすくなりますが、予期せぬバージョンアップによる問題が発生するリスクもあります。
設計アドバイス
Terraformコマンドはv1.0系で後方互換が保証されているため、バージョンを1.0以上や開発開始時点以上(>=または~>)の範囲指定にしても大きな問題は起きないでしょう。
一方でプロバイダーは頻繁にバージョンアップされ、バージョンによってリソース定義の書き方が大きく変わることもあります。範囲指定にすると予期せぬコード修正を余儀なくされるおそれがあるため固定(=)にした方が安全です。
バージョンアップの方針
Terraformコマンドはコマンド自体を置き換えることでバージョンアップします。またはtfenvなどのツールを使ってもバージョンアップできます。
プロバイダーはコード内のバージョン指定を変更しterraform initすることでバージョンアップします。または範囲指定(>=や~>)している場合、terraform initを実行する度に指定した範囲内でバージョンアップされます。
設計アドバイス
Terraformコマンドは定期的にバージョンアップしていくのが良いでしょう。頻度はプロジェクトの状況や新機能の必要性によりますが、例えば年に1回その時点の最新バージョンに上げるなどの方針が良いでしょう。
プロバイダーはモジュールごとに新サービスや新機能が必要な場合にバージョンアップを検討します。また、コマンドと合わせて年1回などの頻度で全モジュールのプロバイダーバージョンを確認し、一律でバージョンアップを行うのも良いかもしれません。あまりに古いバージョンを使い続けるといざバージョンアップする際に大きな変更が必要になる可能性があるためです。ただし、モジュール数が多いと一律バージョンアップは現実的でないかもしれません。
バックエンド
バックエンドの種類と排他制御
tfstateを保管する場所がバックエンドです。バックエンドのデフォルトはローカル(Terraformコマンドを実行した場所)です。複数人で開発を行う場合、tfstateを共有できるリモートの場所を使います。様々な場所を選択でき、例えば Amazon S3 などがバックエンドとして使えます。バックエンドに保管されるtfstateは大事なため、S3ならバージョニングを有効にしてバックアップを取った方がいいです。また、バックエンドに保管するだけでなく、排他制御もします。排他制御は複数人で同じtfstateを同時に変更してしまうことを防ぐための仕組みです。S3をバックエンドにする場合、S3にロックファイルを作成して排他制御する方法が一般的です。(過去にはDynamoDBで制御する方法もありましたが現在はS3ロックファイルが推奨です。)
tfstateにはTerraformで管理しているリソースの情報が保存されます。そのため、tfstateを削除するとリソースがTerraformの管轄から外れてしまいます。仮にapply後にtfstateを削除してしまった場合、再度applyし直すと、tfstate削除前に作ったリソースが残っていても新規でまた作ろうとします。特別な理由がない限りtfstateは削除しないでください。
参考
設計アドバイス
バックエンドは複数人で共有できる場所を選択します。AWSならtfstate専用のS3バケットを用意するのが無難でしょう。tfstate用バケットはバージョニング、暗号化、アクセス制御を設定しておくと安全です。排他制御はS3ロックファイルを使うのが良いでしょう。
tfstateの配置
バックエンド内に各モジュールのtfstateが配置されます。各tfstateがどのモジュールに対応しているかを明確にするため、tfstateの配置を工夫します。例えば、tfstate名にモジュール名を含めたりモジュールごとにディレクトリを分けるなどです。
設計アドバイス
バックエンド内のtfstateの配置はモジュールのディレクトリ構成に合わせて分けるのが良いでしょう。また、tfstate名はモジュール名.tfstateとするか、モジュール名/terraform.tfstateのようにモジュール名を含めるとわかりやすいでしょう。例えばバックエンド内に以下の様に配置します。
モジュール名.tfstateの場合
.
├── network.tfstate
└── workload.tfstate
モジュール名/terraform.tfstateの場合
.
├── network
│ └── terraform.tfstate
└── workload
└── terraform.tfstate
「モジュール名.tfstateの場合」はtfstateファイル名にモジュール名が含まれるので、DLしてローカルで開いた時にどのモジュールのtfstateかわかりやすいです。一方で、「モジュール名/terraform.tfstateの場合」はコードとtfstateの配置を同じにすることができます。どちらもメリットあるため、プロジェクトの状況に合わせて選択します。
実行場所と実行ユーザー
Terraformコマンドを実行する場所と実行ユーザー(権限)について考えます。
実行場所
Terraformコマンドは作業端末や作業用サーバーで実行することが一般的です。また、GitHub Actions等のCI/CDで動かすこともできます。実行場所のOSはmac、Linux、Windowsなどが選べます。
Terraformの実行には以下の通信が必要です。
- クラウドプロバイダーのAPIエンドポイント
- Hashicorpレジストリ(プロバイダーバイナリのDL)
- GitHub等のTerraformコードを格納したリポジトリ
- tfstateのバックエンド
上記のため、Terraformの実行場所はインターネットへ繋がる環境にあるのが理想です。しかし、実行場所から無制限のインターネットアクセスは避けたい事もあります。その場合、プロキシ等でURL制限を施すのが良いでしょう。さらに、インターネットに一切出られないエアギャップ環境で実行したい場合は以下のようにします。
- 実行場所からクラウドAPIへのプライベートな通信経路を用意する(VPCエンドポイント等)
- プロバイダーバイナリは別端末で
terraform providers mirrorコマンド等を使いDLし、バイナリファイルを安全な経路で運搬する - Terraformコードは実行場所から直接pullせず、安全な経路で運搬する
設計アドバイス
OSはLinux系をおすすめします。Windows版はコマンド実行時に時間がかかったり動作が安定しないためです。どこで実行するかはTerraformに限らずクラウド操作に関する要件に従い決めます。例えばクラウドAPIへの接続は特定の端末や拠点等の制限をおこなう必要があるのか、インターネットへのアクセス許可や制限があるか確認します。
作業内容によって実行場所を変えても良いでしょう。例えばplanは GitHub Actions で自動実行し、applyは本番アクセス端末で手動実行するなどです。
実行ユーザー
Terraformによるクラウドの操作はクラウドの認証情報を使い行われます。AWSだとIAMユーザーの認証情報(アクセスキー、シークレットキー)で AWS CLI の認証情報をが設定されていればTerraformも同じ認証情報を使います。AssumeRoleで別のロールを引き受けることもできますし、OIDCで発行された一時的なクレデンシャルを使って実行もできます(GitHub Actions等で実行する時に使います)。
Terraformはクラウドリソースの作成・削除を行うため強い権限が必要になります。AWSだとAdministratorAccess等の権限を使えるのが理想です。しかし、セキュリティポリシーで強い権限の利用が禁止されている場合、Terraform管理対象のサービスのみ操作できる権限を使います。
planとapplyで必要な権限が違うのもポイントです。planは基本的に参照権限のみで実行できます。ただし、State Lockを使用している場合(S3ロックファイル等)、plan時にもLock取得のため書き込み権限が必要な点に注意します。-lock=falseオプションを付けることでState Lockを無効にできますが、複数人で同じtfstateを操作する場合は競合のリスクがある点に注意します。
設計アドバイス
構築・運用の流れを整理して考えます。例えばGitHub Actionsでplanを実行、その後手動でapplyする場合は以下のようにplanをするユーザー(GitHub Actions用)とapplyするユーザーを別々で用意します。
- GitHub Actions(
plan専用)- GitHub Actions用のIAMロールを作成する
- IAMにGitHub OIDCプロバイダーを追加する
- IAMロールには特定のリポジトリ、ブランチを信頼した信頼ポリシーを設定する
- Terraform管理対象のサービスに対する参照を許可したIAMポリシーをアタッチする
- State Lockを使用している場合S3へのPutItem/DeleteItem権限もアタッチする
- GitHub Actions用のIAMロールを作成する
- 手動実行(
apply用)- Apply専用のIAMロールを作成する
- 許可されたIAMユーザーまたはロールからのAssumeRoleを許可した信頼ポリシーを設定する
- Terraform管理対象のサービスに対する参照および書き込みを許可したIAMポリシーをアタッチする
- 作業者用のIAMユーザーを作成する
- AssumeRoleのアクションを許可したポリシーをアタッチする
- Apply専用のIAMロールを作成する
モジュール
Terraformコードを配置しTerraformコマンドを実行するディレクトリをモジュールといいます。モジュールをどのように構成するかはTerraform設計において肝とも言える重要な要素です。
子モジュールの使用
Terraformコマンドを実行するモジュールをルートモジュールといいます。ルートモジュールから子モジュールを呼び出すことができます。Terraformでモジュール化と言うと、この子モジュールを使った構成を指すことが多いです。子モジュールはルートモジュールとは別のディレクトリにtfファイル群を配置したものです。ルートモジュールから子モジュールを呼び出し、変数の値を同時に渡します。こうすることで同じ構成のリソース群を複数作ったり削除したりできます。
有効な使用例としてはマルチテナントなサービスが挙げられます。テナントごとにEC2、ELB、S3などのいくつかのリソースセットを作成したい場合、子モジュールに必要なリソース群のTerraformコードを作成し、variablesブロックで変数を設定できるようにします(例えばテナント名など)。ルートモジュールでは子モジュールの呼び出しと変数の値を設定します。こうすることでテナントを追加したり削除することが容易になります。
以下、ディレクトリ構成およびコードの例です。
terraform/
├── modules/
│ └── tenant/ # 子モジュール: テナント共通リソース
│ ├── main.tf
│ └── variables.tf
└── tenants/
├── tenant-a/ # テナントAのルートモジュール
│ ├── main.tf
│ └── terraform.tfvars
└── tenant-b/ # テナントBのルートモジュール
├── main.tf
└── terraform.tfvars
modules/tenant/variables.tf:
variable "tenant_name" {
type = string
}
tenants/tenant-a/main.tf:
module "tenant" {
source = "../../modules/tenant"
tenant_name = "tenant-a"
}
子モジュールは便利ですがデメリットもあります。
- 可読性が下がる
- 環境差異が多いとき、共通部分のコードが複雑になる
- 構成が複雑になるため、Terraform初学者からすると難易度が高く感じられる
子モジュールは取り入れればいいというものではなく、適材適所で使い分けることが大事です。
設計アドバイス
Terraformに初めて取り組む場合は子モジュールをおすすめしません。子モジュールを使うとコードの難易度が上がるためです。また、環境間(例えば開発/本番)の子モジュールもおすすめしません。開発と本番で構成が異なる(例えば冗長性など)ことが多く、子モジュール側のコードが複雑になりがちだからです。開発で試しに行った修正が誤って本番にも影響しないよう運用の注意も必要になります。
一方で、マルチテナントなサービスなどで同じリソースセットを何度も作成・削除したい要件がある場合、子モジュールは有効です。
モジュールの分割方針
子モジュールを使わない場合でもTerraformの実行はモジュール単位で行われるためどのように分けるか考えます。基本的にはある程度の粒度で分割する方が良いです。以下がその理由です。
- モジュール内のリソースが多いと
planやapplyの実行時間が長くなる - 複数人での作業時に競合が発生しやすくなる
- 範囲が広いと意図しない差分が発生しやすくなりTerraform実行前に調査が必要となる場面が増える
- 作業ミス(
destroy等)による影響範囲が大きくなる
しかし、モジュールを細かく分けすぎるのも良くありません。モジュールが増えすぎると管理が煩雑になりますし、モジュール間の依存関係を考慮する必要があります。また、Terraformコマンドの実行回数が増えるというデメリットもあります。例えばEC2インスタンスにIAMロールやSGなどのサブリソースをアタッチする場合、EC2インスタンス、IAMロール、SGなど各リソースごとにモジュールを分けるのではなく、メインリソースであるEC2インスタンスとそれに付随するサブリソース(IAM、SG)をまとめて一つのモジュールにした方が良いでしょう。
巨大な単一モジュールにした場合もメリットはあります。Terraformはモジュール内の依存関係を自動で判別してくれるため、一回のapplyですべてを構築できます。Terraformを初期構築ツールとして割り切った使い方をする場合はありかもしれません。
設計アドバイス
どのように分割するかはプロジェクトの状況やチームの好みに合わせて決めます。以下の点を考慮して分割方針を決めます。
- どう使うか: Terraformを初期構築ツールとして割り切って使うか、運用でも使うか
- いつ変わるか(ライフサイクル・デプロイ頻度): VPCやIAMなど滅多に変わらないリソースと、EC2やAutoScalingなど頻繁に変わるリソースは分ける。一緒にすると安定リソースにも毎回差分チェックが走り、意図しない変更のリスクが増える。
- 誰が変えるか(チーム・担当者): インフラチームとアプリチームで担当が分かれる場合、その境界でモジュールを分けると権限管理や作業範囲が明確になる。
- 何と一緒に変えるか(変更の独立性・サービス単位): WEBサーバー、APサーバー、DBなど、一緒にデプロイ・変更することが多いリソースをまとめる。逆に独立して変えるリソースは分けておく。
-
壊れたらどれだけ困るか(影響範囲): 誤った変更や
destroyの影響を局所化するため、ビジネス的に重要なリソース(本番DBなど)は他と分けてリスクを最小化する。
ディレクトリ構成
モジュールは.tfファイルを配置したディレクトリです。モジュールのディレクトリ構成はいくつか考えられます。いくつか例を記載します。
フラット
まず最もフラットな構成は以下になります。すべてのコードが同じディレクトリにあるため、Terraformの実行が楽です。しかし、コード量が多くなると可読性が下がり、管理も難しくなります。
.
├── main.tf
├── variables.tf
└── terraform.tfvars
環境分離
次に環境ごとにディレクトリを分けた構成です。環境まるごと1コマンドで実行できるのが特徴です。新たに環境を追加したい場合、環境ディレクトリごとコピーし必要な個所を修正すればいいため、環境追加のしやすさもあります。
.
├── dev
└── prd
環境分離 + サービス分離
環境内をさらにある程度の機能(サービス)で分ける場合は以下のようになります。構成図の構成をそのまま表現するのに近いため直観的な分かりやすさと影響範囲がある程度限定されます。
.
├── dev
│ ├── network
│ ├── tfbackend
│ └── gitlab
└── prd
├── network
└── tfbackend
環境分離(子モジュール) + サービス分離
環境分離 + サービス分離の構成をさらに子モジュール化した構成が以下になります。子モジュール化しているためコードが冗長になりません。一方で、環境間の差異があるとコードが複雑になります。
.
├── modules
│ ├── network
│ ├── tfbackend
│ └── gitlab
├── dev
│ ├── network
│ ├── tfbackend
│ └── gitlab
└── prd
├── network
└── tfbackend
サービス分離 + 環境分離
サービスで分離してから環境を分ける構成もあります。各サービスの環境ごとのコードが近くなるため環境横ぐしの修正がしやすくなります。
.
├── network
│ ├── dev
│ └── prd
├── tfbackend
│ ├── dev
│ └── prd
└── gitlab
└── dev
サービス分離 + 環境分離(子モジュール)
サービス分離 + 環境分離の構成をさらに子モジュール化した構成が以下になります。
.
├── network
│ ├── modules
│ ├── dev
│ └── prd
├── tfbackend
│ ├── modules
│ ├── dev
│ └── prd
└── gitlab
├── modules
└── dev
設計アドバイス
以上の通り、ディレクトリのパターンはいくつか考えられます。どの構成が正解というものはなく、プロジェクトの状況やチームの好みに合わせて設計することになります。大きなポイントは以下2点です。
- 子モジュール: 使うか使わないか(子モジュールを使うとコードの重複が減るが、コードの追い辛さや構成の複雑さが増す)
- 環境分離: ルート等の上層で分けるか下層で分けるか(上層だと環境コピーがしやすく、下層だと環境横ぐしの修正がしやすい)
初めてTerraformを使う場合「環境分離 + サービス分離」が分かりやすさと管理のしやすさのバランスが取れているためおすすめです。
モジュール間の変数の受け渡し
異なるモジュール間で値を受け渡したいことがあります。例えば、ネットワークモジュールで作成したサブネットにワークロードモジュールのEC2をデプロイしたい場合などです。このように、モジュール間で値を受け渡したい場合、tfstateのリモート参照が便利です。モジュール内でoutputブロックを定義すると値がtfstateに別モジュールから参照可能な状態で保存されます。値を参照するモジュールではdataブロックを使い参照先モジュールのtfstateから値を取得します。
設計アドバイス
基本的にモジュール間の値の受け渡しはtfstateのリモート参照を使うのが良いでしょう。モジュール間の依存関係が明確になり、コードの可読性も上がります。別モジュールで使う可能性のある値(リソース名やリソースIDなどがよく使われます)をoutputブロックで宣言しておくようにします。
モジュール変数
プロジェクト名やシステム名など複数のリソースで使う値をコードに直接書いてしまうと、リソースごとに同じ文字列を記述する手間があります。変数を定義し、文字列を使い回せると便利です。また、後から変えたいパラメータ値も変数にしておくと、コード内の記述箇所を探さなくて済むため便利です。Terraformではlocalsやvariablesを使って変数を定義できます。どちらもリソースを作成するTerraformコードに変数を与えることができるものですが、以下の点で違いがあります。
- locals
- 宣言時にタイプ指定はできない
- 1つの
locals{}で複数個を設定できる - 変数の宣言と値の設定を同時に行う
- モジュール外部から値を渡すことはできない(dataで読み込んだ値をlocalsに設定することは可能)
- variables
- 宣言時にタイプ指定ができる
- 1つの
variable{}では1つしか設定できない - 変数の宣言と値の設定を別で行う(デフォルト値を設定することはできる)
- モジュール外部から値を渡すことができる(実行環境の環境変数、
-varフラグなど)
設計アドバイス
ルートモジュールではlocalsとvariableどちらも使えます。どちらか一方に統一するか、併用するかはプロジェクトごとに決めます。
variableで定義しておけば、ルートモジュールを後から子モジュール化することも容易になります。また、terraform-docsなどのドキュメント生成ツールを使う場合、variableで定義した変数は自動的にドキュメントに出力されるため、ドキュメントの充実度を上げたい場合はvariableで定義すると良いでしょう。variableを使う際はタイプ(type)、説明(description)は必ず設定しておきましょう。タイプを設定することで不正な値の混入を防げます。
localsはvariablesよりも宣言が簡単に行えて、コードの記述量を減らせられます。
子モジュールは、子モジュール側でvariableブロックを宣言し、ルートモジュールから値を渡します。
モジュール内のファイル構成
Terraformはコマンド実行時にモジュール(ディレクトリ)内の.tfファイルを全て読み込みます。Terraformのファイル構成に仕様的なルールはあまりありません。すべての定義を1つの.tfファイルにまとめても良いですし、複数の.tfファイルに分割しても良いです。
Terraformでは各ブロックごとに設定を記述します。主要なブロックは以下の通りです。
-
terraform: Terraform本体のバージョンやtfstateのバックエンドを指定する -
provider: プロバイダーの種類やバージョン、プロバイダーごとの特別な設定(リージョン、デフォルトタグ等)を指定する -
resource: 作成するリソースを定義する -
data: 既存のリソースを参照するためのデータソースを定義する -
variable: モジュール外部から指定できる変数を定義する -
output: モジュールが出力する値を定義する -
locals: モジュール内でのみ使用するローカル変数を定義する
Terraformのファイル名は自由につけられますが一般的には以下のファイル名で記述することが多いです。
-
versions.tf:
terraformブロックやproviderブロックを記述する。さらに細かくterraform.tf、provider.tfに分けて書くのがHashiCorp推奨 - variables.tf: モジュール外部から指定できる変数を記述する
- outputs.tf: モジュールが出力する変数を記述する
- locals.tf: local変数の設定を記述する
- main.tf: ルートモジュールから子モジュールの呼び出しやdataの参照を記述する。長くなる場合、リソース定義を別ファイルに分割することもある(例: vpc.tf、iam.tf、ec2.tf)
参考
設計アドバイス
ファイル分割の基準は「ファイルを開かずにどのファイルを編集するか想像できるか」が目安です。リソース数が少ない間はすべてのリソース定義をmain.tfにまとめても良いでしょう。リソースが増えてきたら、リソース種類ごとにファイルを分割しましょう(例: vpc.tf, iam.tf, rds.tf)。
variables.tf / outputs.tf / locals.tf のような専用ファイルへの分離は、チームで統一しやすく、リソース定義とその他の定義が分かりやすいためおすすめです。
1モジュール内のファイル・リソース数が増えすぎた場合は、モジュールの範囲を見直した方が良いかもしれません。適切な粒度でモジュールを分けることで、ファイル数やリソース数を適度に保ち、コードの可読性と保守性を高めることができます。
コードの記述
機密情報の管理
RDSのパスワードなど機密情報をコードにそのまま書いてしまうのは危険です。パスワードがそのままGitにコミットされて漏洩の恐れがあるためです。
また、コードに書く機密情報を暗号化してもTerraformでリソース作成時に指定した機密情報がtfstateにも含まれる点に注意が必要です。
設計アドバイス
tfstateの機密情報問題には、大きく2つのアプローチがあります。
| アプローチ | 概要 |
|---|---|
| 見せなくする | tfstateへのアクセスを制限する |
| 書かせなくする | tfstateに機密情報を書き込まない |
具体的な対策は以下の4つです。
| # | 対策 | アプローチ | 機密情報がtfstateに残るか | 備考 |
|---|---|---|---|---|
| 1 | バックエンドの暗号化 | 見せなくする | 残る(平文) | アクセス制御で補う |
| 2 | manage_master_user_password |
書かせなくする | 残らない | RDS等の対応リソースのみ |
| 3 | ephemeralリソース + write-only引数 | 書かせなくする | 残らない | Terraform >= 1.10 が必要。書き込み専用引数のあるリソースのみ |
| 4 |
lifecycle.ignore_changes + 手動設定 |
書かせなくする(部分的) | 初期パスワードは残る | 最後の手段 |
どの対策を選ぶかは、以下のフローで判断するとよいでしょう。
まず「対策1:バックエンドの暗号化」は基本として実施する
↓
さらに機密情報をtfstateに残したくない場合
↓
対象リソースが manage_master_user_password に対応している?
→ YES:対策2(最もシンプル)
→ NO ↓
write-only引数(password_wo 等)がある?(Terraform >= 1.10)
→ YES:対策3
→ NO :対策4(初期パスワードはtfstateに残るため、バックエンドのアクセス管理を徹底する)
※ 対策2・3が使えるリソースでも対策4を採用し、全リソースで管理方法を統一するという選択肢もある
参考
コーディング規約
複数人で開発・運用する場合、コーディング規約を定めておくことが重要です。命名や書き方ばらつきがあると、コードの可読性が低下し、レビューや障害対応に余分なコストがかかります。また、新規参画者がプロジェクトに慣れるまでの時間も長くなります。規約を統一することで、どう書けばいいか悩むことが減り、チーム全体の生産性が上がります。
規約として定めておくべき主な項目は以下の通りです。
- 命名規則: Terraformリソース名や変数名の命名パターンを統一する。クラウドリソースとTerraformコード内の識別子で区切り文字のルールが異なる点(AWSリソース名はハイフン、Terraform識別子はアンダースコア)に注意する
-
変数の定義方法:
localsとvariableのどちらを使うか、variableを使う場合はタイプや説明を必ず書くなどのルールを決める -
出力値: モジュール間で共有する値を
outputで宣言するルールを決める - 実装方針: ループ処理、モジュール間の値の受け渡し、タグの付与方法など、コードの実装に関するルールを決める
- 書式: リソースブロック内のパラメーターの順番や、ブロックごとの行間の空け方など、コードの書式に関するルールを決める
設計アドバイス
以下のような規約を定めると良いでしょう。プロジェクトの状況やチームの好みに合わせて適宜調整してください。
- 命名
- Terraformリソース名は小文字英数字にする
- 単語の区切りはアンダースコア(_)を使う
- Terraformリソース名はリソースタイプを繰り返さない(NG:resource "aws_vpc" "vpc" 、OK:resource "aws_vpc" "this")
- モジュール内に複数同じタイプのリソースを作成する場合、Terraformリソース名に
this等は使わず、一意な名前にする(例:1つのモジュール内で2つのSGを作成する場合、"aws_security_group" "web"、"aws_security_group" "app"のように命名する) - tfstateのキーはディレクトリ名と同じにする(例: backendモジュールだと、backend/terraform.tfstate)
- リストやマップなど複数の要素を持つ変数名は複数形にする(NG:id OK:ids)
- モジュールの入力変数
- 可変の変数はvariablesを定義して外部から指定できるようにする
- variablesのdefaultは使用しない(設定値を分かりやすくするため)
- 環境差異の発生しない共通パラメーターはハードコードする(variablesの数を抑えて分かりやすさを向上。後から可変にしたくなったらvariablesを定義する)
- descriptionを書く
- typeを指定する
- モジュールの出力変数
- 他モジュールで参照する値を出力する
- 将来参照される可能性が高い値はあらかじめ出力を設定する
- 出力値は作成したリソースからのみにする(入力変数をそのまま出力しない)
- descriptionを書く
- 実装方針
- ループ処理はなるべく
for_eachを使う(countはインデックス管理が煩雑になるため極力使わない) - モジュール間でリソース属性を参照する場合はtfstateのリモート参照を使う
- すべてのリソースに共通して付与するタグはAWSプロバイダーの
default_tagsを使う
- ループ処理はなるべく
- 書式
-
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] } -
- セキュリティ
- 本プロジェクトで扱うコードには機密情報を含めない。デプロイ時に必要な場合は初期パスワードをコードに書いても良いが、デプロイ後は
lifecycleブロックのignore_changesオプションで変更を無視するなどして、tfstateに最新のパスワードが保存されないようにする -
trivyを使いコードのセキュリティスキャンを行う
- 本プロジェクトで扱うコードには機密情報を含めない。デプロイ時に必要な場合は初期パスワードをコードに書いても良いが、デプロイ後は
参考
開発フロー
Terraformコードをどのように開発し、デプロイするかフローを考えます。基本的に以下の流れになります。
- コードの記述
- レビュー
- デプロイ
コードの記述 ではissueやブランチを作るところから始まります。開発端末にクローンしたリポジトリでブランチを作成し、コードを記述します。Terraformコードの開発では、terraform planを実行してコードの構文確認や環境への影響を確認しながら記述できると効率的です。しかし、開発端末から直接環境にアクセスできない場合は、コードを記述した後にCI/CD等でterraform planを実行して確認するフローになります。
レビュー では、作成したコードを他のメンバーに確認してもらいます。コードの品質や設計方針に沿っているかをチェックし、必要に応じて修正を行います。CI/CDで自動チェックが行えると便利です。例えば、terraform fmtでコードのフォーマットをチェックしたり、trivyでセキュリティスキャンを行ったりすることができます。最低限のチェックをCI/CDに保証させ、レビュアーは修正の本質(issueに沿った内容の変更かなど)に集中してレビューできます。
デプロイ では、レビューが完了したコードを実際の環境に反映させます。デプロイ前にterraform planを実行して変更内容を確認し、問題がなければterraform applyで適用します。デプロイもCI/CDで自動化できます。
設計アドバイス
フローはプロジェクトの状況やチームの好みに合わせて設計することになりますが、以下のようなフローを参考にしてください。以下フローではCI/CDの利用を前提としています。また、開発時と運用時でフローを分けて考えてもいいでしょう。(以下は運用時のフローになります。)
- issueの作成: バグ修正や機能追加の要望があればissueを作成する
- ブランチの作成: issue対応用のfeatureブランチをmainブランチから派生させて作成する(GitHubのGUIで作ってもいいし、手元の端末で作ってもいい)
- コードの変更: featureブランチでissueの内容に沿ったコードの変更を行う
- Pull Request の作成: 変更が完了したらmainブランチへの Pull Request を作成する
-
静的解析結果の確認: Pull Request 作成時に自動で以下の静的解析が実行される。結果を確認し、問題があれば修正する
-
terraform fmtでコードのフォーマットをチェック -
trivyでセキュリティスキャンを実行 -
terraform planでコードの構文確認と環境への影響を確認
-
- コードレビュー依頼: 静的解析のチェックが完了したらissue担当者以外のメンバーに内容のレビューを依頼する
-
コードレビュー(レビュアー): 以下の観点でレビューする。指摘があれば Pull Request にコメントを追加する
- 修正内容がissueの内容に沿っているか
- コードの品質が基準を満たしているか(Terraformのコーディングルールに沿っているか)
- コード修正(作成者): レビューで指摘された内容を修正し、Pull Requestに反映する
- Pull Requestの承認(レビュアー): Pull Request の内容に問題がなければ Pull Request を承認する
-
デプロイ: Pull Request のコメントで
terraform applyを実行する - マージ(レビュアー): Pull Request を main ブランチにマージする
- クローズ: issueをクローズする