0
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?

Terramate globalsでTerraformの環境分離をDRYに実装する — dev/stg/prodのコピペ地獄から脱出

0
Last updated at Posted at 2026-03-25

はじめに

Terraformで複数環境(dev/stg/prod)を管理していると、backend.tfprovider.tf を環境ごとにコピペする問題に直面する。Workspaceで分ける手もあるが、state分離の粒度やprovider設定の柔軟性に限界がある。

この記事では、Terramateglobalsgenerate_hcl を使って、環境ごとの設定をDRYに管理する具体的な実装手順を紹介する。

📌 この記事で扱うこと:

  • Terramate globalsの階層的な変数継承の仕組み
  • generate_hcl で backend/provider を環境ごとに自動生成する方法
  • 実際にハマった罠と解決策

環境

項目 バージョン
Terraform 1.9.x
Terramate 0.11.x
AWS Provider ~> 5.0

実装

ディレクトリ構成を設計する

まず、環境分離のためのディレクトリ構成を決める。ポイントはスタック(リソース群)と環境設定を分離することだ。

infra/
├── terramate.tm.hcl        # ルート設定
├── globals.tm.hcl           # 全環境共通のglobals
├── stacks/
│   ├── network/
│   │   ├── stack.tm.hcl
│   │   ├── _gen.tm.hcl     # generate_hcl定義
│   │   ├── main.tf
│   │   └── variables.tf
│   └── app/
│       ├── stack.tm.hcl
│       ├── _gen.tm.hcl
│       ├── main.tf
│       └── variables.tf
└── envs/
    ├── dev/
    │   ├── globals.tm.hcl   # dev固有の値
    │   └── stacks/          # → ../../../stacks をsymlink
    ├── stg/
    │   ├── globals.tm.hcl
    │   └── stacks/
    └── prod/
        ├── globals.tm.hcl
        └── stacks/

ここで envs/dev/stacks/../../stacks/ へのシンボリックリンクだ。これにより、スタックの実体は1箇所で管理しつつ、環境ごとに異なる globals を注入できる。

ルートのglobals を定義する

globals.tm.hcl に全環境で共通の値を定義する:

# infra/globals.tm.hcl
globals {
  project_name = "myproject"
  aws_region   = "ap-northeast-1"

  # 全環境共通のタグ
  common_tags = {
    ManagedBy = "terraform"
    Project   = global.project_name
  }

  # tfstateバケット名のテンプレート
  state_bucket = "tfstate-${global.project_name}-${global.environment}"
}

global.environment はこの時点では未定義だ。子ディレクトリの globals.tm.hcl で定義される前提で参照している。

環境ごとのglobals でオーバーライドする

各環境のディレクトリに globals.tm.hcl を置いて、環境固有の値を定義する:

# infra/envs/dev/globals.tm.hcl
globals {
  environment   = "dev"
  account_id    = "111111111111"
  instance_type = "t3.small"

  # dev だけ削除保護を無効化
  deletion_protection = false
}
# infra/envs/prod/globals.tm.hcl
globals {
  environment   = "prod"
  account_id    = "999999999999"
  instance_type = "m5.large"

  deletion_protection = true

  # prod だけ追加のタグ
  common_tags = tm_merge(global.common_tags, {
    CriticalLevel = "high"
  })
}

globalsの継承ルール: Terramateはディレクトリツリーを上に辿って globals をマージする。子で同じキーを定義するとオーバーライドされる。

generate_hcl で backend/provider を自動生成する

各スタックに _gen.tm.hcl を置いて、globalsから .tf ファイルを生成する:

# stacks/network/_gen.tm.hcl

generate_hcl "_backend.tf" {
  content {
    terraform {
      backend "s3" {
        bucket         = global.state_bucket
        key            = "${global.environment}/network/terraform.tfstate"
        region         = global.aws_region
        dynamodb_table = "terraform-lock-${global.environment}"
        encrypt        = true
      }
    }
  }
}

generate_hcl "_provider.tf" {
  content {
    provider "aws" {
      region = global.aws_region

      assume_role {
        role_arn = "arn:aws:iam::${global.account_id}:role/TerraformRole"
      }

      default_tags {
        tags = tm_merge(global.common_tags, {
          Environment = global.environment
          Stack       = "network"
        })
      }
    }
  }
}

terramate generate を実行すると、各環境のスタックディレクトリに _backend.tf_provider.tf が生成される:

$ cd infra
$ terramate generate

Code generation report:
  envs/dev/stacks/network/_backend.tf:  created
  envs/dev/stacks/network/_provider.tf: created
  envs/stg/stacks/network/_backend.tf:  created
  envs/stg/stacks/network/_provider.tf: created
  envs/prod/stacks/network/_backend.tf: created
  envs/prod/stacks/network/_provider.tf: created

生成された _backend.tf を見ると、globalsの値が展開されていることが確認できる:

# envs/dev/stacks/network/_backend.tf(生成結果)
terraform {
  backend "s3" {
    bucket         = "tfstate-myproject-dev"
    key            = "dev/network/terraform.tfstate"
    region         = "ap-northeast-1"
    dynamodb_table = "terraform-lock-dev"
    encrypt        = true
  }
}

Terramate CLIでスタックを操作する

環境を指定してplanする場合、ディレクトリフィルタを使う:

# devの全スタックをplan
terramate run --chdir envs/dev -- terraform plan

# prodのnetworkスタックだけapply
terramate run --chdir envs/prod/stacks/network -- terraform apply

ハマったポイント

1. globals の遅延評価に気づかず無限ループ

globalsの中で他のglobalsを参照するとき、定義順序は関係ない(遅延評価される)。ただし、循環参照するとエラーになる:

# ❌ これは循環参照でエラー
globals {
  foo = global.bar
  bar = global.foo  # Error: cycle detected
}

自分がハマったのは、もっと間接的なケースだ。ルートで state_bucketglobal.environment を参照し、環境のglobalsで environment の中から global.state_bucket を参照してしまい、循環が発生した。

対処法: globalsの依存グラフを意識する。terramate debug show globals でどのglobalsがどこから来ているか確認できる。

$ terramate debug show globals
/infra:
  project_name = "myproject"
  aws_region   = "ap-northeast-1"

/infra/envs/dev:
  environment = "dev"
  state_bucket = "tfstate-myproject-dev"  # ← ルートで定義、devのenvironmentを参照

2. symlinkとterramate generate の相性

envs/dev/stacks/../../stacks/ へのシンボリックリンクにした場合、terramate generateリンク先の実体ディレクトリに生成ファイルを書いてしまうことがある。

対処法: Terramateの設定で terramate.config.generate.hcl_magic_header_comment_style を指定し、生成先を明示的に制御する。または、シンボリックリンクではなく terramate.config.run.env で環境変数としてパスを渡す方法に切り替えた。

最終的にはシンボリックリンクを廃止し、各環境ディレクトリにスタック定義(stack.tm.hcl)を直接配置する構成に落ち着いた:

envs/dev/network/
├── stack.tm.hcl       # ← 各環境に配置
├── _gen.tm.hcl        # ← importで共通化
└── main.tf            # ← importまたはmodule呼び出し

3. tm_merge の罠 — mapのdeep mergeはされない

tm_merge はトップレベルのキーだけマージする。ネストされたmapはdeep mergeされず、上書きされる:

# ルート
globals {
  config = {
    timeout = 30
    retries = 3
  }
}

# 環境
globals {
  # ❌ configの中身が全部上書きされる(timeoutが消える)
  config = tm_merge(global.config, {
    retries = 5
  })
}

この場合は正しく retries = 5, timeout = 30 になるが、mapの中にさらにmapがある場合は注意が必要だ。ネストが深い場合は、globalsをフラットに保つか、個別のキーに分割するのが安全。

4. generate_hcl の outdated 検知

terramate generate を実行し忘れてglobalsだけ変更してコミットすると、生成ファイルと実際のglobalsに不整合が生じる。

対処法: CIで terramate generategit diff --exit-code を実行し、差分があればfailさせる:

# CIで生成ファイルの不整合を検知
terramate generate
if ! git diff --quiet --exit-code; then
  echo "ERROR: Generated files are outdated. Run 'terramate generate' locally."
  git diff
  exit 1
fi

これをPRのCIに入れておけば、生成忘れを防げる。

まとめ

Terramate globalsを使った環境分離のポイントをまとめると:

  1. ルートglobalsに共通値、環境globalsでオーバーライド — 継承の仕組みを理解する
  2. generate_hclで .tf ファイルを自動生成 — コピペを排除しつつ、生成結果はそのままTerraformで使える
  3. CIで terramate generate の差分チェック — 生成忘れを仕組みで防ぐ
  4. 循環参照・symlinkの罠に注意terramate debug show globals でデバッグ

個人的に一番効果が大きかったのは「backend.tfのコピペミスが0になった」こと。環境が増えるたびに globals.tm.hcl を1ファイル追加するだけで済むのは快適だ。

🏀 NBA Trade Tracker: https://www.nba-iso-flow.com/

0
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
0
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?