Terraform

Terragruntでtfstateをモジュールごとに分割して管理する

TerraformのラッパーツールであるTerragruntを使用して、Terraformモジュールごとに状態管理ファイルのtfstateを管理してみます。

以前、こちらの記事でTerraformモジュール化について説明しました。

この内容ではソースコードは分割して管理できるのですが、tfstate自体は1つであるため、モノリシックで巨大なファイルになり、どうしてもTerraformの実行速度が遅くなってしまいます。

モジュールを指定しての実行も可能ですが、推奨されていません。

またモノリシックであるがゆえ、意図せずtfstateの紛失や、状態情報が壊れて(実際の構成とコードが乖離するなど)しまった場合の影響が甚大になります。

これらのリスクを回避するために、tfstate自体も分割してコンパクトに運用することが理想ですが、いくつかデメリットが発生してしまいます。

そのデメリットをTerragruntが軽減してくれます

Terragruntのメリットをわかりやすく説明するため、比較対象としてTerraformのみで分割したものを、こちらの記事にまとめました。ぜひ参考にしてください。

スポンサーリンク

前提

Terraform,Terragruntインストール方法

それぞれ過去記事で紹介したインストールを行います。

Terraformは、tfenv、Terragruntは、tgenvを使用してインストールします。

Terraform,Terragruntソースコード

Terraformモジュール化についての記事のコードを流用します。

完成版のソースコードはこちらのGitHubにコミットしました。

tfstate管理方法

今回はTerraform,Terragrunt以外の設定の手間を省くため、tfstateはローカルファイルでの管理とします。

実際の現場では、S3や、Terraform Cloudを利用しての運用になると思います。

本題

ディレクトリ・ファイルの構成

GitHubにコミットしているものをtreeコマンドで表示したものが以下になります。

実際にコミットしているファイルだけでなくTerragrunt実行時に自動で作成されるディレクトリ・ファイルも含めて記載しました。

また environments/dev と environments/prod の内容は同じなのでGitHubにはdev配下にのみファイルを用意しました。

$ tree -a manage-tfstate-per-module-with-terragrunt
manage-tfstate-per-module-with-terragrunt
├── environments
│   ├── dev           ・・・ 開発環境ディレクトリ。この配下でTerragrunt を実行。
│   │   ├── .terraform-version  ・・・ tfenv設定ファイル。Terraformバージョンを固定。
│   │   ├── .terragrunt-version ・・・ tgenv設定ファイル。Terragruntバージョンを固定。
│   │   ├── hello-app-server ・・・ 便宜上、記事内ではリソースディレクトリと呼びます。
│   │   │   ├── .terraform.lock.hcl ・・・ プロバイダなどのチェックサムが記載。
│   │   │   │                             terragrunt init時に自動生成。コミットを推奨。
│   │   │   ├── .terragrunt-cache ・・・ Terragruntキャッシュディレクトリ。
│   │   │   │   │                       terragrunt init時に自動生成。
│   │   │   │   └── xxxxx-yyyyy
│   │   │   │       └── zzzzz-Y
│   │   │   │           ├── .terraform
│   │   │   │           │   ├── providers
│   │   │   │           │   │   └── registry.terraform.io
│   │   │   │           │   │       └── hashicorp
│   │   │   │           │   │           └── aws
│   │   │   │           │   │               └── 3.58.0
│   │   │   │           │   │                   └── darwin_amd64
│   │   │   │           │   │                       └── terraform-provider...
│   │   │   │           │   └── terraform.tfstate
│   │   │   │           ├── .terraform.lock.hcl
│   │   │   │           ├── .terragrunt-module-manifest
│   │   │   │           ├── .terragrunt-source-manifest
│   │   │   │           ├── .terragrunt-source-version
│   │   │   │           ├── base.tf
│   │   │   │           ├── main.tf
│   │   │   │           ├── outputs.tf
│   │   │   │           ├── terragrunt.hcl
│   │   │   │           ├── test-key
│   │   │   │           ├── test-key.pub
│   │   │   │           └── variables.tf
│   │   │   ├── terragrunt.hcl
│   │   │   ├── test-key
│   │   │   └── test-key.pub
│   │   ├── network
│   │   │   ├── .terraform.lock.hcl
│   │   │   ├── .terragrunt-cache
│   │   │   │   └── ...
│   │   │   └── terragrunt.hcl
│   │   ├── security
│   │   │   ├── .terraform.lock.hcl
│   │   │   ├── .terragrunt-cache
│   │   │   │   └── ...
│   │   │   └── terragrunt.hcl
│   │   ├── terragrunt.hcl
│   │   ├── tfstate.d           ・・・ tfstate保存ディレクトリ。
│   │   │   ├── .gitkeep
│   │   │   ├── hello-app-server
│   │   │   │   └── terraform.tfstate
│   │   │   ├── network
│   │   │   │   └── terraform.tfstate
│   │   │   └── security
│   │   │       └── terraform.tfstate
│   │   └── variables.hcl
│   └── prod               ・・・ 本番環境ディレクトリ。dev配下と同じファイルを配置。
│       └── .gitkeep
└── modules                ・・・ モジュール本体。機能ごとにそれぞれ分けたもの。
    ├── hello-app-server
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── network
    │   ├── main.tf
    │   └── outputs.tf
    └── security
        ├── main.tf
        ├── outputs.tf
        └── variables.tf
$

environments/dev配下の主要ファイル説明

modules配下と同じ粒度で、network、security、hello-app-serverのディレクトリを作成しています。便宜上、本記事内では、これらをリソース、リソースディレクトリと呼称します。

この単位でtfstateが分割されます。

.terragrunt-cache

terragrunt init時に自動で生成されるディレクトリです。

.terraformやモジュールなどが保存され、Terragruntの実行はコマンドを実行したカレントディレクトリではなく、このディレクトリ内で実行されているようです。

そのため公式ドキュメントでも言及されているように、terragrunt設定ファイル(.hcl)での相対パス指定が自分の認識とずれる場合があるので注意しましょう。

.terraform-version

こちらの記事で紹介したtfenvのバージョン固定用ファイルです。

.terragrunt-version

こちらの記事で紹介したtgenvのバージョン固定用ファイルです。

共通設定用のterragrunt.hcl

dev直下に共通設定用のterragrunt.hclを配置します。

これはTerragruntの設定ファイルで専用の構文で記載します。

少しファイルの記述量が多いので以下、ブロックごとに説明していきます。ブロックの詳細な説明はこちらの公式ドキュメントをご参照下さい。

generateブロック

.terragrunt-cache内にファイルを任意に生成できます。今回はTerraformのときと同じbase.tfという名前で作成します。

generate "base" {
  path      = "base.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "aws" {
  region  = "ap-northeast-1"
  profile = "default"
}
terraform {
  required_version = "1.0.6"
  backend "local" {}
}
EOF
}

backend は次のremote_stateブロックで上書き設定するため空で設定しておきます。

remote_stateブロック

backendの内容をTerragruntの関数を使って動的に設定できます。

このおかげでTerraformのみで分割したときのように部分的に設定が異なるが、ほぼ同じ内容の設定ファイルを、各リソースディレクトリに配置する必要がなくなります。

remote_state {
  backend = "local"
  config = {
    path = "${get_parent_terragrunt_dir()}/tfstate.d/${path_relative_to_include()}/terraform.tfstate"
  }
}

なお、前述のgenerateブロック内で直接定義もできます。今回はremote_stateブロック紹介のため、このように記述しました。

localsブロック

Terraformにも存在するlocal変数と同様の概念になります。(デフォルトは)そのファイルのみ有効の変数となります。

locals {
  varables = read_terragrunt_config("${get_parent_terragrunt_dir()}/variables.hcl")
}
inputsブロック

指定した変数が、環境変数TF_VAR_xxxx として設定されます。Terraformのvariablesと同じ感覚で使用できます。

inputs = merge(
  local.varables.inputs,
  {
    hoge = "fuga"
  }
)

公式サンプルでも明記されていますが、merge関数を利用することで複数の値を一度に指定できます。

個別設定用のterragrunt.hcl

各リソースディレクトリ内に配置しているterragrunt.hclとなります。Terraformのmain.tfの代わりに配置します。

全リソース共通の定義

以下を各リソースのhclファイルに記述します。

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "../../../modules//${path_relative_to_include()}"
}

includeブロックで上位の階層にある共通設定用のterragrunt.hclを読み込みにいきます。

論理名(今回は”root”)の指定が推奨されています。いつから推奨になったか正確に把握できませんでしたが、少なくともバージョン0.31.0時点では指定すると逆にエラーとなりました。


terraformブロックのsourceのディレクトリ指定では、path_relative_to_includeというTerragruntの関数を使用して各リソースディレクトリ名からそれぞれ、”network”、”security”、”hello-app-server” という文字列を抽出するようにしています。


なおTerragruntの構成次第では、この共通の定義も共通設定用のterragrunt.hclに記述できますが、(おそらく)基本的なやり方ではないので、今回はこのように個別ファイルに記述しています。

別のモジュールの出力値を受け取る

Terraformでは別のモジュールの出力値を受け取るために、data “terraform_remote_state” を使用していました

Terragruntでは、代わりに、dependencyブロックinputsブロック を使用します。以下、securityのhclファイルの内容になります。

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "../../../modules/${path_relative_to_include()}"
}

// リソースディレクトリを指定します
dependency "network" {
  config_path = "../network"
}

// dependency.dependencyの論理名.outputs.モジュールのoutputの論理名 と定義します
inputs = {
  vpc-id = dependency.network.outputs.vpc-id
}


また、出力値の直接的なやり取りはないが、リソース間の依存関係を張っておきたい(実行の前後関係を明示的に指定しておきたい)ときは、 dependenciesブロック で定義します。

// 配列で複数同時に定義できます
dependencies {
  paths = ["../vpc","../security"]
}

variables.hcl

環境ごとに設定する変数をまとめました。(ファイル名は任意です)

inputs = {
  my-ip = "192.168.1.1/32"

  ec2-config = {
    ami           = "ami-034968955444c1fd9"
    instance-type = "t2.micro"
  }
}

以上、Terragruntの主要ファイルの説明でした。

個人的にTerragrunt構成を一言でまとめると、tfstate分割の影響で、みんな(各リソース)がバラバラにうごくため、それを管理する司令塔(dev/terragrunt.hcl)が存在するというイメージです。

スポンサーリンク

Terragruntの実行

コマンドの詳細なオプションはこちらの公式ドキュメントをご参照下さい。かなりボリューミーです。

事前準備

Githubからサンプルコードをダウンロードして、こちらの記事の通り、自分のIPアドレスへ書き換えます。

またEC2へ接続するためのssh鍵の作成も行います。

$ git clone git@github.com:zoo200/blog.git
$
$ cd manage-tfstate-per-module-with-terragrunt/environments/dev
$
$ vi variables.hcl
## my-ip  のIPを自分のIPアドレスに変更
inputs = {
  my-ip = "192.168.1.1/32"

  ec2-config = {
    ami           = "ami-034968955444c1fd9"
    instance-type = "t2.micro"
  }
}
$
## ssh鍵を作成します
$ ssh-keygen -N "" -f hello-app-server/test-key
Generating public/private rsa key pair.
...
$

すべてのリソースを一括で操作する

各リソースディレクトリの1階層上で、terragrunt run-all xxx(apply や destroy などのterraform サブコマンドを指定)を実行することで、全リソースを一度に操作できます

$ cd manage-tfstate-per-module-with-terragrunt/environments/dev
$
$ terragrunt run-all apply
...
Are you sure you want to run 'terragrunt apply' in each folder of the stack described above? (y/n) ・・・ y を入力

Initializing the backend...
...
Apply complete! Resources: 7 added, 0 changed, 0 destroyed.
...
Initializing the backend...
...
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
...
Initializing the backend...
...
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
...
$

デフォルトでは自動的に初期化処理(init)を実行してくれるので明示的に run-all initを実行する必要はありません。

なおrun-allの形式は、2021年2月に実装されたようで、それまで使用していた apply-all や destroy-all などの指定方法は現在非推奨となっています


また実行時に以下のような警告が出るのですが、今回のサンプルソースはサブモジュール(モジュールの中のモジュール)を使っていないので無視していただいて大丈夫です。

WARN[0004] No double-slash (//) found in source URL /Users/tester/blog/github/blog/manage-tfstate-per-module-with-terragrunt/modules/hello-app-server. Relative paths in downloaded Terraform code may not work.

リソースを個別に操作する

それぞれのリソースディレクトリに移動して、terragrunt xxx を実行することで個別にリソースを操作できます。

$ cd hello-app-server
$
$ terragrunt  destroy
...
$

全体操作のrun-allは、試行錯誤でインフラ設計しているときや、planで差分がないかチェックするときに非常に便利ですが、ある程度インフラが固まってくると、こちらの個別実行の頻度が多くなってくるかと思います。

リソース依存関係の確認

terragrunt graph-dependenciesで各リソースの依存関係が確認できます。dependency、dependenciesに記述した内容を元に出力されています。

$ terragrunt graph-dependencies
[INFO] Getting version from tgenv-version-name
[INFO] TGENV_VERSION is 0.32.2
digraph {
	"hello-app-server" ;
	"hello-app-server" -> "network";
	"hello-app-server" -> "security";
	"network" ;
	"security" ;
	"security" -> "network";
}
$


dotコマンドと通すことで視覚化できます。

## dotコマンドインストール
$ brew install graphviz
$
## INFOが邪魔なので取り除きます
$ $ terragrunt graph-dependencies | grep -v "[INFO]" | dot -Tpng > graph.png
$
## INFOがあるとエラー
$ terragrunt graph-dependencies | dot -Tpng > graph.png
Error: <stdin>: syntax error in line 1 near '
Warning: syntax ambiguity - badly delimited number '32m' in line 1 of <stdin> splits into two tokens
Warning: syntax ambiguity - badly delimited number '39m' in line 1 of <stdin> splits into two tokens
Warning: syntax ambiguity - badly delimited number '32m' in line 2 of <stdin> splits into two tokens
Warning: syntax ambiguity - badly delimited number '0.32.' in line 2 of <stdin> splits into two tokens
Warning: syntax ambiguity - badly delimited number '39m' in line 2 of <stdin> splits into two tokens
$


以下のような画像を作成できます。


以上、基本的なコマンドの紹介でした。



Tips

Terraformプラグインのディスク使用量を削減する

tfstate分割のデメリットの1つとして、リソース単位でTerraformの実行を行うため、それぞれのディレクトリ配下にプラグインがダウンロードされることが挙げられます。

.terraformディレクトリがそれです。Terragruntの場合は、.terragrunt-cacheディレクトリ配下に作成されます。

まず根本となるawsプロバイダーがMacでは200MB以上となり、場合によっては他のプラグインも必要になってきます。

リソースの分割数次第ですが、僕が携わっているシステムでは10GB弱となっていました。これが更に複数プロジェクト分となると、かなりのディスク容量を消費します。

対策として、terraformrcというTerraformの設定ファイルにplugin_cache_dirを設定することで、プラグインのディレクトリを一つにまとめることができます。

$ vi ~/.terraformrc
...
## 任意のディレクトリを設定します
plugin_cache_dir = "$HOME/.terraform.d/plugin-cache"
$
## なくても自動作成されますが念の為
$ mkdir -p $HOME/.terraform.d/plugin-cache
$


この設定をすることで、個別にダウンロードしていたものが以下のようにplugin_cache_dirへのリンク(103Byte)となりディスク使用量を大幅に削減できます

## environments/dev 配下
$ ls -l ./network/.terragrunt-cache/xxx/yyy/network/.terraform/providers/registry.terraform.io/hashicorp/aws/3.58.0/darwin_amd64
lrwxr-xr-x  1 tester  staff  103  9 19 17:56 ./network/.terragrunt-cache/xxx/yyy/network/.terraform/providers/registry.terraform.io/hashicorp/aws/3.58.0/darwin_amd64 -> /Users/tester/.terraform.d/plugin-cache/registry.terraform.io/hashicorp/aws/3.58.0/darwin_amd64
$

ローカル変数をグローバルに使う

親ディレクトリのhclファイルで定義したlocal変数は、そのままでは子ディレクトリのhclファイルで使用できませんが、include時に expose を指定することで使用可能になります。

親ディレクトリのhclファイルで以下のように定義していた場合、

locals {
  dirname = "network"
}

子ディレクトリで、以下のように include.論理名 と指定することで利用できるようになります。

include "root" {
  path = find_in_parent_folders()
  expose = true
}

terraform {
  source = "../../../modules/${include.root.locals.dirname}"
}

Terragruntの更新情報

Gruntwork Newsletterにまとまっており、とても便利でした。

.terraform.lock.hclをgitignoreしない

Terragruntのリポジトリにある.gitignoreになぜか.terraform.lock.hclが書いてあり、僕は、このignoreも参考にしてたので.terraform.lock.hclの存在をしばらく見過ごしてました。。。

Terraform公式ではコミットが推奨されてるので、ignoreから外したほうが良いかと思います。

今回は以上です〜ノシ

参考

(*ゝω・)ノ ァリガトネー

Terragurnt 公式ドキュメント
知らず知らずのうちに Terraform がディスクをほとんど一杯にしてた

関連書籍