3/30 に X で Terraform がトレンド入りしていて何事かと思ったら Terraform が公式ドキュメントとしてスタイルガイドを出したようです。
いままで Terraform のスタイルに関して信頼できるドキュメントといえば Google Cloud の Terraform を使用するためのベスト プラクティス ぐらいしか知らなかったのですが、 Terraform 公式がようやく出してくれてありがたい限りです。
これでわざわざ社内の Terraform 規約を設けずとも「公式ドキュメントに従いましょう。」の一言で済みます。
ということで一通り読んだのでまとめました。
原文だと構文の簡単な使い方なども書いてありますが以下の要約ではだいたい省略しています。
詳細は原文を読んで確認してください。
要約
スタイルガイドについて
コードのスタイルについての推奨事項と運用とワークフローに関する推奨事項を取り上げている。
Code style
- コードをバージョン管理にコミットする前に
terraform fmt
とterraform validate
を実行する。 - TFLint のような Linter を使用して、組織独自のコーディングのベストプラクティスを実施する。
- コメントには
#
を使う。 - リソース名には名詞を使用し、リソースタイプを名前に含めない。
- 名前の複数の単語を区切るにはアンダースコアを使用する。リソース定義では、リソース・タイプと名前を二重引用符で囲む。
- 参照するリソースの後に依存するリソースを定義する。
- すべての変数に型と説明を含める。
- すべての出力に説明を含める。
- 変数やローカル値の使いすぎに注意する。
- 常にデフォルトのプロバイダ設定を含める。
-
count
とfor_each
は控えめに使用する。
Code formatting
- ネストするごとにスペース 2 つあける。
- 単一行の値を持つ複数の引数が同じネストレベルで連続した行に現れる場合、それらの等号を揃える:
ami = "abc123"
instance_type = "t2.micro"
- ブロック本体の中に引数とブロックの両方が一緒に現れる場合は、引数をすべて一番上にまとめて配置し、その下にネストされたブロックを配置する。引数とブロックを区切るには空行を 1 行使う。
- ブロック内の引数の論理的なグループを区切るには空行を使う。
- 引数と "メタ引数"(Terraform 言語セマンティクスで定義)の両方を含むブロックでは、メタ引数を最初に列挙し、空行 1 行で他の引数と区切る。メタ引数ブロックは最後に配置し、他のブロックとは空白行で区切る。
resource "aws_instance" "example" {
# meta-argument first
count = 2 # 要約追記: = の位置が揃っていないと思ったけど terraform fmt をかけてもこの位置になることを確認済み。
ami = "abc123"
instance_type = "t2.micro"
network_interface {
# ...
}
# meta-argument block last
lifecycle {
create_before_destroy = true
}
}
- 最上位のブロックは常に空白行 1 行で区切らなければならない。ネストされたブロックも空白行で区切らなければならないが、同じ型の関連ブロックをグループ化する場合は例外。
- ブロックタイプがセマンティクスで定義されてファミリーを形成している場合を除き、同じタイプの複数のブロックを異なるタイプの他のブロックとグループ化することは避ける。(例: aws_instance の root_block_device、ebs_block_device、ephemeral_block_device は、AWS ブロックデバイスを記述するブロックタイプのファミリーを形成しているため、グループ化して混在させることができる)。
terraform fmt コマンドで上記の推奨のサブセットにフォーマットできる。
Code validation
terraform validate コマンドで設定が構文的に有効かチェックできる。
詳細は Terraform validate documentation へ。
File names
以下のファイル命名規則を推奨する:
- バックエンドの設定を含む
backend.tf
ファイル - すべてのリソースとデータソースブロックを含む
main.tf
ファイル- ただしコードベースが大きくなった場合は論理的なグループにファイルを分けて整理することを推奨する。
-
outputs.tf
ファイル:すべての出力ブロックをアルファベット順に格納する。 -
providers.tf
すべてのプロバイダブロックと設定を含むファイル -
terraform.tf
ファイル。required_version と required_providers を定義する terraform ブロックを含む。 -
variables.tf
ファイル。すべての変数ブロックをアルファベット順に格納する。 -
locals.tf
ファイル。詳細は Local values を参照 - 設定のオーバーライド定義を含む
override.tf
ファイル。Terraform はこのファイルと_override.tf
で終わるすべてのファイルを最後にロードする。これらのオーバーライドはコードの推論を難しくするので、控えめに使用し、元のリソース定義にコメントを追加する。詳細は Override Files のドキュメントを参照
Linting and static code analysis
Terraform には組み込みの Linter が無いが、 TFLint のようなサードパーティツールを頼ることができる。
自分でルールを書くこともできる。
Comments
- 必要なときだけコメントを使う。
- 1 行でも複数行でも # を使う。
Resource naming
- 一貫性と読みやすさのために説明的な名詞を使用する。
- アンダースコアで単語を区切る。
- リソース識別子にはリソースタイプを含めない。
- リソースタイプと名前は二重引用符で囲む。
Bad
resource aws_instance webAPI-aws-instance {...}
Good
resource "aws_instance" "web_api" {...}
Resource order
コード内のリソースやデータソースの順番は Terraform のビルド方法に影響しないので、読みやすいようにリソースを整理する。
データソースを参照するリソースの前にデータソースを定義する。
data "aws_ami" "web" {
##...
}
data "aws_availability_zones" "available" {
##...
}
resource "aws_instance" "web" {
ami = data.aws_ami.web.id
availability_zone = data.aws_availability_zones.available.names[0]
##...
}
リソースパラメータは以下の順で統一する。
- 存在する場合は、
count
またはfor_each
メタ引数 - リソース固有の非ブロックパラメータ
- リソース固有のブロックパラメータ
- 必要であれば、ライフサイクルブロック
- 必要であれば
depends_on
パラメータ
Variables
- 変数は使いすぎない。
- すべての変数に型と説明を定義する。
- 変数がオプションの場合は、妥当なデフォルト値を定義する。
- パスワードや秘密鍵のような機密性の高い変数には、sensitive パラメータを true に設定します。詳細は secrets management を参照する。
- type validation に加えて、input variable validation を使用して変数値に対する追加のルールを作成する。
variable "web_instance_count" {
type = number
description = "Number of web instances to deploy. This application requires at least two instances."
validation {
condition = var.aws_instance_count > 1
error_message = "This application requires at least two web instances."
}
}
次の順序を推奨する。
- type
- description
- Default
- Sensitive
- バリデーションブロック
Outputs
Output パラメータは次の順で出力する。
- Description
- Value
- Sensitive
variable "db_disk_size" {
type = number
description = "Disk size for the API database"
default = 100
}
variable "db_password" {
type = string
description = "Database password"
sensitive = true
}
output "web_public_ip" {
description = "Public IP of the web instance"
value = aws_instance.web.public_ip
}
Local Values
ローカル値は 2 つの場所のどちらかで定義する
- 複数のファイルでローカル値を参照する場合は、locals.tf という名前のファイルに定義する。
- ローカル値が特定のファイルに限定されている場合は、そのファイルの先頭に定義する。
Provider aliasing
- 常にデフォルトのプロバイダ構成を含め、すべてのプロバイダを同じファイルで定義する。
- プロバイダの複数のインスタンスを定義する場合は、デフォルトを最初に定義する。
- デフォルト以外のプロバイダの場合は、プロバイダ・ブロックの最初のパラメータとしてエイリアスを定義する。
provider "aws" {
region = "us-east-1"
}
provider "aws" {
alias = "west"
region = "us-west-2"
}
Dynamic resource count
リソースがほぼ同じなら count を、整数から導出できない個別の値を必要とする場合は for_each を使用する。
.gitignore
以下のファイルはコミットしない。
-
terraform.tfstate.*
ファイルを含む、terraform.tfstate
ファイル -
.terraform.tfstate.lock.info
ファイル。terraform apply コマンドを実行すると自動的に作成・削除される。 -
.terraform
ディレクトリ。Terraform がプロバイダや子モジュールをダウンロードする場所。terraform plan を実行する際に-out フラットを含めると作成される保存されたプランファイル - 機密情報を含む
.tfvars
ファイル
以下は常にコミットする。
- すべての Terraform コードファイル
-
.terraform.lock.hcl
依存ロックファイル -
.gitignore
ファイル - コード、入力変数、出力を記述した
README.md
Workflow style
このセクションでは、予測可能でセキュアな Terraform ワークフローを可能にする、以下のような標準についてレビューする:
- Terraform、プロバイダ、モジュールのバージョンを固定する。
- Terraform Cloud レジストリを使用する場合、
terraform-<PROVIDER>-<NAME>
の 3 つの名前を使ってモジュールリポジトリに名前を付ける。 - ローカルモジュールは
./modules/<module_name>
に保存する。 - tfe_outputs データソースまたはプロバイダ固有のデータソースを使用して、2 つのステートファイル間でステートを共有します。
- 動的なプロバイダー資格情報または HashiCorp Vault のようなシークレットマネージャーを使用して資格情報を保護する。
- モジュールのテストを書く。
- Terraform Cloud のポリシーエンフォースメントを使ってインフラ運用のガードレールを設定する。
Version pinning
プロバイダやモジュールのアップグレードによってインフラストラクチャに意図しない変更が加えられるのを防ぐには、バージョンの固定を使用する。
また、terraform ブロックの required_version
を使用して、Terraform バイナリの最低必要バージョンを設定することを推奨する。
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.34.0"
}
}
required_version = ">= 1.7"
}
Module repository names
モジュールリポジトリは terraform-<PROVIDER>-<NAME>
という 3 つの部分からなる名前を使用しなければならない。 <NAME>
はモジュールが管理するインフラの種類を表し、 <PROVIDER>
はモジュールが使用する主なプロバイダ。
<NAME>
セグメントにはさらにハイフンを入れることができ、例えば terraform-google-vault
や terraform-aws-ec2-instance
のようにすることができる。
Module structure
モジュールを使って、一緒にプロビジョニングする必要のある論理的に関連するリソースをグループ化する。
詳細は module creation recommended pattern documentation と standard module structure を参照する。
Local modules
ローカルモジュールはリモートモジュールレジストリではなくローカルディスクから取得する。
Terraform Cloud のプライベートモジュールレジストリのようなモジュールレジストリにモジュールを公開することを推奨する。
./modules/<module_name>
ディレクトリに子モジュールを定義することを推奨する。
Repository structure
- 実際のインフラ構成はモジュールコードとは別に保存することを推奨する。
- 各モジュールは個別のリポジトリに保存する。こうすることで、各モジュールを独立してバージョン管理でき、Terraform のプライベートレジストリでモジュールを公開しやすくなる。
- 論理的に関連するリソースをグループ化したリポジトリでインフラストラクチャ構成を整理しする。例えば、コンピュート、ネットワーク、データベースのリソースを必要とする Web アプリケーションを 1 つのリポジトリで管理する。リソースをグループに分けることで、操作の障害によって影響を受ける可能性のあるリソースの数を制限できる。
- あるいはすべてのモジュールとインフラ構成を 1 つのモノリシックリポジトリ(monorepo)にグループ化する。例えば、monorepo はインフラスタックの各コンポーネントのローカルモジュールのコレクションを定義し、ルートモジュールにデプロイすることができる。
.
├── modules
│ ├── function
│ │ ├── main.tf # contains aws_iam_role, aws_lambda_function
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── queue
│ │ ├── main.tf # contains aws_sqs_queue
│ │ ├── outputs.tf
│ │ └── variables.tf
│ └── vpc
│ ├── main.tf # contains aws_vpc, aws_subnet
│ ├── outputs.tf
│ └── variables.tf
├── main.tf
├── outputs.tf
└── variables.tf
- モノリシック・リポジトリは CI/CD の自動化を複雑にする可能性がある。コード変更はリポジトリ全体を操作するデプロイメントをトリガーするので、ワークフローは変更されたディレクトリだけをターゲットにしなければならない。また、リポジトリにアクセスできる人なら誰でもリポジトリ内のファイルを変更できるため、きめ細かなアクセス制御ができなくなる。
Branching strategy
Terraform コードを共同開発するときは GitHub flow に従うことを推奨する。
Multiple environments
- リポジトリの
main
ブランチをすべての環境の真実のソースとすることを推奨する。 - Terraform Cloud と Terraform Enterprise のユーザーには、環境ごとに別々のワークスペースを使うことを推奨する。詳細は Terraform workspace and project best practicesへ。
- Terraform Cloud や Terraform Enterprise を使わない場合は、モジュールを使って設定をカプセル化し、環境ごとにディレクトリを使い、それぞれが個別のステートファイルを持つようにすることを推奨する。
├── modules
│ ├── compute
│ │ └── main.tf
│ ├── database
│ │ └── main.tf
│ └── network
│ └── main.tf
├── dev
│ ├── backend.tf
│ ├── main.tf
│ └── variables.tf
├── prod
│ ├── backend.tf
│ ├── main.tf
│ └── variables.tf
└── staging
├── backend.tf
├── main.tf
└── variables.tf
State sharing
State には機密情報が含まれているため、可能な限り完全な State ファイルの共有は避ける。
Secrets management
リモートステートストレージを設定しない場合、Terraform CLI はローカルディスクにステート全体をプレーンテキストで保存する。ステートにはパスワードや秘密鍵などの機密データが含まれる可能性がある。Terraform Cloud と Terraform Enterprise は HashiCorp Vault を通して状態の暗号化を提供する。
Terraform Cloud または Terraform Enterprise を使用する場合は、以下を推奨
- Terraform Enterprise を使用する場合は、local_exec プロビジョナーまたは外部データソースの使用を防ぐために Sentinel ポリシーを定義し、実施する。
- Terraform Cloud または Terraform Enterprise を使用する場合、動的なプロバイダー認証情報を使用して、長期間の静的な認証情報の使用を避ける。
Terraform Community Edition を使用する場合は、以下を推奨
- プロバイダ固有の環境変数を使ってプロバイダの認証情報を設定する。
- Terraform Vault プロバイダを使って、HashiCorp Vault のようなシークレット管理システムからシークレットにアクセスする。Terraform はこれらの値をステートファイルにプレーンテキストで書き込むことに注意する。
カスタム CI/CD パイプラインを使用する場合は、機密値を管理するための CI/CD ツールのベストプラクティスを確認する。
Integration and unit testing
Terrform モジュールのテストを書いて実行することをおすすめする。
テストはコード自体の動作やロジックを検証する。詳しくは Terraform test documentation や Write Terraform tests tutorial を参照。
Policy
ポリシーは Terraform Cloud が Terraform の実行に対して強制するルール。ポリシーを使って、Terraform プランが組織のベストプラクティスに準拠しているかどうかを検証できる。例えば、以下のようなポリシーを書くことができる:
- Web インスタンスのサイズを制限
- 必要なリソースタグをチェック
- 金曜日のデプロイをブロック
- セキュリティ設定とコスト管理の強制
ポリシーは Terraform コードとは別の VCS リポジトリに保存することを推奨する。
詳細は policy enforcement documentation や enforce policy with Sential や detect infrastructure drift and enforce OPA policies を参照する。
感想
原文がまあまあ長いので要約も長くなってしまいました。
普通に書いてたらまあその通りになりそうなスタイルガイドで違和感ありませんでした。おそらく自分の書いた Terraform のコードはこれを読む前後でほぼ変わらないはずです。
とはいえ、もしも我流で書いてしまう人がいた場合に、「公式ドキュメントにはこう書いてあるんだからこれに合わせてください。」と言えるのはでかいですね。それってあなたの宗教ですよね?みたいな反論を防げるので助かります。
スタイルガイドをアレンジしたい場合でも、「基本的には公式ドキュメントに従うが、例外的にチーム内ではここはこうする」みたいに少しの手間でチーム内スタイルを設定できるのがありがたいです。
またスタイルガイドの公式ドキュメント内で、詳しくはこのページを読め、という参照がいくつもあることに気づきました。
それらのドキュメントを読むだけでも勉強になりそうなので別途読んでみようと思います。
これを機に Terraform の健全なコードが増えることを期待しています。