Stateless Laravel Core
On this page
- Sekilas
- Gagasannya secara singkat
- A. Node tak memegang apa pun
- B. Inti mendelegasikan
- C. Tak ada yang gagal diam-diam
- Kenapa saya memilihnya
- Kesepakatan saya
- A. Apa pun yang bisa memblokir dilempar ke job
- B. File tak pernah menyentuh node
- C. State tinggal di Redis dan database
- D. Satu jenis token, siapa pun provider-nya
- E. Master data di-cache, dan hanya di tempat yang sepadan
- F. Runtime yang cepat, di-build dengan cara yang sama setiap kali
- G. Satu container, diawasi
- H. Health yang berkata jujur
- I. Log dikirim ke collector, bukan ke file
- J. Identitas client hanya dari trusted proxy
- K. Halaman depan dirender di server, aplikasinya terpisah
- L. Konfigurasi yang selamat dari cache dan upgrade
- M. Test yang tak butuh infrastruktur
- N. Setiap dependensi di balik satu pintu
- Cara saya memeriksa service Laravel
- Di mana saya tidak memakainya
- Ketika pekerjaan itu milik saya sendiri
- Ketika seluruh tim menguasai bahasa compiled
- Rujukan
- Istilah
Stateless Laravel Core adalah cara saya memakai Laravel ketika ia menjadi jantung platform yang dibangun satu tim: aplikasi yang memegang user, organisasi, dan catatan, sementara service khusus mengerjakan pekerjaan berat di sekelilingnya. Laravel sendiri — diciptakan Taylor Otwell dan dibentuk komunitasnya — sudah punya struktur yang baku, dan saya mengikutinya. Artikel ini tentang keputusan yang saya tambahkan di atasnya, supaya inti itu bisa disalin ke berapa pun node, tak pernah membuat user menunggu pekerjaan yang lambat, dan selalu bisa diamati.
Inilah salah satu arsitektur yang saya pakai, dan saya memilihnya demi orang sebanyak demi kode. Bila tim yang akan merawat intinya, saya ingin inti itu ditulis dalam bahasa dan framework yang sudah dikenal sebagian besar dari mereka.
Sekilas
| Asal | Framework Laravel, dengan kesepakatan saya sendiri untuk menjalankannya sebagai inti yang stateless |
|---|---|
| Saya pakai untuk | Inti platform yang dirawat tim: banyak domain, banyak user, banyak integrasi |
| Tak saya pakai untuk | Pekerjaan yang sepenuhnya dalam kendali saya sendiri, atau tim yang semuanya fasih bahasa compiled |
| Bahasa | PHP, dengan Laravel |
Gagasannya secara singkat
A. Node tak memegang apa pun
Satu salinan aplikasi yang sedang berjalan tak menyimpan file, session, penghitung, maupun cache miliknya sendiri. Semua yang harus bertahan melewati satu request tinggal di database, di Redis, atau di object storage, jadi node bisa diganti, digandakan, atau dimatikan kapan saja tanpa kehilangan apa pun.
B. Inti mendelegasikan
Pekerjaan yang lambat, khusus, atau milik disiplin lain — penandatanganan, render, storage, sertifikat — menjadi milik service khusus atau job di latar belakang. Inti memutuskan dan mencatat; ia tak mengerjakan semuanya sendiri.
C. Tak ada yang gagal diam-diam
Setiap request, setiap job, dan setiap panggilan ke service lain meninggalkan jejak di satu jalur telemetry, dan inti dirancang supaya masalah muncul di sana sebelum user harus melaporkannya.
Kenapa saya memilihnya
Tim lebih dulu. Inti yang akan dibaca dan diubah banyak orang sepatutnya ditulis dalam bahasa yang sudah dipakai sebagian besar dari mereka. PHP adalah bahasa interpreter yang dikenal luas dan cepat diiterasi, dan Laravel memberi semua orang kesepakatan yang sama untuk routing, validasi, antrean, dan database, sehingga anggota tim baru bisa menemukan jalannya tanpa saya.
Framework-nya sudah baku, jadi tenaga saya tercurah untuk menjalankannya dengan baik. Saya tak merancang ulang struktur Laravel. Yang menentukan apakah aplikasi Laravel tetap sehat di production sebagian besar berada di luar struktur itu: di mana state tinggal, apa yang terjadi pada pekerjaan yang lambat, bagaimana ia di-build dan di-deploy, dan bagaimana ia bisa diamati. Itulah keputusan yang saya ambil dengan sengaja.
Inti yang stateless bertambah besar dengan cara disalin. Bila node tak memegang apa pun, menambah kapasitas berarti menyalakan satu lagi, dan crash tak lebih dari sekadar restart. Sifat itu lebih berharga bagi saya daripada optimasi apa pun di dalam satu proses.
Kesepakatan saya
A. Apa pun yang bisa memblokir dilempar ke job
Mengirim email, menandatangani dokumen, membuat thumbnail, dan memanggil balik sistem lain semuanya berjalan sebagai job di antrean. Request yang memulai pekerjaan panjang langsung dijawab dengan status yang menyatakan pekerjaan sedang berjalan, terpisah dari status yang menyatakan pekerjaan selesai. Job menerima identitas, bukan record utuh, lalu memuat data segar saat dijalankan; rahasia dienkripsi sebelum masuk antrean; dan data yang dibutuhkan job divalidasi sebelum job dimasukkan ke antrean, jadi job tak pernah berangkat dari masukan yang buruk. Tugas sampingan yang gagal, seperti webhook ke client, tak pernah membatalkan pekerjaan utamanya.
B. File tak pernah menyentuh node
Aplikasi tak menulis file ke disk lokal dan tak membawa client S3. Semua storage lewat service storage terpisah, jadi aplikasi tak memegang kredensial storage dan pergantian vendor storage tak sampai menyentuhnya. Browser meng-upload langsung ke storage lewat link berumur pendek, sehingga byte file tak pernah melewati PHP. Upload mendarat di area sementara dan baru dipromosikan ketika menjadi bagian dari satu record, sementara storage sendiri membuang apa pun yang tak pernah dipromosikan. Sebelum apa pun dihapus, satu tempat di kode menjawab apakah objek itu masih dirujuk.
C. State tinggal di Redis dan database
Access token berupa JWT stateless yang ditandatangani private key. Refresh token tinggal di Redis, dirotasi setiap kali dipakai, dan dibawa dalam cookie httpOnly; token yang dicabut dan perangkat yang sedang login juga dicatat di sana. Penghitung rate limit disimpan di Redis, bukan di database, dan sertifikat atau key set yang diambil dari service lain di-cache di Redis, bukan disimpan sebagai file, jadi tak ada node yang butuh apa pun di disknya sendiri.
D. Satu jenis token, siapa pun provider-nya
User bisa login lewat identity provider eksternal, tapi inti tak pernah membiarkan token mereka masuk ke bagian sistem lainnya. Ia menuntaskan login eksternal, memvalidasi identity token provider terhadap key yang diterbitkan provider itu, lalu menerbitkan tokennya sendiri dalam format yang sama dengan setiap login lain. Akun ditautkan lewat subject identifier milik provider, tak pernah lewat alamat e-mail yang belum terverifikasi. Inti menerbitkan signing key-nya sendiri, sehingga service lain bisa memverifikasi tokennya tanpa memanggilnya.
E. Master data di-cache, dan hanya di tempat yang sepadan
Data referensi yang terus dibaca dan jarang diubah di-cache di Redis, diperbarui ketika record disimpan atau dihapus, dan dibaca kembali dengan semua konversi tipenya utuh. Bila Redis tak tersedia, kode kembali ke database dan tak gagal. Tak semuanya di-cache: satu data set besar yang pernah saya cache ternyata hanya dibaca saat suntingan administratif yang jarang, jadi cache-nya saya cabut lagi. Saya mengukur sebelum meng-cache.
F. Runtime yang cepat, di-build dengan cara yang sama setiap kali
Aplikasi berjalan di FrankenPHP dalam worker mode, yang menjaga framework tetap ter-boot di antara request, dengan web server bawaannya melayani HTTP biasa di belakang reverse proxy yang mengakhiri TLS. Setelan PHP yang penting, seperti batas upload, ditetapkan eksplisit, karena image runtime tanpa setelannya sendiri diam-diam kembali ke nilai bawaan yang jauh terlalu kecil. Image di-build bertahap, dengan dependensi di-cache di layer tersendiri dan kode aplikasi disalin paling akhir; satu image di-build sekali dari tag rilis, hanya setelah test lolos, dan image yang sama itulah yang dipromosikan dari environment ke environment.
G. Satu container, diawasi
Tiap container menjalankan web server dan sekumpulan worker antrean berdampingan di bawah process supervisor, sehingga satu image adalah seluruh aplikasi dan menambah salinannya menambah keduanya. Worker didaur ulang setelah sejumlah job, sejumlah waktu, atau batas memori, yang mencegah proses PHP berumur panjang membengkak perlahan. Saat start, container menghangatkan cache konfigurasi, route, dan view, tapi tak pernah menjalankan migration database: dua replika yang start bersamaan akan saling berlomba, jadi migration adalah langkah deployment tersendiri.
[program:web]
command=frankenphp run --config /etc/caddy/Caddyfile
autorestart=true
[program:queue]
command=php artisan queue:work --tries=3 --max-jobs=100 --max-time=3600 --memory=512
numprocs=8
process_name=%(program_name)s_%(process_num)02d
user=www-data
autorestart=true
stopasgroup=true
killasgroup=trueH. Health yang berkata jujur
Health diperiksa di dua tingkat. Di dalam image, satu pemeriksaan memastikan supervisor, web server, dan sedikitnya satu worker sedang berjalan. Di atasnya, endpoint readiness gabungan memeriksa database, Redis, storage, dan setiap service yang dibutuhkan inti, melaporkan masing-masing secara terpisah, dan menjawab 503 bila ada yang mati. Endpoint itu dibebaskan dari rate limit, supaya pemeriksaan milik platform sendiri tak pernah menguncinya. Otomasi yang men-deploy atau me-restart inti hanya bisa dipercaya bila jawaban ini bisa dipercaya.
I. Log dikirim ke collector, bukan ke file
Aplikasi tak mengandalkan file log Laravel di disk lokal. Log, trace, dan metrics dikirim ke collector OpenTelemetry:
- setiap request mendapat id UUIDv7 yang terurut waktu, dan catatan log-nya dikumpulkan selama request lalu dikirim setelah respons, sehingga telemetry tak pernah menunda user;
- logger milik Laravel sendiri dijembatani ke jalur yang sama, dengan penjaga terhadap rekursi ketika collector-nya sendiri tak terjangkau;
- buffer di-flush ketika job selesai, gagal, atau worker-nya berhenti, karena log job yang dimatikan paksa justru yang paling dibutuhkan;
- exception dicatat di satu tempat, dan pesannya tak pernah sampai ke client;
- route API selalu menjawab dalam JSON, tak pernah dengan redirect, dan kegagalan validasi dicatat menurut nama field-nya saja, tak pernah nilainya;
- rahasia diredaksi di satu tempat, event audit ditulis sekali sebagai log record, dan metrics memakai delta temporality;
- trace satu request berlanjut ke setiap service yang dipanggilnya dan ke job yang dimasukkannya ke antrean.
Aturan di bawahnya sederhana: telemetry gagal terbuka, keamanan gagal tertutup. Collector yang rusak tak boleh merusak request; pemeriksaan yang meragukan tak boleh meloloskannya.
J. Identitas client hanya dari trusted proxy
Alamat client diambil dari header forwarded hanya bila request datang dari proxy di daftar rentang alamat yang eksplisit, tak pernah dari proxy mana saja. Rantai forwarded mentah disimpan sebagai bukti forensik tapi tak pernah dipakai untuk memutuskan apa pun, dan nama atribut yang sama dipakai setiap service, jadi satu query bisa mengikuti satu client melintasi semuanya.
K. Halaman depan dirender di server, aplikasinya terpisah
Halaman beranda publik dirender di server, supaya mesin pencari bisa mengindeksnya tanpa menjalankan JavaScript. Aplikasi yang sungguh dipakai orang bekerja adalah single-page application di repository-nya sendiri, dengan siklus build dan rilisnya sendiri, yang membuat keduanya lebih mudah dirawat dan membiarkan antarmukanya berkembang tanpa menyentuh inti.
L. Konfigurasi yang selamat dari cache dan upgrade
Kode aplikasi membaca setelan lewat file konfigurasi, tak pernah langsung dari environment, karena begitu konfigurasi di-cache, pembacaan environment langsung tak mengembalikan apa pun. Tiap dependensi eksternal punya file konfigurasinya sendiri, dengan komentar yang menjelaskan kenapa tiap nilai seperti itu. Blok konfigurasi bawaan Laravel dibiarkan seperti yang dikirim framework, bahkan yang tak dipakai project, supaya upgrade framework tetap mudah. File environment di-mount saat container start dan tak pernah dibakar ke dalam image.
M. Test yang tak butuh infrastruktur
Test berjalan dengan database in-memory, cache dan rate limiter in-memory, antrean sinkron, serta telemetry dan pemeriksaan eksternal yang dimatikan. Test tak butuh server database, Redis, maupun storage, selesai dalam hitungan detik ketimbang menunggu timeout jaringan, dan tak bisa membocorkan state dari satu test ke test berikutnya.
N. Setiap dependensi di balik satu pintu
Tiap service eksternal dijangkau lewat satu class miliknya sendiri, dengan konfigurasi dan timeout-nya sendiri, dan kegagalannya diterjemahkan menjadi keluarga kecil exception — tak ditemukan, request tak sah, tak tersedia — yang bisa ditindaklanjuti bagian inti lainnya. Bila satu dependensi tak punya fitur yang dibutuhkan inti, saya memintanya ke hulu, tak menambalnya di sisi lokal.
| Urusan | Tempatnya |
|---|---|
| Pekerjaan lambat atau eksternal | job di antrean |
| File | object storage, lewat service storage dan link berumur pendek |
| Session, token, penghitung, cache | Redis |
| Record dan data lookup | database relasional |
| Log, trace, metrics, audit | collector OpenTelemetry |
| Perubahan skema | langkah deployment, tak pernah saat container start |
Cara saya memeriksa service Laravel
| Bidang | Pertanyaan saya | Sumber |
|---|---|---|
| State | Bisakah node ini dihapus sekarang juga tanpa kehilangan apa pun? | node tak memegang apa pun |
| File | Adakah kode yang menulis ke disk lokal atau memegang kredensial storage? | file tak pernah menyentuh node |
| Kerja yang memblokir | Adakah request yang menunggu email, penandatanganan, render, atau panggilan ke sistem lain? | apa pun yang bisa memblokir dilempar ke job |
| Antrean | Apakah job membawa identitas, dan rahasia hanya dalam bentuk terenkripsi? | apa pun yang bisa memblokir dilempar ke job |
| Token | Apakah setiap login berakhir dengan token milik inti sendiri? | satu jenis token |
| Runtime | Apakah setelan runtime eksplisit, dan image di-build sekali lalu dipromosikan? | runtime yang cepat |
| Start | Apakah container start tanpa menjalankan migration? | satu container, diawasi |
| Health | Apakah readiness memeriksa setiap dependensi dan menjawab 503 dengan jujur? | health yang berkata jujur |
| Telemetry | Bisakah kegagalan di job atau service lain ditemukan tanpa login ke server? | log dikirim ke collector |
| Konfigurasi | Adakah kode aplikasi yang membaca environment secara langsung? | konfigurasi yang selamat dari cache |
Di mana saya tidak memakainya
Ketika pekerjaan itu milik saya sendiri
Laravel adalah pilihan yang dibuat untuk tim. Ketika satu pekerjaan sepenuhnya dalam kendali saya, tanpa orang lain yang harus membaca atau mengubahnya, saya bebas memilih bahasa compiled dan struktur yang cocok dengan masalahnya, bukan dengan timnya.
Ketika seluruh tim menguasai bahasa compiled
Bila setiap orang yang akan merawat satu service memahami Rust, Go, atau bahasa compiled lain beserta konteks yang dibawanya, saya membangunnya dalam bahasa itu. Bila satu saja anggota tim belum menguasainya, intinya tetap di Laravel, karena codebase yang hanya bisa diubah sebagian tim adalah codebase yang tak benar-benar dimiliki tim itu. Service yang saya bangun dengan Rust mengikuti salah satu bentuk ini:
Rujukan
Dokumentasi Laravel, untuk struktur framework itu sendiri, antrean, caching, dan caching konfigurasi. Adam Wiggins, The Twelve-Factor App (2011): proses yang stateless, konfigurasi di environment, log sebagai aliran event, proses yang bisa dibuang, dan satu build yang dipromosikan melewati setiap environment adalah kerabat dekat kesepakatan di atas. Saya menyebutnya sebagai kemiripan, bukan sebagai sumber; kesepakatan di sini tumbuh dari menjalankan satu platform di production.
Istilah
- Stateless node
- Salinan aplikasi yang berjalan tanpa menyimpan apa pun di antara request, sehingga bisa diganti atau digandakan dengan bebas.
- Job di antrean
- Satuan pekerjaan yang dicatat di antrean dan dikerjakan worker di latar belakang, tak dikerjakan selagi user menunggu.
- Worker mode
- Cara menjalankan PHP di mana aplikasi di-boot sekali lalu melayani banyak request, tak di-boot ulang untuk tiap request.
- Process supervisor
- Program yang menjalankan beberapa proses dalam satu container, menyalakannya kembali saat berhenti, dan menghentikannya bersama-sama.
- Gagal terbuka, gagal tertutup
- Apakah satu komponen membiarkan pekerjaan berlanjut (terbuka) atau menghentikannya (tertutup) ketika ia tak bisa menjalankan tugasnya sendiri.
Technologies covered
- PHP
- Laravel
- FrankenPHP