★★ 中級

12. provisioners と最終手段

HashiCorp 自身が「最終手段にしてくれ」と公式で明言している機能。なぜ非推奨なのか、それでも使う場面、代替案を整理します。

なぜ非推奨なのか

HashiCorp 公式は "Provisioners are a last resort" と明言しています。理由:

3 種類の provisioner

local-exec(Terraform を実行している側で)

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

  provisioner "local-exec" {
    command = "echo ${self.private_ip} >> ./hosts.txt"
  }
}

remote-exec(リソース上で SSH/WinRM 経由)

resource "aws_instance" "web" {
  # ...
  provisioner "remote-exec" {
    connection {
      type        = "ssh"
      user        = "ec2-user"
      private_key = file("~/.ssh/id_rsa")
      host        = self.public_ip
    }
    inline = [
      "sudo dnf install -y nginx",
      "sudo systemctl enable --now nginx",
    ]
  }
}

file(ファイル転送)

provisioner "file" {
  source      = "./conf/nginx.conf"
  destination = "/etc/nginx/nginx.conf"

  connection { /* 上と同じ */ }
}

creation-time と destroy-time

provisioner "local-exec" {
  when    = create   # デフォルト
  command = "echo created"
}

provisioner "local-exec" {
  when    = destroy
  command = "echo destroying ${self.id}"
  on_failure = continue   # 失敗しても止めない
}
destroy-time の制限 destroy provisioner では self. 以外の参照(var、別 resource 等)は使えません。state に保存された情報のみ。

terraform_data(旧 null_resource)

「リソースとは紐づかないが provisioner を発火したい」時に使う。Terraform 1.4+ で terraform_data が登場し、null_resource(hashicorp/null provider 提供)の代替になりました。

resource "terraform_data" "build" {
  triggers_replace = {
    src_hash = filemd5("${path.module}/lambda/index.js")
  }

  provisioner "local-exec" {
    command = "npm run build && zip -r build.zip dist/"
  }
}

# Lambda はこの triggers_replace の変更で再作成
resource "aws_lambda_function" "app" {
  filename = "build.zip"
  # ...

  depends_on = [terraform_data.build]
}

代替案

provisioner を使う前に必ず検討する選択肢:

やりたいことprovisioner ではなく
EC2 起動時にパッケージ入れるuser_data + cloud-init
OS イメージそのものを作るPacker / EC2 Image Builder
アプリの設定変更SSM Run Command / SSM State Manager
シークレットの注入Secrets Manager + 環境変数
Kubernetes リソースの作成kubernetes / helm provider(provisioner 不要)
ローカルでファイル生成local_file resource(hashicorp/local provider)
外部 API への通知EventBridge + Lambda をインフラとして組む

user_data の例(provisioner より圧倒的に推奨)

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

  user_data = <<-EOT
    #!/bin/bash
    dnf update -y
    dnf install -y nginx
    systemctl enable --now nginx
  EOT

  user_data_replace_on_change = true
}

これなら state にハッシュが入り、内容変更で正しく再作成され、Terraform から見える挙動になります。

原則 provisioner を書こうとしたら、まず「これは 本当に Terraform から走らせる必要があるか」と自問する。多くの場合、別ツール(Ansible / Packer / SSM)か user_data の方が綺麗に解決します。