Terragruntによるシステム管理

10 min read読了の目安(約9600字

概要

Terragrunt を使い、複数のディレクトリで定義したTerraformコードを1コマンドで実行できます。
Terragruntでのリソースの管理方法は下記の通りとなります。

  1. ディレクトリで各システムのコンポーネントを構築するTerraformコードの作成
  2. ルートディレクトリでの terragrunt.hclの定義
  3. 各ディレクトリでの terragrunt.hclの定義
  4. Terragruntコマンドによるシステム構築

コードサンプル

Terragruntとは

Terragruntは、Terraformをラップしたツールで、複数のstate ファイルを統合的に管理し、TerraformコードをDRYに管理できるツールです。
Terragruntで実現できることは以下の通りとなります。

  • 複数のディレクトリで定義されたTerraformコードの一括実行
  • 各ディレクトリで重複するコードの自動生成
  • backendブロックでの変数定義

ツールの背景

システムの構築において、開発・ステージング・本番環境と複数の環境を構築し、運用していきます。それら環境をTerraformで構築するにあたり、dev,stg,prd環境をディレクトリごとに下記構成でTerraformコードを作成し管理したとします。
こうしたとき、Terraform実行するには、各ディレクトリ (例 stg/stg/app)へ移動し、そこでコマンド実行する必要があります。また、各ディレクトリにproviderおよび状態を管理するためのbackend情報の定義が必要になります。
よって複数のディレクトリへの同じ内容の定義とコマンド実行が必要になります。
また、 vpc -> db -> appの順で実行しないとすべてのシステムの構築ができない場合、その順序性は、各ディレクトリのTerraformコードに記述することはできません。
これらの解決方法のために、Terragruntが開発されたとされています。
しかし、この例であれば、Terrafom workspaceとTerraform moduleの機能を使うと当該問題は解決可能です。

stack
├── dev
│   ├── app
|   |    |- main.tf
|   |    |- provider.tf
|   |    └─ backend.tf 
│   ├── db
|   |    |- main.tf
|   |    |- provider.tf
|   |    └─ backend.tf
│   └── vpc
|        |- main.tf
|        |- provider.tf
|        └─ backend.tf
├── stg
│   ├── app
|   |    |- main.tf
|   |    |- provider.tf
|   |    └─ backend.tf 
│   ├── db
|   |    |- main.tf
|   |    |- provider.tf
|   |    └─ backend.tf
│   └── vpc
|        |- main.tf
|        |- provider.tf
|        └─ backend.tf
└── prd
    ├── app
    |    |- main.tf
    |    |- provider.tf
    |    └─ backend.tf 
    ├── db
    |    |- main.tf
    |    |- provider.tf
    |    └─ backend.tf
    └── vpc
         |- main.tf
         |- provider.tf
         └─ backend.tf

Terragruntのインストール

Terragruntのインストール方法は、Mac、Windows、Linuxの方法が紹介されています。
ここでは、Macの方法のみ紹介します。その他の方法は公式ドキュメントを参考にしてください。
また、TerragruntのDocker Hubのページは、 当該ページとなっています。
MacのインストールではHomebrewでパッケージ管理されているため、下記コマンドでインストール可能です。

brew install terragrunt

Terragruntの使い方

ここでは、GCPの環境に下記のようなGCEとVPCネットワークからなるシステムを例にTerragruntにて構築する方法を説明します。また、設定値も以下の通りとなっています。
また、当該コンポーネントを構築するにあたり、ディレクトリ構成は下記のように各コンポーネントごとに定義します。構築にあたっては、当該 gce,vpc networkモジュールを活用します。

システム構成

ネットワーク

環境 VPC Network 名 サブネットワーク名 cidr リージョン
dev dev dev 192.168.10.0/24 asia-northeast1
stg stg stg 192.168.10.0/24 asia-northeast1
prd prd prd 192.168.10.0/24 asia-northeast1

gce

環境 gce名 machine type zone subnetwork image disk size
dev dev f1-micro asia-northeast1-b dev ubuntu-os-cloud/ubuntu-2004-lts 10
stg stg f1-micro asia-northeast1-b stg ubuntu-os-cloud/ubuntu-2004-lts 10
prd prd f1-micro asia-northeast1-b prd ubuntu-os-cloud/ubuntu-2004-lts 10

ディレクトリ構成

stack
├── gce
│   └── main.tf
└── network
    └── main.tf

設定方法

Terragruntの設定は、 terragrunt.hcl に定義します。
当該 terragrunt.hclファイルは、各ディレクトリと、その親ディレクトリに定義します。
よって、terragrunt.hclを含むディレクトリ構成は、下記のようになります。

stack
├── gce
│   |── main.tf
|   └── terragrunt.hcl
└── network
│   |── main.tf
|   └── terragrunt.hcl
|
└── terragrunt.hcl

設定できる項目は、下記の通りとなります。 (親: 親のterragrunt.hcl、 子: 子のterragrunt.hcl で定義する内容)

  • 各ディレクトリで共通するロジックの記述 (親)
  • backendの設定 (親)
  • ディレクトリ間の構築の実行順序の依存関係 (子)
  • ディレクトリへの設定値の挿入 (子)

各ディレクトリで共通するロジックの記述

Terragruntは親ディレクトリの terragrunt.hclにgenerate ブロックを定義し、
当該ブロックにロジックを記述することで、子ディレクトリの terragrunt.hclがロジックを参照し、
ロジックの内容のファイルを生成する。
共通するロジックにおいて、必須となるもは下記の通りであるため、
ロジックが各ディレクトリで完全に共通する場合、親のterragrunt.hclにその内容を記述します。

  • provider情報
  • backend情報

親ディレクトリのterragrunt.hclの参照方法

子ディレクトリのterragrunt.hclは、親ディレクトリの terragrunt.hclを参照することができます。
親ディレクトリのterragrunt.hclの参照方法は、 include ブロックで、下記のように記述することで、参照することができます。

include {
  path = find_in_parent_folders()
}

上記の find_in_parent_folders()は、自身よりも上位階層の terragrunt.hclのパスを探し出すterragruntの組込み関数となっています。
当該例では、../terragrunt.hclと親ディレクトリのterragrunt.hclパスを入力します。

provider情報

Terragruntは各ディレクトリでTerraformコマンドを実行するため、 cloud providerの情報を各ディレクトリに定義する必要があります。
親ディレクトリの terragrunt.hclにprovider.tfを作成する下記ロジックを記載すると、子ディレクトリに親ロジックの内容のprovider.tfが作成されます。

generate "provider" {
  path = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents = <<EOF
provider "google" {
  project = "${get_env("TF_VAR_project")}"
}
EOF
}

backend情報

Terraformのstatefileをクラウドのオブジェクトストレージ(s3やgcsなど)で管理する場合、terraformブロックを定義し、保存先ストレージを定義します。当該内容も各ディレクトリで定義する必要があります。
Terragruntでの定義の方法は、下記の通りとなります。

  • 利用するbackendの種類のみの定義 (子)
  • backendの設定内容 (親)

GCSをbackendに指定したとき、下記内容を各子ディレクトリに定義する必要があります。

terraform {
  backend "gcs" {}
}

当該内容も子ディレクトリで共通するので、親のterragrunt.hclのgenerateブロックで下記のように定義することで、1箇所での定義で済みます。

generate "backend" {
  path = "terraform.tf"
  if_exists = "overwrite_terragrunt"
  contents = <<EOF
terraform {
  backend "gcs" {}
}
EOF
}

backendの具体的な設定内容 ( 保存先のバケット名やディレクトリ名など ) は、親ディレクトリの terragrunt.hcl ファイルのremote_stateブロックに下記のように定義します。

remote_state {
  backend = "gcs"

  config = {
    bucket = "${get_env("TF_VAR_project")}-state"
    prefix = "terragrunt/${path_relative_to_include()}"
  }
}

config ブロック内が、backendブロック内に代入されます。

ディレクトリ間の実行順序の依存関係

Terraformはstatefileに跨って参照することができません。そのため、実行順序の制御ができません。よって今回の例であれば、networkのディレクトリでコマンド実行した後にgceのディレクトリのコマンドを実行させたいが、そういった制御ができません。
Terragruntでは、依存先が存在するディレクトリのterragrunt.hclにdependencyブロックを定義し、依存先のパスを定義することで、実行順序の制御が可能となります。
network -> gceの順で、terraformコマンドを実行する場合は、以下のようにgceディレクトリのterragrunt.hclにdependencyブロックを定義すると、gceディレクトリはnetworkディレクトリでterrformコマンドを実行した後に実行されます。

dependency "network" {
  config_path = "../network"
}

ディレクトリへの設定値の挿入

先で述べた通り、Terraformはstatefileを跨って値の参照ができないので、各ディレクトリで実行した値の参照をterraformコマンドで参照することができません。
よって、各ディレクトリで実行した結果は、dataブロックなどを使い参照する必要があります。しかし、すべてのresourceに対して、dataブロックが提供されていないため、dataブロックを使った参照ができないことがあります。 (google_compute_firewallなど)

Terragruntは各ディレクトリの outputブロックの出力結果を outputs変数にmapで管理しています。
networkディレクトリでouput sub を定義した場合、gceディレクトリで次のように参照することができます。
また、networkブロックの構築前にterraform planコマンドを実行すると、networkディレクトリのsubのoutputを参照できずにエラーとなる。そういったことを解消するために、リソース実体がない場合にmocのデータを入力するmock_outputsや当該値を参照するときのterraformコマンドをdependencyブロックに定義する。

dependency "network" {
  config_path = "../network"
  skip_outputs = get_env("SKIP_OUTPUT")

  mock_outputs = {
    sub = "module.network.subnetwork_self_link"
  }
  mock_outputs_allowed_terraform_commands = ["validate", "plan", "workspace"]
}

inputs = {
  subnet_self_link = dependency.network.outputs.sub
}

上記の例であれば、dependencyブロックnetworkのouptutブロックのsubの値をsubnet_self_linkを本terragrunt.hclが存在するディレクトリ(gceディレクトリ)に挿入する。
また、mockではsub変数に module.network.subnetwork_self_linkを入力します。これは、terraform validate , terraform plan, terraform workspace コマンドを実行するときに代入されます。
outputの値を参照するとき(terraform apply, terraform destroy)、 skip_outputs = false とし、 outputの値を参照しないとき(terraform plan, terraform validate, terraform workspace)、 skip_outputs = true とします。

挿入した変数 (subnet_self_link)は、Terraformの環境変数として入力される。そのため、Terraformコードに環境変数を参照するvariableを定義する必要があります。
今回の例であれば、以下の通りとなります。

variable "subnet_self_link" {
  type = string
}

実行方法

terragrunt にterraformコマンドの引数を渡して実行する。Terragruntは、Terraformをラップしたツールであるため、Terraformで実行できるタスクはすべて実行可能となります。
実行方法は、ルートディレクトリでterragruntコマンドを実行します。各タスクの実行方法は下記の通りです。

  • 作成: terragrunt run-all apply
  • 削除: terragrunt run-all destroy
  • 確認: terragrunt run-all plan
  • 切替: terragrunt run-all workspace
  • 検証: terragrunt run-all validate

利用用途

本ツールは、先にも述べた通り、terraform workspaceとモジュールで解決可能です。
そのため、積極的に使うメリットはあまりないですが、下記のようなときは使えると思います。

  • workspaceで設定値の条件分岐をさせない
  • 特定のリソースのみstateファイルを分けたいが、コマンド実行は一括でおこないたい
  • terraformブロックに処理を記述したい
  • リソースの分割に失敗し管理が煩雑になったのを解消させたい

workspaceで設定値の条件分岐をさせない

Terraformのworkspaceはworkspaceを作成すると、terraform.workspaceにより、workspace名を参照できます。この機能を使い、設定値の切り分けが可能となります。

例えば、prd・stg・dev環境でGCEのマシンタイプを変える場合、以下のように実装ができます。

locals {
  machine_type = {
    prd = "n1-standard2"
    stg = "n1-standard1"
    dev = "f1-micro"
  }
}

resource google_compute_instance main {
  name = "sample"
  machine_type = local.machine_type[terraform.workspace]
  ...
}

しかし、上記のように分岐させる設計とすると、設定値を確認するとき、workspaceの処理を考え設定値を理解する必要があり理解に時間がかかります。
よって、workspaceを使わず、処理を分岐させたくないという設計思想でTerraformのコード管理をする場合、Terragruntは選択肢の1つとなります。

特定のリソースのみstateファイルを分けたいが、コマンド実行は一括でおこないたい

statefileの編集が運用しているときに発生します。そういったとき誤って、statefileを削除した場合などのオペレーションミスを防ぎたくファイルを分割させたい。そして、コマンドでの通常運用は、一括でおこないたい場合など活用が見込まれます。

terraformブロックに処理を記述したい

Terraformではterraformブロック内に処理を記述できません。例えば、local変数を参照させたりなどができません。よってterraformブロック内に処理を記述したい場合などは、利用することとなります。
例えば、環境ごとにbackendのバケットを代えたいなどが考えられます。

リソースの分割に失敗し管理が煩雑になった場合

Terraformのディレクトリ設計は始めに変更のライフサイクルも考え、切り分ける必要があります。
そうした考慮のないままに分割した場合、変更ごとに必要なディレクトリへと移動し、コマンド実行が必要となってきます。そうした作業はオペレーションのミスにつながり障害につながります。
そこで構築したterraformコードを統合させようとしたとき、tfstatefileの編集が必要になり、さらに高度なオペレーションが必要となってきます。
そういったとき、単純に依存関係のみ記載し、statefileを別々に運用するようにする当該ツールは、役に立つと考えられます。

まとめ

Terragruntツールの使い方について、GCPにVPCとGCEからなるシステムを構築する例をもちい説明をしました。本ツールはstatefileを跨りterraformの管理を統括できるツールとなっています。
始めから当該ツールを活用することはないですが、運用し続けたシステムを統合するなどがあった場合などに活用が見込まれるものと思います。