รัน Migration ผ่านฉลุยตอนพัฒนา แต่ทำไมพอ Deploy อัตโนมัติเข้า Production ถึงพังได้ง่ายกว่าที่คิด

สรุปสั้น ๆ
GitHub Actions ผูกกับ Supabase ได้จริงทั้งการ deploy database migration ผ่าน `supabase db push` และ deploy Edge Functions ผ่าน `supabase functions deploy` แต่ migration ต่างจากการ deploy โค้ดทั่วไปตรงที่มันแก้ไขข้อมูลจริงและย้อนกลับไม่ได้เสมอไป จุดที่ต้องคุมให้ดีที่สุดคือแยก project staging กับ production ให้ชัดเจน และมีคนรีวิว migration ที่มีความเสี่ยงสูงก่อน apply เข้า production เสมอ ไม่ปล่อยให้ pipeline รันอัตโนมัติทั้งหมดโดยไม่มีจุดหยุด
ทีมที่เริ่มผูก Supabase เข้ากับ GitHub Actions ส่วนใหญ่โฟกัสอยู่แค่จุดเดียวคือ 'ทำให้ workflow รันผ่าน' เขียน step เรียก Supabase CLI แล้ว migration ก็ apply สำเร็จบนเครื่อง local หรือ staging ทุกครั้ง จึงมั่นใจว่าเอาไปใช้กับ production ได้เลยแบบเดียวกัน แต่ความมั่นใจแบบนี้เป็นจุดที่ทำให้หลายทีมเจอปัญหาหนักตอนขึ้นจริง
สาเหตุคือ migration ไม่เหมือนการ deploy โค้ดหน้าเว็บทั่วไปที่ผิดแล้ว rollback กลับเวอร์ชันก่อนหน้าได้ทันที migration คือคำสั่งที่แก้โครงสร้างและข้อมูลจริงในฐานข้อมูล ข้อมูลบน local ที่ใช้ทดสอบมักว่างเปล่าหรือมีน้อย ในขณะที่ production มีขนาดใหญ่กว่า มีความสัมพันธ์ระหว่างตารางที่ local ไม่เคยเจอ และมีทราฟฟิกที่กำลังอ่าน-เขียนอยู่ตลอดเวลาขณะที่ migration กำลังรัน
บทความนี้จะไล่ตั้งแต่พื้นฐานที่ต้องมีก่อนผูก Supabase เข้ากับ GitHub Actions ไปจนถึง workflow ตัวอย่าง การแยก environment ให้ปลอดภัย และจุดที่ต้องมีคนคุมเสมอก่อนปล่อยให้ pipeline รันอัตโนมัติเต็มรูปแบบ
สิ่งที่ต้องมีก่อนผูก Supabase เข้ากับ GitHub Actions
ก่อนเขียน workflow ได้เลย ต้องมีสามอย่างพร้อมก่อน คือ Supabase Access Token, Project Reference ID และ Database Password ทั้งสามตัวนี้ใช้แทนการ login ผ่านเบราว์เซอร์แบบที่ทำตอนใช้ CLI บนเครื่อง local เพราะ workflow บน GitHub Actions ไม่มีหน้าจอให้ล็อกอินแบบ interactive
Access Token สร้างได้จาก Supabase Dashboard ที่หน้า Account > Access Tokens ส่วน Project Reference ID ดูได้จาก Project Settings > General ของแต่ละโปรเจกต์ ส่วน Database Password คือรหัสที่ตั้งไว้ตอนสร้างโปรเจกต์ (หรือรีเซ็ตใหม่ได้ถ้าจำไม่ได้) ทั้งสามค่านี้ต้องเก็บเป็น GitHub Secrets ชื่อ SUPABASE_ACCESS_TOKEN, SUPABASE_PROJECT_ID และ SUPABASE_DB_PASSWORD ห้ามใส่ตรง ๆ ในไฟล์ YAML เด็ดขาด
แนวคิดเรื่องเก็บ secret แยกจากไฟล์ workflow นี้เหมือนกับที่ใช้ตอนผูก GitHub Actions Vercel เข้าด้วยกัน คือต้องสร้างไว้ล่วงหน้าใน repo settings แล้วอ้างอิงผ่าน `secrets.<ชื่อ>` เท่านั้น ต่างกันแค่ชื่อ secret และปลายทางที่ค่านั้นถูกส่งไป
Workflow ตัวอย่างสำหรับ Deploy Migration ผ่าน supabase db push
รูปแบบพื้นฐานที่ใช้กันคือ trigger workflow เมื่อ push หรือ merge เข้า branch ที่ผูกกับ environment เป้าหมาย แล้วให้ step ติดตั้ง Supabase CLI, เชื่อมต่อโปรเจกต์ด้วย `supabase link` แล้วค่อย push migration เข้าไปจริง:
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: supabase/setup-cli@v1
- run: supabase link --project-ref $SUPABASE_PROJECT_ID
env: { SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} }
- run: supabase db push
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
จุดที่ควรทำเพิ่มจากตัวอย่างนี้คือใส่ path filter ให้ job นี้ trigger เฉพาะตอนที่มีการเปลี่ยนแปลงในโฟลเดอร์ `supabase/migrations` เท่านั้น ไม่ใช่ทุกครั้งที่มี push เข้า main เพราะ migration ควรเป็นเหตุการณ์ที่เกิดเฉพาะตอนมีไฟล์ migration ใหม่จริง ๆ ไม่ใช่ทุกรอบ deploy โค้ดปกติ
Deploy Edge Functions อัตโนมัติแยกจาก Migration
Edge Functions ของ Supabase deploy ด้วยคำสั่งคนละตัวคือ `supabase functions deploy <ชื่อฟังก์ชัน> --project-ref $SUPABASE_PROJECT_ID` และควรแยก job ออกจาก job migration อย่างชัดเจน เพราะในทางปฏิบัติฟังก์ชันมักเปลี่ยนบ่อยกว่าโครงสร้างฐานข้อมูล การผูกไว้ job เดียวกันจะทำให้ทุกครั้งที่แก้ function ต้องรัน migration step ไปด้วยทั้งที่ไม่มี schema เปลี่ยนแปลงอะไรเลย
อีกจุดที่พลาดบ่อยคือลืมว่า environment variable ที่ function ใช้งานจริง (เช่น API key ของบริการภายนอกที่ function เรียก) ต้องตั้งแยกต่างหากผ่านคำสั่ง `supabase secrets set` ไม่ใช่ GitHub Secrets ตัวเดียวกับที่ workflow ใช้ ถ้าลืมตั้งจุดนี้ function จะ deploy สำเร็จแต่ทำงานผิดพลาดตอนถูกเรียกจริง เพราะหาค่า env ที่ต้องการไม่เจอ
แยก Staging กับ Production Project ให้ชัดก่อนอนุญาตให้ Actions รันอัตโนมัติ
ข้อที่สำคัญกว่าตัว workflow เองคือต้องมี Supabase project แยกกันจริงสองตัวสำหรับ staging และ production ไม่ใช่ใช้ project เดียวแล้วแยกแค่ schema หรือ branch ภายใน เพราะ Supabase project แต่ละตัวมี Project Reference ID ของตัวเอง การแยก project จริงทำให้ secret ที่ใช้ต่างกันไปตาม environment โดยธรรมชาติ ลดโอกาสที่ migration ทดสอบจะไปโดน production โดยไม่ตั้งใจ
รูปแบบ workflow ที่ใช้กันคือกำหนดเงื่อนไข branch เหมือนที่อธิบายไว้ในบทความ GitHub Actions Vercel คือ push เข้า branch develop ให้ trigger migration เข้า staging project ส่วน merge เข้า main เท่านั้นที่ trigger เข้า production project โดยแต่ละ environment มีชุด secret ของตัวเองแยกกันชัดเจน ไม่ใช้ secret ร่วมกันข้าม environment
ทำไม Migration ที่ผ่านตอน Dev ไม่ได้แปลว่าปลอดภัยตอน Production
เหตุผลเชิงเทคนิคที่ทำให้ migration พังตอน production ทั้งที่ผ่านตอน dev มีหลายชั้น ชั้นแรกคือขนาดข้อมูล คำสั่งอย่าง ALTER COLUMN หรือเพิ่ม NOT NULL constraint บนตารางที่มีข้อมูลไม่กี่แถวใน local จะรันเสร็จในเสี้ยววินาที แต่พอเจอตารางที่มีข้อมูลหลักแสนหรือหลักล้านแถวใน production คำสั่งเดียวกันอาจใช้เวลานานและ lock ตารางระหว่างที่รัน ทำให้ query อื่นที่กำลังทำงานอยู่ค้างไปด้วย
ชั้นที่สองคือลำดับความเข้ากันได้ของโค้ดกับ schema (backward compatibility) ถ้า migration ลบคอลัมน์ที่โค้ดเวอร์ชันเก่ายังใช้งานอยู่ระหว่างที่ deploy โค้ดใหม่ยังไม่เสร็จสมบูรณ์ทุก instance จะเกิดช่วงเวลาที่โค้ดเก่าพังเพราะหาคอลัมน์ที่ถูกลบไปแล้วไม่เจอ รูปแบบที่ปลอดภัยกว่าคือ pattern 'expand-contract' คือเพิ่มคอลัมน์ใหม่ก่อน ปรับโค้ดให้ใช้คอลัมน์ใหม่จนมั่นใจว่าเวอร์ชันเก่าไม่เหลือใช้งานแล้ว จากนั้นค่อยเขียน migration แยกอีกรอบเพื่อลบคอลัมน์เก่าทีหลัง ไม่รวบทำในขั้นตอนเดียว
ชั้นที่สามคือ Row Level Security (RLS) policy ที่กำหนดสิทธิ์การเข้าถึงข้อมูล การแก้ policy ผิดพลาดใน production ส่งผลกระทบทันทีต่อผู้ใช้จริงในแบบที่ local ไม่มีทางจำลองได้ครบ เพราะ local มักไม่มี role หรือ session จริงของผู้ใช้ปลายทางให้ทดสอบครบทุกเงื่อนไข
ปัญหาที่ซ่อนอยู่อีกชั้นคือความแตกต่างของเวอร์ชัน Postgres extension ระหว่าง local กับ production บางทีมติดตั้ง extension เวอร์ชันใหม่กว่าบนเครื่อง local โดยไม่รู้ตัว migration ที่เรียกใช้ฟังก์ชันจาก extension เวอร์ชันใหม่จึงรันผ่านฉลุยตอน dev แต่พอไปเจอ production ที่ยังอัปเดต extension ไม่ทัน คำสั่งเดียวกันกลับ error ทันที ทางแก้คือหมั่นเช็ค extension version ให้ตรงกันระหว่าง environment เป็นระยะ ไม่ใช่ปล่อยให้ต่างกันไปเรื่อย ๆ
Migration ที่พังกลางทาง Rollback คืนเองไม่ได้เหมือน Deploy โค้ด
ความเข้าใจผิดที่พบบ่อยคือคิดว่าถ้า migration พังก็แค่ rollback กลับเหมือนตอน revert deploy เว็บ แต่ Postgres ไม่มีกลไก rollback อัตโนมัติแบบนั้นสำหรับ migration ที่ apply สำเร็จไปบางส่วนแล้ว ถ้า migration ทำสำเร็จไปครึ่งทางแล้ว error กลางคัน ทางแก้ไม่ใช่ 'ย้อนกลับ' แต่ต้องเขียน migration ใหม่อีกไฟล์เพื่อแก้สถานะกลับ (forward-fix) ซึ่งต้องเข้าใจสถานะจริงของฐานข้อมูล ณ ตอนนั้นก่อนเขียน
แนวทางที่ลดความเสี่ยงได้จริงคือเขียน migration ให้ idempotent เท่าที่ทำได้ (รันซ้ำแล้วไม่พัง) และก่อนปล่อยให้ Actions apply migration เสี่ยงสูงเข้า production ควรทดสอบกับข้อมูลตัวอย่างที่มีลักษณะใกล้เคียงของจริง (sanitized snapshot ที่ลบข้อมูลอ่อนไหวออกแล้ว) ไม่ใช่ทดสอบกับข้อมูลสมมติที่แต่งขึ้นเองซึ่งไม่มีทางเจอ edge case แบบเดียวกับข้อมูลจริง
อีกแนวทางที่ทีมที่ต้องคุมความเสี่ยงสูงมักใช้คือแบ่ง migration ก้อนใหญ่ให้เป็นหลายไฟล์เล็ก ๆ เรียงลำดับกัน แทนที่จะเขียนทุกการเปลี่ยนแปลงไว้ในไฟล์เดียว เพราะถ้าไฟล์ใดไฟล์หนึ่งพังกลางทาง จะรู้ได้ชัดว่าพังที่ขั้นตอนไหน และเขียน forward-fix เฉพาะจุดนั้นได้ง่ายกว่าการต้องไล่หาว่าส่วนไหนของ migration ก้อนใหญ่ที่ apply สำเร็จไปแล้วบ้าง
ตารางแบ่งระดับความเสี่ยงของ Migration เพื่อกำหนดว่าต้องมีคนรีวิวก่อนไหม
ใช้ตารางนี้เป็นกรอบตัดสินใจว่า migration แบบไหนปล่อยให้ Actions apply อัตโนมัติได้เลย และแบบไหนต้องมีคนรีวิวก่อนเสมอ:
| ลักษณะ Migration | ความเสี่ยง | ควรมีคนรีวิวก่อน Apply ไหม |
|---|---|---|
| เพิ่มคอลัมน์ใหม่ที่เป็น nullable | ต่ำ | ปล่อยอัตโนมัติได้ |
| แก้ type ของคอลัมน์ที่มีข้อมูลอยู่แล้ว | สูง | ต้องมีคนรีวิวก่อนเสมอ |
| ลบคอลัมน์หรือลบตาราง | สูงมาก | ต้องมีคนอนุมัติ + สำรองข้อมูลก่อน |
| เพิ่ม index บนตารางขนาดใหญ่ | กลาง | ควรรันนอกช่วงทราฟฟิกสูง |
| แก้ไข RLS policy | สูง | ต้องมีคนรีวิว เพราะกระทบสิทธิ์เข้าถึงข้อมูล |
ทำแบบนี้แล้วพัง เพราะปล่อยให้สอง pipeline ไปแตะฐานข้อมูลพร้อมกัน
เคสที่เจอบ่อยคือนักพัฒนารัน `supabase db push` จากเครื่อง local ตรงเข้า production เพื่อความไว โดยไม่รู้ว่า Actions workflow ก็ตั้งให้รันอัตโนมัติจาก PR เดียวกันที่เพิ่ง merge พอดี ผลคือ migration ถูกพยายาม apply สองรอบซ้อนกัน บางครั้งชนกันตอน lock ตาราง หรือรันซ้ำจนเกิด error ที่ไม่มีใครในทีมทันสังเกตเพราะเกิดตอนกลางดึกที่ไม่มีคนเฝ้า
ทางแก้ที่ทีมส่วนใหญ่ใช้คือกำหนดกฎให้ชัดว่ามีทางเดียวเท่านั้นที่มีสิทธิ์รัน migration เข้า production จริง คือผ่าน Actions workflow เท่านั้น ส่วนเครื่อง local ให้เชื่อมได้แค่กับ staging project เพื่อทดสอบ ถ้าจำเป็นต้อง apply migration ฉุกเฉินจาก local เข้า production จริง ๆ ต้องมีขั้นตอนแจ้งทีมและปิด workflow อัตโนมัติชั่วคราวก่อนเสมอ ไม่ใช่ปล่อยให้สองทางทำงานพร้อมกันโดยไม่มีใครรู้
สรุป
ผูก Supabase เข้ากับ GitHub Actions ทำได้ไม่ยากในเชิงเทคนิค แค่ตั้ง secret สามตัวแล้วเรียก CLI ผ่าน workflow แต่สิ่งที่ทำให้หลายทีมเจอปัญหาไม่ใช่การเขียน YAML ผิด แต่คือการมองข้ามว่า migration เป็นคำสั่งที่แก้ข้อมูลจริงและย้อนกลับไม่ได้เสมอไป ต่างจากการ deploy โค้ดทั่วไปที่ผิดแล้วแค่ revert
หลักที่ใช้ตัดสินใจได้ตรงที่สุดคือแยกระดับความเสี่ยงของ migration แต่ละครั้งก่อนปล่อยให้อัตโนมัติ migration ที่เสี่ยงต่ำอย่างเพิ่มคอลัมน์ nullable ปล่อยผ่านได้เลย ส่วน migration ที่แก้โครงสร้างข้อมูลเดิมหรือกระทบสิทธิ์เข้าถึง ต้องมีคนรีวิวก่อนเสมอ ไม่ว่า pipeline จะดูน่าเชื่อถือแค่ไหนก็ตาม
- แยก staging กับ production project จริงสองตัว ไม่ใช้ project เดียวกันทดสอบ migration
- Migration พังกลางทางต้อง forward-fix ไม่มี rollback อัตโนมัติเหมือน deploy โค้ด
- Migration ที่แก้ schema เดิม/ลบข้อมูล/แก้ RLS policy ต้องมีคนรีวิวก่อน apply เสมอ
คำถามที่พบบ่อย
ต้องติดตั้ง Supabase CLI บนเครื่อง local ก่อนตั้ง Actions ไหม
ไม่จำเป็นต้องมีเพื่อให้ Actions ทำงาน แต่แนะนำให้มีไว้ทดสอบคำสั่งกับ staging project ก่อนเสมอ เพื่อให้เข้าใจพฤติกรรมของ migration ก่อนปล่อยให้ workflow รันจริง
SUPABASE_ACCESS_TOKEN กับ SUPABASE_DB_PASSWORD ใช้ต่างกันยังไง
Access Token ใช้ยืนยันตัวตนระดับบัญชีสำหรับคำสั่งที่เรียกผ่าน Supabase API เช่น link โปรเจกต์ ส่วน Database Password ใช้เชื่อมต่อฐานข้อมูล Postgres โดยตรงสำหรับคำสั่งที่ต้องแก้ schema จริง ทั้งสองตัวจำเป็นสำหรับ workflow migration
Migration ที่ apply แล้วพังกลางทาง กู้คืนยังไง
ไม่มี rollback อัตโนมัติ ต้องเขียน migration ใหม่แยกไฟล์เพื่อแก้สถานะกลับ (forward-fix) โดยอิงจากสถานะจริงของฐานข้อมูล ณ ตอนนั้น จึงควรมีการสำรองข้อมูลก่อน apply migration ที่มีความเสี่ยงสูงเสมอ
ต้องผูก Edge Functions กับ Migration ไว้ใน job เดียวกันไหม
ไม่จำเป็นและไม่แนะนำ ควรแยก job เพราะ Edge Functions มักเปลี่ยนบ่อยกว่า schema ฐานข้อมูล การผูกไว้ด้วยกันทำให้ทุกครั้งที่แก้ function ต้องรัน step migration ไปด้วยโดยไม่จำเป็น
ควรให้ Migration apply อัตโนมัติทุก PR หรือเฉพาะตอน merge เข้า main
ควร apply เฉพาะตอน merge เข้า branch ที่ผูกกับ environment เป้าหมายจริงเท่านั้น ไม่ควร apply จาก PR ที่ยังไม่ merge เพราะจะทำให้มี migration หลายเวอร์ชันพยายามแก้ฐานข้อมูลเดียวกันพร้อมกัน
ทีมขนาดเล็กจำเป็นต้องแยก Staging Project จริงจังไหม
แนะนำอย่างยิ่งแม้ทีมเล็ก เพราะ migration ที่ผิดพลาดกระทบข้อมูลจริงโดยตรงและย้อนกลับยากกว่าการ deploy โค้ดทั่วไปที่แค่ rollback เวอร์ชันก็จบ การมี staging project แยกช่วยลดความเสี่ยงนี้ได้มาก และช่วยให้ไฟล์ migration ที่แบ่งเป็นก้อนเล็ก ๆ ตรวจสอบย้อนหลังได้ง่ายกว่าด้วย
อ่านต่อแบบเจาะลึก
ทดลองวัดผลโฆษณาเข้า LINE ฟรี 14 วัน
สมัครแล้วสร้างโปรเจกต์ เชื่อม LINE ติดตั้ง Tracking แล้วเริ่มเห็น Funnel จาก Click ถึง Purchase ได้ทันที ไม่ต้องใช้บัตรเครดิต
เริ่มทดลองใช้ฟรี 14 วันบทความที่เกี่ยวข้อง

ทำไมทีมที่ให้ AI Agent เปิด Pull Request เองต้องตั้ง Branch Protection แน่นกว่าทีมทั่วไป

เขียนไฟล์ Instructions ระดับ Repo กับสั่งทุกครั้งในแชท AI ต่างกันตรงไหน
