Open source

ให้ AI อ่านฐานข้อมูลบน production อย่างปลอดภัยใน Laravel ทำยังไง

laravel-db-tunnel เปิด SSH tunnel ไปฐานข้อมูล production, UAT หรือ QAS ให้เองตอนแอปเรียกใช้ connection แต่ละโปรเจกต์ในเครื่องได้ port ไม่ชนกัน และ Laravel Boost ใช้ query ด้วย role อ่านอย่างเดียวได้ทันที

เผยแพร่
เวลาอ่าน
6 นาที

เปิด tunnel เองใน terminal ก็ใช้ได้ จนมีหลายโปรเจกต์

เวลาต้องดูข้อมูลในฐานข้อมูล production ทางที่ใช้กันมาตลอดคือเปิด phpMyAdmin บนเซิร์ฟเวอร์ หรือตั้ง TablePlus กับ DBeaver ให้ต่อผ่าน SSH ถ้าอยากให้แอปในเครื่องต่อได้ด้วย ก็เปิด tunnel เองใน terminal

ssh -N -L 5433:127.0.0.1:5432 prod

คำสั่งนี้ส่งต่อ port 5433 ในเครื่องเราไปที่ port 5432 บนเซิร์ฟเวอร์ ผ่านการเชื่อมต่อ ssh ที่เรามีอยู่แล้ว เรียกกันว่า SSH tunnel ข้อดีคือ port ฐานข้อมูลปิดไว้ได้ ไม่ต้องเปิดออกอินเทอร์เน็ต ใช้ได้ดีนะ แต่พอมีหลายโปรเจกต์จะเจอเรื่องเดิม ๆ

  • จำไม่ได้ว่าโปรเจกต์ไหนใช้ port อะไร สองโปรเจกต์ตั้ง 5433 เหมือนกัน หรือไปชนกับ Postgres ในเครื่อง
  • tunnel หลุดไปเงียบ ๆ แล้ว query ขึ้น Connection refused
  • ต้องเปิด terminal ค้างไว้ก่อนทุกครั้ง

เรื่องสุดท้ายหนักขึ้นเมื่อให้ AI ช่วยไล่ปัญหา Laravel Boost ให้ Claude Code มี tool ชื่อ database-query ไว้ query ฐานข้อมูลของโปรเจกต์เอง แต่ถ้า tunnel ยังไม่เปิด query แรกก็ต่อไม่ได้แล้ว

laravel-db-tunnel เปิด tunnel ให้ตอนแอปเรียกใช้ connection

แพ็กเกจนี้ผูก tunnel ไว้กับ connection ใน config/database.php พอแอปเรียกใช้ connection ครั้งแรก แพ็กเกจเปิด tunnel ให้ แล้วรอจน port พร้อมก่อนส่ง query ไป

flowchart LR
  A[Claude Code] -->|database-query| B[Laravel Boost]
  B --> C["claude-prod<br>127.0.0.1:15441"]
  C -->|ssh -L| D["เซิร์ฟเวอร์ prod<br>127.0.0.1:5432"]
  D --> E[("role อ่านอย่างเดียว")]

Claude Code query ผ่าน connection ในเครื่อง แพ็กเกจส่งต่อไปที่ฐานข้อมูลบนเซิร์ฟเวอร์ผ่าน ssh

เวลามีปัญหาที่เกิดกับข้อมูลจริง เราจึงให้ Claude Code ไล่ query บน production ได้เลย ไม่ต้องดึงฐานข้อมูลทั้งก้อนลงมา ถ้าต้องการทั้งก้อนจริง ๆ ใช้ db-snapshot-sync-laravel แทน

แพ็กเกจรองรับ PHP 8.3 ขึ้นไป กับ Laravel 12 และ 13 ใช้ได้บน macOS, Linux และ WSL2 ที่มี OpenSSH และต้อง ssh เข้าเซิร์ฟเวอร์ด้วย key ได้อยู่แล้ว เพราะ tunnel รันแบบ BatchMode=yes ไม่ถามรหัสผ่าน

ติดตั้งแล้วเพิ่ม connection แรก

ติดตั้งเป็น dev dependency เพราะใช้เฉพาะในเครื่องนักพัฒนา

composer require --dev phattarachai/laravel-db-tunnel
php artisan vendor:publish --tag=db-tunnel-config

ถ้าโปรเจกต์ใช้ Laravel Boost อยู่ ให้รัน php artisan boost:update แล้วเลือกแพ็กเกจนี้ Claude Code จะได้ skill ที่บอกวิธีใช้ tunnel และวิธีแก้เมื่อ query ไม่ผ่าน

ทีนี้เพิ่ม connection ที่ชี้ไป 127.0.0.1 แล้วบอกแพ็กเกจว่า connection นี้ต้องผ่าน ssh alias ไหน ใช้ชื่อ connection เดียวกันทั้งสองไฟล์

'claude-prod' => [
    'driver' => 'pgsql',
    'host' => '127.0.0.1',
    'port' => env('PROD_DB_PORT'),
    'database' => env('PROD_DB_DATABASE'),
    'username' => env('PROD_DB_USERNAME', 'claude-prod'),
    'password' => env('PROD_DB_PASSWORD'),
    'sslmode' => 'prefer',
],

port กับ password ไม่มีค่าเริ่มต้น เพราะ port ต่างกันไปในแต่ละเครื่อง ส่วนรหัสผ่านควรอยู่ใน .env เท่านั้น จากนั้นให้แพ็กเกจเลือก port ให้

php artisan db:tunnel install claude-prod
INFO  claude-prod: 127.0.0.1:15441 → prod → 127.0.0.1:5432 (PROD_DB_PORT=15441 in .env).

แค่นี้ DB::connection('claude-prod'), php artisan db:show --database=claude-prod หรือ database-query ของ Boost ก็เปิด tunnel เองตอนใช้ครั้งแรก ดูสถานะทุก tunnel ได้ด้วย php artisan db:tunnel status

+-------------+-----------------+--------------------------+----------------------+
| Connection  | Local           | Remote                   | State                |
+-------------+-----------------+--------------------------+----------------------+
| claude-prod | 127.0.0.1:15441 | prod → 127.0.0.1:5432    | open (ssh pid 48213) |
| claude-uat  | 127.0.0.1:15442 | uat → 127.0.0.1:5432     | closed               |
+-------------+-----------------+--------------------------+----------------------+

role บนเซิร์ฟเวอร์ต้อง SELECT ได้อย่างเดียว

connection claude-* มีไว้ให้ AI อ่านข้อมูล จึงควรใช้ role ที่ SELECT ได้อย่างเดียว tunnel ส่ง query เข้ามาทาง loopback ของเซิร์ฟเวอร์เอง role จึงจำกัดไว้ที่ 127.0.0.1 ได้

CREATE ROLE "claude-prod" LOGIN PASSWORD '<generated>';
GRANT CONNECT ON DATABASE app TO "claude-prod";
GRANT USAGE ON SCHEMA public TO "claude-prod";
GRANT SELECT ON ALL TABLES IN SCHEMA public TO "claude-prod";
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO "claude-prod";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "claude-prod";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON SEQUENCES TO "claude-prod";

บรรทัด ALTER DEFAULT PRIVILEGES ของ PostgreSQL ทำให้ตารางที่ migration สร้างทีหลังอ่านได้ด้วย ไม่ต้องกลับมา GRANT ใหม่ทุกครั้ง

port ในเครื่องไม่ชนกันข้ามโปรเจกต์

db:tunnel install เลือก port ครั้งเดียวแล้วใช้ค่าเดิมตลอด แพ็กเกจถือว่า port ไม่ว่างถ้ามีอย่างใดอย่างหนึ่งต่อไปนี้ใช้อยู่

  • โปรเจกต์อื่นในเครื่องเดียวกัน ซึ่งแพ็กเกจจดไว้ใน ~/.config/db-tunnel/ports.json
  • LocalForward ใน ~/.ssh/config รวมไฟล์ที่ Include เข้ามาด้วย
  • process ที่กำลัง listen port นั้นอยู่

ถ้าโปรเจกต์นี้เคยได้ port ไว้แล้ว install ใช้ port เดิม ถ้ายังไม่เคย แต่ port ใน .env อยู่ในช่วงที่กำหนด (ค่าเริ่มต้น 15440–15999) และว่างอยู่ ก็ใช้ port นั้น นอกนั้นจะหยิบ port แรกที่ว่างในช่วง ส่วน .env.example ได้ PROD_DB_PORT= แบบว่าง ๆ เพราะแต่ละเครื่องเลือก port ของตัวเอง

port อยู่ใน config/database.php ที่เดียว เพราะ Boost อ่าน config ครั้งเดียวตอนเริ่ม ถ้า install เปลี่ยน port ต้อง reconnect Boost ใน Claude Code คำสั่งจะเตือนให้

เมื่อไหร่ที่ tunnel เปิดไม่ขึ้นเพราะ port ชน ให้รัน doctor คำสั่งนี้ไล่ดู port ของทุกโปรเจกต์ในเครื่องเทียบกับ ~/.ssh/config และ process ที่ listen อยู่ แล้วบอกว่าชนกับอะไร --prune ลบ port ของโปรเจกต์ที่ลบไปแล้วออกจาก registry

php artisan db:tunnel doctor

สถานะ tunnel ดูจาก process ที่ listen อยู่

แพ็กเกจเช็กว่า tunnel เปิดอยู่ไหมจาก process ที่ listen port นั้น ผ่าน lsof บน macOS หรือ ss บน Linux และ WSL2 ไม่ได้ลองต่อ port ด้วย nc -z เพราะบน WSL2 nc -z บอกว่า port เปิดทั้งที่ไม่มีอะไร listen อยู่ วิธีนี้แยกได้สามกรณี

  • open: process ssh ไปที่ alias ที่ถูกต้องกำลัง listen อยู่
  • closed: ไม่มีอะไร listen
  • port held by …: มี process อื่นใช้ port อยู่ เช่น Postgres ในเครื่อง

db:tunnel close ปิดเฉพาะ process ssh ของ tunnel นั้น ไม่ไปปิดอย่างอื่นที่ใช้ port เดียวกัน ส่วนตอนเปิด แพ็กเกจใส่ ControlMaster=no กับ ControlPath=none ให้ tunnel มีการเชื่อมต่อของตัวเองเสมอ ถ้า alias ตั้ง ControlMaster auto ไว้ การเชื่อมต่อหลักที่เปิดค้างอยู่จะรับ forward ไปแทน แล้ว status ก็หา tunnel ไม่เจอ การ ssh เข้าเซิร์ฟเวอร์ตามปกติยังใช้ multiplexing ได้เหมือนเดิม

ถ้าเปิดไม่สำเร็จ แพ็กเกจโยน TunnelException ที่มีข้อความ error ของ ssh อยู่ข้างใน เช่น Permission denied (publickey) หรือ Could not resolve hostname เราจึงรู้เลยว่าต้องแก้ตรงไหน ถ้า alias ยังไม่มีใน ~/.ssh/config คำสั่ง open กับ install จะพิมพ์ Host block ให้ก๊อปไปใส่

ฐานข้อมูลหลักของแอปอยู่หลัง tunnel ก็ใช้ได้

บางโปรเจกต์ไม่มีฐานข้อมูลในเครื่องเลย ต้องต่อฐานข้อมูล dev บนเซิร์ฟเวอร์ผ่าน ssh ตลอด กรณีนี้ให้ตั้ง 'auto_open' => false ที่ tunnel นั้น แล้วให้ composer dev ดูแล tunnel ไว้แทน

"dev": [
    "Composer\\Config::disableProcessTimeout",
    "npx concurrently \"php artisan db:tunnel watch mysql\" \"php artisan db:tunnel wait mysql && php artisan serve\" \"php artisan db:tunnel wait mysql && php artisan queue:listen --tries=1\" --names=tunnel,server,queue --kill-others"
]

db:tunnel watch เปิด tunnel ค้างไว้ เมื่อหลุดก็ต่อใหม่ ถ้า ssh ค้างที่ขั้น handshake เกิน 90 วินาทีก็ตัดแล้วต่อใหม่เหมือนกัน ส่วน db:tunnel wait ทำให้ serve กับ queue รอจน tunnel พร้อมก่อนเริ่ม

เซิร์ฟเวอร์บน Google Cloud ที่ปิด port 22

ถ้าเซิร์ฟเวอร์อยู่บน Google Compute Engine และรับ SSH ผ่าน Identity-Aware Proxy (IAP) อย่างเดียว ให้บอกชื่อ instance แทน alias

'claude-qas' => [
    'remote_port' => 5432,
    'gcp_iap' => ['instance' => 'qas-db', 'project' => 'acme', 'zone' => 'asia-southeast1-b'],
],

แพ็กเกจใส่ ProxyCommand ที่เรียก gcloud compute start-iap-tunnel กับ key ของ gcloud ให้เอง จึงไม่ต้องเขียน Host block ใน ~/.ssh/config แต่ละเครื่องต้อง gcloud auth login ด้วยบัญชีที่มีสิทธิ์ IAP-secured Tunnel User และรัน gcloud compute ssh qas-db --tunnel-through-iap หนึ่งครั้ง เพื่อสร้าง key และส่งขึ้น instance db:tunnel doctor เช็กให้ทั้งสองอย่าง

ผลลัพธ์ของ query พาข้อมูลลูกค้าเข้าไปในแชตกับ AI ด้วย

role ที่ SELECT ได้อย่างเดียวกัน AI ไม่ให้แก้หรือลบข้อมูลบน production แต่ผลลัพธ์ของทุก query จะเข้าไปอยู่ในบทสนทนากับ AI ด้วย ถ้า query ดึงชื่อ เบอร์โทร หรือเลขบัตรประชาชนของลูกค้าออกมา ข้อมูลพวกนั้นก็ส่งออกไปนอกเซิร์ฟเวอร์ของเราแล้ว

  • ไล่ปัญหาบน UAT หรือ QAS ก่อน แล้วค่อยใช้ production เมื่อจำเป็นจริง ๆ
  • ถ้าตารางมีข้อมูลส่วนบุคคล ให้ role อ่านได้แค่บางคอลัมน์ (PostgreSQL กับ MySQL GRANT SELECT ระดับคอลัมน์ได้) หรือสร้าง view ที่ไม่มีคอลัมน์เหล่านั้นให้ role อ่านแทน
  • skill ที่มากับแพ็กเกจสั่ง Claude Code ให้จำกัดจำนวนแถวด้วย LIMIT หรือ WHERE ทุกครั้ง เน้น aggregate มากกว่าดึงทีละแถว และไม่ก๊อปข้อมูลส่วนบุคคลลงไฟล์หรือ commit แต่ skill เป็นแค่คำแนะนำสำหรับ AI สิทธิ์ของ role บนเซิร์ฟเวอร์เท่านั้นที่บังคับได้จริง

GitHub: phattarachai/laravel-db-tunnel

อ่านต่อ

ล่าสุด

ดูทั้งหมด →