TL;DR
Terraformはlocal-execコマンドが0以外の終了コードを返すと、applyの実行を強制終了します。原因の約90%は2つに絞られます。スクリプトファイルがTerraformの想定する場所に存在しない、またはスクリプトが静かに失敗してTerraformが原因を把握できていないケースです。即効性のある修正方法:
- 作業ディレクトリではなく、モジュールからの相対パスでスクリプトを参照するために
${path.module}を使用する。 - シェルスクリプトに
set -eを追加して、失敗を無音ではなく明示的に表示する。 - プロビジョナーに組み込む前に、コマンドを手動で実行して動作確認する。
エラーの実際の意味
完全なエラーメッセージはこのようになります:
Error: local-exec provisioner error
Error running command 'bash setup.sh': exit status 1. Output: bash: setup.sh: No such file or directory
Terraformのlocal-execプロビジョナーは、リモートリソース上ではなく、terraform applyを実行しているマシン上でコマンドを実行します。0以外の終了コードが返されると、Terraformはそのリソース全体を失敗として扱い、ロールバックするか汚染済みとしてマークします。
bash: setup.sh: No such file or directoryの部分が重要な手がかりです。Terraformはterraform applyを実行したディレクトリでsetup.shを探しますが、それは.tfファイルが存在するモジュールディレクトリとは異なることがほとんどです。
根本原因の詳細
1. 作業ディレクトリが間違っている
次のように記述した場合:
provisioner "local-exec" {
command = "bash setup.sh"
}
Terraformは.tfファイルからの相対パスではなく、terraform applyを実行した場所からの相対パスでsetup.shを解決します。親ディレクトリやCIパイプラインからapplyを実行している場合、スクリプトは単純に見つかりません。
2. スクリプトが0以外のコードで終了する
正しいパスを指定していても、スクリプト内でキャッチされなかった失敗は0以外の終了コードとして伝播します。存在しないバイナリ、失敗したAPI呼び出し、設定が間違った環境変数など、これらすべてがexit status 1以上を生成します。Bashはデフォルトではエラーで停止しないため、出力からは失敗が明確でない場合もあります。
3. Windowsの改行コード(CRLF)
Windows上で作成または編集されたスクリプトにはCRLFの改行コード(\r\n)が含まれます。Linuxでは/bin/bashが余分な\rで失敗し、setup.sh: command not foundやbad interpreterのようなエラーが発生します。これらは改行コードの問題とは全く異なるように見えます。
修正1:スクリプトパスに${path.module}を使用する
path.moduleは「ファイルが見つからない」バリアントを根本から解決します。terraform applyをどこから実行しても、常に現在の.tfファイルが含まれるディレクトリに解決されます:
provisioner "local-exec" {
command = "bash ${path.module}/scripts/setup.sh"
}
または、作業ディレクトリを明示的に設定する方法もあります:
provisioner "local-exec" {
working_dir = "${path.module}/scripts"
command = "bash setup.sh"
}
どちらの方法も、ターミナルの現在地に関わらず、モジュールにパスを固定します。
修正2:スクリプトの失敗を明示的に表示する
プロビジョナーで使用するすべてのシェルスクリプトの先頭にset -euo pipefailを追加します。これにより、エラーが発生するとスクリプトが即座に終了し、Terraformがキャッチできる明確な0以外の終了コードが返されます:
#!/usr/bin/env bash
set -euo pipefail
echo "Running setup..."
apt-get install -y some-package
curl -fsSL https://example.com/init | bash
echo "Done."
set -eがない場合、失敗したcurlや存在しないコマンドは静かに無視されます。スクリプトは0で終了し、Terraformはすべて正常だと判断します。そしてサーバーが正しく設定されていない原因を1時間かけて調査することになります。
修正3:パスをハードコードする代わりに変数を渡す
実行時の値(リソースのIP、出力値、リージョンなど)が必要なスクリプトは、内部で値を解決しようとするのではなく、環境変数として受け取るようにします:
provisioner "local-exec" {
command = "bash ${path.module}/scripts/configure.sh"
environment = {
SERVER_IP = self.private_ip
REGION = var.region
DEPLOY_ENV = var.environment
}
}
このように記述されたスクリプトはローカルでのテストが容易です。それらの変数をエクスポートしてスクリプトを直接実行するだけで、Terraformは不要です。
修正4:想定内の0以外の終了コードを処理する
一部のコマンドは正当に0以外を返すことがあり、それは問題ありません。grepは一致がない場合に1を返します。全体の終了コードが0を維持するように、これらのコマンドをラップします:
provisioner "local-exec" {
command = "grep -q 'pattern' /etc/config || echo 'Pattern not found, skipping'"
}
または、一括フォールバックを追加する方法もあります:
provisioner "local-exec" {
command = "bash ${path.module}/scripts/maybe-fails.sh || true"
}
|| trueは、失敗が本当に許容できる場合にのみ控えめに使用してください。実際のバグを隠すために使用しないようにしましょう。
修正5:CRLFの改行コードを修正する
Windows上で変更されたスクリプトは、Linuxで実行する前に変換が必要です:
# dos2unixを使用する方法
dos2unix scripts/setup.sh
# sedを使用する方法
sed -i 's/\r$//' scripts/setup.sh
# Gitの設定で二度と発生しないように強制する:
echo "*.sh text eol=lf" >> .gitattributes
git add --renormalize .
Git属性の方法が恒久的な修正です。チーム全員の改行コードを正規化します。
失敗したプロビジョナーのデバッグ
プロビジョナーに組み込む前に、必ずコマンドを手動でテストしてください。terraform applyを実行する同じディレクトリから実行します:
# Terraformが実行する内容をシミュレートする
cd /path/to/your/terraform/root
bash path/to/module/scripts/setup.sh
echo "Exit code: $?"
ここで失敗する場合はここで修正してください。Terraform内部ではなく。
Terraformの詳細出力にはTF_LOGを設定します:
TF_LOG=DEBUG terraform apply 2>&1 | grep -A 20 'local-exec'
これにより、Terraformが実行する正確なコマンドと完全なstdout/stderrが出力されます。標準エラー表示では切り捨てられることが多い情報です。
汚染されたリソースへの対処
プロビジョナーの失敗により、リソースは汚染済みとしてマークされます。次のapply時に、Terraformはリソースを破棄して再作成します。スクリプトを修正して、リソースを破棄せずに再試行したい場合は、手動で汚染を解除します:
# 汚染されているものを確認する
terraform state list
terraform show
# リソース自体が実際に問題ない場合は汚染を解除する
terraform untaint aws_instance.my_server
修正が機能したことを確認する
terraform planを実行します。予期しない破棄/作成サイクルが表示されないはずです。terraform applyを実行します。プロビジョナーブロックがクリーンに完了するはずです。- 出力を確認します。Terraformはプロビジョナーのstdout/stderrをインラインで出力します。スクリプトの成功メッセージを探してください。
- さらに確実にするには、コマンドにセンチネル文字列を追加します:
provisioner "local-exec" { command = "bash ${path.module}/scripts/setup.sh && echo 'PROVISIONER_OK'" }
Terraformの出力に`PROVISIONER_OK`が表示されれば、スクリプトが最後まで実行されたことが確認できます。
## local-execを完全に避けるべき場合
複数のステップ、リトライ、条件分岐などの複雑さは危険信号です。`local-exec`はオーケストレーション用に設計されたものではありません。スクリプトがインフラ自体よりもメンテナンスが困難になる場合があります。以下の代替手段を検討してください:
- **null_resource**とトリガーを組み合わせて、特定の変更時のみロジックを再実行する。
- **external data source**は、Terraformにデータを返す読み取り専用スクリプト用。
- **Ansible、Chef、またはcloud-init**は、複雑な設定管理用。まさにこの問題のために設計されたツールです。
- **Terraformの関数**(`templatefile`、`file`)は、スクリプトを実行せずに設定ファイルを生成する用途に。

