มี backup ครบทุกไฟล์ แต่สร้างเครื่องใหม่ไม่ได้
backup ตอบคำถามว่าข้อมูลอยู่ไหน แต่ไม่เคยตอบคำถามว่าเครื่องนี้ทำงานได้เพราะอะไร และคำถามที่สองต่างหากที่เราจะต้องตอบตอนตีสามของวันที่ดิสก์ตาย
สรุปใน 1 นาที
- อาการ/ปัญหา
- backup ครบทุกไฟล์ แต่เอาไปวางบนเครื่องเปล่าแล้วเว็บไม่ขึ้น
- ใครควรอ่าน
- คนดูแลเซิร์ฟเวอร์ที่เชื่อว่า backup ครบแล้วพร้อมสร้างเครื่องใหม่
- ทางแก้แบบสั้น
- จดสิ่งที่ไม่อยู่ใน backup เช่น systemd สองระดับ cron ทางเข้าภายนอก และ rclone.conf แล้วเขียนลำดับกู้พร้อมคำสั่งตรวจผล
พอดีนั่งเก็บ snapshot ของเซิร์ฟเวอร์ตัวหนึ่งลงเครื่องตัวเอง ตอนตรวจไฟล์เสร็จผมก็ถามตัวเองเล่น ๆ ว่าถ้าเอาสามไฟล์นี้ไปวางบนเครื่องเปล่าที่ลง OS มาใหม่ กี่นาทีเว็บถึงจะขึ้น พอลองไล่ทีละขั้นในหัวจริง ๆ ผมเลยพบว่าคำตอบคือไม่ขึ้นครับ ต่อให้ข้อมูลครบร้อยเปอร์เซ็นต์ก็ตาม
ของที่ backup เก็บคือฐานข้อมูล โค้ด และความลับ ส่วนของที่ทำให้เครื่องนั้นทำงานได้จริง ๆ อีกสิบกว่าอย่างไม่เคยถูกเก็บเลยสักครั้ง มันอยู่ในไฟล์ที่กระจายตาม /etc ตาม ~/.config และในตารางที่ไม่มีใครเขียนลงกระดาษ ผมเลยนั่งไล่เก็บสด ๆ จากเครื่องจริงผ่าน SSH แล้วเขียนเป็นเอกสารสร้างใหม่ทั้งระบบไว้ทีเดียว บทความนี้คือสาระของเอกสารนั้นครับ
ชื่อผู้ใช้ โดเมน พาธ และรหัสอ้างอิงต่าง ๆ ในบทความเป็นค่าตัวอย่าง ส่วนโครงสร้างและกับดักเป็นของจริงทั้งหมด
ก่อนอื่น — รู้ว่าอะไร "ไม่" อยู่ใน backup
ข้อแรกที่ต้องเคลียร์กับตัวเองคือขอบเขต ของบางอย่างจงใจไม่เก็บเพราะสร้างใหม่ได้ ซึ่งถูกแล้ว แต่ต้องรู้ว่ามีอะไรบ้างและสร้างใหม่ใช้เวลาเท่าไหร่
สามก้อน จบ
dump ฐานข้อมูลทั้งก้อน (เอกสารลูกค้าเป็น blob อยู่ในนี้ เลยครอบคลุมตู้เอกสารครบ) · tar ของ git bare repo กับโฟลเดอร์ที่ไม่อยู่ใน git · tar ของโฟลเดอร์ความลับกับไฟล์ .env
สร้างใหม่ได้ แต่กินเวลา
vendor ของ composer เกือบสองร้อยเมกะไบต์ · node_modules · virtualenv ของงาน python ที่บวมถึงหลายกิกะไบต์เพราะมีไลบรารีประมวลผลเสียงติดมา · โฟลเดอร์ storage · ไฟล์วิดีโอกับเสียงที่จงใจตัดออก
ตัวที่ผมกังวลที่สุดในรายการขวาไม่ใช่ตัวใหญ่สุดครับ แต่เป็น virtualenv ของงาน python เพราะการสร้างใหม่หมายถึงต้องโหลดไลบรารีก้อนใหญ่ใหม่ทั้งชุด ถ้าวันนั้นเน็ตไม่ดีหรือเวอร์ชันบนดัชนีขยับไปแล้ว งานที่ควรจะเสร็จในสิบนาทีจะกลายเป็นครึ่งวัน ใครมีของแบบนี้ผมแนะนำให้ล็อกเวอร์ชันไว้เป็นไฟล์ให้หมด อย่าปล่อยลอย
ต่างกับ vendor ตรงที่กลุ่มนี้ไม่มีคำสั่งไหนสร้างคืนให้ได้เลย ต้องพิมพ์ใหม่จากความจำหรือจากเอกสาร — ไฟล์ vhost ของเว็บเซิร์ฟเวอร์ · unit ของ systemd ทั้งสองระดับ · บรรทัด cron · ไฟล์ config ของ rclone · ค่า ingress ของ tunnel · การตั้ง sshd ถ้าไม่ได้เขียนไว้ วันกู้จริงคือวันที่ต้องนั่งเดา
ของที่หายไปกับเครื่อง ไม่ใช่กับไฟล์
ผมไล่เก็บของกลุ่มนี้ทีละอย่างจากเครื่องจริง เพราะเอกสารเก่าที่เขียนไว้ตั้งแต่ตอนตั้งเครื่องมันล้าสมัยไปหมดแล้ว รายการที่ได้ออกมาแบ่งเป็นสี่กอง
กองแรก vhost ของ Apache กับการต่อไป php-fpm ตัวไฟล์อยู่ที่ /etc/apache2/sites-enabled/ ซึ่งไม่มีทางติดมากับ tar ของโปรเจกต์
# /etc/apache2/sites-enabled/app.conf (สาระที่ต้องมี)
ServerName app.local
DocumentRoot /opt/appsrv/app/public
SSLCertificateFile /etc/ssl/app/cert.pem # self-signed — ปลาย TLS จริงอยู่ที่ขอบนอก
<Directory /opt/appsrv/app/public>
AllowOverride All # ให้ .htaccess ทำงาน เหมือนตอนอยู่ shared host
Require all granted
Options -Indexes +FollowSymLinks
</Directory>
<FilesMatch \.php$>
SetHandler "proxy:unix:/run/php/php8.4-fpm.sock|fcgi://localhost"
</FilesMatch>
AllowOverride All บรรทัดเดียวนี้สำคัญกว่าที่หน้าตามันบอกครับ เพราะแอปนี้ย้ายมาจาก shared host ที่ .htaccess ทำงานอยู่ ถ้ากู้เครื่องใหม่แล้วลืมบรรทัดนี้ เว็บจะขึ้นหน้าแรกได้ปกติ แต่ทุก URL ที่ไม่ใช่หน้าแรกจะได้ 404 ทันที ซึ่งเป็นอาการที่ไล่หาสาเหตุยากมากถ้าไม่ได้เอะใจตั้งแต่แรก
systemd สองระดับ — และบรรทัดที่ลืมแล้วเว็บล่มทั้งเว็บ
กองที่สองคือ service ซึ่งมีสองระดับ ระดับเครื่องอยู่ที่ /etc/systemd/system/ ระดับผู้ใช้อยู่ที่ ~/.config/systemd/user/ ตอนไล่ผมเจอว่าตัวที่สำคัญที่สุดของทั้งเครื่องดันอยู่ระดับผู้ใช้ นั่นคือ tunnel ที่เป็นทางเข้าเดียวจากภายนอก
# ~/.config/systemd/user/tunnel.service
[Unit]
Description=Outbound tunnel (ทางเข้าเดียวจากภายนอก)
After=network-online.target
[Service]
Type=simple
EnvironmentFile=/opt/appsrv/.private/tunnel.env
ExecStart=/home/appsrv/.local/bin/tunneld tunnel run
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
systemctl --user enable --now tunnel
loginctl enable-linger appsrv # <- บรรทัดที่ขาดไม่ได้
โดยปกติ systemd ระดับผู้ใช้จะเริ่มทำงานตอนคนคนนั้น login และดับทิ้งตอน logout ถ้าไม่สั่ง loginctl enable-linger ไว้ เครื่องที่บูตขึ้นมาโดยไม่มีใคร login จะไม่สตาร์ต unit ของผู้ใช้เลย แปลว่า tunnel ไม่ขึ้น แปลว่าเว็บล่มทั้งเว็บ ทั้งที่ Apache กับฐานข้อมูลรันอยู่ดี ๆ อาการนี้จะโผล่หลังไฟดับครั้งแรกเท่านั้น ตอนทดสอบด้วยมือจะไม่มีวันเจอ
ส่วนระดับเครื่องมีตัวที่ต้อง enable กลับมาสามสี่ตัว ที่ควรจดไว้คือ worker ของคิวงาน เพราะมันมีอาร์กิวเมนต์ที่คิดมาแล้วไม่ใช่ค่าปริยาย
# /etc/systemd/system/app-queue.service — สาระของ ExecStart
ExecStart=/usr/bin/php /opt/appsrv/app/artisan queue:work redis \
--sleep=3 --tries=3 --timeout=240 --max-jobs=200 --max-time=3600
User=appsrv
Restart=always
--max-time=3600 สั่งให้ worker ตายเองทุกชั่วโมงแล้วให้ systemd ปลุกใหม่ กันหน่วยความจำรั่วสะสม จุดที่ต้องรู้คู่กันคือ healthcheck ต้องรู้เรื่องนี้ด้วย ไม่งั้นมันจะไปเจอจังหวะที่ service กำลังรีสตาร์ตพอดีแล้วรายงานว่าล่ม ของเครื่องนี้แก้ด้วยการให้เช็กซ้ำอีกรอบหลังจากนั้นแปดวินาทีก่อนจะตัดสิน
cron สามบรรทัดที่ไม่มีใครนึกถึง
กองที่สามเล็กที่สุดแต่ลืมง่ายที่สุด crontab -l ของ user คนเดียว สามบรรทัด และ root ไม่มี cron เลย ถ้าไม่จดไว้ วันกู้เสร็จเว็บจะขึ้นสวยงามและทุกอย่างจะดูปกติ จนกระทั่งอีกสามวันมีคนถามว่าทำไมไม่มี backup
0 2 * * * /opt/appsrv/app/scripts/backup-all.sh >> /opt/appsrv/.private/backups/backup.log 2>&1
*/5 * * * * /opt/appsrv/app/scripts/healthcheck.sh >> /opt/appsrv/.private/backups/health.log 2>&1
* * * * * cd /opt/appsrv/app && /usr/bin/php artisan schedule:run >> /dev/null 2>&1
ไฟล์ crontab จริงอยู่ใน /var/spool/cron/crontabs/ ซึ่งเป็นของ root และไม่มีทางติดมากับ tar ที่วิ่งอยู่ในโฟลเดอร์โปรเจกต์ วิธีที่ง่ายที่สุดคือให้ backup-all.sh ยิง crontab -l ลงไฟล์ในโฟลเดอร์ความลับก่อน tar ทุกครั้ง จะได้ติดขึ้นไปกับก้อนที่สามเอง
เรื่องเดียวกันนี้ใช้กับรายการ service ด้วยครับ ผมเพิ่ม systemctl list-unit-files --state=enabled กับ systemctl --user list-unit-files --state=enabled ลงไฟล์เดียวกันนั้น มันเป็นข้อความไม่กี่กิโลไบต์ แต่ตอนกู้มันคือความต่างระหว่างรู้กับเดา
ทางเข้าจากภายนอก และข้อห้ามที่ต้องเขียนตัวโต
กองที่สี่คือทางเข้า เครื่องนี้ไม่มีไอพีสาธารณะของตัวเอง ทุกอย่างวิ่งออกผ่าน tunnel เส้นเดียวแล้วให้ผู้ให้บริการที่ขอบเป็นคนรับหน้า ค่า ingress ของ tunnel นี้เป็นแบบจัดการจากส่วนกลาง แปลว่าตัว daemon บนเครื่องดึง config เองตอนสตาร์ต ไม่ได้อ่านจากไฟล์ในเครื่อง
# หน้าตาของ ingress (แก้ผ่าน API หรือหน้าเว็บ ไม่ได้อยู่บนเครื่อง)
app.example.com -> https://localhost:443
hook.example.com -> http://localhost:8787
ssh.example.com -> ssh://localhost:2244
(catch-all) -> http_status:404
ข้อดีของแบบนี้คือกู้เครื่องใหม่แล้วไม่ต้องแก้ ingress อะไรเลย แค่เอา credential ของ tunnel เดิมมาวางก็ต่อกลับได้เหมือนเดิม แต่ข้อดีข้อนี้แหละที่กลายเป็นกับดักใหญ่ที่สุดของการกู้
ถ้ากู้เครื่องใหม่ขึ้นมาแล้วเครื่องเก่ายังหายใจอยู่ และทั้งคู่ถือ credential ของ tunnel เส้นเดียวกัน ปลายทางจะเห็นการเชื่อมต่อสองเส้นแล้วกระจาย request สลับไปมาระหว่างสองเครื่อง ผลคือผู้ใช้บางคนเข้าเว็บเจอข้อมูลเก่า บางคนเจอข้อมูลใหม่ สลับกันไปแบบไม่มีรูปแบบ และ log ทั้งสองฝั่งจะดูปกติดีทุกบรรทัด ปิดของเก่าให้สนิทก่อนเปิดของใหม่เสมอ
ส่วน sshd ตั้งไว้พอร์ตอื่นและปิดรหัสผ่านทั้งหมด อันนี้ต้องจดเพราะถ้ากู้แล้วลืม จะเข้าเครื่องไม่ได้จนกว่าจะไปนั่งหน้าจอเอง
# /etc/ssh/sshd_config — สาระ 4 บรรทัด
Port 2244
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
rclone.conf กับความลับที่อยู่นอกโฟลเดอร์โปรเจกต์
เรื่องนี้ผมเจอตอนกำลังจะปิดงานพอดี ตัว backup เก็บโฟลเดอร์ความลับของโปรเจกต์กับไฟล์ .env ครบ แต่ config ของ rclone ที่ใช้อัปของขึ้นที่เก็บนอกเครื่อง มันอยู่ที่ ~/.config/rclone/rclone.conf ของ user ไม่ได้อยู่ในโฟลเดอร์โปรเจกต์
แปลว่าถ้าเครื่องหายไปจริง ๆ เราจะกู้ทุกอย่างกลับมาได้ แล้วพบว่าตัวที่จะเอาของไปเก็บรอบหน้ากลับไม่มี config ให้ใช้ ต้องไปหยิบ token มาแปลงเป็น S3 credential ใหม่เอง ซึ่งเป็นขั้นตอนที่จำยากพอสมควร
# ต้องทำเองหลังกู้ — ไฟล์นี้ไม่มีใน backup
mkdir -p ~/.config/rclone
cat > ~/.config/rclone/rclone.conf <<'EOF'
[r2]
type = s3
provider = Cloudflare
access_key_id = <TOKEN_ID>
secret_access_key = <SHA256_OF_TOKEN>
endpoint = https://<ACCOUNT_ID>.r2.cloudflarestorage.com
no_check_bucket = true
EOF
chmod 600 ~/.config/rclone/rclone.conf
# และอย่าใช้ rclone ของ apt — เก่าเกินไป ปลายทางจะฟ้อง 501
/usr/local/bin/rclone version | head -n1
ทางแก้ระยะยาวที่ผมทำคือให้สคริปต์ backup ก็อป rclone.conf เข้าโฟลเดอร์ความลับก่อน tar ทุกครั้ง มันจะได้ติดขึ้นไปเองในก้อนที่สาม ใครกังวลเรื่องความลับซ้อนความลับก็ตามสะดวกครับ แต่ก้อนนั้นมี .env อยู่แล้วซึ่งมีของหนักกว่านั้นเยอะ
ลำดับกู้จริง และบรรทัดที่ห้ามพิมพ์
ทีนี้เอามาเรียงเป็นลำดับ ข้อสำคัญคือกู้ความลับก่อนสร้างของ regenerable เสมอ เพราะเครื่องมือหลายตัวอ่าน .env ตอนทำงาน
- ลง OS แล้วสร้าง user ให้ uid ตรงกับของเดิม ปิดโหมดหลับ ตั้งเวลาให้ถูก
- ลง stack ให้ครบ — เว็บเซิร์ฟเวอร์ ฐานข้อมูล cache php ตามเวอร์ชันจริง แล้วเปิด module ที่ต้องใช้
- กู้โค้ดจาก tar ก้อนที่สอง วาง bare repo กลับที่เดิมแล้ว clone ออกมาเป็น working tree
- กู้ความลับจาก tar ก้อนที่สาม แล้วไล่ตั้งสิทธิ์ไฟล์กลับให้เหมือนเดิมทุกไฟล์
- สร้างฐานข้อมูลกับ user แล้ว import dump ก้อนแรก อ่านรหัสจาก .env ที่เพิ่งกู้มา
- สร้างของที่สร้างใหม่ได้ — composer, npm build, storage link, ล้าง cache ทุกชนิด
- วาง vhost ทำใบรับรอง เปิด site แล้ว reload เว็บเซิร์ฟเวอร์
- วาง unit ทั้งสองระดับ enable ให้ครบ แล้วสั่ง enable-linger ให้ user
- ใส่ cron สามบรรทัด ตั้ง sshd แล้วต่อ tunnel กลับ — หลังจากยืนยันว่าเครื่องเก่าดับสนิทแล้ว
ในขั้นที่หกมีบรรทัดหนึ่งที่ห้ามพิมพ์เด็ดขาด และเป็นบรรทัดที่มือมันจะพิมพ์เองตามความเคยชิน
คีย์ของแอปที่อยู่ใน .env คือกุญแจถอดข้อมูลที่เข้ารหัสไว้ทั้งหมด ทั้ง session ทั้งค่าในตารางที่เก็บแบบ encrypted ถ้าสั่งสร้างคีย์ใหม่ทับ ข้อมูลเก่าที่กู้มาจะถอดไม่ออกทั้งหมดถาวร ทั้งที่ dump ยังอยู่ครบทุกไบต์ นี่คือกรณีที่ backup สมบูรณ์แต่ข้อมูลตายจากคำสั่งเดียว
cd /opt/appsrv/app
composer install --no-dev -o
npm install && npm run build
php artisan config:clear && php artisan route:clear && php artisan storage:link
# php artisan key:generate <- ห้าม ถ้ากู้ .env เดิมมาแล้ว
ตรวจว่าสำเร็จ ด้วยคำสั่งที่ให้คำตอบเป็นตัวเลข
ผมไม่ชอบวิธี "เปิดเบราว์เซอร์ดูแล้วรู้สึกว่าปกติ" เพราะมันปิดบังปัญหาที่โผล่ทีหลังได้ง่าย ผมเลยตั้งชุดตรวจที่ให้คำตอบเป็นตัวเลขล้วน ๆ สี่บรรทัด
# 1) แอปตอบจากในเครื่องเอง ตัดตัวแปรเรื่องเน็ตกับ tunnel ออกก่อน
curl -sk -o /dev/null -w '%{http_code}\n' -H 'Host: app.example.com' https://127.0.0.1/login
# ต้องได้ 200
# 2) แล้วค่อยตรวจจากข้างนอกจริง ๆ ผ่าน tunnel
curl -s -o /dev/null -w '%{http_code}\n' https://app.example.com/
# ต้องได้ 302
# 3) ชุดเฝ้าระวังของเครื่องเองต้องผ่านทุกข้อ
/opt/appsrv/app/scripts/healthcheck.sh
# 4) ที่เก็บนอกเครื่องยังคุยได้ (ข้อนี้คนลืมบ่อยสุด)
/usr/local/bin/rclone lsd r2:app-backup
เอกสารเก่าในเครื่องคือของอันตราย
ปิดท้ายด้วยเรื่องที่ผมคิดว่าสำคัญที่สุดในงานรอบนี้ ตอนเริ่มผมตั้งใจจะอ่านเอกสารเก่าที่อยู่ในเครื่องแล้วเอามาปรับ ปรากฏว่าอ่านไปสิบบรรทัดก็ต้องหยุด เพราะตัวเลขทุกตัวในนั้นผิดหมด จำนวน route ที่จดไว้ต่างจากของจริงเป็นเท่าตัว จำนวน migration ก็ไม่ตรง รายการโมดูลขาดไปเกือบครึ่ง และมีหัวข้อที่เขียนว่า "ยังทำไม่เสร็จ" ทั้งที่ทำเสร็จไปนานแล้ว
ส่วนไฟล์ที่ชื่อว่าเป็นคู่มือตั้งเครื่องใหม่ก็ใช้ไม่ได้ ไม่ใช่เพราะผิด แต่เพราะไม่พอ มันครอบคลุมแค่ขั้น clone โค้ดกับติดตั้ง dependency ซึ่งเป็นส่วนที่ง่ายที่สุด ไม่มีสักบรรทัดที่พูดถึง vhost, unit, linger, cron, ingress หรือ sshd เลย
ส่วนตัวผมเลยตั้งกฎกับตัวเองว่า เอกสารกู้ระบบต้องเก็บสดจากเครื่องจริงเท่านั้น ห้ามลอกจากเอกสารเก่าแม้แต่บรรทัดเดียว ถ้าจะเขียนว่าฐานข้อมูลชื่ออะไรก็ต้อง SHOW DATABASES ให้เห็นกับตา ถ้าจะเขียนว่ามี service อะไรบ้างก็ต้อง list-unit-files ออกมาแปะ เอกสารที่เขียนจากความจำจะเน่าเงียบ ๆ ทุกครั้งที่มีคนแก้ระบบแล้วไม่ได้กลับมาแก้เอกสาร ซึ่งก็คือทุกครั้งนั่นแหละ
วิธีที่ถูกกว่าคือทำให้มันเก็บเองอัตโนมัติครับ เขียนสคริปต์สั้น ๆ ที่ยิงคำสั่งสำรวจทั้งชุดลงไฟล์เดียว แล้วให้ cron รันสัปดาห์ละครั้งเก็บไว้ในโฟลเดอร์ที่ backup กวาดอยู่แล้ว
#!/bin/bash
# scripts/snapshot-machine-state.sh — รันสัปดาห์ละครั้ง เก็บลงที่ที่ backup กวาดถึง
out=/opt/appsrv/.private/machine-state.txt
{
echo "=== เก็บเมื่อ $(date -Is) ==="
echo "--- crontab ---"; crontab -l
echo "--- units (system) ---"; systemctl list-unit-files --state=enabled --no-pager
echo "--- units (user) ---"; systemctl --user list-unit-files --state=enabled --no-pager
echo "--- linger ---"; loginctl show-user "$USER" -p Linger
echo "--- vhost ---"; cat /etc/apache2/sites-enabled/*.conf
echo "--- php ---"; php -v | head -n1; php -m | tr '\n' ' '
echo "--- ports ---"; ss -tlnp 2>/dev/null
echo "--- versions ---"; mysql -V; /usr/local/bin/rclone version | head -n1
} > "$out"
chmod 600 "$out"
ไฟล์นี้ใหญ่ไม่ถึงร้อยกิโลไบต์ แต่มันคือความต่างระหว่างกู้เครื่องเสร็จในสองชั่วโมงกับนั่งเดาทั้งวัน ใครจะเพิ่มคำสั่งอะไรลงไปอีกก็ตามสะดวกครับ ของผมยังไม่ได้ใส่ส่วนของ firewall กับ sysctl ลงไป เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



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