TerraformのHCL組み込み関数実践ガイド|templatefile・jsonencode・try・mergeでリソース設定を動的に組み立てる方法

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Terraform > TerraformのHCL組み込み関数実践ガイド|templatefile・jsonencode・try・mergeでリソース設定を動的に組み立てる方法
「terraform.tfvarsで変数をベタ書きしているうちは、モジュールが増えるたびに同じ値をコピペするはめになる。」

Terraformを使い始めてしばらく経つと、こんな場面に直面します。複数のリソースに同じタグセットを使いたい、外部ファイルからJSON設定を動的に生成したい、存在しないかもしれないattributeを安全に参照したい……。

これらはすべて、HCLの組み込み関数を使うと解決できます。この記事では、実務でよく登場する組み込み関数を機能別に整理し、実際に動くコード例と設計パターンで解説します。

この記事のポイント

・mergeで共通タグと個別タグを合成すると重複コードが消える
・templatefile・jsonencode でuser_data・IAMポリシーをHCL構文で書ける
・tryとcanで存在しない属性を安全に参照してモジュールを柔軟にできる
・terraform console で関数をその場で対話検証できる


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

HCL組み込み関数が必要になる場面

1. 変数設計だけでは限界が来る

Terraformを学び始めると、まず `variable` と `locals` でほとんどの値を管理できます。しかし構成が複雑になると、こんな壁にぶつかります。

・環境(dev・stg・prod)ごとにCIDRブロックのリストを違う形に変換したい
・複数のリソースに共通タグを自動付与したい
・外部JSONファイルを読み込んでリソース定義に組み込みたい
・存在するかどうか分からない属性を参照してもエラーにしたくない

これらを変数の値渡しだけで解決しようとすると、HCL外でシェルスクリプトを別途書いたり、同じ定義があちこちに重複したりします。

HCLの組み込み関数を使えば、これらをTerraformの宣言の中で完結させられます。

2. 関数のカテゴリと terraform console での確認方法

Terraformの組み込み関数は大きく5系統に分類されます。

文字列操作: format・join・replace・trimspace・upper・lower
コレクション操作: merge・concat・flatten・lookup・tolist・toset
型変換: tostring・tonumber・tobool・jsonencode・jsondecode
テンプレート生成: templatefile・templatestring
エラー対策: try・can・coalesce

`terraform console` を起動すれば、対話形式でその場に確認できます。ファイルを作って `terraform apply` するより素早くデバッグできるので、新しい関数を試すときはまずこちらで動作確認する習慣をつけてください。

$ terraform console > format("sg-%s-%s", "dev", "web") "sg-dev-web" > join(", ", ["ap-northeast-1a", "ap-northeast-1c"]) "ap-northeast-1a, ap-northeast-1c" > merge({a = 1}, {b = 2, a = 99}) { "a" = 99 "b" = 2 }

コレクション操作関数の実践

コレクション操作は「複数リソースをまとめて設定したい」場面で必須になります。

1. mergeで共通タグと個別タグを合成する

実務でいちばん使用頻度が高いのが `merge` です。全リソース共通のタグと、リソース個別のタグを合成します。

locals { common_tags = { Project = var.project_name Environment = var.environment ManagedBy = "Terraform" } } resource "aws_instance" "web" { ami = data.aws_ami.amazon_linux.id instance_type = "t3.micro" tags = merge(local.common_tags, { Name = "web-${var.environment}" Role = "web" }) }

後勝ちのルールがあるため、同じキーがあれば個別タグ側の値が使われます。共通タグを `locals` に1箇所だけ定義しておき、`merge` で上書き合成するパターンが基本です。タグポリシーが変わっても1箇所を直すだけで済みます。

2. flattenでネストしたリストを平坦化する

環境別に定義したCIDRリストをfor_eachに渡す際、ネストしたリストのままでは使えません。`flatten` で1次元リストに変換します。

locals { # 環境ごとのCIDRリスト(ネスト構造) subnet_cidrs = { public = ["10.0.1.0/24", "10.0.2.0/24"] private = ["10.0.11.0/24", "10.0.12.0/24"] } # flattenで1次元リストに変換 all_cidrs = flatten(values(local.subnet_cidrs)) # → ["10.0.1.0/24", "10.0.2.0/24", "10.0.11.0/24", "10.0.12.0/24"] }

`values(map)` でマップの値のリストを取り出し、`flatten` でそれらをひとつのリストにまとめるコンビが頻出します。for_each・for式との組み合わせでリソースの動的生成に使えます。

3. lookupでデフォルト値付きのマップ参照をする

環境名をキーに値を取り出す際、キーが存在しない場合にエラーでなくデフォルト値に倒したい場面があります。

locals { instance_type_map = { dev = "t3.micro" stg = "t3.small" prod = "t3.medium" } # 第3引数がデフォルト値(キーが存在しない場合に使われる) instance_type = lookup( local.instance_type_map, var.environment, "t3.micro" ) }

`lookup(map, key, default)` の第3引数を省略すると、キーが存在しない場合にエラーになります。意図しない環境名の入力を静かにデフォルトへ倒したい場合は第3引数を指定してください。環境名を厳密にバリデーションしたい場合は、逆に省略してエラーを出す設計もあります。

テンプレート生成:templatefileとjsonencodeの実務パターン

1. templatefileで外部テンプレートからuser_dataを生成する

EC2のuser_dataや複雑な設定ファイルをTerraformで生成する際に `templatefile` を使います。`.tftpl` 拡張子のテンプレートファイルにHCL変数を埋め込める形式です。

# templates/user_data.sh.tftpl の内容例 #!/bin/bash hostnamectl set-hostname ${hostname} yum install -y ${package_name} echo "${app_config_json}" > /etc/app/config.json systemctl enable --now nginx

# main.tf resource "aws_instance" "app" { ami = data.aws_ami.amazon_linux.id instance_type = "t3.small" user_data = templatefile("${path.module}/templates/user_data.sh.tftpl", { hostname = "app-${var.environment}-01" package_name = "nginx" app_config_json = jsonencode(local.app_config) }) tags = merge(local.common_tags, { Name = "app-${var.environment}" }) }

テンプレートの変数参照は `${変数名}` で行います。テンプレートファイル内でHCLのfor文やif文のディレクティブ(`%{ for item in list ~}...%{ endfor ~}`)も使えるため、配列のループ展開も実現できます。

2. jsonencodeでIAMポリシーをHCL構文で書く

IAMポリシーのJSONをヒアドキュメント(`<

resource "aws_iam_policy" "s3_read" { name = "s3-read-${var.environment}" # jsonencodeを使うとHCLの構文チェックが効く policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:ListBucket", ] Resource = [ "arn:aws:s3:::${var.bucket_name}", "arn:aws:s3:::${var.bucket_name}/*", ] } ] }) }

ヒアドキュメントとの違いは、タイプミスによる無効JSON生成を `terraform validate` の段階で検出できる点です。JSONキーの閉じ忘れや、末尾カンマの有無でハマる事故がなくなります。

TerraformのIaC設計力を体系的に身につけたい方へ

HCL関数・モジュール設計・マルチ環境運用まで、実機ハンズオンで学べる現役エンジニアによるセミナーの詳細はこちらから確認できます。

Terraform実践セミナーの詳細を見る >>

エラー対策関数:tryとcanとcoalesceの使い分け

1. tryで存在しない属性を安全に参照する

オプション設定を受け取るモジュールでは、呼び出し元が設定を省略した場合に「Unsupported attribute」エラーが出ることがあります。`try` を使うと、評価に失敗した場合のフォールバック値を指定できます。

locals { # vpc_configが設定されている場合はそのCIDR、なければデフォルト値 vpc_cidr = try(var.vpc_config.cidr, "10.0.0.0/16") # monitoring設定が省略されたモジュール呼び出しでもエラーにしない monitoring_enabled = try(var.monitoring.enabled, false) }

`try(expr1, expr2, ...)` は左から順に評価し、エラーが出なかった最初の値を返します。オプションの設定オブジェクトを受け取る汎用モジュールで特に重宝します。複数のフォールバック値を並べることもできます。

2. canで属性の有無をboolで取得する

`try` は値を返しますが、`can` はboolを返します。属性が有効かどうかを条件分岐(`count`・`for_each`)で使いたい場合に使います。

locals { # monitoring設定が提供されているかを bool で取得 has_monitoring_config = can(var.monitoring.enabled) # URLがhttps://で始まるかを検証 is_https = can(regex("^https://", var.endpoint_url)) } # has_monitoring_configがfalseなら count=0でリソースを作らない resource "aws_cloudwatch_metric_alarm" "cpu_high" { count = local.has_monitoring_config ? 1 : 0 alarm_name = "${var.project}-cpu-high-${var.environment}" comparison_operator = "GreaterThanThreshold" threshold = try(var.monitoring.cpu_threshold, 80) # ... }

3. coalesceでnull・空文字列を除外した最初の値を取得する

複数の候補値のうち、最初のnullでない・空文字列でない値を取り出したい場合に `coalesce` を使います。

locals { # instance_nameが指定されていれば使い、空ならproject_nameを使う final_name = coalesce(var.instance_name, var.project_name, "default-server") }

`try` と `coalesce` の違いを押さえてください。`try` はエラー(属性不存在・型不一致)をスキップします。`coalesce` はnullと空文字列をスキップします。属性が存在するかどうかの問題なら `try`、値が空かどうかの問題なら `coalesce` を使います。

実務でよく使う設計パターン

1. for式とmergeで動的なタグマップを生成する

複数のサブネットにNameタグを一括付与するパターンです。for式とmergeを組み合わせます。

locals { subnet_config = { "public-1a" = { cidr = "10.0.1.0/24", az = "ap-northeast-1a" } "public-1c" = { cidr = "10.0.2.0/24", az = "ap-northeast-1c" } "private-1a" = { cidr = "10.0.11.0/24", az = "ap-northeast-1a" } "private-1c" = { cidr = "10.0.12.0/24", az = "ap-northeast-1c" } } } resource "aws_subnet" "main" { for_each = local.subnet_config vpc_id = aws_vpc.main.id cidr_block = each.value.cidr availability_zone = each.value.az tags = merge(local.common_tags, { Name = "${var.project}-${var.environment}-${each.key}" Tier = startswith(each.key, "public") ? "public" : "private" }) }

サブネットが増えてもマップに1行追加するだけで、タグ付きのサブネットが一括作成されます。

2. jsondecodeで外部JSONファイルを読み込む

大量のIPホワイトリストや設定値を別ファイルで管理し、Terraformで読み込む方法です。

# config/allowed_ips.json # { # "office": ["203.0.113.0/24"], # "vpn": ["198.51.100.0/24"] # } locals { ip_config = jsondecode(file("${path.module}/config/allowed_ips.json")) # flatten でネストを解消して1次元リストに allowed_ips = flatten(values(local.ip_config)) # → ["203.0.113.0/24", "198.51.100.0/24"] } resource "aws_security_group_rule" "allow_office" { type = "ingress" from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = local.allowed_ips security_group_id = aws_security_group.web.id }

IPリストをJSONで管理すると、セキュリティグループの変更をTerraformの知識がないチームメンバーでもプルリクエストで対応できる運用になります。Terraformコードには手を入れさせず、JSONファイルだけ編集してもらうフローです。

トラブルシュート:よくある関数のエラーと対処

「Error: Invalid function argument」が出たとき

関数の引数に渡した型が想定外の場合に出ます。

# NG: joinにstringを渡している(リストが必要) join(", ", var.single_string) # Error: Invalid function argument # Call to function "join" failed: # argument must be a list or set.

`terraform console` で `type(var.single_string)` を実行すると変数の実際の型を確認できます。`tolist([var.single_string])` でリストに変換するか、変数の `type = list(string)` に変更して呼び出し元を修正してください。

tryで「Error: Can't evaluate expression」が解消されないとき

`try` はランタイムで発生した属性参照エラーをキャッチしますが、構文エラーや未定義変数への参照は解消できません。

# NG: var.non_existent_variable自体が未定義 → tryで包んでも解消されない value = try(var.non_existent_variable, "default") # OK: 変数は存在するが、その中のフィールドが省略可能な場合はtryで解消できる value = try(var.config.optional_field, "default")

`try` が有効なのは「変数は存在するが、その中のフィールドが存在しない・型が合わない」ケースです。変数自体が未定義の場合は `variable` ブロックに定義する必要があります。

templatefile内で「Error: Function call not allowed」が出たとき

`.tftpl` テンプレートファイル内では、一部の関数呼び出しに制約があります。

# NG: テンプレートファイル内でjsonencodeを呼ぼうとするとエラーになるケース # templates/config.tftpl config = ${ jsonencode(settings) } # エラーが出るケースあり # OK: main.tfでjsonencodeを先に実行し、文字列として渡す user_data = templatefile("templates/config.tftpl", { settings_json = jsonencode(local.settings) # 文字列として渡す }) # templates/config.tftpl config = ${settings_json}

テンプレートに渡す値は、できるだけHCLの `locals` や `jsonencode` で事前に整形してから渡す設計にしてください。テンプレートファイルは「文字列の組み立て」だけを担当する役割分担にすると、デバッグが容易になります。

本記事のまとめ

やりたいこと 使う関数
共通タグと個別タグを合成する merge(common_tags, {...})
ネストしたリストを平坦化する flatten(values(nested_map))
デフォルト値付きのマップ参照 lookup(map, key, default)
外部テンプレートファイルを読み込む templatefile(path, vars)
HCLマップをJSON文字列に変換する jsonencode({...})
JSONファイルをHCLマップで読み込む jsondecode(file(path))
オプション属性を安全に参照する try(expr, fallback)
属性の有無をboolで取得する can(expr)
null・空文字列を除いた最初の値を取得 coalesce(v1, v2, default)
HCLの組み込み関数を活用すると、環境別の設定差分・共通タグ管理・外部設定ファイルの取り込みをTerraformの宣言内で完結させられます。まず `terraform console` で動作を確認しながら試してみてください。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、HCL関数・モジュール設計・マルチ環境Terraform運用まで実機ハンズオンで学べる現役エンジニアによるセミナーの詳細はこちらから確認できます >>

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。