Terraform で構成が肥大化してくると、同じ式やタグ定義、命名規則が複数のリソースブロックに散在し始める。local 値はこの重複を一箇所に集約するための基本機能だが、単純な変数置き換えとしてしか使われていないケースが多い。本記事では、locals ブロックを「合成可能な中間レイヤー」として設計し、重複を削減しつつ可読性とメンテナンス性を両立させる実践パターンを解説する。
locals の基本概念を再確認する
locals ブロックは、モジュール内でのみ有効な名前付き値を定義する機能であり、variable のように外部から注入されるものではなく、output のように外部へ公開されるものでもない。あくまでモジュール内部の「計算結果のキャッシュ」として振る舞う。
locals {
environment = terraform.workspace
name_prefix = "${var.project}-${local.environment}"
}
重要な特性は次の3点である。
- 宣言順に依存しない:
localsブロック内の値は、Terraform がグラフ解析によって依存関係を自動的に解決するため、定義順は実行順序に影響しない。 - 複数の
localsブロックを定義できる: 論理的なグルーピングのために、ファイルを分けて複数のlocals {}を書いても問題ない。 - 式のみで、副作用を持たない:
localは値であり、リソースのようにcreate_before_destroyなどのライフサイクルを持たない。そのため値の再計算は常に決定的になる。
合成パターン:ローカル値を積み上げて設計する
重複削減の核心は、「小さな locals を定義し、それらを組み合わせてより大きな locals を作る」という合成的な設計である。典型的な例がタグ管理だ。
locals {
common_tags = {
Project = var.project
Environment = terraform.workspace
ManagedBy = "terraform"
}
cost_tags = {
CostCenter = var.cost_center
Owner = var.owner
}
# 合成:複数の locals を merge して最終形を作る
resource_tags = merge(
local.common_tags,
local.cost_tags,
{ Name = "${local.name_prefix}-ec2" }
)
}
resource "aws_instance" "web" {
# ...
tags = local.resource_tags
}
このように、merge() や concat()、三項演算子を使って「部品となる locals」を組み合わせることで、各リソースブロック側は tags = local.resource_tags の一行で済む。タグ要件が追加された場合も、変更箇所は locals ブロックの一箇所に限定される。
条件分岐を locals に閉じ込める
環境ごとに異なる設定値(インスタンスサイズ、レプリカ数など)をリソースブロック内で count や ?: を使って直接書くと、リソース定義が読みにくくなる。これを locals に委譲する。
locals {
env_config = {
dev = { instance_type = "t3.micro", min_size = 1, max_size = 2 }
staging = { instance_type = "t3.small", min_size = 2, max_size = 4 }
prod = { instance_type = "m5.large", min_size = 3, max_size = 10 }
}
current_config = local.env_config[terraform.workspace]
}
resource "aws_launch_template" "app" {
instance_type = local.current_config.instance_type
}
resource "aws_autoscaling_group" "app" {
min_size = local.current_config.min_size
max_size = local.current_config.max_size
}
map によるルックアップパターンは、環境分岐を locals の宣言的なデータ構造に落とし込み、リソースブロックからロジックを排除できる点が利点である。環境が増えても env_config に1行追加するだけで済む。
for 式による変換の合成
リスト・マップの変換処理も locals に切り出すべき典型例である。
variable "subnets" {
type = list(object({
name = string
cidr = string
az = string
}))
}
locals {
subnet_map = {
for s in var.subnets :
s.name => {
cidr = s.cidr
az = s.az
tags = merge(local.common_tags, { Tier = s.name })
}
}
}
resource "aws_subnet" "this" {
for_each = local.subnet_map
cidr_block = each.value.cidr
availability_zone = each.value.az
tags = each.value.tags
}
for_each と組み合わせることで、入力データの形を locals 側で正規化し、リソース側は構造が整った each.value を消費するだけになる。入力バリデーションや変換ロジックが複雑な場合は、locals を段階的に分割する(例: local.raw_subnets → local.validated_subnets → local.subnet_map)ことで、各段階が単体で読める粒度に保てる。
state との関係
locals の値は Terraform の state ファイルに直接「リソースとして」記録されるわけではないが、その計算結果はリソースの属性値として state に反映される。つまり local.resource_tags の中身が変わればそれを参照しているリソースの tags 属性が変わり、結果として plan に差分が出る。
ここで注意すべきは、locals 自体に life cycle や state アドレスは存在しないという点だ。terraform state list に local.xxx が出てくることはない。リファクタリングで locals の構造を変えても、それを参照しているリソースの属性値が変わらない限り、plan は no-op になる。逆に、for_each のキー生成に locals を使っている場合、locals の計算結果(キーの文字列)が変わると、Terraform はリソースの「追加・削除」と解釈してしまう。
# 危険な例:workspace 名を含めるとキーが変わり、リソースが re-create される
locals {
subnet_map = {
for s in var.subnets : "${terraform.workspace}-${s.name}" => s
}
}
for_each のキーに揺れやすい値(タイムスタンプ、workspace 名、環境変数など)を混ぜると、意図しない destroy/create が発生するため、キーは安定した識別子(name や id)に限定するべきである。
モジュールでの locals 設計
モジュール境界を越えて locals を公開することはできない。モジュール内部の locals は呼び出し元から不可視であり、必要な値は明示的に output として公開する必要がある。
# modules/network/main.tf
locals {
vpc_cidr_blocks = [for cidr in var.cidr_ranges : cidrsubnet(var.base_cidr, 4, cidr)]
}
output "subnet_cidrs" {
value = local.vpc_cidr_blocks
}
モジュール設計における locals のベストプラクティスは以下の通り。
- 「入力の正規化」はモジュール先頭の locals に集約する:
var.*を直接複数箇所で参照せず、一度 locals で検証・デフォルト値補完してから使う。 - 「出力の整形」は末尾の locals に集約する: output ブロックの式を複雑にせず、
local.output_xxxを経由させることで、output ブロック自体はシンプルな参照のみにする。 - モジュール間で同じ locals パターンを共有したい場合は、
localsを変数化するのではなく、共通ロジックをモジュール自体として切り出す: locals はモジュールをまたいで再利用できない機能だからである。
注意点
- 過度な合成は可読性を損なう: locals を5段階以上積み重ねると、最終的な値がどう組み立てられているかを追うのに
terraform consoleを使わざるを得なくなる。2〜3段階を目安にし、複雑になりすぎたらモジュール分割を検討する。 - 循環参照はエラーになる:
local.aがlocal.bを参照し、local.bがlocal.aを参照すると、Terraform は依存関係グラフでサイクルを検出してエラーを出す。合成的な設計をするほどこのリスクは高まるため、各 locals が「一方向に積み上がる」ように設計する。 - sensitive な値の扱い:
localブロックの値自体にsensitive属性を直接付与することはできない(Terraform 1.x 時点)。機密値を扱う場合は、変数側でsensitive = trueを設定し、その変数を参照する locals も自動的に sensitive として扱われる点を理解しておく。 terraform consoleでのデバッグ: 複雑な locals の合成結果を確認する際は、terraform console内でlocal.resource_tagsのように直接評価すると実際の計算結果を確認できる。plan を都度実行するより高速にデバッグできる。
クラウドプロバイダーとの関係
locals 自体はプロバイダー非依存の Terraform コア機能であり、AWS・GCP・Azure のどのプロバイダーを使っていても同じ書き方になる。ただし実務上の使われ方には差がある。
- AWS: タグ戦略(
common_tagsの合成)や、リージョン・AZ ごとのパラメータテーブルを locals の map で表現するケースが非常に多い。aws_caller_identityやaws_regionなどのデータソースの結果を locals で加工して再利用するパターンも定番である。 - GCP: プロジェクトID・ネットワーク命名規則(
project-env-serviceのような規則)を locals で一元化し、google_projectやgoogle_compute_networkなど複数リソースで共有することが多い。 - Azure: リソースグループ名やロケーションのマッピングを locals の map で管理し、
azurermプロバイダーの命名規則の制約(長さ制限や許可文字種)に対応するための文字列加工(lower(),substr()など)を locals に集約するパターンが見られる。
いずれのプロバイダーでも、「命名規則」「タグ/ラベル」「環境別パラメータ」の3種類が locals 合成の主戦場になる点は共通している。
FAQ
Q1. locals と variable はどう使い分けるべきか?
variable は呼び出し元(root module やモジュール利用者)からの入力を受け取るためのインターフェースであり、locals はモジュール内部でのみ使う計算結果である。外部から変更してほしい値は variable、内部ロジックの結果としてのみ存在する値は locals に置く、という基準で判断する。入力値をそのまま使わず、デフォルト値の補完や文字列結合を行う場合は、必ず一度 locals を経由させるのが安全である。
Q2. locals を使いすぎると plan の実行速度に影響するか?
locals は単なる式評価であり、リソースのように API 呼び出しを伴わないため、数百個程度の locals であれば実行速度への影響は無視できるレベルである。ただし、for 式の中で別の for 式をネストしすぎたり、大きなリストに対して cidrsubnet() のような計算コストのある関数を繰り返し呼ぶ場合は、plan 時間が伸びることがある。パフォーマンスより先に可読性を優先し、問題が実測された場合にのみ最適化を検討すべきである。
Q3. モジュール間で同じ locals の定義を共有したいが、どうすればよいか?
locals ブロック自体はモジュールをまたいで import・共有することができない機能である。同じ命名規則やタグ合成ロジックを複数モジュールで使いたい場合は、(1) 共通ロジックを variable として受け取り各モジュール内で同じ locals 式を再実装する、または(2)ロジックそのものを小さな「ユーティリティモジュール」として切り出し、output を通じて計算済みの値を各モジュールに渡す、のいずれかを選択する。ロジックが複雑であれば後者の方が重複削減効果が高い。
locals は単体で見ると地味な機能だが、「小さな部品を合成して最終形を作る」という設計思想を徹底することで、タグ管理・命名規則・環境別パラメータといった Terraform コードで繰り返し発生する重複を大幅に削減できる。合成の段数と循環参照のリスクに注意しながら、モジュール境界を意識した locals 設計を心がけたい。