Open source

laravel-cloudflare ล้าง cache ตั้ง DNS และ WAF ของ Cloudflare ด้วยคำสั่ง artisan

แพ็กเกจที่รวมงาน Cloudflare ที่แอป Laravel ต้องทำตอน deploy ไว้ในคำสั่ง artisan และแตะเฉพาะของที่แอปตัวเองประกาศไว้ จึงใช้ได้อย่างปลอดภัยใน zone ที่มีหลายแอปอยู่ด้วยกัน

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

ปัญหาที่เจอ

แอปของผมหลายตัวอยู่ใต้โดเมนเดียวกัน คือ Cloudflare zone เดียว และวิ่งผ่าน cloudflared tunnel ตัวเดียวกัน งานที่ต้องทำกับ Cloudflare ซ้ำ ๆ มีอยู่ไม่กี่อย่าง

  • หลัง deploy ต้องล้าง cache ของไฟล์ CSS/JS ชุดใหม่ ถ้ากด Purge Everything ก็จะล้าง cache ของแอปอื่นใน zone ไปด้วย
  • ขึ้นแอปใหม่ต้องเพิ่ม CNAME ชี้ไปที่ tunnel
  • อยากให้หน้า /admin มี challenge กันบอทที่ edge ก่อนถึงเซิร์ฟเวอร์

ทั้งหมดทำใน dashboard ได้ แต่ต้องจำขั้นตอนเอง และไม่มีอะไรบันทึกไว้ใน repo ของแอป เลยรวมไว้เป็นแพ็กเกจ ให้แต่ละแอปประกาศของที่ตัวเองเป็นเจ้าของไว้ใน config/cloudflare.php แล้วสั่งด้วย artisan

หลายแอปใน zone เดียว แต่ละแอปแตะเฉพาะของตัวเอง

แต่ละแอปสั่งได้เฉพาะของที่อยู่ใน config ของตัวเอง

ติดตั้ง

composer require phattarachai/laravel-cloudflare
php artisan vendor:publish --tag=cloudflare-config

ใส่ zone id ใน .env และใส่ tunnel id ด้วยถ้าจะใช้คำสั่ง DNS ทั้งสองค่าเป็นตัวระบุสาธารณะ ไม่ใช่ความลับ commit ไว้ได้

CLOUDFLARE_ZONE_ID=...
CLOUDFLARE_TUNNEL_ID=...

API token

CLOUDFLARE_API_TOKEN เป็นความลับ ไม่ต้องใส่ใน repo ของแอป แพ็กเกจจะอ่าน config ก่อน ถ้าไม่เจอจะอ่านจาก environment ของ process ตอนรัน ถ้าใช้ self-hosted runner ใส่ token ไว้ใน .env ของ runner ครั้งเดียว ทุกแอปที่ deploy ผ่าน runner นั้นก็ใช้ได้

ที่ต้องอ่านจาก environment ตอนรัน เพราะขั้นตอน deploy มักรัน php artisan optimize ก่อน ถ้าตอนนั้นยังไม่มี token ใน .env ของแอป config ที่ cache ไว้จะได้ค่า null

สร้าง token ใน Cloudflare ให้มีสิทธิ์เท่าที่ใช้

คำสั่ง สิทธิ์ที่ต้องมี
cloudflare:purge Zone → Cache Purge
cloudflare:dev-mode Zone → Zone Settings Edit
cloudflare:dns Zone → DNS Edit
cloudflare:waf Zone → Zone WAF Edit

ถ้าไม่มี token หรือไม่มี zone id ทุกคำสั่งจะแค่เตือนแล้วจบด้วย exit 0 โดยไม่เรียก API บนเครื่อง dev ใน test และใน repo ที่ยังไม่ได้ตั้งค่า CI จึงยังผ่านตามปกติ

ล้าง cache หลัง deploy

php artisan cloudflare:purge

คำสั่งนี้อ่าน Vite manifest (public/build/manifest.json) แล้วล้าง cache ทีละ URL ทั้งหน้า / และไฟล์ JS/CSS ทุกตัวที่ build ออกมา โดยใช้ host จาก APP_URL (หรือ CLOUDFLARE_HOST) ไม่ใช้ Purge Everything จึงไม่กระทบแอปอื่นใน zone ถ้า URL เกิน 100 ตัว ซึ่งเป็นเพดานต่อ request ของแพลน Free, Pro และ Business จะแบ่งส่งเป็นหลาย batch

ใส่เป็นขั้นตอนสุดท้ายของ deploy

- name: Purge Cloudflare cache
  run: php artisan cloudflare:purge || true

ที่ต่อท้ายด้วย || true ก็เพื่อให้ deploy ไม่ถูกนับว่าล้มเหลวเมื่อ Cloudflare มีปัญหาชั่วคราว เพราะถึงขั้นนี้แอปขึ้นไปแล้ว

ถ้ามีหน้าอื่นที่ต้องล้างด้วย ให้ระบุ URL เพิ่ม ใส่ --url ซ้ำได้หลายครั้ง

php artisan cloudflare:purge --url=https://example.app/sitemap.xml

ถ้าจำเป็นต้องล้างทั้ง zone จริง ๆ ต้องใส่ --force คู่กับ --everything เสมอ ถ้าไม่ใส่ คำสั่งจะถามยืนยันก่อน

php artisan cloudflare:purge --everything --force

ล้างทั้ง host ของแอป

ถ้า cache rule ของ zone เก็บหน้า HTML ไว้ด้วย การล้างแค่ / กับ assets จะไม่พอ ตั้งแต่ v2.0 สั่งล้างทุก URL ที่ cache ไว้ของ host แอปนี้ได้ใน request เดียว โดยไม่แตะแอปอื่นใน zone

php artisan cloudflare:purge --host

ถ้าอยากให้ deploy ใช้แบบนี้ทุกครั้ง ตั้งใน .env แทน คำสั่ง cloudflare:purge ใน CI ก็ไม่ต้องแก้

CLOUDFLARE_PURGE_MODE=host

การล้างตาม hostname เคยใช้ได้เฉพาะแพลน Enterprise แต่ตั้งแต่ 1 เม.ย. 2025 Cloudflare เปิดการล้างทุกแบบให้ทุกแพลน รวมถึงแพลน Free ซึ่งล้างได้ 5 request ต่อนาทีต่อบัญชี (รายละเอียดและ rate limit, การล้างตาม hostname) deploy หนึ่งครั้งใช้แค่ request เดียว

Development Mode

ตอนไล่แก้ปัญหาบนเครื่องจริง บางครั้งต้องการให้ request วิ่งตรงถึงเซิร์ฟเวอร์โดยไม่ผ่าน cache ของ edge Development Mode ทำแบบนั้นได้ประมาณ 3 ชั่วโมง แล้วปิดเอง

php artisan cloudflare:dev-mode        # ดูสถานะ และเวลาที่เหลือ
php artisan cloudflare:dev-mode on
php artisan cloudflare:dev-mode off

DNS สำหรับแอปใหม่บน tunnel

คำสั่ง DNS ทำงานแบบ upsert คือสร้างใหม่ถ้ายังไม่มี และอัปเดตถ้ามีอยู่แล้ว (เทียบจากชื่อและ type) ไม่เคยลบ record ที่ไม่ได้ประกาศไว้

php artisan cloudflare:dns --name=new-app.example.app --tunnel

--tunnel จะสร้าง CNAME แบบ proxied ชี้ไปที่ <CLOUDFLARE_TUNNEL_ID>.cfargotunnel.com ถ้าอยากให้ record อยู่กับแอป ก็ประกาศไว้ใน config แล้วรันคำสั่งเปล่า ๆ

'dns' => [
    'tunnel_id' => env('CLOUDFLARE_TUNNEL_ID'),

    'records' => [
        ['name' => 'new-app.example.app', 'tunnel' => true],
    ],
],
php artisan cloudflare:dns          # upsert ทุก record ที่ประกาศไว้
php artisan cloudflare:dns --list   # ดู record ทั้ง zone

record แบบอื่นก็สร้างได้ เช่น TXT แบบไม่ผ่าน proxy

php artisan cloudflare:dns --name=example.app --type=TXT --content='v=spf1 -all' --dns-only

การลบต้องระบุชื่อ และต้องใส่ --type ถ้าไม่ใช่ CNAME เพราะค่าเริ่มต้นของ type คือ CNAME

php artisan cloudflare:dns --name=old.example.app --type=TXT --delete --force

WAF: challenge หน้า /admin

กรณีที่ใช้บ่อยที่สุดคือใส่ Managed Challenge ให้ /admin บอทและสคริปต์ที่ยิงหน้า login จะเจอ challenge ที่ edge ส่วนคนที่ใช้เบราว์เซอร์จริงแทบไม่รู้ตัวว่าผ่าน challenge ไป

'waf' => [
    'rules' => [
        ['tag' => 'admin-challenge', 'paths' => ['/admin']],
    ],
],
php artisan cloudflare:waf          # sync กฎที่ประกาศไว้
php artisan cloudflare:waf --list   # ดูกฎ custom ทั้ง zone
php artisan cloudflare:waf --remove=admin-challenge --force

ถ้าประกาศแค่ paths แพ็กเกจจะสร้าง expression ให้ และผูกกับ host ของแอปนี้ให้อัตโนมัติ กฎแต่ละตัวมี tag ซึ่งจะเก็บไว้ในคำอธิบายของกฎเป็น [admin-challenge] ตอน sync แพ็กเกจหากฎจาก tag นี้ แล้วอัปเดตเฉพาะกฎของตัวเอง

ถ้าแอปเดียวกันมีหลาย environment ใน zone เดียว เช่น prod กับ qas ให้เขียน expression เองแล้วใส่ host ให้ครบ ทุก environment จะ upsert กฎตัวเดียวกัน แทนที่จะแย่ง tag เดียวกันไปมา

['tag' => 'admin-challenge', 'expression' => '(starts_with(http.request.uri.path, "/admin")) and (http.host in {"app.example.app" "app-qas.example.app"})'],

วิธีเช็กว่ากฎทำงานแล้ว

curl -sI https://app.example.app/admin | grep -i cf-mitigated

ถ้าได้ cf-mitigated: challenge กับสถานะ 403 แปลว่ากฎทำงาน (curl ผ่าน JavaScript challenge ไม่ได้ จึงได้ผลนี้ทุกครั้ง) ส่วนในเบราว์เซอร์ หลังผ่าน challenge แล้วจะไม่มี header นี้ และจะเห็น cookie cf_clearance ใน DevTools → Application → Cookies

ข้อควรรู้ก่อนใช้

  • ค่าเริ่มต้นของ purge ล้างแค่หน้า / กับไฟล์ใน build/ ถ้า cache rule ของ zone เก็บหน้า HTML อื่นไว้ด้วย ให้ใช้ --host หรือส่ง --url เพิ่ม
  • ถ้าบาง batch ล้มเหลว batch ที่ส่งไปก่อนหน้าถูกล้างไปแล้ว คำสั่งจะจบด้วย error และบอกว่า batch ไหนล้มเหลว
  • host มาจาก APP_URL ถ้าแอปเสิร์ฟหลายโดเมน ตั้ง CLOUDFLARE_HOST ให้ตรงกับโดเมนที่ต้องการ ถ้าไม่มีทั้งสองค่า purge จะจบด้วย error
  • ถ้าอ่านข้อมูลจาก Cloudflare ไม่สำเร็จ คำสั่งจะไม่เขียนอะไรเลย เช่น cloudflare:waf อ่าน ruleset ไม่ได้เพราะ API ตอบ 5xx หรือ token ไม่มีสิทธิ์ จะจบด้วย error ว่าไม่มีอะไรเปลี่ยน ส่วน --tunnel ที่ยังไม่ได้ตั้ง CLOUDFLARE_TUNNEL_ID ก็จะหยุดก่อนสร้าง record

เทียบกับ CLI ของ Cloudflare

Cloudflare มี CLI ใหม่ชื่อ cf ซึ่งเปิด beta เมื่อ 28 ก.ย. 2026 และครอบคลุม API ของ Cloudflare ทั้งหมด ถ้าต้องจัดการเรื่องที่แพ็กเกจนี้ไม่มี หรือทำงานที่ทำครั้งเดียว cf หรือ dashboard ตอบโจทย์กว่า

แพ็กเกจนี้เหมาะกว่าตอนที่งานผูกอยู่กับแอป Laravel

  • รู้จักแอป อ่าน Vite manifest และ host จาก APP_URL เอง ไม่ต้องเขียนสคริปต์หา URL ของ assets
  • config อยู่ใน repo ของแอป DNS และกฎ WAF ที่แอปต้องมี ถูก review และ deploy ไปพร้อมกับโค้ด
  • ปลอดภัยใน zone ที่ใช้ร่วมกัน ทุกคำสั่งเป็น upsert ที่แตะเฉพาะของที่ประกาศไว้ และเมื่อไม่ได้ตั้งค่าไว้ก็จะไม่ทำอะไรเลย
  • ใช้แค่ PHP runner ที่ deploy แอป Laravel มี PHP อยู่แล้ว ไม่ต้องติดตั้ง Node หรือ binary เพิ่ม

เวอร์ชันที่รองรับ

  • PHP 8.3 ขึ้นไป
  • Laravel 12 และ 13

แอปที่ยังใช้ Laravel 11 หรือ PHP 8.2 ใช้เวอร์ชัน 1.x (phattarachai/laravel-cloudflare:^1.1) ได้ แต่ --host และฟีเจอร์ในโพสต์นี้บางส่วนมีเฉพาะใน v2

GitHub: phattarachai/laravel-cloudflare

อ่านต่อ

ล่าสุด

ดูทั้งหมด →