Terraform 基礎 #4 データソースと式 — 作らずに読み取る、条件式と for

読了 5分

ここまでのコードは、すべてを自分で作ってきました。しかし実務の Terraform コードは、すでに存在するものの間で動きます。別のチームが作った VPC の中にサーバーを立てる必要があり、AWS が管理する最新の AMI ID を取得する必要があり、今どのアカウントで実行しているのかを知る必要があります。今回は、作らずに読み取る道具であるデータソースと、読み取った値を加工する式を扱います。#2 のブロック文法と #3 の変数を前提に進めます。

data ブロック — 作らずに読み取ります #

resource ブロックが「存在すべきだ」という宣言だとすれば、data ブロックは「存在するものを探してほしい」という照会です。いちばんよくある例が AMI の検索です。EC2 インスタンスを作るには AMI ID が必要ですが、この ID はリージョンごとに異なり、新しいバージョンが出るたびに変わります。ハードコードすると、すぐに古いイメージになってしまいます。

main.tf
data "aws_ami" "al2023" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023*-x86_64"]
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.al2023.id
  instance_type = "t3.micro"
}

参照の文法はリソースとほぼ同じで、先頭に data. が付くことだけが違います。plan を実行するたびに条件に合う最新の AMI を照会するので、ID をコードに書く必要がなくなります。実行中のアカウント情報を返す aws_caller_identity、現在のリージョンを返す aws_region、既存の VPC を探す aws_vpc のように、プロバイダーごとにさまざまなデータソースがあり、ドキュメントで Resources の隣にある Data Sources の一覧から確認できます。別のチームやコンソールで作られたリソースを参照だけしたいとき、管理権限を引き取らずに読み取ることがデータソースの役割です。

条件式 — 環境によって値を変える #

#3 で作った env 変数を活用するときが来ました。本番環境だけインスタンスを大きくしたいなら、条件式を使います。

main.tf
resource "aws_instance" "web" {
  ami           = data.aws_ami.al2023.id
  instance_type = var.env == "prod" ? "m7i.large" : "t3.micro"
}

条件 ? 真のときの値 : 偽のときの値 という形で、多くのプログラミング言語の三項演算子と同じです。#6 で見ますが、この条件式を count と組み合わせると「本番環境にだけリソースを作る」といった分岐も可能です。ただし、条件式が 3 重、4 重にネストし始めると読めないコードになるので、そのときは locals で名前を付けて分けたほうがよいです。

for 式とスプラット — コレクションの加工 #

リストやマップを変形するときは for 式を使います。角かっこで囲めばリストが、波かっこで囲めばマップが得られます。

main.tf
locals {
  team = ["alice", "bob", "carol"]

  # リスト → リスト: 各名前にプレフィックスを付ける
  usernames = [for name in local.team : "dev-${name}"]

  # リスト → マップ: 名前をキーにしたタグマップを作る
  member_tags = { for name in local.team : name => "member" }

  # 条件フィルター: if を付けて絞り込む
  admins = [for name in local.team : name if name != "carol"]
}

複数のリソースから同じ属性だけを抜き出すときは、スプラット(splat)文法 [*] が近道です。たとえばサブネットを複数作ったなら、aws_subnet.public[*].id が ID だけを集めたリストになります。for でも同じことはできますが、「全部からこの属性だけ」というよくあるケースを短く書くための文法です。

組み込み関数 — 自作はできず、選んで使うもの #

Terraform には関数を自分で定義する文法がありません。代わりに組み込み関数が豊富に用意されています。よく使うものだけに絞ると次のとおりです。

関数役割
length(list)コレクションの長さ
merge(map1, map2)マップの結合(共通タグ + 個別タグのパターン)
format("app-%s", var.env)文字列のフォーマット
file("policy.json")ファイルの内容を文字列として読み取る
jsonencode({...})HCL の値を JSON 文字列に(IAM ポリシー作成の定番)
toset(list)リストをセットに(#6 の for_each で必要)

この中で merge は、実務では毎日使うことになります。すべてのリソースに付ける共通タグを locals に置き、リソースごとのタグと結合するパターンです。

main.tf
tags = merge(local.common_tags, { Name = "web-server" })

全部の一覧を覚える必要はなく、関数のドキュメントを開いておいて、必要なときに探せば十分です。

terraform console — 式の実験場 #

式がどんな値を作るのか、apply なしで確認したいときがあります。terraform console を実行すると対話型のプロンプトが開きます。

実行結果
$ terraform console
> [for name in ["alice", "bob"] : "dev-${name}"]
[
  "dev-alice",
  "dev-bob",
]
> format("app-%s", "prod")
"app-prod"

現在のディレクトリの変数やリソースも読み取れるので、data.aws_ami.al2023.id を打ってみると実際の照会結果が返ってきます。for 式を初めて書くときは、ここで何度か試してからコードに移すと、plan を繰り返しながら試行錯誤する時間が大幅に減ります。

まとめ #

今回扱った内容です。

  • data ブロックは Terraform が作っていない既存リソースを照会し、data.タイプ.名前.属性 で参照します。最新 AMI の検索が代表例です
  • 条件式 条件 ? a : b で環境ごとの分岐を作り、ネストが深くなったら locals で分けます
  • for 式は角かっこでリストを、波かっこでマップを作り、if でフィルタリングします。同じ属性だけを集めるときはスプラット [*] が短く書けます
  • 関数は自作できず、組み込み関数を使います。merge で共通タグを結合するパターンと jsonencode は実務の定番です
  • terraform console は式を apply なしで実験する道具です

次回(#5 state の正体)では、#1 からずっと先送りにしてきた質問に答えます。Terraform は自分が作ったものをどうやって覚えているのでしょうか。terraform.tfstate ファイルを開きながら、Terraform の動作原理の中心をのぞいていきます。

X