★★ 中級

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]

多用されるのは countfor_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