Terraformのノウハウをまとめる
以下の公式のドキュメントが充実しているため、基本的にはこれに従うようにし、個人的に得たノウハウを記載します。
Terraformの使い方やコードの書き方など、基礎的な内容は省略します。
https://developer.hashicorp.com/terraform/language
なお、Terraform Cloudは使ったことがないため、これTerraform Cloud使った方がいいよというものがあるかもしれません。
基本的なディレクトリ構成
構築するシステム構成に合わせて好きに考えてよいですが、再利用性や可読性を考えてモジュール化は行った方がよいです。
ただし、モジュールが不必要に増えすぎると構成が複雑になるため、適度に意味のある単位でモジュール化を行うのが望ましいです。
だいたいの場合は、以下のような構成にするのが無難だと考えます。
/
├─ doc/
├─ environment/
│ ├─ prod/
│ │ └─ network/
│ │ └─ main.tf
│ ├─ staging/
│ └─ develop/
└─ modules/
├─ network/
│ ├─ Readme.md
│ └─ main.tf
└─ user/
モジュール関連
モジュールのファイル構成
参考:https://developer.hashicorp.com/terraform/language/modules/develop/structure
コードの他に、モジュールを説明するReadme、モジュールへのインプット(変数定義)、モジュールのアウトプットの3ファイルがほぼ必須です。
公式ではLICENSEファイルも置いておくことを推奨しています。
また、必ず作成するterraform
ブロックも別のファイルに外出ししておいた方が管理しやすいです。
モジュールによってはmain.tfの1ファイルにすべてを記載すると可読性が悪くなってしまう場合もあるため、これは適宜分割するのもよいと思います。
以上より、基本的なモジュールのファイル構成は以下のようになります。
modules/
└ network/
├─ Readme.md
├─ variables.tf
├─ outputs.tf
├─ versions.tf # terraformブロックを記載する
├─ main.tf
├─ subnet.tf # main.tfだけではコードの見通しが悪くなる場合は適度に分割
└─ LICENSE
モジュールの変数定義
参考:https://developer.hashicorp.com/terraform/language/values/variables
variables.tf
にモジュールへのインプットとして与えられる変数を定義します。
必須ではありませんが、descriptionとtypeは省略せずに適切に指定しておくことが望ましいです。
基本的に変更することを想定していない変数やオプションとしたい変数にはdefaultを指定しておくとよいです。
なお、可読性などのためにモジュール内でのみ使用したい変数はvariableではなく、localsを使います。
モジュールの変数に接頭辞のパラメータを用意する
複数人で並行して作業する場合にオススメの方法です。
S3バケットなど、リソースによっては重複した名前で作成できないものがあるため、複数人で同時に作業しようとすると競合してしまう問題が発生します。
そこで、重複した名前が許されないリソースには任意の接頭辞を付けるように実装しておきます。
各々で任意の文字列をname_prefix
に指定しておけば、他の作業者のことを気にせずに作業を行えるようになります。
ブルーグリーンデプロイメントのように、一時的にリソースを2セット作成したい場合などにも有効です。
variables.tf
に以下を定義
variable "name_prefix" {
description = "作成するリソースの名前に付与する接頭辞"
type = string
}
main.tf
に記載するリソースでは以下のようにパラメータを指定しておく。
resource "aws_s3_bucket" "foo" {
bucket = "${var.name_prefix}-foo-files"
}
モジュールのアウトプット
参考:https://developer.hashicorp.com/terraform/language/values/outputs
主に他のモジュールから参照したいパラメータや、実行後に値を確認したいパラメータをアウトプットに記載します。
必須ではありませんが、変数と同様にdescriptionは適切に指定しておくことが望ましいです。
なお、モジュールを使用する側の方のtfファイル(本記事ではenvironment以下に作成するtfファイル)で以下のように記載するとモジュールで定義しているアウトプットをまとめてすべてアウトプットできます。
output "network" {
description = "networkモジュールのOutput"
value = module.network
}
モジュール内のterraformブロック
古いバージョンのTerraformやプロバイダでは実行できない可能性があるため、現在使用しているバージョンより新しいバージョンでのみ実行を許可するように定義しておくのがよいです。
モジュールごとにバラバラでも問題はないと思いますが、なるべく統一した方が混乱しにくいです。
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.38.0"
}
}
required_version = ">= 1.7.4"
}
モジュールのReadme
参考:https://devops-blog.virtualtech.jp/entry/20230310/1678416395
terraform-docsを利用すると、コードを解析して自動的に「作成されるリソース」「変数」「アウトプット」などの情報を埋め込んだファイルを生成できます。
モジュールの目的や注意事項なども記述しておくのが望ましいですが、変数やアウトプットのdescriptionをしっかりと書いておけば、これだけでも最低限のReadmeとして使えると思います。
また、Terraformにはterraform graph
というコードの関係をグラフとして出力してくれる機能があるため、これもReadmeに載せると便利そうに思えますが、実際に出力してみるとかなり視認性の悪いグラフが出てきます。
これを改良したようなツールはいくつか公開されており、個人的にはterraform-graph-beautifierがオススメです。
用途にもよると思いますが、個人的には以下のようにlocal
, var
, output
を除外したグラフがリソースの関連だけ見られてオススメです。
terraform graph | terraform-graph-beautifier --output-type=graphviz --embed-modules=false --exclude="local\\." --exclude="var\\." --exclude="output\\." | dot -Tpng > ../../../doc/images/$(basename $(pwd)).png
この2つのツールを使用するだけで、簡易的にかなりの情報量を含んだReadmeを生成できます。
運用関連
パラメータを外出しする
通常はenvironment内の各ディレクトリのmain.tf
に各モジュールの変数へ与える値を直接記載しますが、多くの場合に複数のmain.tf
で共通にしたい値が存在すると思われるため、設定ファイルを外出しすると便利です。
main.tf
内でyamldecode
関数やjsonencode
関数を使うと、これを実現できます。
locals {
config = yamldecode(file("../config.yaml"))
}
module "network" {
source = "../../../modules/network"
az = local.config.az
tags = local.config.tags
}
az: "ap-northeast-1c"
tags:
Project: "hoge"
TerraformVersion: "1.7.4"
Environment: "staging"
※上記は、以下のようなディレクトリ構成を想定しています。
/
├─ environment/
│ ├─ prod/
│ │ ├─ config.yaml
│ │ └─ network/
│ │ └─ main.tf # 上記の内容のtfファイル
└─ modules/
├─ network/
└─ user/
セキュアなパラメータを扱う
コードをGit管理したいが、設定値にIPやパスワードなどのセキュアな情報が含まれていて、その部分はGitに含めたくないといった場合があります。
このような場合は以下のような方法が考えられます。
- 環境変数で定義する
- AWS Systems Manager Parameter Storeのようなクラウド上で管理できる場所にパラメータを定義しておき、そこから取得した値を使用するコードにしておく
-
SOPSを利用する
- TerraformでSOPSを利用する場合の参考記事: https://zenn.dev/himekoh/articles/202209041050
SOPSはPGPや各種クラウドのキーマネジメントサービスのキーなどを使用してファイルの暗号化/複合を行うツールです。
これを利用するとGitには暗号化したファイルが置かれるため、通常通りにGit管理を行うことができます。
パラメータの用途や運用によって適した方法は異なるため、適宜使い分けるとよいと思います。
initを効率化する
参考:https://developer.hashicorp.com/terraform/cli/config/config-file
通常、terraform init
を実行すると実行したディレクトリ毎にプロバイダがダウンロードされます。
たとえば、awsプロバイダを使用するモジュールを実行するディレクトリが3箇所存在する場合、3箇所に同じawsプロバイダがダウンロードされることになります。
意図的にこの仕様を利用したい場合を除いて、プロバイダは共通のディレクトリにダウンロードし、そこを参照して利用するように設定しておくのがオススメです。
効率的なバージョンアップ対応
environmentやmodules内にバージョンを記載しているため、すべての値を書き換える必要があります。
sedで置換してもいいですが、tfupdateを使って以下のようなスクリプトを実行するのがわかりやすくて簡単です。
このためにterraform
ブロックを別のファイルに外出ししておくことを個人的にオススメします。
TERRAFORM_VERSION=1.7.4
AWS_PROVIDER_VERSION=5.38.0
cd environment
find -name versions.tf | while read line; do
tfupdate terraform -v ${TERRAFORM_VERSION} ${line}
tfupdate provider aws -v ${AWS_PROVIDER_VERSION} ${line}
done
cd modules
find -name versions.tf | while read line; do
tfupdate terraform -v ">= ${TERRAFORM_VERSION}" ${line}
tfupdate provider aws -v ">= ${AWS_PROVIDER_VERSION}" ${line}
done
コードの静的解析
スペースの個数や僅かな書き方の違いで差分が出るのは煩わしいので、共通のルールを作っておくのがいいです。
可能であれば、GitHub Actionsなどで検知を自動化しておくと実行し忘れ防止や手間の削減になります。
後から導入すると修正対応が面倒になるため、なるべく初期から導入しておく方が楽です。
- シンプルなフォーマット整形
- リポジトリのトップで
terraform fmt -recursive
を実行する- コードの見た目をキレイに整えてくれる
- 動作への影響は無い
- リポジトリのトップで
- 不正なパラメータの検知
-
tflint
- EC2インスタンスで存在しないインスタンスタイプを指定しているなど、書き方としては問題ないが実行するとエラーになるパラメータを検知してくれる
-
tflint
- 非推奨な設定で記載されているリソースの検知
-
tfsec
- S3をパブリック公開する設定になっているなど、実行は可能だがセキュリティ的に懸念点のあるリソースを検知してくれる
-
tfsec
モジュール依存関係の可視化(Terragrunt)
モジュールを作成していくと、「networkモジュールでVPC周りを作成する→作成したネットワークにeksモジュールでEKS周りを作成する」のように依存関係のあるモジュールが増えていきます。
同一モジュール内でのリソース同士の依存は自動的に適切な順序で作成してくれますが、モジュールの場合は実行時に依存関係を考慮しながら実行しなければなりません。
順序を間違えると中途半端にリソースが作られてしまったり、古い状態のアウトプットを参照してしまうおそれがあります。
このような依存関係の問題は、Terragruntを使用するとだいぶ解消されます。
ただし、本気でTerragruntを使おうとすると仕組みやルールが複雑になってしまって危険な雰囲気がしたので、ここでは依存関係の可視化を行うだけのシンプルな使い方を紹介しておきます。
この使い方だけであれば、既存のTerraformのみでの環境にも気軽に導入できると思います。
/
├─ environment/
│ ├─ prod/
│ │ ├─ config.yaml
│ │ ├─ terragrunt.hcl # ①
│ │ ├─ network/
│ │ │ └─ 省略
│ │ └─ eks/ # networkモジュールに依存したモジュール
│ │ ├─ main.tf
│ │ ├─ terragrunt.hcl # ②
│ │ └─ その他省略
└─ modules/
├─ network/
└─ eks/
上記は、以下の構成を仮定したディレクトリ構成になっています。
- networkモジュールとeksモジュールの2つのモジュールが存在する
- eksモジュールはnetworkモジュールに依存しており、先にnetworkモジュールを実行する必要がある
config.yaml
にはtfstateを保管するS3バケットの情報などを定義しておきます。
backend_s3:
bucket: "hoge-staging-terraform-tfstate" # tfstateを格納するS3バケット
region: "ap-northeast-1" # tfstateを格納するS3バケットのリージョン
folder: "prod" # バケットに作成するフォルダ
①のterragrunt.hcl
ファイルには、参照したいすべてのtfstateファイルを読み込むように以下のような記載をしておきます。
key
の部分は環境に合わせて適切に書き換えてください。
ここではprod
という名前のフォルダに{モジュールのディレクトリ名}.tfstate
という名前で各モジュールのtfstateを保管するルールになっていると仮定しています。
# Terragrunt root file
locals {
config = yamldecode(file("config.yaml"))
}
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
bucket = local.config.backend_s3.bucket
region = local.config.backend_s3.region
key = "${local.config.backend_s3.folder}/${path_relative_to_include()}.tfstate"
encrypt = true
}
}
②のterragrunt.hcl
ファイルには、依存しているモジュールのディレクトリのパスをdependencies
に定義しておきます。
include "root" {
path = find_in_parent_folders()
}
dependencies {
paths = ["../network"]
}
この状態でprod
ディレクトリでterragrunt graph-dependencies | dot -Tsvg > graph.svg
のようなコマンドを実行すると、以下の
モジュール依存関係のグラフが出力されます。
なお、依存しているモジュールの数が多くてすべて正確に記載すると複雑になりすぎてしまう場合は適宜間引いて定義することを検討してみるのもよいです。
例えば、aモジュールにbモジュールが依存、aとbモジュールにcモジュールが依存となっている場合、cがaに依存しているかどうかに関係なくa→b→cという依存関係になるため、c→aの部分は省略しても順序的には問題ありません。