12. provisioners と最終手段
HashiCorp 自身が「最終手段にしてくれ」と公式で明言している機能。なぜ非推奨なのか、それでも使う場面、代替案を整理します。
なぜ非推奨なのか
HashiCorp 公式は "Provisioners are a last resort" と明言しています。理由:
- べき等性が壊れる: terraform は「あるべき状態」を表現する宣言型。手続き的なシェル実行は「何度走らせても同じ結果」を保てない
- 失敗時の挙動が複雑: provisioner が失敗するとリソースが
tainted状態になり、次の apply で再作成される - state に何も残らない: シェルが何をしたかは Terraform が把握できない。差分検知不能
- 並列実行が難しい: 他の resource との依存解決がローカル実行に縛られる
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 # 失敗しても止めない
}
- create: リソース作成直後(デフォルト)
- destroy: リソース削除前
- on_failure:
fail(停止、デフォルト) /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 の方が綺麗に解決します。