บทความ › เซิร์ฟเวอร์และ DevOps
Deploy ทีเดียวทั้งก้อน ไม่มีเว็บครึ่ง ๆ และย้อนกลับได้ในคำสั่งเดียว
ก๊อปไฟล์ทับโฟลเดอร์เว็บตรง ๆ ผู้ใช้จะเจอเว็บที่อัปโหลดไปครึ่งเดียว วิธีสลับโฟลเดอร์ที่เว็บนี้ใช้ ช่องว่างเล็ก ๆ ที่ยังเหลือ และแบบ symlink ที่สลับได้ทันที
- อย่าอัปโหลดทับของที่กำลังให้บริการ เตรียมของใหม่ให้ครบในที่อื่นก่อน แล้วสลับทีเดียว
- การเปลี่ยนชื่อในดิสก์เดียวกันเกิดทันที ไม่มีสถานะครึ่ง ๆ แต่การสลับสองขั้นยังมีช่องว่างสั้น ๆ
- เก็บเวอร์ชันก่อนไว้เสมอ ย้อนกลับควรเป็นคำสั่งเดียวที่ไม่ต้องคิด
วิธี deploy ที่ง่ายที่สุดคือก๊อปไฟล์ใหม่ทับโฟลเดอร์เว็บ ใช้ได้ส่วนใหญ่ แต่ในช่วงไม่กี่วินาทีที่ไฟล์กำลังส่ง ผู้ใช้ที่เข้ามาพอดีจะได้หน้า HTML ใหม่ที่ชี้ไปหาไฟล์ CSS ที่ยังไม่มาถึง หน้าเว็บก็โผล่มาแบบไม่มีสไตล์ หรือได้ HTML เก่าผสม JavaScript ใหม่ แล้วพังแบบอธิบายไม่ได้ บทความนี้เล่าวิธีที่เว็บนี้ใช้ deploy ทุกครั้ง จุดอ่อนเล็ก ๆ ที่ยังมี และวิธีที่ไม่มีจุดอ่อนนั้น
ทดสอบบน Ubuntu 24.04 กับ nginx เว็บนี้เป็น static site ขนาดราว 1.3MB ทำด้วย Astro
ปัญหาของการก๊อปทับ
เว็บที่ build ด้วยเครื่องมือสมัยใหม่ ตั้งชื่อไฟล์ CSS และ JavaScript ตามเนื้อหา เช่น index.xB_X6hmL.css แก้เนื้อหาเมื่อไหร่ชื่อก็เปลี่ยน ข้อดีคือให้เบราว์เซอร์ cache ไฟล์พวกนี้ได้เป็นปี แต่ก็แปลว่า HTML รุ่นใหม่ใช้ได้กับไฟล์รุ่นใหม่เท่านั้น
ถ้าก๊อปทับ ลำดับที่ไฟล์มาถึงไม่แน่นอน
| เวลา | ในโฟลเดอร์เว็บ | ผู้ใช้ที่เข้ามาตอนนี้ |
|---|---|---|
| วินาทีที่ 0 | ของเก่าครบ | ปกติ |
| วินาทีที่ 1 | index.html ใหม่ แต่ CSS ใหม่ยังไม่มา | หน้าไม่มีสไตล์ |
| วินาทีที่ 2 | ครบ | ปกติ |
หลักการ: อย่าแก้ของที่กำลังให้บริการ เตรียมของใหม่ให้ครบในที่อื่น แล้วสลับทีเดียว
วิธีที่เว็บนี้ใช้
สคริปต์ deploy ทั้งหมดอยู่ในไฟล์เดียว รันด้วย npm run deploy
npm run build
tar -C dist -czf /tmp/sixam-dist.tgz .
scp -q /tmp/sixam-dist.tgz prod:/tmp/sixam-dist.tgz
ssh prod 'set -e
rm -rf /tmp/sixam-new && mkdir -p /tmp/sixam-new
tar -C /tmp/sixam-new -xzf /tmp/sixam-dist.tgz
sudo rm -rf /var/www/6amdev.old
if [ -d /var/www/6amdev ]; then sudo mv /var/www/6amdev /var/www/6amdev.old; fi
sudo mv /tmp/sixam-new /var/www/6amdev
sudo chown -R www-data:www-data /var/www/6amdev
curl -s -o /dev/null -w "6amdev.com %{http_code}\n" https://6amdev.com/'
ทีละขั้น
- build ในเครื่อง แล้วอัดเป็นไฟล์เดียว ส่งไฟล์เดียวเร็วกว่าส่งไฟล์ย่อยหลายร้อยไฟล์ และไม่มีโอกาสส่งไปครึ่งเดียวแล้วถูกใช้งาน
- แตกไฟล์ในโฟลเดอร์ใหม่
/tmp/sixam-newซึ่งยังไม่มีใครเห็น ถ้าขั้นนี้พังset -eหยุดสคริปต์ เว็บจริงไม่ถูกแตะเลย - ย้ายของเก่าไปเป็น
.oldเก็บไว้ย้อนกลับ - ย้ายของใหม่เข้าที่
- ตรวจว่าเว็บตอบ 200
ทำไม mv ถึงเร็วทันที
mv ภายในดิสก์เดียวกัน ไม่ได้ก๊อปไฟล์ แค่เปลี่ยนชื่อในสารบัญของระบบไฟล์ เกิดขึ้นทันทีไม่ว่าโฟลเดอร์จะใหญ่แค่ไหน และไม่มีสถานะครึ่ง ๆ คือเปลี่ยนแล้วหรือยังไม่เปลี่ยน
แต่ต้องอยู่ ดิสก์เดียวกัน จริง ถ้า /tmp เป็นอีกพาร์ทิชันหรือเป็นหน่วยความจำ (tmpfs) mv จะกลายเป็นก๊อปแล้วลบ ซึ่งกลับไปมีสถานะครึ่ง ๆ อีก ตรวจได้ด้วย
df -h /tmp /var/www
บนเครื่องของเรา ทั้งสองอยู่บน /dev/vda1 เดียวกัน
ช่องว่างที่ยังเหลือ
วิธีนี้ดีกว่าก๊อปทับมาก แต่ยังไม่สมบูรณ์ ระหว่างขั้น 3 กับขั้น 4 มีเสี้ยววินาทีที่โฟลเดอร์ /var/www/6amdev ไม่มีอยู่เลย คำขอที่มาตรงจังหวะนั้นจะได้ 404 ช่องว่างนี้สั้นมาก สำหรับเว็บความรู้ยอมรับได้ แต่สำหรับระบบที่มีคนใช้ตลอดเวลา ควรปิดให้สนิท
อีกจุดเล็ก ๆ คือ chown ทำหลังสลับ ควรย้ายไปทำก่อนสลับ ให้ของใหม่พร้อมสมบูรณ์ตั้งแต่วินาทีแรกที่ถูกใช้
แบบไม่มีช่องว่าง: symlink
แทนที่ nginx จะชี้ไปโฟลเดอร์จริง ให้ชี้ไปที่ symlink ชื่อ current แล้วแต่ละรุ่นเป็นโฟลเดอร์แยก
/var/www/6amdev/
├── releases/
│ ├── 20261015-1731/
│ └── 20261016-0930/
└── current -> releases/20261016-0930
nginx ตั้ง root /var/www/6amdev/current; ตอน deploy
REL=/var/www/6amdev/releases/$(date +%Y%m%d-%H%M)
mkdir -p "$REL" && tar -C "$REL" -xzf /tmp/site.tgz
chown -R www-data:www-data "$REL"
ln -s "$REL" /var/www/6amdev/current.tmp
mv -T /var/www/6amdev/current.tmp /var/www/6amdev/current
บรรทัดสุดท้ายคือหัวใจ mv -T เปลี่ยนชื่อ symlink ใหม่ทับตัวเก่า ด้วยการเปลี่ยนชื่อครั้งเดียว ทุกคำขอเห็นรุ่นเก่าหรือรุ่นใหม่อย่างใดอย่างหนึ่งเสมอ ไม่มีจังหวะที่ไม่มีอะไรเลย การสร้าง symlink ชื่อชั่วคราวก่อนแล้วค่อย mv -T ทำให้มั่นใจได้ว่าการสลับเป็นการเปลี่ยนชื่อครั้งเดียวจริง โดยไม่ต้องพึ่งว่าเครื่องมือแต่ละเวอร์ชันทำงานข้างในยังไง
ย้อนกลับ
ทุกวิธีต้องย้อนกลับได้ด้วยคำสั่งเดียว เพราะตอนที่ต้องย้อน คือตอนที่กำลังมีปัญหาและไม่ควรต้องคิดเยอะ
แบบสลับโฟลเดอร์ของเว็บนี้
cd /var/www && sudo mv 6amdev 6amdev.bad && sudo mv 6amdev.old 6amdev
แบบ symlink แค่ชี้ current กลับไปรุ่นก่อน
ln -s /var/www/6amdev/releases/20261015-1731 /var/www/6amdev/current.tmp
mv -T /var/www/6amdev/current.tmp /var/www/6amdev/current
แบบ symlink เก็บได้หลายรุ่น ลบรุ่นที่เก่ากว่า 5 รุ่นทิ้งเป็นระยะ ส่วนแบบสลับโฟลเดอร์เก็บได้รุ่นเดียว ซึ่งพอสำหรับเว็บเล็ก
ตรวจหลังขึ้นให้มากกว่าหน้าแรก
สคริปต์ตรวจแค่ว่าหน้าแรกตอบ 200 ซึ่งเป็นขั้นต่ำ เราตรวจเพิ่มทุกครั้งที่มีหน้าใหม่
- หน้าที่เพิ่งเพิ่ม ทั้งสองภาษา
- ไฟล์ CSS หนึ่งไฟล์ ว่าได้ header ครบ (เล่าในบทความเรื่อง security headers)
- เว็บอื่นที่อยู่เครื่องเดียวกัน ยังเปิดได้ เพราะเครื่องนี้มีหลายเว็บใช้ nginx ตัวเดียวกัน
ข้อผิดพลาดที่เจอบ่อย
- ก๊อปหรือ rsync ทับโฟลเดอร์ที่กำลังให้บริการ
- แตกไฟล์ใน
/tmpที่เป็น tmpfs แล้วคิดว่าmvเปลี่ยนชื่อทันที - ไม่เก็บรุ่นก่อน พอพังต้อง build ใหม่ในขณะที่เว็บล่ม
- ย้อนกลับต้องทำหลายขั้นและต้องคิด
- สลับ symlink ด้วยการลบตัวเก่าก่อนแล้วค่อยสร้างตัวใหม่ ซึ่งมีจังหวะที่ไม่มี symlink อยู่
- deploy แล้วตรวจแค่หน้าแรก
คำถามที่พบบ่อย
ใช้กับเว็บที่มี backend ได้ไหม
หลักการเดียวกัน แต่ต้องคิดเรื่องฐานข้อมูลด้วย ถ้า deploy มีการเปลี่ยนโครงสร้างตาราง การย้อนโค้ดกลับอาจไม่พอ ต้องออกแบบการเปลี่ยนตารางให้โค้ดเก่าและใหม่ใช้ได้ทั้งคู่ในช่วงเปลี่ยนผ่าน
ทำไมไม่ใช้ rsync
rsync ดีสำหรับส่งไฟล์ แต่ถ้า rsync ลงโฟลเดอร์ที่กำลังให้บริการตรง ๆ ระหว่างที่ไฟล์ยังส่งไม่ครบ ผู้ใช้ก็เจอสถานะครึ่ง ๆ เหมือนเดิม ใช้ rsync ส่งไปโฟลเดอร์ใหม่ แล้วค่อยสลับ ได้ประโยชน์ทั้งสองทาง
คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ