hacomono TECH BLOG

フィットネスクラブやスクールなどの顧客管理・予約・決済を行う、業界特化型SaaS「hacomono」を提供する会社のテックブログです!

Terramateで始めるIaCオーケストレーション


はじめに

こんにちは、株式会社hacomono プラットフォーム部のおりちゃんこと居石(@hetre70914)です。
2/1からhacomonoにジョインしたばかりで、日々新しい環境で刺激を受けながらプラットフォームとして求められることはなんだろう?を突き詰めています。
プライベートでは冬季は白馬に籠り、エンジニアリングも趣味のスキーも全力で楽しんでいます。

Terraformの課題

hacomonoの既存インフラはTerraformで管理しています。
プラットフォーム基盤としての将来を考えると、Terraformのみでは以下のような課題が起きると考えています。

  • terraform apply の実行時間増加
  • tfstate間の依存関係複雑化
  • コードの重複に伴う構成変更の抜け漏れ

そこでTerramateを利用し、このような課題を解決できないかと考えました。

Terramateとは?

Terramateは複数のstateに跨るTerraformの管理を便利にしてくれるCLIツールで

  • コード生成
  • オーケストレーション (依存関係の解決や並列実行)
  • スクリプトの定義

などを行うことが可能です。

実行環境やダッシュボードを提供するTerramate Cloudもありますが、ここではTerramate CLIについてのみ言及します。

Terramateの仕様

差分適用

Stack

TerramateではStackと呼ばれる単位でリソースを管理し、基本的には 1 Stack = 1 tfstate となります。
Stackはひとつのディレクトリで、その中に stack.tm.hcl というファイルで設定が記述されます。

stack_1
├── main.tf
├── provider.tf
├── backend.tf
└── stack.tm.hcl

stack.tm.hclの内容は以下のようになっており、 terramate create stack_1 で自動生成されます。

stack {
  name        = "dev"
  description = "dev"
  id          = "f960092d-5148-4671-a929-8fac47a938cb"
}

またStackはネストすることもでき、以下のような階層構造も可能です。

stacks
└── dev
    ├── shared
    │   ├── cluster
    │   │   ├── main.tf
    │   │   └── stack.tm.hcl
    │   ├── database
    │   │   ├── main.tf
    │   │   └── stack.tm.hcl
    │   └── network
    │       ├── main.tf
    │       └── stack.tm.hcl
    └── stack.tm.hcl


オーケストレーション

Terramateはgit diffから差分となるStackのみを検出し実行してくれるため、実行時間を短縮することが可能です。
また依存関係を明示することで、実行順序の制御や並列実行を制御することが可能です。

コード差分の適用は terramate run --changed -- terraform apply を実行します。
Terramateが前回適用時からのGit差分を取得し、変更のあるStackのみ適用してくれます。
またこの際に terramate run --changed --parallel 5 -- terraform apply とすると、依存関係のないStack同士を並列適用することも可能です。

またStack間に依存関係がある場合、依存先から順に実行されます。
実行順序の確認は terramate list --run-order コマンドを使います。
依存関係を構成する要素は以下の2つとなります。

  • Stackがネストしている場合、子は親に依存 (親の方が先に実行)
  • config.tm.hcl にbefore, afterで明示


例えば /stacks/cluster/stack.tm.hcl に以下のような記述をした場合、

stack {
  name        = "cluster"
  ...
  after = [
    "/stacks/dev/shared/network",
    "/stacks/dev/shared/database"
  ]
}

terramate list --run-order の結果は以下となります。

stacks/dev
stacks/dev/shared/database
stacks/dev/shared/network
stacks/dev/shared/cluster

一方で after = ... の記述を削除した場合、依存関係がなくなりフラットになります。

stacks/dev
stacks/dev/shared/cluster
stacks/dev/shared/database
stacks/dev/shared/network


コード生成

import

Terramateにはコード生成機能があります。
コードをDRYに保ちつつ、生成したコードはリポジトリに残るため可読性も損ないません。

例えば以下のようなディレクトリ構成を想定します。

.
├── imports
│   └── generate_backend.tf
└── stacks
    └── stack_1
        ├── main.tf
        ├── import.tm.hcl
        └── stack.tm.hcl

generate_backend.tm.hcl は以下の通りです。

generate_hcl "backend.tf" {
  content {
    terraform {
      backend "s3" {
        region       = "ap-northeast-1"
        bucket       = "terramate-example-bucket"
        key          = "dev/terraform.tfstate"
        encrypt      = true
        use_lockfile = true
      }
    }
  }
}

import.tm.hcl は以下の通りです。

import {
  source = "/imports/generate_backend.tm.hcl"
}

terramate generate を実行すると、 ./stacks/stack_1/backend.tf が生成されます。

変数の使用

コード生成には変数も使用することが可能です。
以下のようなディレクトリ構造を想定します。

.
├── imports
│   └── generate_backend.tm.hcl
└── stacks
    └── dev
        ├── shared
        │   └── network
        │       ├── main.tf
        │       ├── import.tm.hcl
        │       ├── configu.tm.hcl
        │       └── stack.tm.hcl
        └── stack.tm.hcl

ここでは変数を config.tm.hcl に定義し、imports.tm.hcl でコードを生成する際に使用します。
またStackに親子関係がある場合、親の変数は子に受け継がれます。

./imports/generate_backend.tm.hcl

generate_hcl "backend.tf" {
  content {
    terraform {
      backend "s3" {
        region       = global.terraform.backend.s3.region
        bucket       = global.terraform.backend.s3.bucket
        key          = "${global.env.value}/${terramate.stack.name}/terraform.tfstate"
        encrypt      = global.encrypt.enabled
        use_lockfile = true
      }
    }
  }
}

./stacks/dev/config.tm.hcl

globals "terraform" "backend" "s3" {
  region = "ap-northeast-1"
  bucket = "terramate-example-bucket"
}
globals "env" {
  value = "dev"
}

./stacks/dev/shared/network/config.tm.hcl

globals "encrypt" {
  enabled = true
}


ここで terramate generate 実行で生成される./stacks/dev/shared/network/backend.tfは以下の通りです。

// TERRAMATE: GENERATED AUTOMATICALLY DO NOT EDIT
terraform {
  backend "s3" {
    bucket       = "terramate-example-bucket"
    encrypt      = true
    key          = "dev/network/terraform.tfstate"
    region       = "ap-northeast-1"
    use_lockfile = true
  }
}


ここで全て紹介はしませんが、以下のような機能もありコード生成でできる幅もかなり広いです。


スクリプト

Terramateは特定のワークフローをscriptとして作成することができます。
※ こちら2025.03.12時点ではexperimental featureとなっております。

experimental featureを扱うため、リポジトリのルートに terraform.tm.hcl を作成します。

terramate {
  config {
    experiments = [
      "scripts"
    ]
  }
}

Stackに含まれる config.tm.hcl にスクリプトを定義します。

script "deploy" {
  description = "Run Terraform deployment"
  lets {
    provisioner = "terraform"
  }
  job {
    name = "deploy-dev"
    commands = [
      [let.provisioner, "init"],
      [let.provisioner, "validate"],
      ["tfsec", "."],
      [let.provisioner, "apply", "-auto-approve"],
    ]
  }
}

terramte script list でスクリプト一覧が見られ、 terramate script run deploy で実行することができます。

またStackが親子関係にある場合、親にスクリプトを定義すると子のStackに対しても実行されるようになります。
terramate script tree を実行すると、スクリプトのツリー構造を見ることが可能です。

運用するなら?

Terramateの仕様について紹介してきました。
最後にもし本番導入するとするならどのような運用方法にするか考えてみたのでご紹介します。

ディレクトリ構造

ディレクトリ構成は以下の通りです。

.
├── imports    # 生成ファイルの定義 (backend, provider)
├── modules    # Terraformモジュール
└── stacks     # Stack
    ├── dev    # 開発環境向け
    │   ├── shared    # テナント共通のリソース
    │   │   ├── network
    │   │   └── cluster...
    │   └── tenant    # テナント個別に作成するリソース
    │       ├── tenant1
    │       └── tenant2...
    └── prd
        ├── shared
        │   ├── network
        │   └── cluster...
        └── tenant
            ├── tenant1
            └── tenant2...

まずStackですが、影響範囲を最低限に抑えるためライフサイクルごとに作成する方針をとっています。
またコード生成でbackendやproviderを作成し、Terraformやプロバイダのバージョン差異が起きないようにします。
一方で運用の簡便さと可読性を担保するため、リソース定義には適用せずTerraformのモジュールを用います。

Git Hooks

terramate generate の実行忘れなどでリポジトリに生成後のコードが存在しないと、可読性を損なうことがあります。
そのためGitHooksを使って漏れを防ぐ必要があります。

pre-commit

terramate fmt
terramate generate

pre-push

terramate generate
# stacks配下にのみ生成ファイルができる想定
git diff --exit-code stacks


ローカル実行

開発環境など、コミット前に適用したいケースは多々あります。
ただしTerramateはデフォルトで未コミットのファイルがリポジトリにある場合、

Error: repository has untracked files

というエラーが発生してしまいます。

その場合は ./terramate.tm.hcl に disable_safeguards プロパティを設定してください。

terramate {
  config {
    experiments = [
      "scripts"
    ]
    disable_safeguards = [
      // ローカルからの実行を考慮し、未コミットの適用を許可
      "git-untracked",
      "git-uncommitted"
    ]
  }
}

ただしこの設定はプロジェクトルートのみ有効で、「dev Stack以下でのみ許可」といった設定ができません。
多少ワークアラウンド感はありますが、普段はコメントアウトしておき、手元から適用したい場合のみ外すといった運用が必要になると思います。

おわりに

Terramateをご紹介させていただきました。
コード生成によるDRY化、Stackのオーケストレーション、Terraformの可読性を下げすぎない、といった点でかなり使い勝手の良いツールだなと感じました。
一方で生成ファイルがリモートリポジトリから漏れないよう工夫する必要がある、disable_safeguardsがStack単位でON/OFFを切り分けられないといった問題もありました。
ただ総合的にはTerraformの管理運用が楽になりそうだと感じています。

hacomonoでは事業の圧倒的成長に向け、プラットフォーム基盤を作り上げていきます。
共に作り上げる仲間を募集しておりますので、もし少しでも興味を持っていただけたならご連絡くださいませ。


株式会社hacomonoでは一緒に働く仲間を募集しています。
エンジニア採用サイトや採用ウィッシュリストもぜひご覧ください!