そういった悩みを抱えているエンジニアは多い。null_resourceは確かに便利だが、使い方を誤るとTerraformコードの可読性が急落し、インフラのドリフト検出も困難になる。
この記事では、null_resourceとterraform_dataの違い・使い分け、そしてlocal-execプロビジョナーを「いつ使っていいか」「いつ代替手段を選ぶべきか」の判断基準を実践的に解説する。
この記事のポイント
・null_resourceはTerraform 0.x時代の回避策、terraform_dataが現代の正解
・local-execは「Terraformで管理できないもの」に限定して使う
・スクリプト実行はnull_resource/terraform_dataではなくAnsible等に委ねるのが設計上正しい
・triggers_replace引数の正しい使い方を理解すれば意図しない再実行は防げる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
null_resourceとterraform_dataとは何か
TerraformはAWS・GCP・Azureなどのリソースを「宣言」し、実際のAPIを叩いてインフラを作る。しかし現実の運用では「APIを叩くだけでは完結しない作業」が発生する。・EC2が起動した後にSSHしてパッケージを入れたい
・RDSのエンドポイントが決まった後にアプリ設定ファイルを書き換えたい
・外部コマンドを実行してその出力をTerraformの変数として渡したい
こうした「IaCの外の処理」をTerraformコード内に引き込むために生まれたのがnull_resourceとterraform_dataだ。
どちらも「実際にはクラウド上のリソースを作らない」という点が共通している。Terraform管理下のダミーリソースとして存在し、そこにプロビジョナー(provisioner)を付けることで任意の処理を実行できる仕組みだ。
null_resourceが生まれた背景とその限界
1. null_resourceが登場した経緯
null_resourceはTerraformのhashicorp/nullプロバイダーに含まれるリソースで、長年「プロビジョナーの受け皿」として使われてきた。例えばEC2インスタンスを作成した後に初期セットアップを走らせたい場合、次のような書き方が典型例として広まった。
# null_resource の基本的な使い方(Terraform 0.x~1.x時代) resource "aws_instance" "web" { ami = "ami-0abcdef1234567890" instance_type = "t3.micro" } resource "null_resource" "setup" { triggers = { instance_id = aws_instance.web.id } provisioner "local-exec" { command = "echo ${aws_instance.web.public_ip} > /tmp/inventory.txt" } }
2. null_resourceの根本的な問題点
null_resourceは便利に見えるが、実際の運用で次のような問題に直面することが多い。問題1:terraform planに何も映らない
プロビジョナーが実行する処理の内容はterraform planに表示されない。インフラとして何が変わるのかが不透明になる。
問題2:冪等性の保証がない
local-execで実行したコマンドがTerraform管理のstateに反映されない。スクリプトが失敗してもstateには「成功」と記録されるケースがあり、再apply時の挙動が予測しにくい。
問題3:triggersの設計が難しい
triggersに指定した値が変わると再実行される仕組みだが、値を間違えると「いつも実行される」「決して再実行されない」という両極端になる。
問題4:hashicorp/nullプロバイダーが別途必要
required_providersにnullプロバイダーを追加しなければならず、コードが煩雑になる。
terraform_dataとの違い:何が変わったのか
1. terraform_dataはTerraform 1.4で導入された組み込みリソース
Terraform 1.4(2023年3月リリース)から、terraform_dataが組み込みリソースとして追加された。これはnull_resourceの後継にあたる位置づけで、hashicorp/nullプロバイダーを別途インストールする必要がない。# terraform_data の書き方(Terraform 1.4以降推奨) resource "aws_instance" "web" { ami = "ami-0abcdef1234567890" instance_type = "t3.micro" } resource "terraform_data" "setup" { triggers_replace = [ aws_instance.web.id ] provisioner "local-exec" { command = "echo ${aws_instance.web.public_ip} > /tmp/instance-ip.txt" } }
2. null_resource vs terraform_dataの比較
| 項目 | null_resource | terraform_data |
|---|---|---|
| 利用可能バージョン | Terraform 0.x~(レガシー) | Terraform 1.4以降 |
| プロバイダー | hashicorp/null(別途追加要) | 組み込み(追加不要) |
| 再実行トリガー | triggers = \{ key = value \} のmap | triggers_replace = [ ... ] のlist |
| input / output | なし | input引数・output属性あり |
| 今後の推奨 | 非推奨(使い続けてもよいが移行を推奨) | 公式推奨 |
3. terraform_dataのinput・outputを活用する
terraform_dataにはinput引数とoutput属性がある。これを使うと、処理の入力値をstateで追跡できるようになる。# input/outputを使ったterraform_dataの例 resource "terraform_data" "bootstrap" { input = { instance_id = aws_instance.web.id region = var.aws_region } triggers_replace = [ aws_instance.web.id ] provisioner "local-exec" { command = "echo 'Instance: ${self.input.instance_id} in ${self.input.region}' > /tmp/deploy.log" } } output "bootstrap_info" { value = terraform_data.bootstrap.output }
>> Terraform実践セミナーの詳細はこちら
local-execプロビジョナーを使っていいケース・ダメなケース
ここが本記事の核心だ。local-execは「Terraform実行マシン上でコマンドを実行する」プロビジョナーだが、使い方を間違えると設計が崩れる。1. 使っていいケース
ケース1:Terraformプロバイダーが存在しないAPIの呼び出しAWS CLIやcurlでしか叩けないAPIがある場合、local-execは正当な選択肢になる。
# Terraformプロバイダーがない独自APIを呼ぶ例 resource "terraform_data" "notify_deploy" { triggers_replace = [var.app_version] provisioner "local-exec" { command = "curl -X POST https://internal-deploy-hook.example.com/webhook -d 'version=${var.app_version}'" } }
EKSクラスター作成直後にkubectlでリソースを投入する場面は、local-execが現実的な選択肢になることがある(ただしkubernetesプロバイダーで代替できるなら優先する)。
ケース3:ローカルへのファイル書き出し(デバッグ・ログ)
インフラ構築時のIPアドレスや接続情報をローカルファイルに書き出すだけの用途なら、local-execは合理的だ。
2. 使ってはいけないケース
ダメなケース1:サーバー上でのパッケージインストール・設定変更EC2にSSHしてyum installやapt-getを実行したい場合、local-execではなくAnsible・Chef・Puppetなどの構成管理ツールを使うべきだ。Terraformはインフラの「宣言」を担い、OS内の構成管理は専門ツールに委ねるという棲み分けが重要だ。
ダメなケース2:外部スクリプトによる状態変更をTerraformで管理しようとする
スクリプトがリソースの状態を変更する場合、その結果をTerraform stateは把握できない。結果として「Terraformが知らない変更」が増え続け、ドリフト検出が破綻する。
ダメなケース3:Terraformプロバイダーが存在するのに使う
S3バケットにファイルをアップロードしたいなら
aws_s3_objectリソースを使う。RDSにデータベースを作りたいならaws_db_instanceで書く。プロバイダーが提供している機能をlocal-execで代替するのは設計の後退だ。実践的な判断フローチャート
null_resource/terraform_dataとlocal-execを使うかどうかは、次の順番で判断する。1. 判断の手順
ステップ1:Terraformプロバイダーで実現できるか?・できる → プロバイダーのリソースを使う(null_resource/terraform_dataは不要)
ステップ2:構成管理ツール(Ansible等)で実現できるか?
・できる → TerraformはインフラだけをIaC化し、OS設定はAnsibleに委ねる
ステップ3:Terraformの外のスクリプト・CIパイプラインで実現できるか?
・できる → GitHub Actions等のCI/CDでterraform apply後に別ステップとして実行する
ステップ4:上記のいずれでも実現できない処理か?
・YESの場合のみ → terraform_data + local-execの使用を検討する
2. triggersの設計ルール
terraform_dataのtriggers_replaceは、「このリストの値が変わったときだけ再実行する」という制御だ。設計ミスを防ぐには次の鉄則を守る。・変化を検知したいリソースのIDや属性を明示的に指定する
・timestamp()は使わない(applyのたびに毎回再実行されてしまう)
・固定文字列だけを指定しない(決して再実行されなくなる)
# triggers_replace の正しい例と誤った例 # [OK] リソースIDが変わった時だけ再実行 resource "terraform_data" "good" { triggers_replace = [aws_instance.web.id] } # [注意] timestamp()はapplyのたびに毎回再実行される(意図した場合のみ使う) resource "terraform_data" "always_run" { triggers_replace = [timestamp()] } # [注意] 固定値は一度も再実行されない(destroyして再作成しても変わらない) resource "terraform_data" "never_rerun" { triggers_replace = ["static-value"] }
よくある誤用パターンとその代替手段
1. 誤用パターン1:user_dataの代わりに使う
EC2の起動時設定をnull_resource + local-execで行おうとするコードをよく見かける。しかしEC2にはuser_dataとcloud-initという専用の仕組みがある。サーバー内部の初期化処理は
user_data引数に直接記述するか、cloud-initを活用する方がstateとの整合性が保たれる。注意が必要な点として、null_resource/terraform_dataとlocal-execはEC2の外部(Terraformを実行しているローカルマシン)でコマンドを実行する仕組みだ。EC2内部でコマンドを実行するremote-execとは別物なので混同しないようにしよう。
2. 誤用パターン2:AnsibleのplaybookをTerraform経由で起動する
# よく見かける誤用:AnsibleをTerraform内から呼ぶ # 本番環境でこの書き方は要注意 resource "null_resource" "configure" { provisioner "local-exec" { command = "ansible-playbook -i '${aws_instance.web.public_ip},' site.yml" } }
代替手段として、GitHub ActionsやJenkinsのCIパイプラインで次の順番でステップを分ける設計を推奨する。
・ステップ1:terraform apply(インフラ構築)
・ステップ2:terraform output でIPアドレス等を取得
・ステップ3:ansible-playbook(OS設定・アプリデプロイ)
3. 誤用パターン3:Terraformのデータソースの代わりに使う
外部のAPIやコマンドから値を取得してTerraformの変数として使いたい場合、local-execではなくdataソースやexternalプロバイダーを使う方が適切なケースが多い。# externalプロバイダーで外部スクリプトの値を取得する例 # get_values.py は {"ami_id": "ami-xxx"} を標準出力に返すスクリプト data "external" "my_values" { program = ["python3", "${path.module}/get_values.py"] query = { environment = var.environment } } resource "aws_instance" "web" { ami = data.external.my_values.result["ami_id"] instance_type = "t3.micro" }
local-execが失敗した場合のトラブルシュート
実際の現場でlocal-execを使った際によく発生するエラーと対処法をまとめる。1. 「Error: local-exec provisioner error」が出た時
local-execプロビジョナーが失敗するとTerraformは次のエラーを表示する。# local-exec失敗時の典型的なエラー出力例 Error: local-exec provisioner error with terraform_data.setup, on main.tf line 10, in resource "terraform_data" "setup": 10: provisioner "local-exec" { Error running command 'ansible-playbook -i 192.168.1.10, site.yml': exit status 1. Output: ERROR! the playbook: site.yml could not be found
・コマンドがTerraform実行マシン上で単体で動くか確認する(local-execはローカル実行)
・`interpreter`引数で明示的にシェルを指定する(`interpreter = ["bash", "-c"]`等)
・環境変数が必要なコマンドは`environment`ブロックで渡す
2. 「tainted」状態になった時の対処
local-execが途中で失敗すると、そのterraform_dataリソースが「tainted」(汚染)状態になる。この状態のまま再度terraform applyを実行すると、リソースが一度destroyされてから再作成される。
・要注意:aws_instanceのような本番リソースにlocal-execを付けていると、インスタンスが再作成(再起動ではなく削除・作成)される危険がある
・対処:terraform_dataを別リソースとして切り出し、本番リソースとの分離を徹底する
3. on_failureオプションの活用
# on_failure = continue で失敗しても処理を続行する resource "terraform_data" "optional_task" { triggers_replace = [aws_instance.web.id] provisioner "local-exec" { command = "curl -s https://notify.example.com/deploy || true" on_failure = continue } }
まとめ
null_resourceとterraform_dataの使いどころ、そしてlocal-execの判断基準をまとめる。| やりたいこと | 推奨するアプローチ |
|---|---|
| AWSリソースを作成・管理する | aws_*等のプロバイダーリソースを使う |
| EC2の起動時に初期設定をする | aws_instanceのuser_dataとcloud-initを使う |
| サーバーのOS・ミドルウェア設定を管理する | Ansibleなどの構成管理ツールをCIパイプラインで呼ぶ |
| 外部スクリプトの出力値をTerraformで使う | externalプロバイダーのdataソースを使う |
| プロバイダーがないAPIを叩く(どうしても必要な場合) | terraform_data + local-execを使う(Terraform 1.4以降) |
| null_resourceを使っている既存コード | terraform_dataへ移行する(hashicorp/null不要になる) |
Terraform 1.4以降を使っているなら、null_resourceの新規使用はやめてterraform_dataに統一しよう。理由はシンプルで、プロバイダーの追加が不要になり、input/output属性でデータ追跡ができるようになるからだ。
>> Terraform実践セミナーの詳細はこちら
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら
- 前のページへ:TerraformでIAMポリシーをコード管理する方法|aws_iam_policy_documentで最小権限を宣言的に書く
- この記事の属するカテゴリ:Terraformへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。
登録無料・いつでも解除できます