06. 式と関数
HCL は単なる「設定書き」ではなく、ちゃんとした 式 が書けます。三項演算子・for 式・splat・組み込み関数。これらを使えると、コードがぐっと「動く」ようになります。
演算子
| カテゴリ | 演算子 |
|---|---|
| 算術 | + - * / % |
| 比較 | == != < <= > >= |
| 論理 | && || ! |
# 三項演算子: var.is_prod が true なら 3、false なら 1
# → 本番だけリソースを 3 台に冗長化する用途
count = var.is_prod ? 3 : 1
# 算術: ディスク本数 × 1 本あたり容量 = 合計容量(GB など)
total_size = var.disk_count * var.disk_size
# 論理 OR: 「dev 環境のとき」または「明示的に許可フラグを立てたとき」 true
# → dev では常に許可、prd では allow_admin_in_prod = true のときだけ許可
allow_admin = var.environment == "dev" || var.allow_admin_in_prod
条件式(三項演算子)
# 三項演算子の基本書式: condition が true なら true_value、false なら false_value を返す
condition ? true_value : false_value
# 例: 本番環境 (prd) では t3.large、それ以外(dev/stg)では t3.micro を使う
instance_type = var.environment == "prd" ? "t3.large" : "t3.micro"
# null を使った「条件付き引数」のテクニック
# → 本番では 30 日保持、それ以外は null(=引数を省略したのと同じ=デフォルト値が適用)
backup_retention = var.is_prod ? 30 : null
for 式
コレクションを変換/フィルタする式。Python の内包表記に似ています。
list を変換(list 内包)
# list の各要素 s を取り出して upper(s) で大文字化、新しい list として返す
[for s in var.azs : upper(s)]
# input: ["ap-northeast-1a", "ap-northeast-1c"]
# output: ["AP-NORTHEAST-1A", "AP-NORTHEAST-1C"]
map を変換(object 内包)
# map の各キー k と値 v を取り出し、値だけを upper(v) で大文字化した新しい map を返す
# 構文: { for KEY, VALUE in MAP : NEW_KEY => NEW_VALUE }
{ for k, v in var.tags : k => upper(v) }
# input: { Env = "dev", Project = "x" }
# output: { Env = "DEV", Project = "X" }
フィルタ(if 句)
# for 式の末尾に if を付けると「条件を満たすものだけ」を残せる
# 下の式 = 全 subnet のうち、Tier タグが "public" のものだけ ID を集めた list
[for s in aws_subnet.all : s.id if s.tags.Tier == "public"]
map → list、list → map への変換
# list of object → key 付き map に変換
# 各 subnet の name 属性をキーにして、subnet オブジェクトそのものを値にした map を作る
# 例: [{name="a",...},{name="b",...}] → {a={name="a",...}, b={name="b",...}}
{
for s in var.subnets :
s.name => s
}
# map → list(キー=値 の文字列を要素にした list)
# 例: { Env = "dev", Project = "x" } → ["Env=dev", "Project=x"]
[for k, v in var.tags : "${k}=${v}"]
splat 式
list / set の各要素から同じ属性を取り出すショートカット。for 式の特殊形と思って OK。
# splat 式: list の全要素から .id 属性を取り出して新しい list にする
aws_subnet.public[*].id
# ↑ は次の for 式と完全に同じ意味(splat は短縮形)
[for s in aws_subnet.public : s.id]
多用されるのは count や for_each で複数作ったリソースの ID をまとめて取りたい時です。
文字列テンプレートディレクティブ
ヒアドキュメント等の文字列内で、分岐や反復 を書ける構文。%{ ... } がディレクティブ。
# EC2 起動時に流すシェルスクリプトを動的に組み立てる例
# ヒアドキュメント (<<-EOT ... EOT) の中に %{ ... } で if/for を埋め込める
user_data = <<-EOT
#!/bin/bash
echo "Hello, ${var.name}"
%{ if var.install_nginx ~}
# ↑ install_nginx が true のときだけ、この行〜endif までが出力される
yum install -y nginx
systemctl enable --now nginx
%{ endif ~}
%{ for ip in var.allow_ips ~}
# ↑ allow_ips の各要素 ip ごとに、この行〜endfor までを繰り返し展開
iptables -A INPUT -s ${ip} -j ACCEPT
%{ endfor ~}
EOT
~} は前後の改行・空白を削除する印。テンプレ出力をきれいに保つのに使います。
組み込み関数(カテゴリ別)
関数は 9 カテゴリ に整理されています。よく使うものを抜粋:
文字列
# printf 形式で書式化(%s=文字列、%03d=数値を 3 桁ゼロ埋め)
format("%s-%03d", "node", 7) # "node-007"
# 大文字/小文字変換
upper("abc") # "ABC"
lower("ABC") # "abc"
# 区切り文字を挟んで連結(join)/ 分割(split)
join("-", ["a", "b"]) # "a-b" ← list を 1 つの文字列に
split(",", "a,b,c") # ["a", "b", "c"] ← 1 つの文字列を list に
# 部分文字列の置換(第 2 引数すべてを第 3 引数で置換)
replace("a.b.c", ".", "_") # "a_b_c"
# 前後の空白除去
trimspace(" x ") # "x"
# 部分文字列の切り出し(開始位置, 長さ)
substr("hello", 1, 3) # "ell"
数値
# 最大値・最小値(引数は可変長)
max(1, 2, 3) # 3
min(1, 2, 3) # 1
# 絶対値
abs(-5) # 5
# 切り上げ / 切り下げ(小数→整数)
ceil(4.2) # 5
floor(4.8) # 4
コレクション
# 要素数を取る
length(["a", "b"]) # 2
# 要素が含まれるかチェック(list / set / 文字列)
contains(["a", "b"], "a") # true
# map のキー一覧 / 値一覧
keys({a = 1, b = 2}) # ["a", "b"]
values({a = 1, b = 2}) # [1, 2]
# 複数の map を統合(後の引数が優先で上書き)
merge({a = 1}, {b = 2}) # {a = 1, b = 2}
# list を連結
concat([1,2], [3,4]) # [1,2,3,4]
# 重複を除去
distinct([1, 1, 2]) # [1, 2]
# ネストした list を 1 段平坦化
flatten([[1,2], [3]]) # [1, 2, 3]
# 2 つの list を「キー list と値 list」として map に変換
zipmap(["a","b"], [1,2]) # {a = 1, b = 2}
エンコード/パース
# HCL の値(map / list 等)を JSON 文字列に変換(IAM ポリシー作成で頻出)
jsonencode({ a = 1 }) # "{\"a\":1}"
# JSON 文字列を HCL の値(map / list)にパース
jsondecode("{\"a\":1}") # { a = 1 }
# YAML 文字列に変換
yamlencode({ a = 1 }) # "a: 1\n"
# Base64 エンコード(EC2 user_data 等で必要)
base64encode("hello") # "aGVsbG8="
ファイル
# 指定パスのファイルを読み込み、その中身を文字列として返す(user_data 等で使用)
file("./script.sh")
# ファイルが存在するかの真偽値
fileexists("./local-only.txt") # true / false
# パターンマッチするファイル名の集合を返す(S3 一括アップロードで頻出)
fileset("./html", "**/*.html")
# ファイル内容の MD5 ハッシュ(aws_s3_object の etag に使う典型例)
filemd5("./script.sh")
# テンプレートファイルに変数を差し込んでレンダリング
# user_data.tpl 内の ${name} が "web" に置き換わって返る
templatefile("./user_data.tpl", { name = "web" })
ハッシュ・暗号
# 文字列のハッシュ値を計算(16 進数文字列で返す)
md5("abc")
sha256("abc")
# SHA256 をさらに Base64 エンコード(CloudFront キャッシュキー等で使用)
base64sha256("abc")
# ランダム UUID を生成
# ⚠ 毎回違う値が返るので、plan のたびに差分が出て state ドリフトを起こす
# → resource の id 生成には random プロバイダを使うこと
uuid()
日時
# 現在時刻を ISO 8601 形式で取得(UTC)
# ⚠ plan のたびに値が変わるので、属性値に直接使うと毎回差分が出る
timestamp() # "2026-05-10T12:00:00Z"
# 日時を任意のフォーマットで整形
formatdate("YYYY-MM-DD", timestamp()) # "2026-05-10"
# 第 2 引数の期間を加算("24h", "1h30m", "-7d" 等)
timeadd("2026-05-10T00:00:00Z", "24h") # "2026-05-11T00:00:00Z"
IP / CIDR
# 親 CIDR を分割して N 番目のサブネット CIDR を作る
# 第 2 引数 = newbits(さらに何ビット細分化するか)、第 3 引数 = netnum(0 始まり何番目か)
# 10.0.0.0/16 を 8 ビット細分化 → /24 のサブネット群、その 1 番目を取得
cidrsubnet("10.0.0.0/16", 8, 1) # "10.0.1.0/24"
# CIDR の N 番目のホスト IP を返す
cidrhost("10.0.0.0/16", 5) # "10.0.0.5"
# CIDR のネットマスクを文字列で返す
cidrnetmask("10.0.0.0/24") # "255.255.255.0"
型変換・try/can
# 型を明示的に変換する関数群(自動変換が効かない時に使う)
tostring(123) # "123" ← 数値を文字列に
tonumber("3.14") # 3.14 ← 文字列を数値に
tolist(set) # set を list に(順序が安定する)
toset(list) # list を set に(重複が除去される)
# try: 第 1 引数の式を評価、エラーになったら第 2 引数を返す
# ↓ subnets が空でも落ちずにデフォルト CIDR が返る
try(var.subnets[0].cidr, "10.0.0.0/24")
# can: 第 1 引数の式が「評価可能か」を真偽値で返す(値そのものは返さない)
# variable の validation の condition で頻出パターン
can(cidrnetmask(var.cidr))
terraform console で試す
関数や式の挙動は、terraform console で対話的に試せます。コードに書く前に必ず確認できる、地味だけど超便利なコマンド。
$ terraform console
> cidrsubnet("10.0.0.0/16", 8, 1)
"10.0.1.0/24"
> merge({a=1}, {b=2})
{
"a" = 1
"b" = 2
}
> var.environment
"dev"
> exit