Terraform で効率アップ!補間構文とその代替手段を徹底解説

2025-06-06

具体的には、以下の3つの主要な目的で使用されます。

  1. 変数(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 インスタンスのタグにリージョン名を埋め込んでいます。

  2. 出力値(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 に設定しています。

  3. 式(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 実行中に、参照しようとしたリソースがまだ作成されていない、またはその属性の値が確定していないためにエラーになる場合。
  1. terraform validate を実行する
    terraform validate コマンドは、設定ファイルの構文チェックと基本的なセマンティックチェックを行います。適用前にエラーを発見するのに非常に役立ちます。
  2. terraform plan を実行する
    terraform plan は、実際にリソースがどのように変更されるかを示します。補間された値が期待通りに解決されているか、未定義の値がないかなどを確認できます。(known after apply) と表示される場合は、その値が terraform apply が実行されるまで確定しないことを意味します。
  3. 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) # リソースがまだ作成されていないため
    
  4. 公式ドキュメントを参照する
    エラーメッセージに記載されているリソースやプロバイダーの公式ドキュメントで、利用可能な引数や属性、期待される型を確認します。
  5. TF_LOG 環境変数を使用する
    より詳細なデバッグ情報を得るには、TF_LOG=DEBUGTF_LOG=TRACE を設定して Terraform コマンドを実行します。大量のログが出力されますが、問題の根本原因を特定するのに役立つ場合があります。
    TF_LOG=TRACE terraform apply
    
  6. コードのフォーマットとリンティング
    • 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.keyeach.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_datatemplatefile 関数を使用し、cloud-init.tftpl ファイルを読み込んでいます。{ hostname = var.server_hostname, ... } の部分で、テンプレート内で利用可能な変数を定義しています。テンプレート内では、通常の ${hostname} のような補間に加えて、%{ for user in users ~} のような for ループディレクティブを使って、複数のユーザーを動的に追加しています。

jsonencode / yamlencode 関数

もし生成したい文字列が JSON や YAML 形式である場合、jsonencodeyamlencode 関数を使用することが強く推奨されます。これにより、手動でクォートやエスケープ処理を行う手間が省け、構文エラーのリスクを大幅に減らすことができます。

特徴

  • 手動でのエスケープが不要。
  • 複雑なネストされた構造も正確に処理される。
  • 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"
}

解説
jsonencodeyamlencode は、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 で変数 regionenvironment を渡しています。スクリプトが返す JSON の server_listtimestamp は、data.external.my_custom_data.result.server_list のように参照でき、他のリソースの属性に設定できます。