Terraformを使い始めてしばらく経つと、こんな場面に直面します。複数のリソースに同じタグセットを使いたい、外部ファイルからJSON設定を動的に生成したい、存在しないかもしれないattributeを安全に参照したい……。
これらはすべて、HCLの組み込み関数を使うと解決できます。この記事では、実務でよく登場する組み込み関数を機能別に整理し、実際に動くコード例と設計パターンで解説します。
この記事のポイント
・mergeで共通タグと個別タグを合成すると重複コードが消える
・templatefile・jsonencode でuser_data・IAMポリシーをHCL構文で書ける
・tryとcanで存在しない属性を安全に参照してモジュールを柔軟にできる
・terraform console で関数をその場で対話検証できる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
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" }) }
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"] }
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" ) }
テンプレート生成: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}" }) }
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}/*", ] } ] }) }
TerraformのIaC設計力を体系的に身につけたい方へ
HCL関数・モジュール設計・マルチ環境運用まで、実機ハンズオンで学べる現役エンジニアによるセミナーの詳細はこちらから確認できます。
エラー対策関数: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) }
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") }
実務でよく使う設計パターン
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" }) }
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 }
トラブルシュート:よくある関数のエラーと対処
「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.
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")
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}
本記事のまとめ
| やりたいこと | 使う関数 |
|---|---|
| 共通タグと個別タグを合成する | 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) |
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:TerraformでWAF v2(aws_wafv2_web_acl)をALBに適用する方法|マネージドルールとカスタムルールの設計
- この記事の属するカテゴリ:Terraformへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます