🌟

最近のterragrunt: Stacksを使う

に公開

はじめに

terragrunt v1.0がいよいよ近づいてきており楽しみですね。

stacksとは、terragruntの比較的新しい[1]機能です。

公式ドキュメントでは、stacksが2種類定義されています。

  • 暗黙的なstacks
  • 明示的なstacks

このうち、暗黙的なstacksは(当時はstacksとは呼ばれていなかったと思われますが)これまでのterragruntの記事でよく言及されている形です。
今回は明示的なstacksについてを主題とします。

明示的なstacksを利用することで、インフラ構造を論理的にまとめることができ、またコードのコピペを削減しよりDRYにすることが可能です。

stacksという言葉が出てくる以前のterragruntの利用方法についてはこの記事では詳細を解説していません。「terragrunt 入門」等で検索すると先人の記事がいくつか出てくるかと思います。

用語の紹介

unit

ユニットとは、terragrunt.hclを含むディレクトリのことです。
従来の構成でお馴染みのやつです。

stack

スタックとは、関連があるユニットの集合です。
自由に定義可能ですが、例えば、vpcユニットとec2ユニットが1つのスタックと定義されたり、dev環境にデプロイする必要があるユニット全てが1つのスタックとして定義されます。

冒頭で言及した通り、暗黙的なスタックと明示的なスタックがあります。

  • 暗黙的なスタック
    • ワーキングディレクトリ以下のユニットを全て1つのスタックと見なします。
    • 配下に幾つかのユニットがあるディレクトリ(例えばdev/やprod/)でterragrunt run --all applyをすると一括で実行されるかと思いますが、この挙動を暗黙的なスタックと呼びます。
  • 明示的なスタック
    • terragrunt.stack.hclというファイル内に任意のユニットを記述し、スタックとして定義します。
    • .terragrunt-cacheと同じような挙動で、.terragrunt-stackディレクトリ配下にユニットを生成します。

補足: 従来のterragrunt構成パターン

従来のterragruntディレクトリ構成(= 暗黙的なスタック)の例を以下に示します。

├── live
│   ├── root.hcl
│   ├── dev ← 暗黙的なスタック
│   │   ├── ec2 ← ユニット
│   │   │   └── terragrunt.hcl
│   │   └── vpc ← ユニット
│   │       └── terragrunt.hcl
│   └── prod ← 暗黙的なスタック
│       ├── ec2 ← ユニット
│       │   └── terragrunt.hcl
│       └── vpc ← ユニット
│           └── terragrunt.hcl
└── modules
    ├── ec2
    │   └── main.tf
    └── vpc
        └── main.tf

このとき、各terragrunt.hclの中身は次のような内容になっているはずです。

live/dev/ec2/terragrunt.hcl
include "root" {
  path = find_in_parent_folders("root.hcl")
}

terraform {
  source = "<何らかのtg関数>/modules/ec2"
}

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

inputs = {
  <key0> = dependency.vpc.outputs.value0
  <key1> = "dev value1"
  <key2> = "dev value2"
}
live/prod/ec2/terragrunt.hcl
include "root" {
  path = find_in_parent_folders("root.hcl")
}

terraform {
  source = "<何らかのtg関数>/modules/ec2"
}

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

inputs = {
  <key0> = dependency.vpc.outputs.value0
  <key1> = "prod value1"
  <key2> = "prod value2"
}

modules/vpcのvariableに更新があった場合、devのterragrunt.hclのinputsブロックを更新し、prodのterragrunt.hclのinputsブロックを更新し、さらに別のアカウント/環境等があるのであれば、その数だけ更新しなければならない可能性があります。

terragrunt.stack.hclの構文

terragrunt.stack.hclファイルは、2つのブロック(とlocalsブロック)をサポートします。

unitブロック

unitブロックは、ユニット、つまりterragrunt.hclを持つディレクトリを生成するためのブロックです。

terragrunt.hclの生成はmoduleとユニットの関係に似ています。前項で例示した2環境分のterragrunt.hclは、key1,2に与える値のみが異なり、他の部分は同一です。

この場合、共通部分を書き出したterragrunt.hcl(これはカタログと呼称されています)を参照しつつ、環境間(厳密にはユニット間)で値が異なる部分を次のように与えます。

catalog/units/ec2/terragrunt.hcl
# 共通部分を書き出したterragrunt.hcl
include "root" {
  path = find_in_parent_folders("root.hcl")
}

terraform {
  source = "<何らかのtg関数>/modules/ec2"
}

dependency "vpc" {
  config_path = values.config_path # config_pathは変数にした方が柔軟に扱えます
}

inputs = {
  <key0> = dependency.vpc.outputs.value0
  <key1> = values.<key1>
  <key2> = values.<key2>
}
dev/terragrunt.stack.hcl
# vpcのカタログは省略
unit "vpc" {
  source = "${find_in_parent_folders("catalog/units")}/vpc"
  path   = "vpc"
  values = {
    cidr = "10.0.0.0/16"
  }
}

unit "ec2-1" {
  source = "${find_in_parent_folders("catalog/units")}/ec2"
  path   = "ec2-1"
  values = {
    config_path = "../vpc"
    <key1>      = "dev value1"
    <key2>      = "dev value2"
  }
}

unit "ec2-2" {
  source = "${find_in_parent_folders("catalog/units")}/ec2"
  path   = "ec2-2"
  values = {
    config_path = "../vpc"
    <key1>      = "dev value3"
    <key2>      = "dev value4"
  }
}

この例では、terragrunt.stack.hclは1つのカタログからec2ユニットを2つ生成します。sourceではベースとなる共通部分を書き出したterragrunt.hclを、pathではterragrunt.hclが生成されるディレクトリ名(= ユニット名)を指定できます。
また、valuesキーで値を渡すことができます。

stackブロック

unitブロックではユニットを生成しますが、stackブロックではスタックを生成できます。
スタックはユニットの集合でした。上で例示したdev/terragrunt.stack.hclはスタックです。stackブロックはterragrunt.stack.hcl自体をカタログと見なし、terragrunt.stack.hclで呼び出すことができます。いわゆるネストが可能です。

catalog/stacks/two-ec2/terragrunt.stack.hcl
unit "ec2-1" {
  source = "${find_in_parent_folders("catalog/units")}/ec2"
  path   = "ec2-1"
  values = {
    config_path = values.config_path
    <key1>      = values.<1-key1>
    <key2>      = values.<1-key2>
  }
}

unit "ec2-2" {
  source = "${find_in_parent_folders("catalog/units")}/ec2"
  path   = "ec2-2"
  values = {
    config_path = values.config_path
    <key1>      = values.<2-key1>
    <key2>      = values.<2-key2>
  }
}
dev/terragrunt.stack.hcl
unit "vpc" {
  source = "${find_in_parent_folders("catalog/units")}/vpc"
  path   = "vpc"
  values = {
    cidr = "10.0.0.0/16"
  }
}

stack "two-ec2-1" {
  source = "${find_in_parent_folders("catalog/stacks")}/two-ec2"
  path   = "two-ec2-1"
  values = {
    config_path = "../../../vpc"
    <1-key1>    = "dev value1"
    <1-key2>    = "dev value2"
    <2-key1>    = "dev value3"
    <2-key2>    = "dev value4"
  }
}

stack "two-ec2-2" {
  source = "${find_in_parent_folders("catalog/stacks")}/two-ec2"
  path   = "two-ec2-2"
  values = {
    config_path = "../../../vpc"
    <1-key1>    = "dev value1"
    <1-key2>    = "dev value2"
    <2-key1>    = "dev value3"
    <2-key2>    = "dev value4"
  }
}

値も上のレイヤーからvaluesのネストで渡せます。スタックもユニットと同様に再利用可能です。
複数のstackブロック、unitブロックを同時に使用でき、これはインフラの管理単位を表現できます。

ディレクトリ構成と操作方法

ディレクトリ構成は公式ドキュメントでもまだ揺れています。
今回はチュートリアルに従っていますが、管理しやすいように自由に構成すれば良いと思います。

├── live
│   ├── root.hcl
│   ├── dev
│   │   └── terragrunt.stacks.hcl
│   └── prod
│       └── terragrunt.stacks.hcl
└── catalog
    ├── modules
    │   ├── ec2
    │   │   └── main.tf
    │   └── vpc
    │       └── main.tf
    ├── units
    │   ├── ec2
    │   │   └── terragrunt.hcl
    │   └── vpc
    │       └── terragrunt.hcl
    └── stacks
        └── two-ec2
            └── terragrunt.stacks.hcl

基本的な使い方は従来のterragruntを利用していれば特に難しくなく、
terragrunt stack generateコマンドでユニットを生成し、terragrunt stack run plan/applyで実行可能です。
terragrunt plan/applyinitが省略できるように、generateなしでplan/applyも可能です。

ワーキングディレクトリ以下のファイルに応じてユニットを生成する使用感も従来と同様です。
live/で実行すればdevとprodがどちらも生成されます。stackのネストを使えば、devとprodのフォルダをなくし単一のterragrunt.stack.hclで賄うことも可能ではあります。

まとめ

スタックは、unitブロックによって従来のterragruntのユニットに対し、ユニット(任意の1単位のインフラコード)を抽象化した上位レイヤーを1層追加できます。また、stackブロックのネストによってさらに上位の抽象化レイヤーを追加できます。

これにより開発者は、開発・運用・保守において管理可能な単位をファイルの記述によって定義できます。この柔軟さは、ディレクトリ構成でしか管理可能な単位を表現できなかった従来の暗黙的スタックに比べた利点となります。
また、terragruntの設計思想であるDRYをより強化する仕組みになっています。

一方で、新しい概念の習得が必要であったり、インフラの規模、開発・運用・保守チームの規模、ライフサイクル等によっては複雑になりすぎる可能性もあり、採用には検討が必要です。また、コードのコピペはかなり軽減されるものの完全には0にはならない点、(少なくとも当分は)下位(= 暗黙的スタック)互換が守られる点も注意する必要があります。

明示的なスタックは暗黙的なスタックの単純な上位互換ではないということですね。

公式ドキュメント

脚注
  1. とはいえstacksのexperimentが完了したのは25年5月ですが、日本のterragrunt入門系の記事にはあまり登場しないくらいの現状のように思います。そもそもユーザーが少ないという話は…あるかもしれません
    https://github.com/gruntwork-io/terragrunt/releases/tag/v0.78.0 ↩︎

Discussion