3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Terragruntを使ってみたらTerraformのコピペが激減して感動したのですぐ実践できる基礎的な記法を解説してみる

Last updated at Posted at 2025-12-07

はじめに

こんにちは!ディップ株式会社でSRE見習いをしている新卒2年目ひよっこエンジニアの大賀です。

Terraform × AWSでプロダクトを運用していると、マルチアカウントで日々terraform applyを叩いたり複数の環境に水平展開したりするケースがよくあると思います。

多分多くの方が以下のような思いをした事があるのではないでしょうか。

  • ルートモジュール(実行ディレクトリ)ごとにbackend.tfとかprovider.tfとかterraform.tf書くの面倒だし管理大変。。。
  • 変数ファイル(.tfvars)が環境ごとに散らばるし形が定まらない
  • モジュール間の依存関係が把握しづらい/値を渡したりがゴチャる

Terraformはすごく自由度が高く記述できるし、ここ最近のTerraformやAWS Providerのバージョンはよくできていてかなりよしなにやってくれるので、割と適当に書いても全然普通に動くと思います。

だからこそ人によってコーディングスタイルがかなり変わってくるのでキャッチアップに苦心する場面もあるかと思います。

そんな課題は、Terragruntを導入することで、綺麗さっぱり忘れ去る事ができます。

かくいう私も使い始めて古のスパゲッティコードやとんでもなく面倒なバージョンアップ対応からおさらばできてTerraformを書くのが本当に楽しくなりました

本記事では、実際のプロダクトで運用しているTerragruntの構成を例に、DRYで保守しやすいTerraformコード管理の実践方法を解説しながら汎用的に使えるコードをまとめてみました!!

こんな人に読んでほしい

  • Terraformを使っているが、複数環境の管理に困っている方
  • Moduleの切り方、ディレクトリ構成に度々迷う方
  • Terragruntを聞いたことはあるが、具体的な使い方がわからない方
  • 既存のTerragrunt構成を改善したい方

Terragruntとは

Terragruntは、Gruntwork社が開発したTerraformのラッパーツールです。主な機能として、

  • DRYな変数管理: 重複した変数の定義/代入や値の抜け漏れによるエラーを減らせる
  • 設定の継承: 親設定ファイルから子へ設定を継承できる
  • 依存関係管理: モジュール間の依存関係を明示的に定義して実行順を最適化
  • コード生成: 全ルートモジュールのterraform.tf/backend.tf/provider.tf1ファイルにまとめられる

があり、Terraformの記述と管理を楽にしてくれます。
とはいえ自分で書いておきながら言うのもアレですが、これ初見じゃあんまりピンと来ないだろうなぁ...と思うので早速コードベースで解説していきます!

ディレクトリ構成

今回紹介する構成は以下のとおりです。

ディレクトリ構成
terraform/
├── root.hcl                    # ルート設定(全環境共通)
├── modules/                    # モジュール
│   ├── common/
│   ├── vpc/
│   ├── db/
│   ├── front/
│   ├── backend/
│   └── ...
└── envs/                       # 環境別設定
    ├── dev/
    │   ├── env.hcl             # dev環境固有設定(変数値代入ファイル)
    │   ├── vpc/                # ここの位置がルートモジュール
    │   │   └── terragrunt.hcl  # Terragrunt実行ファイル
    │   ├── db/
    │   │   └── terragrunt.hcl
    │   └── ...
    ├── stg/
    │   └── ...
    └── prod/
        └── ...

ポイント

  • modules/に環境非依存のTerraformモジュールを配置
  • envs/配下に環境別のTerragrunt設定(実行ディレクトリ)を配置
  • root.hclで全環境共通の設定を管理

見た事ないファイルがいくつかあると思いますが、概ね一般的なTerraformの構成と変わりません。

root.hcl - 全環境共通設定

まず、プロジェクトのルートに配置するroot.hclを見てみましょう。

root.hcl全文
root.hcl
#-----------------------------------------------
# root.hcl
#
# Terragruntルート設定ファイル
#-----------------------------------------------
################################################
# Locals
################################################
locals {
  tfstate_bucket_name = {
    prod = "prod-tfstate"
    stg  = "stg-tfstate"
    dev  = "dev-tfstate"
  }

  product_name = "test"
  env = basename(dirname(get_terragrunt_dir()))

  aws_profile = lookup({
    prod = "aws-prod"
    stg  = "aws-stg"
    dev  = "aws-dev"
  }, local.env, "")

  aws_account_id = {
    prod = "012345678901"
    stg  = "234567890123"
    dev  = "456789012345"
  }
}

################################################
# Backend Config
################################################
remote_state {
  backend = "s3"
  config = {
    bucket  = local.tfstate_bucket_name[local.env]
    key     = "${path_relative_to_include()}/terraform.tfstate"
    region  = "ap-northeast-1"
    encrypt = true
    profile = local.aws_profile
  }

  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
}

################################################
# Terraform Config
################################################
generate "terraform" {
  path      = "terraform.tf"
  if_exists = "overwrite_terragrunt"

  contents = <<EOF
terraform {
    required_providers {
        aws = {
            source  = "hashicorp/aws"
            version = "6.25.0"
        }
    }
    required_version = "1.14.1"
}
EOF
}

################################################
# Provider Config
################################################
generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"

  contents = <<EOF
    provider "aws" {
        region  = "ap-northeast-1"
        profile = "${local.aws_profile}"

    default_tags {
        tags = {
            Product        = "${local.product_name}"
            env            = "${local.env}"
            management     = "terraform"
        }
    }
}
EOF
}

各ブロックごとに解説していきます!

locals - 環境の判定

HCL
################################################
# Locals
################################################
locals {
  # 環境別のtfstateバケット名
  tfstate_bucket_name = {
    prod = "prod-tfstate"
    stg  = "stg-tfstate"
    dev  = "dev-tfstate"
  }

  product_name = "test"

  # 実行ディレクトリの絶対パスから環境名を取得
  # envs/dev/vpc → "dev"
  env = basename(dirname(get_terragrunt_dir()))

  # AWSプロファイルを↑のenvから判定
  aws_profile = lookup({
    prod = "aws-prod"
    stg  = "aws-stg"
    dev  = "aws-dev"
  }, local.env, "")

  # 環境別のAWSアカウントID
  aws_account_id = {
    prod = "012345678901"
    stg  = "234567890123"
    dev  = "456789012345"
  }
}

ポイント解説

1. basename(dirname(get_terragrunt_dir()))で環境名を自動取得

env = basename(dirname(get_terragrunt_dir()))

get_terragrunt_dir()はTerragruntの固有関数で、現在のterragrunt.hclがある実行ディレクトリの絶対パスを返します。

例:/path/to/terraform/envs/dev/vpc/terragrunt.hclを実行すると

  • get_terragrunt_dir()/path/to/terraform/envs/dev/vpc
  • dirname()/path/to/terraform/envs/dev
  • basename()dev

といった感じで、ディレクトリ構造から環境名を判定できます。

2. lookup()で環境別設定をマッピング

aws_profile = lookup({
  prod = "aws-prod"
  stg  = "aws-stg"
  dev  = "aws-dev"
}, local.env, "")

lookup()関数で環境名からAWSプロファイル名を取得します。第3引数はデフォルト値で、マッチしなかった場合は値を入れていればそれを、値を入れなければエラーになります。

remote_state - backend.tfの生成

HCL
################################################
# Backend Config
################################################
remote_state {
  backend = "s3"
  config = {
    bucket  = local.tfstate_bucket_name[local.env]
    key     = "${path_relative_to_include()}/terraform.tfstate"
    region  = "ap-northeast-1"
    encrypt = true
    profile = local.aws_profile
  }

  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
}

ポイント解説

1. path_relative_to_include()でユニークなstate key

key = "${path_relative_to_include()}/terraform.tfstate"

この関数は、root.hclから見た実行ディレクトリの相対パスを返します。

例:envs/dev/vpc/terragrunt.hclから呼ばれると

  • path_relative_to_include()envs/dev/vpc
  • S3 Backendのkey → envs/dev/vpc/terraform.tfstate

このため、モジュールごとに自動でユニークなstate keyを設定できます。

2. generateブロックで backend.tf を自動生成

HCL
generate = {
  path      = "backend.tf"
  if_exists = "overwrite_terragrunt"
}

この記述をするとterragrunt plan/apply実行時に、実行ディレクトリに作成される.terragrunt-cacheディレクトリ(←.gitignore対象)に以下のようなbackend.tfが自動生成され、これの設定を利用してTerraformがstateを書き込みます。

.terragrunt-cache/backend.tf
# 自動生成される backend.tf
terraform {
  backend "s3" {
    bucket  = "dev-tfstate"
    key     = "envs/dev/vpc/terraform.tfstate"
    region  = "ap-northeast-1"
    encrypt = true
    profile = "aws-dev"
  }
}

generate - provider.tf と terraform.tf の生成

HCL
################################################
# Terraform Config
################################################
generate "terraform" {
  path      = "terraform.tf"
  if_exists = "overwrite_terragrunt"

  contents = <<EOF
terraform {
    required_providers {
        aws = {
            source  = "hashicorp/aws"
            version = "6.25.0"
        }
    }
    required_version = "1.14.1"
}
EOF
}

################################################
# Provider Config
################################################
generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"

  contents = <<EOF
provider "aws" {
    region  = "ap-northeast-1"
    profile = "${local.aws_profile}"

    default_tags {
        tags = {
            Product        = "${local.product_name}"
            env            = "${local.env}"
            management     = "terraform"
        }
    }
}
EOF
}

ポイント解説

1. Terraform/Providerのバージョン変更はここを触るだけでOK

HCL
terraform {
    required_providers {
        aws = {
            source  = "hashicorp/aws"
            version = "6.25.0"
        }
    }
    required_version = "1.14.1"
}

Terragruntの場合、Terraformや各種Providerのバージョンアップ時はこの箇所を操作して実行ディレクトリでterragrunt init -upgradeコマンドを実行するだけでOKです。

おそらくこれをご覧になって「え、どのModuleも固定されるのはちょっと...」と思われる方もいるかとは思いますが、基本的にはバージョンは高いものに合わせて変更していく方がコードヘルスの維持には効果的だと思いますので、Module側で巻き取る対応をお勧めします。(もちろん、>=などの比較演算子も引き続き利用可能ですのでご心配なく!)

2. default_tagsで全リソースに共通タグを自動付与

HCL
default_tags {
    tags = {
        Product        = "${local.product_name}"
        env            = "${local.env}"
        management     = "terraform"
    }
}

AWS Provider 3.38.0以降で使えるdefault_tagsにより、Resourceブロックでtagsを定義しなくてもタグ付けができるすべてのAWSリソースに自動でタグが付与されます。

Terragruntの場合はこれがより強化された形で、どの環境、どの実行ディレクトリでもterragrunt.hclroot.hclを参照すれば必ず適切な環境のタグを付与できます。

  • 必要なタグ(例:CmBillingGroupなど)の付け忘れ防止
  • 環境識別が容易
  • Terraform管理リソースの識別が一括して可能

など、かなり便利です。

実行ディレクトリでの設定継承(include + expose)

各環境のterragrunt.hcl(例:envs/dev/vpc/terragrunt.hcl)では、root.hclの設定を継承します。

terragrunt.hcl
include "root" {
  path   = find_in_parent_folders("root.hcl")
  expose = true  # ← これが重要!
}

dependencies {
  paths = ["../common"]
}

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

terraform {
  source = "../../../modules/vpc/"
}

inputs = {
  # include.root.locals.xxx で親のlocalsを参照
  product_name = include.root.locals.product_name
  env          = include.root.locals.env

  # 環境固有の設定(terraformで言うmain.tfに記載する変数への値の代入)
  cidr_block   = "192.168.32.0/20"
  public_subnets = [
    "192.168.32.0/23",
    "192.168.34.0/23",
    "192.168.36.0/23"
  ]
  private_subnets = [
    "192.168.38.0/23",
    "192.168.40.0/23",
    "192.168.42.0/23"
  ]

  # 他モジュールの出力を参照
  log_bucket = dependency.common.outputs.log_bucket
}

ポイント解説

1. expose = trueで指定したファイルのlocalsを参照可能に

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

expose = trueを指定することで、include.root.locals.xxxの形式で親ファイル(root.hcl)のlocalsにアクセスできます。

HCL
inputs = {
  product_name = include.root.locals.product_name  # → "test"
  env          = include.root.locals.env           # → "dev"
}

これにより、環境名やプロダクト名を各terragrunt.hclで定義する必要がなくなります
ミスが減る上、使いまわしやすくもなるので本当に助かります。

find_in_parent_folders("root.hcl")はTerragruntの固有関数でカレントディレクトリから親ディレクトリまで遡って、最初に見つかったroot.hclを読み取る、という内容です。

2. dependencyブロックでモジュール間依存を管理

HCL
dependency "common" {
  config_path = "../common"
}

inputs = {
  log_bucket = dependency.common.outputs.log_bucket
}

Terragruntの目玉機能の1つです。dependencyブロックを使うと、他モジュールのoutputsを参照できます。

詳しい仕組みの説明は省きますが、Terragruntは他モジュールのoutputsdependencyの記載から依存関係を解析し、適切な順序でplan/applyを実行します。(スゴい!😭)

実行コマンド

/path/to/terraform $
# 特定モジュールのplan
cd envs/dev/vpc
terragrunt run plan

# 特定モジュールのapply
terragrunt run apply

# 全モジュールを依存順にapply(envs/dev配下すべて)
cd envs/dev
terragrunt run --all apply

# 全環境の全モジュールのplan
cd envs
terragrunt run --all plan #(--terragrunt-parallelismオプションで最大並列実行数を指定可能)

# Terraform/Providerバージョンアップ時(plan前に実行)
cd envs #(任意の粒度)
terragrunt run --all init -upgrade 

なお、TerragruntにはAuto-init機能があるためTerraform/Providerをバージョンアップした時以外はterragrunt run initコマンドを実行する必要はありません
TerragruntのCLIは引数や利用法が多岐に渡るため、詳しくはドキュメントもご確認ください。

まとめ

いかがでしたでしょうか。

Terragruntを導入してみて、今まで素のTerraformでコピペしまくってたterraform.tf/backend.tf/provider.tfを書かなくて良くなったし、terragrunt.hclのうちかなりの割合が使いまわせるし、基本のタグ付けに悩まなくなったし、実行順がAWSリソースのリレーションに基づいて最適化されるので一括実行の頻度が上がったし、そのおかげでかれこれ半年以上余計な差分が出なくなったし、面倒だったバージョンアップも習慣化できて、コードの質がかなり高まりました。

ぜひTerraformを書かれる皆様は1度お試しいただければと嬉しいです!

長々と書いてしまいましたが、お読みいただきありがとうございました!

参考リンク

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?