Skip to main content

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คำอธิบาย
inlineList of commands
scriptPath ไฟล์ script (local)
scriptsList 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)
# }
}

เปรียบเทียบ

MethodProsCons
remote-execFamiliar, simpleNeed SSH/WinRM, slow, idempotency manual
user_dataNo SSH, fast, retryLimited size, runs once
Pre-baked AMIFastest boot, testedNeed Packer pipeline
SSM Run CommandNo SSH, audit, retryAWS-only, more setup
Ansible (after TF)Idempotent, flexibleSeparate tool/pipeline

สรุป

  • remote-exec = รัน command บน remote (SSH/WinRM)
  • 3 forms: inline, script, scripts
  • ต้องมี connection block + ต้องเปิด port
  • ทางเลือกดีกว่า: user_data, Packer AMI, SSM Run Command, Ansible
  • ใช้เฉพาะเมื่อทางเลือกอื่นไม่ work

ต่อไป → Custom Provisioners