Seluruh perjalanan section ini — konfigurasi produksi, environment variable, secrets, PHP-FPM, Docker, workflow, health check, migrasi database, hingga zero-downtime — bermuara pada satu momen: hari aplikasi pertama kali melayani pengguna sungguhan. Production readiness checklist adalah gerbang kualitas terakhir yang memastikan tidak ada aspek penting yang terlewat pada momen tersebut. Pada chapter penutup ini kita susun checklist per kategori dengan rujukan ke chapter pembahasnya, lalu melengkapi checklist tersebut dengan tiga alat kerja yang bisa dieksekusi: runbook pra-rilis, smoke test otomatis pasca-deploy, dan sanity check kapasitas.

Cara Menggunakan Checklist Ini #

Checklist yang hanya dibaca sekali tidak melindungi apa pun. Perlakukan setiap kategori berikut sebagai gerbang yang harus lulus sebelum tombol rilis ditekan: jalankan pemeriksaan berurutan, catat tanggal dan hasilnya, dan tetapkan pemilik untuk setiap item yang gagal. Di tim kecil, pemilik tersebut mungkin selalu orang yang sama — tidak masalah, yang penting keputusannya eksplisit, bukan “kira-kira sudah aman”. Checklist juga bukan dokumen sekali-jadi: setiap insiden produksi seharusnya melahirkan baris baru di kategori yang relevan.

Konfigurasi #

PemeriksaanKondisi lulusLihat
display_errors matiGalat tidak pernah tercetak ke layar penggunaProduction Configuration
Nilai per environment terpisahKonfigurasi dev/staging/produksi lewat environment variable, bukan edit kodeEnvironment Variables
Secrets di luar repositoriKredensial dari secret manager atau .env yang di-ignoreManaging Secrets
Timezone dan locale eksplisitTidak bergantung default serverProduction Configuration

Keamanan #

PemeriksaanKondisi lulusLihat
Escaping outputSemua output dinamis melewati htmlspecialchars()Output Escaping
Query berparameterTidak ada penyusunan SQL lewat penggabungan stringSQL Injection
Perlindungan CSRFForm yang mengubah state memakai tokenCross-Site Request Forgery
Security headers terpasangX-Frame-Options, Content-Security-Policy dikonfigurasiSecurity Headers
Dependensi bebas celahcomposer audit bersih di CIDependency Security

Performa #

PemeriksaanKondisi lulusLihat
Opcode cache aktifOPcache menyala dengan konfigurasi produksiOpcode Cache
Bebas query N+1Halaman utama tidak memicu ratusan queryQuery Optimization
Cache layer dipasangData yang jarang berubah dilayani dari cacheApplication Caching
Baseline performa tercatatAngka latensi normal diketahui sebagai pembandingBenchmarking

Observability #

PemeriksaanKondisi lulusLihat
Log terstrukturGalat masuk log dengan konteks yang bisa dicariStructured Logging
Error reporting produksiLevel tepat, detail teknis tidak bocor ke penggunaError Reporting Configuration
Metrik kunci terekamRPS, latensi, dan error rate terlihat di dashboardApplication Metrics
Jalur diagnosis siapRunbook insiden dan production diagnostics tersediaProduction Diagnostics

Deployment #

PemeriksaanKondisi lulusLihat
Workflow berulangDeploy dijalankan satu perintah dari CI, bukan manualDeployment Workflows
Health check hidup/healthz dipantau load balancerHealth Checks
Shutdown anggunSIGTERM tidak memutus request berjalanGraceful Shutdown
Strategi rilis dipilihSymlink/blue-green/canary diputuskan dan terdokumentasiZero-Downtime Deployment

Data #

PemeriksaanKondisi lulusLihat
Backup otomatis + restore terujiBackup rutin berjalan dan pernah benar-benar di-restoreDatabase Deployment
Migrasi kompatibelMengikuti pola expand-contract, bisa di-rollbackDatabase Deployment
Data uji dibersihkanAkun demo dan data dummy tidak ikut ke produksi—

Runbook Pra-Rilis #

Checklist menjawab “sudah siap atau belum”; runbook menjawab “apa langkah berikutnya”. Urutan berikut adalah kerangka yang bisa Anda sesuaikan, dengan asumsi strategi rilis dari chapter sebelumnya sudah dipilih:

  1. Bekukan perubahan — tidak ada commit baru yang masuk rilis setelah titik ini.
  2. Cadangkan database dan pastikan salinan tersebut pernah diuji restore-nya, bukan hanya berhasil dibuat.
  3. Deploy artefak ke lingkungan target tanpa mengaktifkannya — rilis duduk di direktori atau lingkungan idle.
  4. Jalankan tahap migrasi expand saja — penambahan struktur baru yang tidak merusak versi lama.
  5. Aktifkan rilis lewat mekanisme yang dipilih (symlink flip atau router flip).
  6. Jalankan smoke test otomatis — bagian berikutnya membangun alatnya.
  7. Pantau metrik 30–60 menit pertama dengan ambang rollback yang sudah tertulis sebelum rilis, bukan diputuskan saat panik.
  8. Jadwalkan tahap contract (pembersihan struktur lama) untuk rilis berikutnya, bukan hari ini.

Smoke Test Setelah Deploy #

Smoke test adalah pemeriksaan pasca-deploy yang menjawab satu pertanyaan: apakah fungsi paling vital aplikasi ini hidup? Bedakan smoke test dari test suite lengkap — smoke test sengaja hanya menyentuh beberapa endpoint kritis (health, beranda, satu endpoint bisnis, satu aset statis) dan harus selesai dalam hitungan detik. Alat berikut memodelkan runner-nya sebagai fungsi murni dengan respons yang disuntikkan, sehingga logika keputusannya teruji tanpa web server:

php
<?php
function cekEndpoint(array $target, ?array $respons): array
{
    if ($respons === null) {
        return ["path" => $target["path"], "lulus" => false, "alasan" => "tidak ada respons (timeout)"];
    }
    if ($respons["status"] !== 200) {
        return ["path" => $target["path"], "lulus" => false, "alasan" => "HTTP " . $respons["status"]];
    }
    if (!str_contains($respons["body"], $target["harusBerisi"])) {
        return ["path" => $target["path"], "lulus" => false, "alasan" => "penanda hilang: \"{$target['harusBerisi']}\""];
    }

    return ["path" => $target["path"], "lulus" => true, "alasan" => "ok"];
}

function jalankanSmokeTest(array $targetList, array $responsMap): bool
{
    $semuaLulus = true;

    foreach ($targetList as $target) {
        $hasil = cekEndpoint($target, $responsMap[$target["path"]] ?? null);
        printf("%s %-18s %s\n", $hasil["lulus"] ? "[LULUS]" : "[GAGAL]", $hasil["path"], $hasil["alasan"]);

        if (!$hasil["lulus"]) {
            $semuaLulus = false;
        }
    }

    return $semuaLulus;
}

$targetList = [
    ["path" => "/healthz", "harusBerisi" => "ok"],
    ["path" => "/", "harusBerisi" => "Toko Budi"],
    ["path" => "/api/produk", "harusBerisi" => "Kopi Gayo"],
    ["path" => "/static/logo.png", "harusBerisi" => "PNG"],
];

$responsMap = [
    "/healthz" => ["status" => 200, "body" => "{\"status\":\"ok\"}"],
    "/" => ["status" => 200, "body" => "<h1>Selamat datang di Toko Budi</h1>"],
    "/api/produk" => ["status" => 500, "body" => "Internal Server Error"],
    "/static/logo.png" => null,
];

$semuaLulus = jalankanSmokeTest($targetList, $responsMap);
echo $semuaLulus
    ? "SMOKE TEST LULUS - aman melanjutkan rilis\n"
    : "SMOKE TEST GAGAL - jalankan rollback sekarang\n";

Output yang Dihasilkan:

text
[LULUS] /healthz           ok
[LULUS] /                  ok
[GAGAL] /api/produk        HTTP 500
[GAGAL] /static/logo.png   tidak ada respons (timeout)
SMOKE TEST GAGAL - jalankan rollback sekarang

Bedah singkat: setiap target didefinisikan sebagai pasangan path dan harusBerisi — penanda teks yang wajib ada di body. Pemeriksaan dilakukan berlapis: respons null berarti timeout/koneksi gagal, kode selain 200 langsung gugur, dan body yang tidak memuat penanda dianggap “hidup tapi salah”. Skenario simulasi sengaja memuat dua kegagalan — endpoint API yang meledak dengan HTTP 500 dan aset statis yang tidak merespons — untuk membuktikan runner benar-benar menangkap masalah, bukan selalu menyapa lulus. Nilai kembalian bool membuat runner mudah disambungkan ke pipeline CI: exit code bukan nol berarti deploy otomatis di-rollback.

Untuk runner sungguhan, satu-satunya bagian yang berubah adalah sumber respons — HTTP call nyata menggantikan map simulasi:

php
<?php
function ambilUrl(string $url): ?array
{
    $context = stream_context_create([
        "http" => ["timeout" => 5, "ignore_errors" => true],
    ]);
    $body = @file_get_contents($url, false, $context);

    if ($body === false) {
        return null;
    }

    preg_match("#HTTP/\S+\s+(\d{3})#", $http_response_header[0] ?? "", $cocok);

    return ["status" => (int) ($cocok[1] ?? 0), "body" => $body];
}

// $responsMap = array_map(
//     fn(string $path) => ambilUrl("https://toko.example.com" . $path),
//     array_column($targetList, "path"),
// );

Opsi ignore_errors membuat file_get_contents() tetap mengembalikan body untuk respons 4xx/5xx alih-alih false, sehingga kode status bisa dibaca dari $http_response_header dan logika cekEndpoint() yang sudah teruji bekerja tanpa perubahan.

Sanity Check Beban Sebelum Membuka Pintu #

Checklist dan smoke test menjawab kebenaran fungsi; pertanyaan terakhir sebelum rilis adalah kebenaran kapasitas. Perhitungannya memakai hukum Little: jumlah worker yang sibuk kira-kira sama dengan laju request dikali latensi rata-rata. Bagi hasil tersebut dengan jumlah worker yang tersedia untuk mendapatkan utilisasi — angka di bawah 70% menyisakan ruang untuk lonjakan:

php
<?php
function cekKapasitas(array $profil): array
{
    $utilisasi = ($profil["rpsPuncak"] * $profil["latensiRataDetik"]) / $profil["worker"];

    return [
        "utilisasi" => round($utilisasi, 2),
        "aman" => $utilisasi < 0.7,
    ];
}

foreach ([
    "weekday pagi" => ["rpsPuncak" => 200.0, "latensiRataDetik" => 0.25, "worker" => 100],
    "flash sale" => ["rpsPuncak" => 320.0, "latensiRataDetik" => 0.25, "worker" => 100],
] as $nama => $profil) {
    $hasil = cekKapasitas($profil);
    printf(
        "%-15s utilisasi=%.2f -> %s\n",
        $nama,
        $hasil["utilisasi"],
        $hasil["aman"] ? "kapasitas cukup" : "RAWAN: tambah worker atau turunkan latensi"
    );
}

Output yang Dihasilkan:

text
weekday pagi    utilisasi=0.50 -> kapasitas cukup
flash sale      utilisasi=0.80 -> RAWAN: tambah worker atau turunkan latensi

Pada skenario weekday, 200 request per detik dengan latensi rata-rata 0,25 detik menempati 50 worker — tepat setengah dari 100 worker yang tersedia, utilisasi 0,50, masih aman. Skenario flash sale menaikkan beban menjadi 320 RPS sehingga utilisasi menyentuh 0,80: di atas ambang 70%, dan satu-satunya jalan keluarnya menambah worker atau menurunkan latensi (biasanya lewat cache). Angka latensi untuk perhitungan ini berasal dari baseline benchmark yang sudah Anda catat di kategori Performa — inilah contoh bagaimana baris checklist saling menopang.

Latihan Rollback Sebelum Dibutuhkan #

Rollback yang belum pernah dilatih adalah harapan, bukan rencana. Luangkan waktu sebelum hari rilis untuk menjalankan rollback penuh di staging: ukur berapa lama sampai versi lama kembali melayani, periksa apakah data yang ditulis versi baru tetap terbaca oleh versi lama (ujian sesungguhnya dari disiplin expand-contract), dan pastikan orang yang bertugas tahu perintah apa yang dijalankan. Catat angka waktunya — “rollback 90 detik” yang terukur memberi keberanian mengambil keputusan cepat saat insiden, sedangkan rollback yang diperkirakan “cepat kok” hampir selalu lambat.

Ringkasan #

Production readiness adalah sintesis dari seluruh disiplin section ini: konfigurasi dan keamanan yang benar, performa yang terukur, observability yang bisa menjawab pertanyaan, deployment yang berulang dan aman, serta data yang selalu bisa dikembalikan. Checklist menjadikannya gerbang yang eksplisit, runbook menjadikannya urutan yang bisa diikuti, smoke test menjadikannya verifikasi otomatis, dan latihan rollback menjadikannya jaring pengaman yang teruji. Dengan gerbang ini terlewati, aplikasi PHP Anda siap menghadapi pengguna sungguhan — dan saat aplikasi tumbuh besar, struktur internalnya sendiri akan menjadi tantangan berikutnya; di situlah Section Framework Development mengambil alih.

Referensi #