remote-exec Provisioner
รัน command บน resource ที่สร้าง (เช่น EC2 instance) ผ่าน SSH/WinRM
Syntax
resource "aws_instance" "web" {
# ...
connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
provisioner "remote-exec" {
inline = [
"sudo yum install -y httpd",
"sudo systemctl start httpd",
"echo 'Hello' | sudo tee /var/www/html/index.html"
]
}
}
Arguments
| Argument | คำอธิบาย |
|---|---|
inline | List of commands |
script | Path ไฟล์ script (local) |
scripts | List of script paths |
ใช้ตัวใดตัวหนึ่งเท่านั้น
ตัวอย่าง 3 รูปแบบ
Inline Commands
provisioner "remote-exec" {
inline = [
"sudo apt update",
"sudo apt install -y nginx",
"sudo systemctl start nginx"
]
}
Single Script
provisioner "remote-exec" {
script = "scripts/setup.sh"
}
Terraform จะ upload script → execute → cleanup
Multiple Scripts
provisioner "remote-exec" {
scripts = [
"scripts/01-install.sh",
"scripts/02-config.sh",
"scripts/03-start.sh"
]
}
Connection Block (Required)
connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
port = 22 # default
timeout = "5m" # default
}
WinRM (Windows)
connection {
type = "winrm"
user = "Administrator"
password = var.windows_admin_password
host = self.public_ip
port = 5986
https = true
insecure = true # self-signed cert
}
Bastion / Jump Host
connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/id_rsa")
host = self.private_ip # private IP
bastion_host = aws_instance.bastion.public_ip
bastion_user = "ec2-user"
bastion_private_key = file("~/.ssh/id_rsa")
}
ตัวอย่าง: Web Server Setup
resource "aws_security_group" "web" {
name = "web"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # ⚠️ open SSH — restrict in prod!
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_key_pair" "deployer" {
key_name = "deployer"
public_key = file("~/.ssh/id_rsa.pub")
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0" # Amazon Linux 2
instance_type = "t2.micro"
key_name = aws_key_pair.deployer.key_name
vpc_security_group_ids = [aws_security_group.web.id]
connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
timeout = "5m"
}
provisioner "remote-exec" {
inline = [
"sudo yum update -y",
"sudo yum install -y httpd",
"sudo systemctl start httpd",
"sudo systemctl enable httpd",
"echo '<h1>Hello from Terraform</h1>' | sudo tee /var/www/html/index.html"
]
}
}
output "url" {
value = "http://${aws_instance.web.public_ip}"
}
ทางเลือก: user_data (แนะนำ)
แทน remote-exec — ใช้ user_data:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo '<h1>Hello from Terraform</h1>' > /var/www/html/index.html
EOF
}
ข้อดีของ user_data เหนือ remote-exec:
- ✅ ไม่ต้อง SSH (faster + secure)
- ✅ ไม่ต้องเปิด SSH port
- ✅ ไม่ต้องจัดการ key pair
- ✅ Run ใน boot — automatic retry on fail
- ✅ Logs ใน
/var/log/cloud-init-output.log
Common Pitfalls
1. SSH Connection Timeout
Error: timeout - last error: SSH authentication failed
สาเหตุ:
- Security group ไม่เปิด port 22
- Instance ยังไม่ ready (boot ใช้เวลา)
- Wrong username (Amazon Linux =
ec2-user, Ubuntu =ubuntu) - Wrong key file
แก้: เพิ่ม timeout
connection {
# ...
timeout = "10m"
}
2. ต้องเปิด SSH Port (Security Risk)
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # ⚠️ ทั่วโลก SSH ได้!
}
แก้:
- จำกัด CIDR เฉพาะ IP ของ Terraform ที่รัน
- ใช้ Bastion host
- ดีที่สุด: ใช้ user_data + ไม่เปิด SSH เลย
3. State ไม่บอกว่ารัน Provisioner
ถ้า command เปลี่ยน — Terraform ไม่ rerun เอง ต้อง force replace:
terraform apply -replace="aws_instance.web"
4. Idempotency
provisioner "remote-exec" {
inline = [
"yum install -y httpd" # idempotent ✅
"echo 'foo' >> /etc/file" # NOT idempotent — duplicates ❌
]
}
ทำให้ idempotent:
provisioner "remote-exec" {
inline = [
"grep -qF 'foo' /etc/file || echo 'foo' >> /etc/file"
]
}
Run Script Locally First
ใช้ file provisioner คู่กัน — copy script → exec:
resource "aws_instance" "web" {
# ... connection ...
# 1. Copy script
provisioner "file" {
source = "setup.sh"
destination = "/tmp/setup.sh"
}
# 2. Execute
provisioner "remote-exec" {
inline = [
"chmod +x /tmp/setup.sh",
"sudo /tmp/setup.sh"
]
}
}
ทางเลือก Modern: SSM Run Command
resource "aws_ssm_document" "configure" {
name = "configure-${var.env}"
document_type = "Command"
content = jsonencode({
schemaVersion = "2.2"
description = "Configure web server"
mainSteps = [{
action = "aws:runShellScript"
name = "install"
inputs = {
runCommand = [
"yum install -y httpd",
"systemctl start httpd"
]
}
}]
})
}
resource "aws_ssm_association" "configure" {
name = aws_ssm_document.configure.name
targets {
key = "tag:Name"
values = ["web-server"]
}
}
✅ ข้อดี:
- ไม่ต้อง SSH
- ไม่ต้อง public IP
- มี audit log
- Automatic retry
ตัวอย่าง: Script Reuse
locals {
setup_script = <<-EOT
#!/bin/bash
set -e
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
EOT
}
resource "aws_instance" "web" {
# ...
user_data = local.setup_script
# หรือใช้ provisioner ถ้าจำเป็น
# provisioner "remote-exec" {
# inline = split("\n", local.setup_script)
# }
}
เปรียบเทียบ
| Method | Pros | Cons |
|---|---|---|
remote-exec | Familiar, simple | Need SSH/WinRM, slow, idempotency manual |
user_data ⭐ | No SSH, fast, retry | Limited size, runs once |
| Pre-baked AMI | Fastest boot, tested | Need Packer pipeline |
| SSM Run Command | No SSH, audit, retry | AWS-only, more setup |
| Ansible (after TF) | Idempotent, flexible | Separate tool/pipeline |
สรุป
remote-exec= รัน command บน remote (SSH/WinRM)- 3 forms:
inline,script,scripts - ต้องมี
connectionblock + ต้องเปิด port - ทางเลือกดีกว่า: user_data, Packer AMI, SSM Run Command, Ansible
- ใช้เฉพาะเมื่อทางเลือกอื่นไม่ work
ต่อไป → Custom Provisioners