TL;DR
Terraform hủy quá trình apply bất cứ khi nào lệnh local-exec trả về mã thoát khác 0. Hai nguyên nhân chiếm khoảng 90% trường hợp: file script không nằm ở nơi Terraform tìm kiếm, hoặc script thất bại âm thầm mà Terraform không biết lý do. Cách khắc phục nhanh:
- Dùng
${path.module}để tham chiếu script tương đối với module, không phải thư mục làm việc hiện tại. - Thêm
set -evào shell script để lỗi được báo rõ ràng, không bị bỏ qua âm thầm. - Chạy lệnh thủ công trước để xác nhận hoạt động đúng trước khi đưa vào provisioner.
Ý Nghĩa Thực Sự Của Lỗi
Lỗi đầy đủ trông như sau:
Error: local-exec provisioner error
Error running command 'bash setup.sh': exit status 1. Output: bash: setup.sh: No such file or directory
Provisioner local-exec của Terraform chạy lệnh trên máy đang thực thi terraform apply — không phải trên tài nguyên remote. Bất kỳ mã thoát nào khác 0 đều khiến Terraform coi toàn bộ tài nguyên là thất bại và hoặc rollback hoặc đánh dấu là tainted.
Phần bash: setup.sh: No such file or directory mới là manh mối thực sự. Terraform tìm setup.sh trong thư mục bạn đã chạy terraform apply — thường không phải thư mục module chứa các file .tf của bạn.
Phân Tích Nguyên Nhân Gốc Rễ
1. Thư mục làm việc sai
Khi bạn viết:
provisioner "local-exec" {
command = "bash setup.sh"
}
Terraform phân giải setup.sh tương đối với nơi bạn chạy terraform apply, không phải tương đối với file .tf. Chạy apply từ thư mục cha hoặc trong CI pipeline? Script đơn giản là sẽ không được tìm thấy.
2. Script thoát với mã khác 0
Dù đường dẫn đúng, bất kỳ lỗi chưa được xử lý nào bên trong script đều nổi lên thành mã thoát khác 0. Thiếu binary, API call thất bại, biến môi trường cấu hình sai — tất cả đều tạo ra exit status 1 hoặc cao hơn. Bash mặc định không dừng khi gặp lỗi, nên lỗi có thể không rõ ràng trong output.
3. Ký tự xuống dòng kiểu Windows (CRLF)
Script được tạo hoặc chỉnh sửa trên Windows mang theo ký tự xuống dòng CRLF (\r\n). Trên Linux, /bin/bash bị nghẹt với ký tự \r thừa, tạo ra các lỗi như setup.sh: command not found hoặc bad interpreter — trông không liên quan gì đến vấn đề ký tự xuống dòng cả.
Cách Sửa 1: Dùng ${path.module} Cho Đường Dẫn Script
path.module giải quyết triệt để trường hợp "file not found". Nó luôn phân giải về thư mục chứa file .tf hiện tại, bất kể bạn chạy terraform apply từ đâu:
provisioner "local-exec" {
command = "bash ${path.module}/scripts/setup.sh"
}
Hoặc, đặt thư mục làm việc một cách tường minh:
provisioner "local-exec" {
working_dir = "${path.module}/scripts"
command = "bash setup.sh"
}
Cả hai cách đều neo đường dẫn vào module — không phụ thuộc vào vị trí terminal của bạn.
Cách Sửa 2: Làm Cho Script Báo Lỗi Rõ Ràng
Thêm set -euo pipefail ở đầu mỗi shell script dùng trong provisioner. Script sẽ thoát ngay lập tức khi gặp bất kỳ lỗi nào, cho Terraform mã thoát khác 0 rõ ràng để nhận biết:
#!/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."
Không có set -e, một lệnh curl thất bại hoặc lệnh bị thiếu sẽ bị nuốt chửng âm thầm. Script thoát với mã 0, Terraform nghĩ mọi thứ ổn, và bạn mất cả tiếng đồng hồ tự hỏi tại sao server không được cấu hình đúng.
Cách Sửa 3: Truyền Biến Thay Vì Hardcode Đường Dẫn
Script cần giá trị runtime — IP của tài nguyên, giá trị output, region — nên nhận chúng qua biến môi trường thay vì tự tìm hiểu bên trong:
provisioner "local-exec" {
command = "bash ${path.module}/scripts/configure.sh"
environment = {
SERVER_IP = self.private_ip
REGION = var.region
DEPLOY_ENV = var.environment
}
}
Script viết theo cách này dễ test cục bộ — chỉ cần export các biến đó và chạy script trực tiếp, không cần Terraform.
Cách Sửa 4: Xử Lý Mã Thoát Khác 0 Mà Bạn Thực Sự Mong Đợi
Một số lệnh hợp lệ trả về khác 0 và điều đó bình thường. grep trả về 1 khi không tìm thấy kết quả. Hãy bọc các lệnh đó để mã thoát tổng thể vẫn là 0:
provisioner "local-exec" {
command = "grep -q 'pattern' /etc/config || echo 'Pattern not found, skipping'"
}
Hoặc thêm fallback tổng quát:
provisioner "local-exec" {
command = "bash ${path.module}/scripts/maybe-fails.sh || true"
}
Dùng || true một cách thận trọng — chỉ khi lỗi thực sự chấp nhận được và bạn không chỉ đang che giấu lỗi thực.
Cách Sửa 5: Sửa Ký Tự Xuống Dòng CRLF
Script bị chạm vào trên Windows cần chuyển đổi trước khi chạy được trên Linux:
# Dùng dos2unix
dos2unix scripts/setup.sh
# Hoặc dùng sed
sed -i 's/\r$//' scripts/setup.sh
# Đặt LF trong Git để không bao giờ xảy ra nữa:
echo "*.sh text eol=lf" >> .gitattributes
git add --renormalize .
Cách dùng Git attribute là giải pháp vĩnh viễn — nó chuẩn hóa ký tự xuống dòng cho tất cả mọi người trong nhóm.
Gỡ Lỗi Provisioner Bị Thất Bại
Luôn test lệnh thủ công trước khi nhúng vào provisioner. Chạy từ cùng thư mục nơi bạn sẽ gọi terraform apply:
# Mô phỏng những gì Terraform sẽ chạy
cd /path/to/your/terraform/root
bash path/to/module/scripts/setup.sh
echo "Exit code: $?"
Thất bại ở đây? Sửa ở đây — không phải bên trong Terraform.
Để xem output chi tiết của Terraform, đặt TF_LOG:
TF_LOG=DEBUG terraform apply 2>&1 | grep -A 20 'local-exec'
Lệnh này in ra chính xác lệnh Terraform gọi cùng với toàn bộ stdout/stderr, thứ thường bị cắt ngắn trong màn hình lỗi tiêu chuẩn.
Xử Lý Tài Nguyên Bị Tainted
Provisioner thất bại sẽ đánh dấu tài nguyên là tainted. Ở lần apply tiếp theo, Terraform sẽ xóa và tạo lại nó. Đã sửa script và muốn thử lại mà không xóa tài nguyên? Hãy untaint thủ công:
# Kiểm tra những gì đang bị tainted
terraform state list
terraform show
# Untaint nếu bản thân tài nguyên thực sự ổn
terraform untaint aws_instance.my_server
Xác Nhận Bản Sửa Đã Hoạt Động
- Chạy
terraform plan— không nên xuất hiện chu kỳ destroy/create bất ngờ nào. - Chạy
terraform apply— khối provisioner phải hoàn thành sạch sẽ. - Kiểm tra output: Terraform in stdout/stderr của provisioner trực tiếp. Tìm thông báo thành công của script bạn.
- Để chắc chắn hơn, thêm chuỗi sentinel vào lệnh:
provisioner "local-exec" { command = "bash ${path.module}/scripts/setup.sh && echo 'PROVISIONER_OK'" }
Thấy `PROVISIONER_OK` trong output Terraform xác nhận script đã chạy đến hoàn thành.
## Khi Nào Nên Tránh Dùng local-exec Hoàn Toàn
Nhiều bước, retry, điều kiện — sự phức tạp đó là dấu hiệu cảnh báo. `local-exec` chưa bao giờ được thiết kế để điều phối. Đến một lúc nào đó script trở nên khó bảo trì hơn cả cơ sở hạ tầng. Hãy cân nhắc các thay thế sau:
- **null_resource** với triggers để chạy lại logic chỉ khi có thay đổi cụ thể.
- **external data source** cho script chỉ đọc trả dữ liệu về cho Terraform.
- **Ansible, Chef, hoặc cloud-init** cho quản lý cấu hình phức tạp — các công cụ được xây dựng chính xác cho vấn đề này.
- **Terraform functions** (`templatefile`, `file`) để tạo file cấu hình mà không cần chạy script nào cả.

