EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Linux / สั่งงานอัตโนมัติบนเครื่อง Windows ระยะไกลผ่าน SSH + Task Scheduler
Linux

สั่งงานอัตโนมัติบนเครื่อง Windows ระยะไกลผ่าน SSH + Task Scheduler

โดย Ton-Or ·26 ส.ค. 2026·อ่าน 9 นาที
สั่งงานอัตโนมัติบนเครื่อง Windows ระยะไกลผ่าน SSH + Task Scheduler

เครื่องแรงตัวหนึ่งที่ตั้งอยู่อีกห้องคือทรัพยากรที่คุ้มที่สุดถ้าสั่งงานมันได้จากที่ทำงานประจำ Windows ทุกวันนี้มี OpenSSH Server ในตัวแล้ว แต่พอ ssh เข้าไปจริงจะพบว่าอีกฝั่งไม่ใช่ Linux และนิสัยการพิมพ์คำสั่งที่ติดมาจะพังทันทีตั้งแต่บรรทัดแรก

พอดีผมมีงานประมวลผลหนักอยู่ชุดหนึ่ง แปลงไฟล์เป็นชุดใหญ่ทีละหลายพันไฟล์ รันบนโน้ตบุ๊กที่นั่งทำงานอยู่ทีไรเครื่องร้อนจนทำอย่างอื่นไม่ได้ทั้งวัน ส่วนเครื่องเวิร์กสเตชันที่อยู่อีกห้องซึ่งซีพียูและการ์ดจอแรงกว่ากันหลายเท่ากลับนั่งว่างเปล่า ผมเลยตั้งใจว่าจะยกงานทั้งชุดไปรันที่นั่น แล้วสั่งจากเครื่องที่นั่งทำงานอยู่ตามปกติ

Windows 10/11 ทุกวันนี้มี OpenSSH Server มาให้ในตัวแล้ว ไม่ต้องลง third-party อะไร เปิดสองคำสั่งจบ แต่พอ ssh เข้าไปได้จริงถึงรู้ว่ากำแพงอยู่หลังจากนั้นทั้งหมด บทความนี้เป็นบันทึกกับดักที่ผมเดินชนมาเองครับ ตัวเลข IP ชื่อเครื่อง และชื่อไฟล์กุญแจในบทความเปลี่ยนเป็นค่าตัวอย่างหมดแล้ว ส่วนโครงสร้างและคำสั่งเป็นของจริง

เปิด SSH Server ฝั่ง Windows

รันใน PowerShell แบบ admin บนเครื่องปลายทาง:

Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
New-NetFirewallRule -Name sshd -DisplayName "OpenSSH Server" `
  -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22

ฝั่งเครื่องที่นั่งทำงาน ตั้ง alias ไว้ใน ~/.ssh/config จะได้ไม่ต้องพิมพ์ยาว:

Host winbox
    HostName 192.168.10.50
    User workuser
    IdentityFile ~/.ssh/id_ed25519
กุญแจของ admin ไม่ได้อยู่ที่เดียวกับคนอื่น

ถ้าบัญชีปลายทางอยู่ในกลุ่ม Administrators ตัว sshd ของ Windows จะไม่อ่าน C:\Users\ชื่อ\.ssh\authorized_keys แต่ไปอ่าน C:\ProgramData\ssh\administrators_authorized_keys แทน แล้วไฟล์นั้นต้องให้สิทธิ์เฉพาะ Administrators กับ SYSTEM เท่านั้นด้วย ไม่งั้นมันเงียบแล้วถามรหัสผ่านต่อไปเรื่อยๆ โดยไม่บอกสาเหตุ อาการ "ใส่คีย์แล้วยังถาม password" เกือบทั้งหมดคือเรื่องนี้ครับ

กับดักที่ 1: อีกฝั่งไม่ใช่ bash และไม่ใช่ PowerShell 7 ด้วย

ค่าเริ่มต้นของ sshd บน Windows คือ cmd.exe และถ้าตั้งให้เป็น PowerShell มันจะได้ Windows PowerShell 5.1 ตัวที่ติดมากับ OS ไม่ใช่ PowerShell 7 ที่เราลงเพิ่มไว้ นิสัยการพิมพ์ที่ติดมาจาก Linux จะพังทันทีตั้งแต่คำสั่งแรก ผมเสียเวลากับเรื่องนี้ไปหลายรอบกว่าจะจดเป็นตารางไว้ข้างจอ

อย่าใช้ (ติดจาก Linux) ใช้อันนี้แทนบน PS 5.1
cmd1 && cmd2 cmd1; cmd2
~ หรือ $HOME $env:USERPROFILE หรือพิมพ์ C:\... เต็มๆ
2>/dev/null 2>$null
head / tail Select-Object -First N / Get-Content -Tail N
cat file | grep x Select-String x file
which cmd (Get-Command cmd).Source
mkdir -p New-Item -ItemType Directory -Force
emoji ใน print() ASCII ล้วน

บรรทัดสุดท้ายไม่ใช่เรื่องความสวยงามครับ PS 5.1 ใช้ code page cp1252 เป็นค่าตั้งต้น สคริปต์ Python ที่ print() อักขระนอกช่วงนั้นจะโยน UnicodeEncodeError แล้วงานตายกลางทางทั้งที่ตัวงานไม่ได้ผิดอะไรเลย ผมเจอครั้งแรกแบบงงมาก เพราะสคริปต์ตัวเดียวกันรันบนเครื่องตัวเองผ่านตลอด

การรันสคริปต์ Python ผ่าน ssh เลยหน้าตาประมาณนี้:

ssh winbox "conda activate ml313; python C:\jobs\convert.py --batch 200"

ถ้าอยากให้งานอยู่ต่อหลังตัด ssh ต้องสั่งแบบ detach ไม่งั้นพอ session ปิดงานตายตาม:

ssh winbox "Start-Process -WindowStyle Hidden 'C:\Users\workuser\miniconda3\envs\ml313\python.exe' -ArgumentList 'C:\jobs\convert.py'"
แก้ไขเพิ่มเติม ต.ค. 2026

ตัวอย่างข้างบนผมเขียนไว้ผิด Start-Process -WindowStyle Hidden แค่แยกงานออกจากหน้าต่าง console แต่ sshd ของ Windows ยังผูกโปรเซสลูกทุกตัวไว้ใน job object เดียวกัน พอ session ปิด งานยาวก็ตายตามอยู่ดี ที่ผมเคยเห็นว่ารอดเป็นเพราะงานสั้นจนจบก่อน ถ้าจะให้งานยาวอยู่ต่อจริงให้ใช้ Task Scheduler ตามหัวข้อถัดไปของบทความนี้ หรือเปิดโปรเซสด้วยธง breakaway from job

PS 5.1เชลล์ที่ได้จริง
cp1252code page ตั้งต้น
;ตัวคั่นคำสั่ง
$nullแทน /dev/null

กับดักที่ 2: ตั้งงานด้วย schtasks ต้องผ่านไฟล์ .bat เสมอ

พอสั่งมือได้แล้วก็ถึงเวลาให้มันวิ่งเอง ฝั่ง Windows ใช้ schtasks ซึ่งเป็น CLI ของ Task Scheduler ปัญหาคือการยัดคำสั่งยาวๆ พร้อม argument เข้าไปในบรรทัดเดียวมันเป็นนรกเรื่องเครื่องหมายคำพูด — quote ซ้อน quote ผ่านสองเชลล์ ผมล้มเหลวหลายรอบจนยอมแพ้แล้วเปลี่ยนวิธี

ทางที่นิ่งที่สุดคือเขียน .bat ตัวห่อไว้บนเครื่องปลายทาง แล้วให้ schtasks เรียกไฟล์นั้นตัวเดียว:

@echo off
"C:\Users\workuser\miniconda3\envs\ml313\python.exe" "C:\jobs\convert.py" >> "C:\jobs\run.log" 2>&1
อย่าสร้าง .bat ด้วย echo หรือ printf ข้าม ssh

ผมเคยขี้เกียจ scp เลยสั่ง echo ยิงข้อความเข้าไปเขียนไฟล์ตรงๆ ผลคือ backslash ในพาธโดนเชลล์ฝั่งส่งกินไปเป็น escape sequence — \envs กลายเป็น nvs, \Users ก็หายไปทั้งท่อน ได้ไฟล์ .bat ที่พาธเพี้ยนแบบมองด้วยตาไม่ออกเพราะมันดูเกือบถูก แล้วงานก็เงียบไปเฉยๆ ทุกคืนโดยไม่มี error ให้เห็น ให้เขียนไฟล์ให้จบก่อน แล้ว scp ขึ้นไป เท่านั้น อย่าประกอบไฟล์ผ่านบรรทัดคำสั่งข้ามเครื่อง

ส่งไฟล์ขึ้นไปแล้วตั้งงาน — ข้อสังเกตคือ scp ต้องใช้ forward slash และพาธเต็มขึ้นต้นด้วย C:/ ใช้ ~ ไม่ได้:

scp ./run.bat "winbox:C:/jobs/run.bat"

ssh winbox "schtasks /Create /TN ConvertBatch /TR 'C:\jobs\run.bat' /SC DAILY /ST 23:30 /F"
ssh winbox "schtasks /Run /TN ConvertBatch"          # ยิงทดสอบเดี๋ยวนั้น
ssh winbox "schtasks /Query /TN ConvertBatch /FO LIST"

งานที่ต้องวิ่งซ้ำในช่วงเวลาหนึ่ง เช่นทุก 30 นาทีระหว่างหกโมงเย็นถึงเที่ยงคืน ใช้ /RI กับ /DU คู่กัน:

ssh winbox "schtasks /Create /TN ConvertBatch /TR 'C:\jobs\run.bat' /SC DAILY /ST 18:00 /RI 30 /DU 0006:00 /F"
ทางเดินของงานหนึ่งรอบเครื่องทำงานssh / scp->winboxPS 5.1->Task Schedulerschtasks + run.bat->งานประมวลผลเขียนผลลง SQLite->เซิร์ฟเวอร์plink -batch

ข้อควรรู้เรื่องสิทธิ์ — งานที่สร้างแบบนี้จะรันในบริบทของบัญชีที่ล็อกอินอยู่ ถ้าเครื่องล็อกหน้าจอไว้หรือไม่มีใครล็อกอินค้าง งานที่ต้องใช้หน้าจอ (เปิดเบราว์เซอร์แบบเห็นหน้าต่าง, จับภาพหน้าจอ) จะไม่ทำงาน ส่วนงานที่เป็นการคำนวณล้วนไม่มีปัญหา ผมเลยแยกให้ชัดตั้งแต่ออกแบบ ว่างานไหนต้องมี desktop จริง งานไหนไม่ต้อง

กับดักที่ 3: ส่งผลลัพธ์ออกไปเซิร์ฟเวอร์แล้วค้างนิ่ง

อันนี้เป็นเรื่องที่กินเวลาผมมากที่สุดในงานนี้ครับ

ตอนจบงานแต่ละรอบ สคริปต์บนเครื่องปลายทางต้องส่งผลลัพธ์ออกไปเก็บที่เซิร์ฟเวอร์ ผมเขียนแบบธรรมดาที่สุดคือเรียก ssh/scp ของ OpenSSH ผ่าน subprocess ของ Python รันมือในหน้าต่าง ssh ผ่านฉลุย แต่พอ Task Scheduler เรียกเองค้างนิ่งจนหมดเวลา ทุกครั้ง ไม่มี error ไม่มีอะไรใน log ทั้งสองฝั่ง

ที่ผมลองแล้วไม่ช่วยเลย: ใส่ -o BatchMode=yes, ป้อน stdin เป็นค่าว่างกันมันรอ input, ใส่ -o StrictHostKeyChecking=no, ย้าย known_hosts ไปวางให้ครบทั้งบัญชี SYSTEM และบัญชีผู้ใช้ ทั้งหมดยังค้างเหมือนเดิม อาการมันคือ OpenSSH ฝั่ง Windows ไปรอ console ที่ไม่มีอยู่จริงในบริบทของ scheduled task

ทางออกที่ใช้ได้จริงคือเลิกใช้ OpenSSH ในเส้นทางนี้ แล้วเปลี่ยนไปใช้ plink ของ PuTTY แบบ batch พร้อมระบุ host key ตรงๆ:

import subprocess

PLINK = r"C:\jobs\plink.exe"
KEY   = r"C:\jobs\<KEY>.ppk"
HOSTKEY = "SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

def push(payload: bytes) -> None:
    cmd = [PLINK, "-ssh", "-P", "2233", "-i", KEY,
           "-hostkey", HOSTKEY, "-batch",
           "[email protected]",
           "mysql --default-character-set=utf8mb4 -u USER -pPASS DBNAME"]
    r = subprocess.run(cmd, input=payload, capture_output=True, timeout=90)
    if r.returncode != 0:
        raise RuntimeError(r.stderr.decode("utf-8", "replace"))
OpenSSH ผ่าน subprocess

ค้างเมื่อไม่มี console

รันมือผ่าน ssh ผ่านตลอด แต่พอ Task Scheduler เรียกจะค้างจนหมด timeout ไม่มีข้อความบอกเหตุ ไล่ log สองฝั่งก็ไม่เห็นอะไร

plink -batch -hostkey

ไม่ถามอะไรเลย

โหมด batch ตัดการถามทั้งหมด ส่วน -hostkey เอาลายนิ้วมือเซิร์ฟเวอร์ไปฝังไว้ เลยไม่ต้องพึ่ง known_hosts ของบัญชีไหนทั้งสิ้น ใช้ได้ทั้งตอนรันมือและตอน scheduler เรียก

ที่ต้องใส่ -hostkey เพราะโหมด batch จะไม่ถามยืนยันกุญแจเครื่องปลายทาง เจอกุญแจที่ไม่รู้จักมันจะขึ้น "Cannot confirm a host key in batch mode" แล้วออกทันที วิธีเอาค่ามาคือรัน plink ด้วยมือหนึ่งครั้ง มันจะพิมพ์ SHA256:... ให้ เอาค่านั้นมาใส่แล้วจบ ส่วนไฟล์กุญแจต้องแปลงเป็น .ppk ก่อน เพราะ plink ไม่กินรูปแบบของ OpenSSH ตรงๆ

กับดักที่ 4: ภาษาไทยกลายเป็นตัวยึกยือ

พอ pipeline เดินได้แล้ว ข้อมูลที่ส่งขึ้นเซิร์ฟเวอร์กลับมีภาษาไทยเพี้ยนทั้งหมด ต้นเหตุอยู่ที่ subprocess ตัวเดียวกันนั่นแหละครับ

ถ้าเขียนว่า text=True Python จะเข้ารหัส stdin ตาม locale ของระบบ ซึ่งบน PS 5.1 คือ cp1252 — ภาษาไทยไม่มีใน cp1252 เลยกลายเป็นเครื่องหมายคำถามหรือตัวประหลาดตั้งแต่ยังไม่ออกจากเครื่อง แก้ด้วยการส่งเป็นไบต์ UTF-8 เอง อย่าให้ Python เดา:

# ผิด - ให้ระบบเลือก encoding ให้ตาม locale (cp1252)
subprocess.run(cmd, input=payload_str, text=True)

# ถูก - บังคับเป็น UTF-8 ไบต์ตรงๆ
subprocess.run(cmd, input=payload_str.encode("utf-8"), capture_output=True)

ฝั่งปลายทางต้องบอกให้ชัดด้วยเช่นกัน ไม่งั้นเสียของตอนสุดท้าย — ต่อ MySQL ให้ใส่ --default-character-set=utf8mb4 และขึ้นต้นชุดคำสั่งด้วย SET NAMES utf8mb4; ทุกครั้ง

กฎที่ผมยึดหลังเจอเรื่องนี้

ทุกจุดที่ข้อมูลข้ามขอบเขตระหว่างระบบ ให้ระบุ encoding ด้วยตัวเองเสมอ อย่าปล่อยให้ค่าตั้งต้นเลือกให้ ที่เจ็บกว่าตัวอักษรเพี้ยนคือมันเพี้ยนเงียบๆ ไม่มี error ให้เห็น กว่าจะรู้ตัวก็มีข้อมูลเสียสะสมอยู่ในฐานแล้วหลายวัน

สรุป

โครงที่ผมใช้อยู่ทุกวันนี้เรียบง่ายมาก: ตัวเครื่องทำงานสั่งงานผ่าน ssh เวลาต้องการทันที ส่วนงานประจำปล่อยให้ Task Scheduler เรียก .bat ตัวเดียวตามเวลา และช่วงส่งผลออกใช้ plink แบบ batch ไม่แตะ OpenSSH เลย ล็อกไว้ในไฟล์เดียวบนเครื่องปลายทาง

ส่วนตัวผมสรุปได้สามข้อ หนึ่ง — อย่าคิดว่า ssh เข้า Windows แล้วจะเป็น Linux ต้องยอมรับว่าเป็น PowerShell 5.1 แล้วเขียนตามกติกาของมัน สอง — อะไรที่เป็นไฟล์ ให้เขียนเป็นไฟล์แล้วส่งขึ้นไป อย่าประกอบผ่านบรรทัดคำสั่งข้ามเครื่อง สาม — เครื่องมือที่ทำงานตอนเรารันมือ ไม่ได้แปลว่าจะทำงานตอน scheduler เรียก ต้องทดสอบในบริบทจริงเสมอ

ใครมีเครื่องแรงนั่งว่างอยู่อีกห้อง ลองยกงานหนักไปไว้ที่นั่นดูได้ตามสะดวกครับ คุ้มกว่าที่คิดเยอะ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย