ส่งไฟล์ขึ้นโฮสต์แล้วทับกันเงียบๆ สามกับดักตอนคัดลอกไฟล์
ความผิดพลาดตอน deploy ที่อันตรายที่สุดไม่ใช่ตัวที่ทำให้เว็บล่ม เพราะเว็บล่มแล้วเรารู้ทันที ตัวที่อันตรายกว่าคือตัวที่ทุกคำสั่งคืน exit code 0 ทุกหน้าตอบ 200 แต่ของข้างในไม่ใช่ของที่เราตั้งใจส่งขึ้นไป
สรุปใน 1 นาที
- อาการ/ปัญหา
- ส่งไฟล์ขึ้นโฮสต์แล้วทับกันเงียบๆ ทุกหน้าตอบ 200 แต่หน้าอังกฤษกลายเป็นหน้าไทย
- ใครควรอ่าน
- คนที่ deploy ด้วย pscp หรือเครื่องมือคัดลอกไฟล์ขึ้นโฮสต์
- ทางแก้แบบสั้น
- ระวังปลายทางเป็นโฟลเดอร์ ตัวมิเรอร์ที่ข้ามไฟล์ และการจับมือต่อไฟล์ มัดเป็นก้อนเดียวแล้วตรวจไฟล์หลังคัดลอก ไม่เชื่อ exit code
พอดีผมต้องอัปเดตเว็บสองภาษาของลูกค้าตัวหนึ่ง เป็นเว็บ PHP ธรรมดา 26 หน้า โครงคือหน้าไทยอยู่รากเว็บ แล้วหน้าอังกฤษเป็นกระจกเงาอยู่ในโฟลเดอร์ en/ ชื่อไฟล์เหมือนกันทุกตัว ผมแก้ทั้งสองภาษาเสร็จแล้วส่งขึ้นโฮสต์ทีเดียวสองไฟล์ เสร็จภายในสิบวินาที ไม่มี error อะไรเลย
อีกสองวันลูกค้าทักมาว่าหน้าอังกฤษเป็นภาษาไทย
เรื่องเขียนสคริปต์บนวินโดวส์แล้วเอาไปรันบนลินุกซ์ ทั้งพาธที่ค้างมาจากเครื่องตัวเองและไฟล์ขยะที่ลืมเก็บบนเซิร์ฟเวอร์ ผมเขียนไว้แยกแล้วที่ เขียนสคริปต์บน Windows ไปรันบน Linux เล่มนี้ผมพูดถึงความเสียหายเงียบตอนคัดลอกไฟล์ ล้วนๆ คือกรณีที่ไฟล์ถูกส่งขึ้นไปสำเร็จจริง เครื่องมือรายงานว่าผ่านจริง แต่ผลลัพธ์บนโฮสต์ไม่ใช่สิ่งที่เราตั้งใจ
กับดักที่หนึ่ง ปลายทางเป็นโฟลเดอร์ แปลว่าชื่อไฟล์คือ basename
คำสั่งที่ทำให้พังหน้าตาแบบนี้ ผมตัดส่วนยืนยันตัวตนกับชื่อโฮสต์จริงออกแล้ว เหลือแต่รูปคำสั่ง
# ผิด: ปลายทางลงท้ายด้วย / คือโฟลเดอร์
pscp -P 22 -i deploy.ppk \
site/index.php site/en/index.php \
[email protected]:domains/example.com/public_html/
scp และ pscp มีกติกาเดียวกันคือ ถ้าปลายทางเป็นโฟลเดอร์ มันจะตั้งชื่อไฟล์ปลายทางจากชื่อไฟล์ล้วนๆ ของต้นทาง โดยโยนโครงสร้างโฟลเดอร์ของต้นทางทิ้งทั้งหมด และไม่มีอ็อปชันแบบ --relative ให้ใช้
ผลคือทั้งสองไฟล์ไปลงที่ public_html/index.php ตำแหน่งเดียวกัน ตัวที่สองในบรรทัดคำสั่งเขียนทับตัวแรก โปรแกรมรายงานว่าโอนสำเร็จสองไฟล์ ขึ้น 100% ทั้งคู่ exit code เป็นศูนย์ ไม่มีคำเตือนสักตัว
เว็บนี้มีไฟล์ชื่อซ้ำกันระหว่างสองภาษาอยู่หลายคู่ ทั้ง index.php, sitemap.php และไฟล์บทความในโฟลเดอร์ย่อยอีกหลายตัว แปลว่ากับดักนี้รอผมอยู่ทุกครั้งที่แก้สองภาษาพร้อมกัน ซึ่งเป็นเรื่องปกติของเว็บสองภาษา
วิธีแก้ตรงไปตรงมามาก คือระบุชื่อไฟล์ปลายทางให้ครบทุกครั้ง ห้ามปล่อยให้ปลายทางเป็นโฟลเดอร์เปล่าเมื่อส่งหลายไฟล์พร้อมกัน
# ถูก: ปลายทางเป็นชื่อไฟล์เต็ม หนึ่งคำสั่งต่อหนึ่งไฟล์
pscp -P 22 -i deploy.ppk site/index.php \
[email protected]:domains/example.com/public_html/index.php
pscp -P 22 -i deploy.ppk site/en/index.php \
[email protected]:domains/example.com/public_html/en/index.php
เครื่องมือตระกูลนี้มีอ็อปชันให้ใส่รหัสผ่านตรงๆ ในบรรทัดคำสั่งได้ และมันสะดวกมากจนติดเป็นนิสัย แต่บรรทัดคำสั่งไปโผล่ใน history ของเชลล์ ใน log ของสคริปต์ และในรายการโปรเซสที่คนอื่นบนเครื่องเดียวกันมองเห็นได้ ใช้คีย์เถอะครับ ถ้าจำเป็นต้องใช้รหัสผ่านจริงๆ ให้อ่านจากตัวแปรสภาพแวดล้อมหรือไฟล์ที่ตั้งสิทธิ์ไว้แน่นหนา อย่าพิมพ์ลงไปในคำสั่งตรงๆ และอย่าเอาไปแปะในบทความหรือใน repo
อาการบนเว็บจริง ตอบ 200 ทุกหน้า แต่เป็นเนื้อหาผิดภาษา
สิ่งที่ทำให้กับดักนี้อยู่รอดได้นานคือมันไม่สร้างอาการที่เครื่องมือตรวจจับได้เลย
- ไม่มี 404 เพราะไฟล์อยู่ครบ
- ไม่มี 500 เพราะ PHP ในไฟล์นั้นถูกต้องสมบูรณ์
- error log ว่างเปล่า
- ระบบเช็กว่าเว็บล่มหรือเปล่ารายงานเขียว 100%
อาการเดียวที่มีคือคนอ่านเปิดหน้าอังกฤษแล้วเจอภาษาไทย แล้วท้ายหน้ายังได้ footer ผิดภาษาติดมาด้วย เพราะหน้าเป็นคนเลือกว่าจะ include footer ตัวไหน พอไฟล์ถูกทับ มันก็ลากทุกอย่างของอีกภาษามาพร้อมกันหมด รวมถึงปุ่มสลับภาษาที่ชี้กลับมาหาตัวเอง
ผมตรวจเจอด้วยการเทียบเนื้อหา ไม่ใช่เทียบสถานะ วิธีที่ถูกคือหลัง deploy ทุกครั้ง ยิง URL สำคัญแล้วเช็กว่าเนื้อข้างในมีคำที่ควรมีจริงหรือเปล่า
# ตรวจว่าได้ 200 แล้วยัง "มีข้อความที่คาดว่าจะมี" ด้วย
for u in "" "en/" "en/contact"; do
code=$(curl -s -o /tmp/page.html -w "%{http_code}" "https://example.com/$u")
hit=$(grep -c "Contact us" /tmp/page.html)
echo "/$u http=$code marker=$hit"
done
หน้าที่อยู่ใต้ /en/ แล้ว marker ของภาษาอังกฤษเป็นศูนย์ คือหน้าที่โดนทับ เช็กแค่ http=200 อย่างเดียวจับไม่ได้แน่นอน
กับดักที่สอง ตัวมิเรอร์ที่ข้ามไฟล์ขึ้นต้นด้วยจุด
กับดักตัวที่สองอยู่ฝั่งเครื่องตัวเอง ตอนผมซิงก์โฟลเดอร์งานไปที่ XAMPP เพื่อทดสอบก่อนส่งขึ้นโฮสต์ ผมใช้ robocopy โหมดมิเรอร์
robocopy "$src" "$dst" /MIR /XD assets\img\sources /NFL /NDL /NJH /NJS /NP
# /MIR ไม่ได้คัดลอก .htaccess มาให้สม่ำเสมอ ต้องสั่งคัดลอกเองต่างหาก
Copy-Item "$src\.htaccess" "$dst\.htaccess" -Force
ผลคือบางรอบ .htaccess มา บางรอบไม่มา แล้ว Apache ตอบ 403 พร้อมข้อความทำนองว่าอ่านไฟล์ htaccess ไม่ได้ ซึ่งเป็นข้อความที่ชวนให้ไปไล่เรื่องสิทธิ์ไฟล์ทั้งที่ต้นเหตุคือไม่มีไฟล์นั้นอยู่
สาเหตุจริงไม่ได้อยู่ที่จุดหน้าชื่อไฟล์ เพราะบน NTFS จุดนำหน้าไม่มีความหมายพิเศษ ตัวที่มีผลคือแอตทริบิวต์ Hidden กับ System ที่ติดมากับไฟล์ ไฟล์ที่ได้มาจากการแตก zip จากการ checkout หรือจากการคัดลอกข้ามเครื่องมาก่อนหน้า อาจจะติด H มาโดยที่เราไม่รู้ ต้นทางเดียวกันแต่แอตทริบิวต์ต่างกัน ผลเลยต่างกัน ซึ่งเป็นที่มาของคำว่า "บางรอบ"
ที่ทำให้แย่กว่าเดิมคือชุด /NFL /NDL /NJH /NJS /NP ที่ผมใส่ไว้ให้จอสะอาด มันปิดรายงานทั้งหมด รอบที่ไม่คัดลอกอะไรเลยกับรอบที่คัดลอกครบ หน้าจอออกมาเหมือนกันเป๊ะ
แบบแรกคือไม่มีไฟล์ แล้วคอนฟิกข้างบนบังคับให้ต้องมี ผลเป็น 403 ทั้งเว็บ แบบที่สองคือมีไฟล์แต่ขนาด 0 ไบต์ อันนี้ร้ายกว่า เพราะหน้าแรกยังตอบ 200 ปกติ เมนูยังขึ้นครบ แต่ URL ที่พึ่ง rewrite ทุกตัวกลายเป็น 404 หมด คนทดสอบที่เปิดแต่หน้าแรกจะไม่เห็นอะไรผิดเลย
เว็บตัวนี้ใช้ URL แบบไม่มีนามสกุลทั้งเว็บ ซึ่งทั้งหมดวางอยู่บนไฟล์ตัวนี้ตัวเดียว
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.php -f
RewriteRule ^(.*)$ $1.php [L]
RewriteCond %{THE_REQUEST} \s/+(.+?)\.php([\s?])
RewriteRule ^ /%1 [R=301,L]
</IfModule>
หายไปเมื่อไหร่ ลิงก์ภายในทุกลิงก์ของเว็บตายหมด ในขณะที่หน้าแรกยังเสิร์ฟได้จาก DirectoryIndex ตามปกติ
อีกเรื่องที่ทำให้วินิจฉัยผิดทางได้ง่ายคือ โฮสต์หลายเจ้าตอนนี้เป็น nginx ข้างหน้าแล้วมี Apache อยู่ข้างหลัง พอ header ตอบว่า nginx คนมักสรุปว่าโฮสต์นี้ไม่อ่าน .htaccess อยู่แล้ว แล้วเลิกสงสัยไฟล์ตัวนี้ ทั้งที่ Apache ข้างหลังอ่านมันอยู่เต็มๆ
ไฟล์ 0 ไบต์ที่ทุกเครื่องมือบอกว่าสำเร็จ
เรื่องไฟล์ 0 ไบต์ผมเคยเจอแบบที่ทำเว็บล่มทั้งเว็บมาแล้ว และต้นเหตุคือคำสั่งบรรทัดเดียวที่ดูไม่มีพิษภัย
# ผิด: 'wb' ตัดไฟล์เหลือ 0 ไบต์ ก่อนที่ตัวอ่านข้างในจะได้อ่าน
open(p, 'wb').write(open(p, 'rb').read().replace(b'\r', b''))
# ถูก: อ่านให้จบก่อน ปิดไฟล์ แล้วค่อยเขียนทับ
data = open(p, 'rb').read().replace(b'\r', b'')
open(p, 'wb').write(data)
ตัวที่ผิดจะได้ไฟล์เปล่าที่สมบูรณ์แบบ แล้วพอเราส่งไฟล์เปล่านั้นขึ้นโฮสต์ ทุกขั้นตอนรายงานว่าสำเร็จ เพราะการโอนไฟล์ 0 ไบต์ก็คือการโอนที่สำเร็จ
กฎที่ผมใช้หลังจากนั้นคือวัดขนาดไฟล์หลังแตะไฟล์ทุกครั้ง ทั้งฝั่งเครื่องตัวเองและฝั่งโฮสต์ ไม่ต้องไปดูเนื้อข้างในก็ได้ แค่ดูว่าขนาดไม่ใช่ศูนย์และไม่เปลี่ยนไปจากที่ควรเป็น
อ้อ อีกเรื่องคือการไล่ตัด CR ออกจากทุกไฟล์แบบเหมารวมนั้น ส่วนใหญ่ไม่จำเป็น Apache อ่าน .htaccess ที่ลงท้ายบรรทัดแบบวินโดวส์ได้ปกติ ที่ต้องตัดจริงๆ คือสคริปต์เชลล์ที่จะเอาไปรันบนลินุกซ์ เพราะ CR ที่ติดมาบนบรรทัด shebang ทำให้ได้ error ว่าหาตัวแปลคำสั่งไม่เจอ
กับดักที่สาม 26 ไฟล์ = 26 การจับมือ
กับดักตัวสุดท้ายไม่ได้ทำของเสียหาย แต่มันทำให้คนเลือกทางที่เสี่ยงกว่าโดยไม่รู้ตัว
เว็บนี้มี 26 หน้า และสคริปต์ช่วยงานของผมหลายตัวแก้ทีเดียวทั้ง 26 ไฟล์ เช่นตัวเปลี่ยน footer ตัวเปลี่ยนโลโก้ ตัวแทรกไอคอน พอส่งขึ้นโฮสต์ทีละไฟล์ก็เท่ากับเปิดเซสชัน SSH ใหม่ 26 รอบ
ต่อครั้งหนึ่งคือ TCP หนึ่งรอบ แลกกุญแจหนึ่งรอบ ยืนยันตัวตนหนึ่งรอบ เปิดระบบย่อยสำหรับโอนไฟล์อีกหนึ่งรอบ แล้วปิดทิ้ง ทั้งที่เนื้อไฟล์จริงรวมกันไม่ถึงหลักร้อยกิโล เวลาที่เสียไปคือเวลาจับมือล้วนๆ ประมาณ 2-3 วินาทีต่อไฟล์
พอมันช้าขนาดนั้น คนเลยเริ่มขี้เกียจ แล้วกลับไปใช้ท่าส่งทีเดียวหลายไฟล์ลงโฟลเดอร์ ซึ่งก็คือกับดักที่หนึ่งเป๊ะๆ กับดักสองตัวนี้ป้อนกันเอง
มัดเป็นก้อนเดียว แก้สองกับดักพร้อมกัน
วิธีที่ผมใช้ตอนนี้คือมัดเป็นก้อนเดียวแล้วแตกที่ปลายทาง
tar czf ./out.tar.gz -C ./ site/.htaccess site/includes site/index.php site/en
pscp -P 22 -i deploy.ppk ./out.tar.gz [email protected]:~/out.tar.gz
plink -ssh -P 22 -i deploy.ppk -batch [email protected] \
"cd domains/example.com/public_html \
&& tar xzf ~/out.tar.gz --strip-components=1 \
&& rm ~/out.tar.gz && echo DEPLOYED"
ท่านี้แก้กับดักที่หนึ่งไปในตัวด้วย เพราะ tar เก็บพาธสัมพัทธ์ไว้ครบ site/index.php กับ site/en/index.php เป็นคนละรายการในก้อนเดียวกัน ไม่มีทางยุบชื่อชนกันได้เลย ส่วน --strip-components=1 คือตัวตัดคำนำหน้า site/ ออกตอนแตก ให้ไฟล์ลงตรงรากเว็บพอดี
สามรายละเอียดที่ผมจดไว้กันลืม หนึ่ง ต้องใส่ site/.htaccess ลงไปในรายการเองแบบระบุชื่อเต็ม เพราะ tar ก็มีเรื่องไฟล์ขึ้นต้นด้วยจุดของมันเหมือนกัน สอง คำสั่งลบไฟล์ก้อนต้องอยู่ในชุดคำสั่งเดียวกันกับตอนแตก ไม่ใช่ค่อยไปลบทีหลัง เพราะเดี๋ยวลืม สาม tar ไม่เก็บโฟลเดอร์ว่างให้ ถ้าโปรเจกต์ต้องการโฟลเดอร์ว่างไว้เขียน cache หรือ log ต้อง mkdir -p แล้วตั้งสิทธิ์เองหลังแตกทุกครั้ง
ปลอดภัยถ้าระบุปลายทางครบ
เหมาะกับงานแก้ไฟล์เดียวสองไฟล์ ข้อดีคือเห็นชัดว่าอะไรไปลงตรงไหน ข้อเสียคือช้า และพอช้าแล้วคนจะเริ่มมัดรวมส่งลงโฟลเดอร์ ซึ่งพาไปเจอกับดัก basename ทันที
เร็วกว่าและกันชื่อชนในตัว
หนึ่งเซสชันจบ พาธสัมพัทธ์ครบ ไม่มีการยุบชื่อ แต่ต้องระวังไฟล์ขึ้นต้นด้วยจุด โฟลเดอร์ว่างที่หายไป และต้องเก็บไฟล์ก้อนทิ้งในคำสั่งเดียวกัน
ตรวจหลังคัดลอก ไม่ใช่เชื่อ exit code
ทุกอาการในบทความนี้มีจุดร่วมข้อเดียวครับ คือทุกคำสั่งคืนค่าสำเร็จ เครื่องมือโอนไฟล์ทำหน้าที่ของมันครบถ้วนทุกครั้ง สิ่งที่ผิดคือความคาดหวังของเรา ไม่ใช่การทำงานของมัน แปลว่าจะกันได้ทางเดียวคือ ตรวจผลลัพธ์จริงหลังคัดลอกเสมอ
- ตรวจว่ามีไฟล์และขนาดไม่ใช่ศูนย์ โดยเฉพาะไฟล์ที่ขึ้นต้นด้วยจุด
- ยิง URL สำคัญแล้วเช็กทั้งรหัสตอบกลับและข้อความข้างใน ไม่ใช่เช็กแค่ 200
- ถ้าเป็นเว็บหลายภาษาหรือมีไฟล์ชื่อซ้ำ ให้เทียบ marker เฉพาะของแต่ละภาษาทีละหน้า
- ถ้าเป็นเว็บที่มี cache หรือ CDN อยู่ข้างหน้า ต้องล้าง cache หรือเติมตัวแปรท้าย URL ก่อนตรวจ ไม่งั้นจะได้ของเก่ามาแล้วสรุปผิด
- เก็บคำสั่งตรวจเหล่านี้ไว้เป็นสคริปต์ ไม่ใช่ทำด้วยมือ เพราะวันที่รีบคือวันที่คนข้ามขั้นตอนตรวจ
ส่วนตัวผมสรุปได้ประโยคเดียวว่า เครื่องมือโอนไฟล์ไม่มีหน้าที่รู้ว่าเราตั้งใจอะไร มันรู้แค่ว่าเราสั่งอะไร คำสั่งที่เขียนกำกวมแล้วมันตีความไปอีกทาง ไม่นับว่าเป็นข้อผิดพลาดของมัน เราเลยต้องเขียนคำสั่งให้ไม่มีที่ให้ตีความ แล้วตรวจผลทุกครั้ง
ใครมีสคริปต์ deploy ที่ส่งไฟล์หลายตัวลงโฟลเดอร์เดียวอยู่ตอนนี้ ลองไปไล่ดูก่อนว่ามีชื่อไฟล์ซ้ำกันข้ามโฟลเดอร์อยู่หรือเปล่า ใช้เวลาสองนาที แต่ถ้ามี มันรอทับกันอยู่แล้วครับ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



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