Terraformのnull_resourceとterraform_dataの使いどころ|local-execを乱用しないための判断基準

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Terraform > Terraformのnull_resourceとterraform_dataの使いどころ|local-execを乱用しないための判断基準
「Terraformで外部スクリプトを実行したいんだけど、null_resourceを使えばいいの?」 「terraform planを実行するたびに余計なスクリプトが走って困っている。」

そういった悩みを抱えているエンジニアは多い。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引数の正しい使い方を理解すれば意図しない再実行は防げる


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
図解60P/登録10秒/解除も3秒 / 詳細はこちら

null_resourceとterraform_dataとは何か

TerraformはAWS・GCP・Azureなどのリソースを「宣言」し、実際のAPIを叩いてインフラを作る。しかし現実の運用では「APIを叩くだけでは完結しない作業」が発生する。

・EC2が起動した後にSSHしてパッケージを入れたい
・RDSのエンドポイントが決まった後にアプリ設定ファイルを書き換えたい
・外部コマンドを実行してその出力をTerraformの変数として渡したい

こうした「IaCの外の処理」をTerraformコード内に引き込むために生まれたのがnull_resourceterraform_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 }

現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、Terraform null_resourceとterraform_dataの使い分けからlocal-exec設計パターンまで、実務で即使えるスキルを習得できるセミナーを開催しています。
>> 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}'" } }

ケース2:Kubernetesリソース作成後のkubectl apply
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" } }

これが問題なのは、Terraform applyとAnsibleの実行が密結合になることだ。Terraform applyが失敗すればAnsibleも実行されず、Ansibleが失敗してもTerraformのstateには「リソース作成済み」のまま残る。

代替手段として、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" }

externalプロバイダーは標準JSONを返すスクリプトを呼び出せるデータソースだ。local-execと異なり、取得した値をTerraformのリソース定義に直接埋め込める。

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 } }

`on_failure = continue`を指定すると、コマンドが失敗してもterraform applyは継続される。通知系など「失敗しても構わないサブタスク」に限って使うとよい。

まとめ

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の設計原則はシンプルだ。「Terraformでできることはプロバイダーリソースで書く。Terraformの外の処理は外のツールに任せる。」この棲み分けを守れば、null_resource/terraform_dataやlocal-execが活躍する場面は自然と限定される。

Terraform 1.4以降を使っているなら、null_resourceの新規使用はやめてterraform_dataに統一しよう。理由はシンプルで、プロバイダーの追加が不要になり、input/output属性でデータ追跡ができるようになるからだ。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、Terraform null_resourceとterraform_dataの使い分けからlocal-exec設計パターンまで、実務で即使えるスキルを習得できるセミナーを開催しています。
>> Terraform実践セミナーの詳細はこちら

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

登録10秒/合わなければ解除3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。