はじめに
初めまして!
プラットフォーム開発部バックエンドエンジニアの acchan です。
みらい翻訳ではインフラストラクチャの構築に AWS + Terraform を採用しているのですが、Terraform は自由度が高い反面、「この場合どちらの書き方が良いのだろう?」と悩むことがよくあります。
そこで今回は Terraform をより良く使うため、公式 と Terraform AWS Modules の著者 に明言されているベストプラクティスを要約してみました。
参考文献
上述の通り、本記事は以下の Web サイトを参考にしています。
特に今回、現在のチームで利用しているリソースの大半が AWS 系であることを踏まえ、後者の文献を参照させて頂いております。
前提
Key concepts - Terraform Best Practices
前提として Terraform によるインフラストラクチャは細かい順に、
Resource < Resource module < Infrastructure module < Composition
の階層構造を成しているものと考えます。
Infrastructure module は同じ目的を持った複数の Resource module の集合であり、この単位で provider が定義されます。Composition は更に Infrastructure module を束ねたもので、組織またはプロジェクトに必要なインフラストラクチャの全体を記述するために使用されます。
またデータソース terraform_remote_state は、上位レベルのモジュールと Composition を結合するために用いられます。
Simple Infrastructure Composition (画像引用: Key concepts - Terraform Best Practices より)
注記:具体的な例について
文献中に言及はないのですが「Infrastrucure module」には個々のアプリケーションやマイクロサービス等が、「結合に用いられる terraform_remote_state」には共有 VPC 等のネットワークリソース等が、それぞれ該当してくるだろうと考えております。
コードの構成
Code structure - Terraform Best Practices
一般的に推奨されるコードの構成は以下の通りです。
- リソースの数は極力少なく保つ
- セキュリティ事故等が起きた場合も被害の範囲が小さくて済む
- バックエンドは remote state を使う
- tfstate ファイルを git で管理するのはただの悪夢
- 構造と命名規則を一貫させる
- resource module をできる限りシンプルに保つ
- 変数として渡せたり data source を利用できる値をハードコードしない
- data source と terraform_remote_state を使って複数の infrastructure module を結合する(前項の通り)
基本的な設定ファイルの構成
Code structure - Terraform Best Practices
基本的には main.tf に必要なリソースを記述します。また「大半のケースにおいて、以下の様にファイルを分割しておくのは良い方法である」とされています。
variables.tf outputs.tf versions.tf
注記1:Resource module の実装
個別の Resource module を実装する際の注意事項については、後述する モジュールの開発について の項目も参照願います。
注記2:シンボリックリンクについて
公式に記載は見受けられないですが、Terraform で DRY な記述を行う場合、シンボリックリンクを利用する手法が散見されます。これを利用する場合、上記のファイル群に加えて共通化できない部分をさらに分割する必要があります。
privider(s).tf backend.tf locals.tf
注記3:variables.tf, outputs.tf の要否について
分割しておくのが良いとは書かれていますが、必ず作るべきと明記されてもいない様です。特にアプリケーション用のルートモジュールを他から参照させることを許可すると依存関係が複雑化する傾向があるため、outputs.tf は作らない方が良い場合もあるかと思います。
ディレクトリ構造
Medium-size infrastructure with Terraform - Terraform Best Practices
例示プロジェクトとしては、以下のディレクトリ構造が提示されています。
modules - module1 - module2 prod (自社で使っている環境識別子に準拠) stage (〃)
注記:Terraform とアプリケーションのコード分離について
Terraform とアプリケーションで「コードリポジトリを分けるか否か」という所ですが、公式には言及されていない様です。 考え方の一例として、HashCorp のコミュニティでは以下のような意見が挙げられていました。
Organizing Terraform config with app code - Terraform - HashiCorp Discuss
命名規約
Naming conventions - Terraform Best Practices
以下のルールが明言されています。
全体
- 基本的に
-
ではなく_
を使う - 半角英数字を使う
リソースとデータソースの引数
- リソースの名前は部分的にでも繰り返さない
-
resource "aws_route_table" "public_route_table"
みたいなのは NG
-
- リソース名の候補が一切ない場合は
this
を使う- 説明として main が妥当な候補であれば無論それを使って構わない
- 単数形の名詞を使う
- リソースに渡す内部的な引数や、値が人の目に触れる場所(例: RDS インスタンスの DNS 名)では適宜
-
を使用する count
/for_each
引数はブロックの先頭に記述し、空行を置くtags
引数は引数の末尾、depends_on
/lifecycle
の手前に記述するcount
/for_each
の判定には出来るだけ boolean を使う
variable 名
name
,description
,default
の値は正しく使う- 変数の
validation
は制限が多いため慎重に使う list
,map
型の変数名は複数形を使うdescription
,type
,default
,validation
の順に定義するdescription
は絶対に書く(後々の自分のために)object
型よりも限定的な型を優先して使う- コレクションの型も限定的なものを優先
map(map(string))
>map(map(object))
- type
any
は、適切な階層の時または複数の型を許容するときに使う {}
は map だったり object だったりするため、tomap
して map に寄せる
output 名
- 通常の命名よりも、より説明的な名称を使う
{name}_{type}_{attribute}
の書式が良い- 返す値が複数リソースから変換されて出来る項目は、特に汎用的な名前にする
- コレクションを返す場合は複数形を使う
- どれ程自明な場合でも必ず
description
を書く sensitive
の設定は避けるelement(concat(...))
よりもtry()
を優先的に使用する
スタイル規約
Style Conventions - Configuration Language | Terraform | HashiCorp Developer
原則として terraform fmt
で整形される書式に統一します。
- インデント毎に半角空白 2 つ
- 等号は縦に揃える
- ブロック内では、引数 > 子ブロックの順で宣言し、間に空白行を置く
- ブロックの中身を論理的に区切るための空白行を置く
- メタ引数 > 引数 > 子ブロック > メタ子ブロックの順で宣言し、間に空白行を置く
- ブロック間は特に関連性が強い場合を除き、間に空白行を置く
- 論理的なより強いグルーピングができる場合を除き、同じタイプのブロックは一箇所に固めて記述する
モジュールの開発について
概要
Creating Modules | Terraform | HashiCorp Developer
モジュールは独立した複数リソースの組み合わせとして宣言し、そのモジュールが表す概念に応じた名前を付けます。
下記公式ドキュメントにある通り、可能な限り単一のリソースのみから成るモジュールを作らない様に留意して下さい。
優れたモジュールは、プロバイダーによって提供されるリソースタイプから構築されるアーキテクチャの新しい概念を記述することによって、抽象化のレベルを高める必要があります。
(中略)
他の1つのリソースタイプに対する単なる薄いラッパーであるモジュールを記述することはお勧めしません。内部のメインリソースタイプと同じではないモジュールの名前を見つけるのに問題がある場合、それはモジュールが新しい抽象化を作成しておらず、モジュールが不必要な複雑さを追加していることを示している可能性があります。代わりに、呼び出し元モジュールでリソースタイプを直接使用します。
標準構成
Standard Module Structure | Terraform | HashiCorp Developer
独立した再利用可能なモジュールを作成するにあたって、推奨される標準的な構成は以下の通りです。
- ルートモジュールを配置する(唯一の必須要素)
- README を配置する
- 公開する場合は LICENSE を配置する
main.tf
,variables.tf
,outputs.tf
をそれぞれ配置する- 複雑なモジュールを記述する場合、
main.tf
を適宜分割する - 入出力には必ず descriptions を書く
- モジュールのネスト (modules/parent/child 等) は禁止しない
- 複雑な構成を分割する目的で使用し、parent 以外からは参照させない
- 公式 によると禁止していないが推奨もしていない模様
- 必要に応じて modules と同階層に examples を配置する
注記1:モジュールの main.tf を分割する
公式な明記はないですが、スタイル規約上 同じタイプのブロックは一箇所に固めて記述する 事になるため、s3.tf
, security_group.tf
等のリソースタイプ名で分割する方式が管理しやすいと考えられます。
注記2:公開しない場合について
原文は作成したモジュールを公開する前提で記述されていますが、社内で業務インフラを組む目的で構成する場合、LICENSE, examples 等は考慮しなくて大丈夫だと思われます。
モジュール開発のベストプラクティス
Module Composition | Terraform | HashiCorp Developer
情報量が非常に多いため要旨のみを記載させて頂きますが、タイトルの通り非常に重要な内容を含むため、是非とも原文をご一読下さい。
複数モジュールの結合
Terraform の module ブロックを導入することで、原理的には深く階層的なリソースツリーを作成する事ができます。
ですがほとんどの場合、モジュールツリーをフラットに保ち、ルートモジュールから依存関係を受け取る形でモジュール間の関係を記述することを強くお勧めします。
以下では Terraform を使用して大規模なシステムを記述するときに役立つ可能性のある、具体的な構成パターンについて説明します。
依存関係の反転
モジュールが モジュール(親)> ネットワーク(子) の様な依存関係を持つ場合、ネットワークを独立したモジュールとして定義して ID だけを渡すのは良い方法です。
これによりモジュール自体を相対的に小さく保ち、また必要に応じてネットワークを共有することが出来るようになります。
この方法であれば「商用環境は共有ネットワークがあるが、開発環境は専用のネットワークを作っている」様な場合でも、柔軟に構成を組み分ける事が可能です。
前提と保証
モジュールは自分自身が期待する「前提(Assumptions)」と、利用者に提供するデータの「保証(Guarantees)」を持ちます。
Custom Condition Checks の仕掛けを使い、これらを捕捉・検証する様にして下さい。
複数クラウドの抽象化
Terraform 自体は、異なるベンダーが提供する類似サービスを意図的に抽象化しようとはしていません。
ですが例えば DNS 設定等の共通性が高いものであれば、各ベンダ固有の便利機能を使わないことと引き換えに、自作モジュールによる抽象化を実現できる場合があります。
Data-only Modules
システムがいくつかのサブシステム構成に分割されており、かつ共通の IP ネットワークなど、すべてのサブシステムで共有される特定のインフラストラクチャがある場合、Data-only Modules の利用を検討して下さい。
共有ネットワークを join-network-aws 等の名前で「既存のインフラストラクチャに関する情報を取得するだけのモジュール」として記述することで、データの取得方法を正確にカプセル化する事ができます。
従来のモジュールと同様に、モジュールが何らかの方法で抽象化のレベルを上げる場合にのみ、この手法を使用することをお勧めします。
センシティブなデータの扱い
State: Sensitive Data | Terraform | HashiCorp Developer
Terraform state には、リソースの ID とすべての属性が含まれます。特にデータベースなどのリソースの場合、初期パスワードが含まれることがあるため注意が必要です。
推奨される管理方法
機密データを Terraform (データベースパスワード、ユーザーパスワード、秘密鍵など) で管理する場合は、state 自体を機密データとして扱います。
remote state を使用する場合、state は Terraform が利用する瞬間のみメモリに保持されます。保存時に暗号化される場合がありますが、これは特定の remote state のバックエンドに依存します。
S3 バックエンドは encrypt オプションが有効な場合、保存時の暗号化をサポートしてくれます。IAM ポリシーとログは、無効なアクセスを識別するために使用できます。状態の要求は TLS 接続を介して行われます。
最弱のコーディング規約
要約と言いつつも順番に書き並べていくと結構な長文となってしまいましたが、いかがでしたでしょうか。
Terraform Best Practices にも「少なくともこれらのルールに従わない理由はない筈だ」と言及されている通り、これらのプラクティスはあくまでもオフィシャルに言及されている最低限(=最弱)の共通事項であって、実務上はプロジェクトの特性に合わせてたルールの追加が必要になるかと思います。
もしも仮に誰かが「『最強の Terraform コーディング規約』を作りたい」と考えた際に、本稿が僅かなりともお役に立てば幸いです。
最後に
みらい翻訳では、エンジニアを募集しています。
ご興味のある方は、ぜひ下記リンクよりご応募・お問い合わせをお待ちしております。