Terraform で効率アップ!補間構文とその代替手段を徹底解説
具体的には、以下の3つの主要な目的で使用されます。
-
変数(Variables)の参照
定義した変数(variable
ブロックで宣言されたもの)の値を参照するために使用します。 例:variable "region" { description = "AWS region" type = string default = "us-east-1" } resource "aws_instance" "example" { ami = "ami-0abcdef1234567890" instance_type = "t2.micro" tags = { Name = "my-instance-${var.region}" } }
この例では、
var.region
を補間構文で参照し、EC2 インスタンスのタグにリージョン名を埋め込んでいます。 -
出力値(Outputs)の参照
他のリソースから出力された値(output
ブロックで宣言されたもの)を参照するために使用します。これは、異なる Terraform 設定ファイル間で情報を渡す際や、他の Terraform 設定で利用可能な値を提供する場合に特に便利です。 例:resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" tags = { Name = "main-vpc" } } output "vpc_id" { description = "The ID of the VPC" value = aws_vpc.main.id } resource "aws_subnet" "example" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" tags = { Name = "main-subnet" } }
この例では、
aws_vpc.main.id
を補間構文で参照し、サブネットのvpc_id
に設定しています。 -
式(Expressions)の評価
文字列の連結、算術演算、条件式、関数呼び出しなど、より複雑なロジックを埋め込むことができます。 例:variable "instance_count" { description = "Number of instances" type = number default = 2 } resource "aws_instance" "app" { count = var.instance_count ami = "ami-0abcdef1234567890" instance_type = "t2.micro" tags = { Name = "app-instance-${count.index + 1}" # count.index はインスタンスのインデックス } } output "instance_names" { value = [for i in range(var.instance_count) : "app-instance-${i + 1}"] }
この例では、
count.index + 1
のように算術演算を行い、インスタンス名に連番を振っています。また、for
式を使って出力値のリストを動的に生成しています。
- 参照できない値
まだ作成されていないリソースの属性や、存在しない変数などを参照しようとするとエラーになります。Terraform は実行計画(plan)を生成する際に、これらの依存関係を解決しようとします。 - 非文字列のコンテキスト
数字や真偽値など、文字列以外の値が期待される場所で補間構文を使用する場合、Terraform は自動的に適切な型に変換しようとします。 - 文字列リテラル内
文字列リテラル(""
で囲まれた部分)の中で補間構文を使用する場合、補間された値は文字列として扱われます。
補間構文の一般的なエラーとトラブルシューティング
不適切な構文 (Syntax Errors)
最も基本的なエラーは、補間構文の書き方を間違えている場合です。
- 解決策
正しい補間構文を使用します。
文字列の中に変数を埋め込む場合は、上記のように全体を引用符で囲み、tags = { Name = "${var.name}-web_app" }
${}
の中に式を記述します。 - 原因
補間構文は必ず${}
で囲む必要があります。また、変数を参照する場合はvar.
プレフィックスが必要です。 - 考えられるエラーメッセージ
Error: Invalid character
Error: Invalid expression
Error: Expected the start of an expression, but found an invalid expression token.
- エラー例
Name = $var.name-web_app
未定義の変数/属性の参照 (Undefined Variable/Attribute Reference)
存在しない変数や、リソースの未定義の属性を参照しようとするとエラーになります。
- 解決策
- 変数を正しく
variable
ブロックで定義するか、正しい変数名を使用します。 - 参照しようとしている属性が、そのリソースタイプに存在するかを Terraform のドキュメントで確認し、正しい属性名を使用します。
terraform plan
を実行して、計画段階でどのような値が利用可能かを確認することも有効です。
- 変数を正しく
- 原因
variable
ブロックで変数を定義していない。- 参照しようとしているリソースの属性が、そのリソースタイプに存在しない。公式ドキュメントで確認が必要です。
- 考えられるエラーメッセージ
Error: Reference to undeclared input variable
Error: Unsupported attribute
- エラー例
またはresource "aws_instance" "example" { ami = "ami-0abcdef1234567890" instance_type = var.non_existent_type # この変数はどこにも定義されていない }
resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" } output "vpc_name" { value = aws_vpc.main.name # VPCリソースには'name'属性がない }
型の不一致 (Type Mismatch)
期待される型と、補間された値の型が一致しない場合にエラーが発生します。
- 解決策
型変換関数 (tonumber()
,tostring()
,tobool()
) を使用して明示的に型を変換します。
または、変数自体の型定義を見直します。resource "aws_security_group_rule" "http_in" { type = "ingress" from_port = tonumber(var.port_number) to_port = tonumber(var.port_number) protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] }
- 原因
変数や式の結果の型が、割り当て先の引数が期待する型と異なる。 - 考えられるエラーメッセージ
Error: Invalid value for input variable
Error: Argument is not a number
(Terraform 0.12以降は改善されていますが、以前のバージョンや特定の状況で発生)
- エラー例
(実際には Terraform がある程度自動変換しますが、複雑なケースで発生することがあります)variable "port_number" { type = string default = "8080" } resource "aws_security_group_rule" "http_in" { type = "ingress" from_port = var.port_number # from_portはnumber型を期待する to_port = var.port_number protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] }
循環参照 (Circular Dependencies)
2つ以上のリソースが互いに参照し合い、無限ループのような依存関係を作成するとエラーになります。
- 解決策
- 依存関係を解除する
循環依存を避けるようにリソースの設計を見直します。上記の例では、セキュリティグループのルールをaws_security_group_rule
リソースとして分離することで解決できます。resource "aws_security_group" "sg_a" { name = "sg-a" } resource "aws_security_group" "sg_b" { name = "sg-b" } resource "aws_security_group_rule" "sg_a_to_sg_b" { type = "ingress" from_port = 80 to_port = 80 protocol = "tcp" security_group_id = aws_security_group.sg_a.id source_security_group_id = aws_security_group.sg_b.id } resource "aws_security_group_rule" "sg_b_to_sg_a" { type = "ingress" from_port = 22 to_port = 22 protocol = "tcp" security_group_id = aws_security_group.sg_b.id source_security_group_id = aws_security_group.sg_a.id }
- depends_on の慎重な使用
depends_on
は明示的な依存関係を定義しますが、不適切に使うと循環参照を引き起こすことがあります。できる限り暗黙的な依存関係(補間構文による参照)を利用し、depends_on
は最後の手段として検討します。
- 依存関係を解除する
- 原因
リソースAがリソースBに依存し、同時にリソースBがリソースAに依存している状況。Terraformはどちらを先に作成すべきか判断できません。 - 考えられるエラーメッセージ
Error: Cycle: aws_security_group.sg_a, aws_security_group.sg_b.
- エラー例
resource "aws_security_group" "sg_a" { name = "sg-a" ingress { from_port = 80 to_port = 80 protocol = "tcp" security_groups = [aws_security_group.sg_b.id] # sg_bを参照 } } resource "aws_security_group" "sg_b" { name = "sg-b" ingress { from_port = 22 to_port = 22 protocol = "tcp" security_groups = [aws_security_group.sg_a.id] # sg_aを参照 } }
リソースのプロビジョニング順序に関する問題
補間構文は、参照されるリソースがすでに存在しているか、少なくとも計画段階で値が決定できる場合に機能します。
- 解決策
- 暗黙的な依存関係の活用
可能な限り補間構文による参照を使用し、Terraform に依存関係を自動検出させます。 - depends_on の使用 (最終手段)
どうしても順序を明示したい場合は、depends_on
メタ引数を使って、特定のリソースが他のリソースの後に作成されるように強制します。ただし、乱用は避けるべきです。resource "aws_instance" "web" { # ... depends_on = [ aws_security_group.web_sg # web_sgが作成されてからwebインスタンスを作成 ] }
- 暗黙的な依存関係の活用
- 原因
Terraform は依存関係グラフに基づいてリソースを作成しますが、稀に複雑なシナリオで予期しない順序になることがあります。 - エラー例
terraform apply
実行中に、参照しようとしたリソースがまだ作成されていない、またはその属性の値が確定していないためにエラーになる場合。
- terraform validate を実行する
terraform validate
コマンドは、設定ファイルの構文チェックと基本的なセマンティックチェックを行います。適用前にエラーを発見するのに非常に役立ちます。 - terraform plan を実行する
terraform plan
は、実際にリソースがどのように変更されるかを示します。補間された値が期待通りに解決されているか、未定義の値がないかなどを確認できます。(known after apply)
と表示される場合は、その値がterraform apply
が実行されるまで確定しないことを意味します。 - terraform console を使用する
terraform console
は、Terraform の式をインタラクティブに評価できる便利なツールです。特定の補間式が期待する結果を返すかを確認するのに役立ちます。$ terraform console > var.region "us-east-1" > "my-instance-${var.region}" "my-instance-us-east-1" > aws_vpc.main.id (known after apply) # リソースがまだ作成されていないため
- 公式ドキュメントを参照する
エラーメッセージに記載されているリソースやプロバイダーの公式ドキュメントで、利用可能な引数や属性、期待される型を確認します。 - TF_LOG 環境変数を使用する
より詳細なデバッグ情報を得るには、TF_LOG=DEBUG
やTF_LOG=TRACE
を設定して Terraform コマンドを実行します。大量のログが出力されますが、問題の根本原因を特定するのに役立つ場合があります。TF_LOG=TRACE terraform apply
- コードのフォーマットとリンティング
terraform fmt
: コードのフォーッグを自動で整形し、構文的な問題を一部修正してくれることがあります。- TFLint: Terraform のコード品質をチェックし、潜在的な問題やベストプラクティスからの逸脱を警告してくれるツールです。
変数の参照 (Referencing Variables)
最も基本的な使用例で、variable
ブロックで定義した値を使用します。
main.tf
# 1. 変数定義
variable "aws_region" {
description = "AWSデプロイリージョン"
type = string
default = "us-east-1"
}
variable "instance_type" {
description = "EC2インスタンスタイプ"
type = string
default = "t2.micro"
}
variable "project_name" {
description = "プロジェクト名"
type = string
default = "MyWebApp"
}
# 2. 変数の補間
resource "aws_instance" "example_instance" {
ami = "ami-0abcdef1234567890" # 仮のAMI ID
instance_type = var.instance_type # var.instance_type を参照
tags = {
Name = "${var.project_name}-web-server" # ${var.project_name} を参照して文字列に埋め込む
Region = var.aws_region # var.aws_region を参照
}
}
# 3. 出力値での変数の補間
output "instance_name_tag" {
description = "インスタンスのNameタグ"
value = "作成されたインスタンス名: ${aws_instance.example_instance.tags.Name}"
}
output "instance_region" {
description = "インスタンスがデプロイされたリージョン"
value = var.aws_region
}
解説
"${aws_instance.example_instance.tags.Name}"
:aws_instance.example_instance
リソースのtags
マップからName
キーの値を取得し、出力値の文字列に埋め込んでいます。"${var.project_name}-web-server"
:project_name
変数の値(MyWebApp
)を文字列の中に埋め込んでいます。結果としてMyWebApp-web-server
というタグが設定されます。var.instance_type
:instance_type
変数の値(t2.micro
)を直接参照しています。
ローカル値の参照 (Referencing Local Values)
main.tf
variable "environment" {
type = string
default = "dev"
}
variable "service_name" {
type = string
default = "api"
}
# 1. ローカル値の定義
locals {
full_resource_name = "${var.environment}-${var.service_name}-01"
common_tags = {
Environment = var.environment
Service = var.service_name
ManagedBy = "Terraform"
}
}
# 2. ローカル値の補間
resource "aws_s3_bucket" "my_bucket" {
bucket = local.full_resource_name # local.full_resource_name を参照
tags = local.common_tags # local.common_tags を参照
}
output "bucket_id" {
description = "作成されたS3バケットのID"
value = aws_s3_bucket.my_bucket.id
}
解説
local.common_tags
: 定義済みのタグマップをtags
引数に直接渡しています。local.full_resource_name
:locals
ブロックで定義したfull_resource_name
の値(例:dev-api-01
)を S3 バケット名として使用しています。
リソース属性の参照 (Referencing Resource Attributes)
Terraform がプロビジョニングするリソースの特定の属性(ID、IPアドレスなど)を参照します。これはリソース間の依存関係を構築する上で不可欠です。
main.tf
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "main-vpc"
}
}
# 1. VPCのIDを参照
resource "aws_subnet" "public_subnet" {
vpc_id = aws_vpc.main.id # aws_vpc.main リソースの id 属性を参照
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
tags = {
Name = "public-subnet"
}
}
# 2. サブネットのIDを参照
resource "aws_instance" "web_server" {
ami = "ami-0abcdef1234567890" # 仮のAMI ID
instance_type = "t2.micro"
subnet_id = aws_subnet.public_subnet.id # aws_subnet.public_subnet リソースの id 属性を参照
tags = {
Name = "web-server-instance"
}
}
output "vpc_id" {
value = aws_vpc.main.id
}
output "subnet_id" {
value = aws_subnet.public_subnet.id
}
output "instance_private_ip" {
value = aws_instance.web_server.private_ip # web_server インスタンスの private_ip 属性を参照
}
解説
aws_instance.web_server.private_ip
:aws_instance
タイプのweb_server
という名前のリソースがプロビジョニングされた後に利用可能になるprivate_ip
属性を参照しています。aws_subnet.public_subnet.id
:aws_subnet
タイプのpublic_subnet
という名前のリソースのid
属性を参照しています。aws_vpc.main.id
:aws_vpc
タイプのmain
という名前のリソースのid
属性を参照しています。これにより、サブネットは作成された VPC に関連付けられます。
関数の呼び出し (Function Calls)
Terraform に組み込まれている様々な関数(文字列操作、数値計算、リスト/マップ操作など)を補間構文内で使用できます。
main.tf
variable "bucket_prefix" {
type = string
default = "my-unique-app"
}
variable "server_count" {
type = number
default = 3
}
# 1. 文字列操作関数
resource "aws_s3_bucket" "logs_bucket" {
# lower() 関数で文字列を小文字に変換
bucket = lower("${var.bucket_prefix}-logs-${random_string.suffix.result}")
}
# ユニークな文字列を生成するリソース
resource "random_string" "suffix" {
length = 8
special = false
upper = false
}
# 2. 数値計算とリスト操作
output "server_names" {
description = "生成されるサーバー名のリスト"
# range() と format() 関数を使用して、0からserver_count-1までのリストを生成し、サーバー名をフォーマット
value = [for i in range(var.server_count) : "server-${format("%02d", i + 1)}"]
}
# 3. 条件分岐 (Conditional Expressions)
output "env_message" {
description = "環境に応じたメッセージ"
# var.environment が "prod" なら "本番環境です"、そうでなければ "開発/テスト環境です"
value = var.environment == "prod" ? "本番環境です" : "開発/テスト環境です"
}
解説
var.environment == "prod" ? "本番環境です" : "開発/テスト環境です"
: 三項演算子 (? :
) を使用して、environment
変数の値に基づいて異なる文字列を出力しています。[for i in range(var.server_count) : "server-${format("%02d", i + 1)}"]
:for
式(高度な補間)とrange()
、format()
関数を組み合わせて、server-01
,server-02
,server-03
のようなサーバー名のリストを動的に生成しています。lower("${var.bucket_prefix}-logs-${random_string.suffix.result}")
:lower()
関数を使って、バケット名を全て小文字に変換しています。random_string.suffix.result
は、別のリソースが生成したユニークな文字列を参照しています。
複数のインスタンスを作成する際に、それぞれのインスタンス固有の値を補間構文で生成できます。
main.tf
variable "instance_names" {
type = list(string)
default = ["web-01", "db-01", "app-01"]
}
# 1. count と組み合わせる
resource "aws_instance" "servers_count" {
count = length(var.instance_names) # instance_names の数だけインスタンスを作成
ami = "ami-0abcdef1234567890"
instance_type = "t2.micro"
tags = {
Name = var.instance_names[count.index] # count.index を使用してリストから名前を取得
}
}
# 2. for_each と組み合わせる (推奨される方法)
variable "users" {
type = map(object({
id = string
email = string
}))
default = {
"alice" = {
id = "user-alice"
email = "[email protected]"
},
"bob" = {
id = "user-bob"
email = "[email protected]"
}
}
}
resource "aws_iam_user" "team_users" {
for_each = var.users # users マップの各キー/値ペアに対してユーザーを作成
name = each.key # for_each のキー (例: "alice") をユーザー名に
tags = {
UserID = each.value.id # for_each の値 (マップ) から id を取得
Email = each.value.email # for_each の値 (マップ) から email を取得
}
}
output "user_names_from_count" {
value = [for instance in aws_instance.servers_count : instance.tags.Name]
}
output "iam_user_names" {
value = [for user in aws_iam_user.team_users : user.name]
}
each.key
とeach.value
:for_each
を使用して複数のリソースを作成する場合、each.key
は現在の反復のキー(例:"alice"
)を提供し、each.value
は現在の反復の値(例:{"id": "user-alice", "email": "[email protected]"}
)を提供します。これにより、マップのキーと値を使って動的にリソースを設定できます。count.index
:count
を使用して複数のリソースを作成する場合、count.index
は現在のインスタンスのゼロベースのインデックス(0, 1, 2...)を提供します。これにより、リストから対応する名前を取り出しています。
主な代替方法または補完的な方法を以下に説明します。
templatefile 関数(テンプレートファイルの使用)
複雑な文字列の組み立てや、設定ファイルの内容を動的に生成したい場合に非常に有用です。特に、設定ファイルが JSON、YAML、XML などの構造化された形式であり、その中に動的な値を埋め込みたい場合に適しています。
補間構文との違い
- templatefile
独立したテンプレートファイル(例:.tpl
、.tmpl
)を作成し、そのファイル内のプレースホルダーに変数を渡して最終的な文字列を生成します。 - 補間構文
基本的にTerraformの設定ファイル(.tf
)内で直接値を埋め込むために使われます。
コード例
cloud-init.tpl
(テンプレートファイル)
#cloud-config
users:
- name: ${username}
groups: sudo
shell: /bin/bash
ssh_authorized_keys:
- ${ssh_key}
runcmd:
- echo "Hello from ${environment} environment!" > /tmp/hello.txt
- echo "Instance name: ${instance_name}" >> /tmp/hello.txt
main.tf
(Terraform 設定ファイル)
variable "instance_name" {
type = string
default = "my-web-server"
}
variable "admin_username" {
type = string
default = "adminuser"
}
variable "public_key" {
type = string
description = "SSH公開鍵"
}
variable "environment" {
type = string
default = "development"
}
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890" # 仮のAMI ID
instance_type = "t2.micro"
# templatefile 関数を使用してcloud-initスクリプトを生成
user_data = templatefile("${path.module}/cloud-init.tpl", {
username = var.admin_username
ssh_key = var.public_key
environment = var.environment
instance_name = var.instance_name
})
tags = {
Name = var.instance_name
}
}
output "instance_user_data" {
value = aws_instance.web.user_data
sensitive = true # ログに表示されないようにする
}
解説
templatefile
関数は第一引数にテンプレートファイルのパスを、第二引数にテンプレート内で使用する変数をマップとして渡します。テンプレートファイル内では、${変数名}
の形式でプレースホルダーを定義し、Terraform がこれらのプレースホルダーを実際の値で置き換えてくれます。
データソース(Data Sources)
既存のリソースの情報を取得したり、外部システムからデータをフェッチしたりするために使用します。これにより、Terraform の管理外の情報を参照したり、他のTerraform設定で作成されたリソースの情報を参照したりできます。
補間構文との違い
- データソース
Terraformが管理「していない」既存のインフラストラクチャや、外部のサービス/システムからデータを「読み込み」ます。 - 補間構文
主にTerraformが現在管理しているリソースや変数、ローカル値からデータを参照します。
コード例
main.tf
# 既存のVPCをIDで検索し、その情報を取得
data "aws_vpc" "existing_vpc" {
id = "vpc-0abcdef1234567890" # 既存のVPCのID
}
# 既存のAMIを最新版で検索し、そのIDを取得
data "aws_ami" "latest_amazon_linux" {
owners = ["amazon"]
most_recent = true
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
resource "aws_instance" "example" {
ami = data.aws_ami.latest_amazon_linux.id # データソースからAMI IDを参照
instance_type = "t2.micro"
vpc_security_group_ids = [data.aws_vpc.existing_vpc.default_security_group_id] # データソースからセキュリティグループIDを参照
subnet_id = data.aws_vpc.existing_vpc.default_subnet_id # データソースからデフォルトサブネットIDを参照
tags = {
Name = "instance-in-existing-vpc"
}
}
output "found_ami_id" {
value = data.aws_ami.latest_amazon_linux.id
}
output "found_vpc_cidr_block" {
value = data.aws_vpc.existing_vpc.cidr_block
}
解説
data "aws_ami" "latest_amazon_linux"
: 最新のAmazon Linux AMIの情報を取得し、そのIDをdata.aws_ami.latest_amazon_linux.id
で参照できるようにします。data "aws_vpc" "existing_vpc"
: 既存のVPCの情報を取得し、その属性(id
,cidr_block
など)をdata.aws_vpc.existing_vpc.属性名
の形式で参照できるようにします。
external データソース(外部スクリプトの実行)
Terraform の組み込み機能だけでは対応できない複雑なロジックや、シェルスクリプト、Pythonスクリプトなどで処理を行いたい場合に利用します。外部スクリプトは JSON 形式でデータを読み込み、JSON 形式で結果を返します。
補間構文との違い
- external データソース
Terraform の外部で任意のスクリプトを実行し、そのスクリプトの出力結果をTerraformに渡します。 - 補間構文
Terraform の式言語内で直接処理が完結します。
コード例
scripts/generate_random_string.sh
(実行可能な外部スクリプト)
#!/bin/bash
# JSON形式で入力を受け取る
eval "$(jq -r '@sh "LENGTH=\(.length) PREFIX=\(.prefix)"')"
# ランダムな文字列を生成 (ここでは単純化)
RANDOM_STRING=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c "${LENGTH}")
# JSON形式で結果を出力
jq -n --arg random_str "${PREFIX}-${RANDOM_STRING}" '{"generated_string": $random_str}'
main.tf
(Terraform 設定ファイル)
data "external" "random_suffix" {
program = ["bash", "${path.module}/scripts/generate_random_string.sh"]
query = {
length = "8"
prefix = "app"
}
}
resource "aws_s3_bucket" "my_bucket" {
bucket = data.external.random_suffix.result.generated_string # 外部スクリプトの出力結果を参照
tags = {
ManagedBy = "Terraform"
}
}
output "generated_bucket_name" {
value = aws_s3_bucket.my_bucket.bucket
}
解説
data.external.random_suffix.result.generated_string
: 外部スクリプトが標準出力にJSONで出力した結果(例:{"generated_string": "app-abcdefg1"}
)をresult
マップとして取得し、その中のgenerated_string
キーの値を参照しています。data "external" "random_suffix"
:program
で指定されたスクリプトを実行します。query
でスクリプトに渡す入力を JSON 形式で定義します。
- external データソース
Terraform の式言語では実現が困難な複雑なロジックを、外部スクリプトで実行し、その結果をTerraformに渡す場合に利用する。 - データソース
Terraform が管理していない既存のインフラや、外部サービスから情報を「読み込む」場合に利用する。 - templatefile 関数
複雑なテキストファイルや設定ファイルを動的に生成したい場合に、外部のテンプレートファイルを利用する。 - 補間構文(${})
Terraform 内で直接値を埋め込むための基本。最も頻繁に使用される。
これらの方法は、補間構文を「代替」するというよりは、補間構文では対応しきれない複雑なシナリオや、外部との連携が必要なシナリオにおいて、Terraform の能力を拡張するための「補完的な手段」として理解するのが適切です。 Terraform の補間構文は、HCL (HashiCorp Configuration Language) の強力な機能であり、ほとんどの動的な値の埋め込みに使用されます。しかし、特定のシナリオでは、補間構文だけでは対応しきれない、またはより柔軟な方法が求められる場合があります。ここでは、補間構文の代替となる、あるいは補間構文と組み合わせて使うことでさらに高度な処理を実現する方法をいくつか説明します。
templatefile 関数
Terraform 0.12 以降で導入された templatefile
関数は、ファイルからテンプレートを読み込み、そこに変数値を埋め込むことができる、非常に強力な機能です。特に、アプリケーションのコンフィグファイル、シェルスクリプト、User Data(EC2インスタンス起動時に実行されるスクリプト)など、複数行にわたる複雑なテキストを動的に生成したい場合に最適です。
特徴
templatefile
関数にマップ形式で変数を渡すことで、テンプレート内でそれらの変数を参照できる。- テンプレート内では通常の補間構文 (
${...}
) に加えて、for
ディレクティブやif
ディレクティブといったより高度なテンプレートディレクティブが使用可能。 - 外部ファイル (
.tftpl
拡張子が推奨される) にテンプレートを記述できる。
使用例
cloud-init.tftpl
(テンプレートファイル)
#cloud-config
package_update: true
packages:
- nginx
- curl
runcmd:
- echo "Welcome to ${hostname} from ${environment} environment!" > /etc/motd
- echo "Private IP: ${private_ip_address}" >> /tmp/my_info.txt
%{ for user in users ~}
- useradd -m ${user.username}
- echo "${user.username}:${user.password}" | chpasswd
%{ endfor ~}
main.tf
variable "environment" {
type = string
default = "dev"
}
variable "server_hostname" {
type = string
default = "web-server"
}
variable "app_users" {
type = list(object({
username = string
password = string
}))
default = [
{ username = "admin", password = "password123" },
{ username = "guest", password = "guestpass" }
]
}
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890" # 仮のAMI ID
instance_type = "t2.micro"
# templatefile 関数を呼び出し、変数とリソース属性を渡す
user_data = templatefile("${path.module}/cloud-init.tftpl", {
hostname = var.server_hostname
environment = var.environment
private_ip_address = self.private_ip # self は現在のリソースを参照
users = var.app_users
})
tags = {
Name = var.server_hostname
}
}
output "instance_private_ip" {
value = aws_instance.web.private_ip
}
解説
user_data
に templatefile
関数を使用し、cloud-init.tftpl
ファイルを読み込んでいます。{ hostname = var.server_hostname, ... }
の部分で、テンプレート内で利用可能な変数を定義しています。テンプレート内では、通常の ${hostname}
のような補間に加えて、%{ for user in users ~}
のような for
ループディレクティブを使って、複数のユーザーを動的に追加しています。
jsonencode / yamlencode 関数
もし生成したい文字列が JSON や YAML 形式である場合、jsonencode
や yamlencode
関数を使用することが強く推奨されます。これにより、手動でクォートやエスケープ処理を行う手間が省け、構文エラーのリスクを大幅に減らすことができます。
特徴
- 手動でのエスケープが不要。
- 複雑なネストされた構造も正確に処理される。
- Terraform のデータ構造(マップ、リストなど)を直接 JSON/YAML 文字列に変換する。
使用例
main.tf
variable "backend_servers" {
type = list(object({
ip = string
port = number
}))
default = [
{ ip = "10.0.1.10", port = 8080 },
{ ip = "10.0.1.11", port = 8081 }
]
}
locals {
# jsonencode を使用して JSON 文字列を生成
nginx_config_json = jsonencode({
http = {
server = {
listen = 80
location = {
"/" = {
proxy_pass = "http://backend_pool"
}
}
}
},
upstreams = {
backend_pool = [
for server in var.backend_servers : {
server = "${server.ip}:${server.port}"
}
]
}
})
# yamlencode を使用して YAML 文字列を生成
k8s_config_yaml = yamlencode({
apiVersion = "v1"
kind = "ConfigMap"
metadata = {
name = "my-app-config"
namespace = "default"
}
data = {
"app.properties" = <<-EOT
app.name=${var.app_name}
app.environment=${var.environment}
EOT
"servers.yaml" = yamlencode(var.backend_servers) # YAMLの中にYAMLを埋め込む
}
})
}
resource "aws_s3_bucket_object" "nginx_config" {
bucket = "my-config-bucket-12345" # 既存のバケット名
key = "nginx.conf"
content = local.nginx_config_json # JSON文字列を直接ファイルコンテンツとして使用
content_type = "application/json"
}
resource "aws_s3_bucket_object" "k8s_config" {
bucket = "my-config-bucket-12345"
key = "k8s_config.yaml"
content = local.k8s_config_yaml # YAML文字列を直接ファイルコンテンツとして使用
content_type = "application/yaml"
}
解説
jsonencode
や yamlencode
は、Terraform のマップやリストの構造を直接引数として受け取り、適切な JSON/YAML 形式の文字列を返します。これにより、複雑な設定ファイルをプログラム的に生成する際に、手動でフォーマットを気にする必要がなくなります。特に、for
式と組み合わせることで、リスト内の要素を動的に JSON/YAML の配列として表現できます。
ごく稀なケースですが、Terraform の組み込み機能やプロバイダーだけでは実現できない複雑なロジックや、外部システムの情報を動的に取得する必要がある場合、external
データソースを利用できます。これは、外部のスクリプト(Python, Bash, Node.jsなど)を実行し、その標準出力から JSON 形式のデータを受け取ることで、Terraform に情報を取り込む方法です。
特徴
- 注意点
外部プログラムへの依存が発生するため、ポータビリティが低下する可能性があります。また、データソースは読み取り専用であり、外部プログラムでサイドエフェクト(リソース作成/変更など)を起こすべきではありません。 - 実行結果を JSON で返し、Terraform の他の場所で参照可能になる。
- Terraform の外部で任意のロジックを実行できる。
使用例
get_my_data.py
(外部Pythonスクリプト)
#!/usr/bin/env python3
import json
import sys
# Terraform から JSON 形式で入力クエリを受け取る
input_data = json.load(sys.stdin)
region = input_data.get("region", "unknown")
env = input_data.get("environment", "default")
# 何らかのロジックでデータを生成 (例: 外部API呼び出し、複雑な計算など)
output_data = {
"server_list": f"server-A-{region}-{env},server-B-{region}-{env}",
"timestamp": "2025-06-05T12:00:00Z" # ここではハードコードだが、実際には動的に生成
}
# 結果を JSON 形式で標準出力に出力
json.dump(output_data, sys.stdout)
main.tf
variable "aws_region" {
type = string
default = "ap-northeast-1"
}
variable "environment" {
type = string
default = "prod"
}
# 1. external データソースの定義
data "external" "my_custom_data" {
program = ["python3", "${path.module}/get_my_data.py"] # 実行するスクリプトを指定
query = { # スクリプトに渡す入力データ (JSONとしてstdinに渡される)
region = var.aws_region
environment = var.environment
}
}
resource "aws_instance" "example" {
ami = "ami-0abcdef1234567890"
instance_type = "t2.micro"
tags = {
# external データソースの結果を参照
Servers = data.external.my_custom_data.result.server_list
Timestamp = data.external.my_custom_data.result.timestamp
}
}
output "generated_server_list" {
value = data.external.my_custom_data.result.server_list
}
output "generated_timestamp" {
value = data.external.my_custom_data.result.timestamp
}
解説
data "external" "my_custom_data"
ブロックで get_my_data.py
スクリプトを実行し、query
で変数 region
と environment
を渡しています。スクリプトが返す JSON の server_list
と timestamp
は、data.external.my_custom_data.result.server_list
のように参照でき、他のリソースの属性に設定できます。