Terraform 基礎 #4 データソースと式 — 作らずに読み取る、条件式と for
ここまでのコードは、すべてを自分で作ってきました。しかし実務の Terraform コードは、すでに存在するものの間で動きます。別のチームが作った VPC の中にサーバーを立てる必要があり、AWS が管理する最新の AMI ID を取得する必要があり、今どのアカウントで実行しているのかを知る必要があります。今回は、作らずに読み取る道具であるデータソースと、読み取った値を加工する式を扱います。#2 のブロック文法と #3 の変数を前提に進めます。
data ブロック — 作らずに読み取ります #
resource ブロックが「存在すべきだ」という宣言だとすれば、data ブロックは「存在するものを探してほしい」という照会です。いちばんよくある例が AMI の検索です。EC2 インスタンスを作るには AMI ID が必要ですが、この ID はリージョンごとに異なり、新しいバージョンが出るたびに変わります。ハードコードすると、すぐに古いイメージになってしまいます。
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 変数を活用するときが来ました。本番環境だけインスタンスを大きくしたいなら、条件式を使います。
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 式を使います。角かっこで囲めばリストが、波かっこで囲めばマップが得られます。
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 に置き、リソースごとのタグと結合するパターンです。
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 の動作原理の中心をのぞいていきます。