k6 SLO Quality Gate untuk CI/CD Bitbucket Pipelines

Foto oleh ChrisO via Openverse (CC BY-SA)
SLO quality gate adalah langkah pipeline otomatis yang menjalankan uji performa singkat terhadap build yang baru saja di-deploy dan menggagalkan pipeline jika hasilnya melanggar service-level objective yang ditentukan, misalnya batas maksimum waktu respons p95 atau error rate. Alih-alih mengandalkan seseorang untuk mengingat pengujian performa secara manual, pipeline itu sendiri yang memblokir build buruk agar tidak dipromosikan lebih jauh. Ini mengubah pemeriksaan manual yang mudah terlewat menjadi langkah yang berjalan otomatis di setiap deploy.
Load test komparatif penuh bisa berjalan selama beberapa menit dengan banyak virtual user dan ditujukan untuk mengkarakterisasi performa di bawah tekanan, sehingga terlalu lambat dan terlalu berat sumber daya untuk dijalankan di setiap deploy. Smoke test dengan lima virtual user selama dua menit cukup cepat untuk masuk dalam siklus rilis normal, sekaligus tetap menghasilkan traffic yang cukup untuk menangkap endpoint yang jelas rusak atau regresi latensi besar. Metodologi load testing penuh tetap menjadi proses terpisah yang dipicu manual untuk analisis lebih dalam.
k6 mengevaluasi threshold yang dikonfigurasi di akhir run dan keluar dengan kode proses bukan nol jika ada threshold yang dilanggar. Bitbucket Pipelines memperlakukan exit code bukan nol sebagai langkah yang gagal dan menghentikan pipeline kustom di titik itu, yang berarti tahap manual Deploy to Production yang biasanya mengikuti tidak pernah tersedia. Build yang gagal terblokir dari production tanpa perlu ada orang yang menyadari regresinya secara manual.
k6 memungkinkan sebuah skrip mendefinisikan kriteria lolos dan gagal langsung di ekspor options-nya di bawah kunci thresholds, misalnya http_req_duration diatur gagal begitu persentil ke-95 mencapai nilai milidetik tertentu, dan metrik error rate kustom diatur gagal begitu mencapai persentase tertentu. k6 melacak setiap permintaan terhadap aturan ini sepanjang run dan menerapkan keputusannya secara otomatis saat pengujian selesai, tanpa perlu skrip assertion terpisah.
Pada setup ini, gate menambah sekitar dua menit ke setiap siklus deploy staging: sekitar tiga puluh detik menunggu layanan selesai startup, ditambah dua menit traffic smoke test. Itu biaya nyata yang berulang di setiap rilis, tetapi kecil dibandingkan biaya regresi performa yang sampai ke production tanpa terdeteksi, yang sebelumnya sepenuhnya bergantung pada seseorang yang ingat untuk menguji secara manual.

Foto oleh ChrisO via Openverse (CC BY-SA)
Ringkasan Utama
Tulisan ini menjelaskan cara mengubah alur rilis Bitbucket Pipelines yang sudah ada menjadi gate performa otomatis, cukup dengan menambahkan satu langkah k6 smoke test tepat setelah deploy ke staging. Langkah ini memeriksa ambang latensi p95 dan error rate, mengirim anotasi lolos atau gagal ke Grafana, dan otomatis memblokir tahap promosi ke production begitu regresi terdeteksi.
Selama pengerjaan skripsi saya di SGU bersama Commsult Indonesia untuk Ontego Traces, sebuah platform logistik Java Spring Boot yang berjalan di atas Docker Swarm, saya menemukan bahwa pipeline rilis sudah memiliki kerangka load testing, auto-scaler, dan stack observability yang lengkap, tetapi jalur sesungguhnya dari commit yang di-merge sampai ke deploy production tetap tidak memiliki validasi performa sama sekali. Query yang lambat atau regresi akibat lock contention bisa lolos code review, lolos test suite Java, dan mendarat di production, dan orang pertama yang menyadarinya adalah pelanggan yang mengeluh layar terasa lambat. Tulisan ini membahas pilar keempat dari proyek skripsi tersebut: mengubah alur stage-release Bitbucket Pipelines yang sudah ada menjadi sebuah gate otomatis yang menangkap regresi tersebut sebelum ada manusia yang sempat menekan tombol Deploy to Production.
Pipeline stage-release sebenarnya sudah melakukan banyak hal dengan benar. Ia membangun proyek Maven, mendorong image Docker yang sudah diberi versi ke registry, dan memakai Ansible untuk men-deploy image baru ke staging stack. Yang tidak dilakukannya adalah menanyakan satu pertanyaan paling penting sebelum promosi ke production: apakah build ini masih memenuhi target latensi dan error rate-nya? Validasi performa sepenuhnya manual. Seseorang harus ingat untuk membuka terminal, menjalankan skrip k6 secara manual, membaca angkanya, lalu menilai apakah hasilnya cukup baik. Pada praktiknya, langkah itu sering terlewat karena tekanan tenggat waktu, dan regresi yang menaikkan latensi p95 atau memunculkan endpoint yang gagal bisa sampai ke tahap manual Deploy to Production tanpa terdeteksi sama sekali. Pilar-pilar lain dari proyek skripsi ini, yaitu skrip load testing k6, auto-scaler berbasis CPU, dan stack observability Grafana, sudah ada dan berjalan baik masing-masing, tetapi tidak ada yang menghubungkannya ke satu momen ketika build yang buruk masih bisa dihentikan: celah antara deploy ke staging dan promosi ke production.
Daripada membangun pipeline pengujian performa terpisah yang juga harus diingat seseorang untuk dijalankan, saya menambahkan satu langkah ke pipeline kustom stage-release yang sudah ada, tepat setelah langkah Ansible deploy-to-staging dan tepat sebelum tahap manual Deploy to Production. Langkah baru ini berjalan segera setelah setiap deploy ke staging, tanpa trigger terpisah dan tanpa perlu klik tambahan dari siapa pun. Ia menjalankan k6 smoke test, lima virtual user selama dua menit, sengaja dibuat lebih ringan dibanding profil beban komparatif 10 VU selama 120 detik yang dipakai di sisi benchmarking skripsi, terhadap staging stack yang baru saja di-deploy, dan keluar dengan exit code bukan nol jika waktu respons atau error rate melewati ambang batas yang dikonfigurasi. Karena Bitbucket Pipelines menghentikan pipeline kustom pada langkah pertama yang gagal, exit code bukan nol di sini otomatis menonaktifkan tahap promosi production manual yang mengikutinya. Tidak ada lagi jalur bagi build yang gagal untuk sampai ke manusia yang berdiri di depan tombol Deploy to Production.
k6 menyediakan kriteria lolos dan gagal sebagai blok thresholds kelas satu di dalam ekspor options skrip itu sendiri, sehingga logika gate berada di file yang sama dengan profil beban, bukan skrip assertion terpisah yang ditempel belakangan. Gate ini memperlakukan dua kondisi sebagai kegagalan: persentil ke-95 dari http_req_duration mencapai atau melebihi 5000 milidetik, dan error rate permintaan mencapai atau melebihi 5 persen. Kedua angka itu sengaja dibuat longgar untuk lingkungan staging yang berjalan pada satu replika dengan cache yang masih dingin. Tujuan pada tahap ini adalah menangkap regresi yang jelas dan kerusakan total, bukan menegakkan anggaran latensi ketat yang nantinya dibutuhkan SLO production. k6 mengevaluasi setiap threshold secara otomatis di akhir run dan mengatur exit code proses miliknya sendiri sesuai hasil itu, dan itulah tepatnya sinyal yang dibutuhkan Bitbucket Pipelines untuk menggagalkan langkah tersebut dan menghentikan pipeline.
// k6/ci-smoke-test.js — thresholds live next to the load profile
export const options = {
stages: [
{ duration: "20s", target: 5 }, // ramp up
{ duration: "90s", target: 5 }, // steady load
{ duration: "10s", target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ["p(95)<5000"], // fail if p95 >= 5000ms
error_rate: ["rate<0.05"], // fail if error rate >= 5%
},
};
Sengaja buat threshold di CI lebih longgar daripada SLO production yang sebenarnya. Gate ini berjalan melawan staging stack satu replika dengan cache dingin, jadi target p95 setara production yang ketat akan gagal di hampir setiap deploy karena alasan yang tidak ada hubungannya dengan kode yang baru saja dikirim. Perketat threshold hanya setelah bottleneck di level aplikasi benar-benar diperbaiki, bukan sebelumnya.
Langkah ini berjalan di atas image Docker k6 resmi, jadi tidak perlu instalasi binary terpisah di runner. Ia menunggu sekitar tiga puluh detik agar layanan Spring Boot yang baru di-deploy selesai melakukan startup, lalu menjalankan skrip smoke test dan menyimpan exit code-nya ke sebuah variabel sebelum melakukan apa pun dengan nilai itu. Menyimpan exit code terlebih dahulu, alih-alih membiarkan perintah k6 menjadi baris terakhir langkah tersebut, penting karena itu memungkinkan langkah ini mengirim anotasi Grafana penutup dan tetap meneruskan hasil lolos atau gagal yang asli ke Bitbucket Pipelines sesudahnya.
# bitbucket-pipelines.yml — appended after "Deploy to Stage",
# directly before the manual "Deploy to Production" stage
- step:
name: "Performance Quality Gate (k6)"
image:
name: grafana/k6:0.49.0
script:
# Give the freshly deployed service time to finish startup
- sleep 30
# Mark the run boundary on the shared Grafana dashboard
- |
curl -s -X POST "${GRAFANA_URL}/api/annotations" \
-H "Authorization: Bearer ${GRAFANA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tags\":[\"cicd\",\"pipeline-start\"],
\"text\":\"CI pipeline #${BITBUCKET_BUILD_NUMBER} started\"}"
# Run the smoke test against the just-deployed staging stack
- |
k6 run \
--out influxdb=http://${INFLUXDB_URL}/k6 \
-e PIPELINE_ID=${BITBUCKET_BUILD_NUMBER} \
k6/ci-smoke-test.js
- GATE_EXIT=$?
# Mark pass/fail on the same dashboard, then propagate the result
- |
RESULT_TAG=$([ $GATE_EXIT -eq 0 ] && echo "pass" || echo "fail")
curl -s -X POST "${GRAFANA_URL}/api/annotations" \
-H "Authorization: Bearer ${GRAFANA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tags\":[\"cicd\",\"pipeline-end\",\"${RESULT_TAG}\"],
\"text\":\"CI #${BITBUCKET_BUILD_NUMBER} finished\"}"
- exit $GATE_EXIT
Smoke test ini mengalirkan setiap permintaan langsung ke InfluxDB lewat flag output InfluxDB bawaan k6, time-series store yang sama yang sudah dipakai stack observability live skripsi ini untuk menulis metrik container dan JVM. Artinya, sebuah run gate tidak menghasilkan laporan terpisah yang harus dicari-cari belakangan. Metrik latensi dan error rate-nya mendarat di dashboard Grafana yang sama persis dengan yang sudah dibuka operator untuk memantau traffic production. Langkah pipeline ini juga memanggil Grafana HTTP API dua kali, sekali tepat sebelum run k6 dimulai dan sekali tepat setelah selesai, untuk menuliskan anotasi yang menandai awal dan akhir gate pada sumbu waktu dashboard yang sama itu, diberi label lolos atau gagal. Gate yang gagal bukan sekadar log build yang harus dibaca seseorang belakangan. Ia adalah penanda merah di dashboard yang sudah dipantau tim, berada tepat di stempel waktu ketika regresi itu tertangkap.
Simpan exit code k6 ke sebuah variabel sebelum memanggil exit, dan kirim anotasi penutup di antaranya. Jika exit terjadi di baris yang sama dengan pemanggilan k6, run yang gagal akan melewatkan panggilan anotasi kedua sepenuhnya, dan dashboard tidak akan pernah mencatat bahwa gate-nya gagal, yang menggagalkan seluruh tujuan menaruh penanda itu sejak awal.
Langkah gate ini menambah sekitar dua menit ke setiap siklus deploy staging: tiga puluh detik menunggu layanan selesai startup, lalu dua menit traffic k6 terhadapnya. Itu adalah biaya nyata dan berulang yang dibayar di setiap rilis, terlepas dari apakah build tertentu itu kebetulan memunculkan regresi atau tidak. Sebagai gantinya, ia otomatis memblokir kelas regresi yang sebelumnya sepenuhnya bergantung pada seseorang yang ingat untuk menguji secara manual. Query yang lambat, pola N+1 yang baru muncul, atau endpoint rusak yang dulu bisa sampai ke production tanpa terdeteksi, sekarang menggagalkan build sebelum ada manusia yang melihat tombol promosi production manual. Dua tulisan lain dari proyek skripsi yang sama ini membahas metodologi load testing di balik skrip k6 itu sendiri dan auto-scaler yang bereaksi terhadap traffic yang dihasilkan gate ini; tulisan ini hanya membahas langkah otomasi yang mengikat keputusan lolos atau gagal ke pipeline rilis.