CI/CD: Pipeline yang Membantu, Bukan yang Cuma Hijau
Tim bangga "kita sudah pakai CI/CD" — padahal yang jalan cuma npm test di PR, dan deploy masih manual scp ke server Jumat malam. Saat rollout gagal, tidak ada yang tahu apa yang beda dari minggu lalu karena tidak ada tag rilis. CI/CD bukan "punya pipeline", tapi disiplin: setiap perubahan melewati jalur yang sama, teruji, dan bisa di-rollback. Artikel ini tidak mendefinisikan continuous integration dari buku. Kita bedah keputusan nyata: stage apa yang wajib, kapan self-hosted runner masuk akal, gimana kelola secret tanpa bocor, dan kenapa banyak pipeline hijau tapi deploy tetap berantakan.
CI dan CD itu dua hal beda
Singkatan sering disatuin padahal tanggung jawabnya beda: - CI (Continuous Integration): setiap push/PR di-build, di-test, di-lint. Tujuannya: tahu cepat kalau ada yang patah. - CD (Continuous Delivery/Deployment): hasil CI yang lulus otomatis masuk ke staging (Delivery) atau produksi (Deployment).
Tim kecil sering cukup di CI + manual deploy (Delivery), bukan auto-deploy ke prod (Deployment). Auto-deploy ke prod butuh kepercayaan tinggi ke test — kalau test tipis, auto-deploy = auto-outage.
Stage wajib, jangan skip
Pipeline minimal yang berguna: 1. Lint + typecheck — tangkap error bodoh sebelum build. 2. Unit test — logika inti. 3. Build — pastikan benar-benar bisa di-compile/bundle. 4. Integration test (kalau ada DB/cache) — pakai container ephemeral. 5. Deploy ke staging — verifikasi artefak jalan di environment mirip prod. 6. Manual approve → prod (atau auto kalau sudah yakin).
Yang sering dilewatkan: stage 4 dan 5. Tanpa integration test, "hijau" cuma arti "kode compile". Tanpa staging, bug environment baru kelihatan di prod.
# GitHub Actions: stage berurutan, fail-fastjobs: test: steps: [checkout, setup-node, lint, test, build] deploy-staging: needs: test if: github.ref == 'refs/heads/main' steps: [deploy] deploy-prod: needs: deploy-staging environment: production # butuh approve manual steps: [deploy]Jangan jalankan test di pipeline yang tidak iso dengan prod
Bug klasik: "di CI lulus, di prod error". Penyebabnya: CI pakai Node 18, prod Node 20; atau CI Windows, prod Linux (path separator beda). Pin version di kedua sisi. Di GitHub Actions, actions/setup-node@v4 dengan node-version: 20 — sama persis dengan Docker image prod.
# .nvmrc / package.json engines"engines": { "node": "20.x" }Alasan: pipeline yang tidak mereplikasi prod hanya menguji keberuntungan, bukan kebenaran.
Self-hosted runner vs cloud: keputusan biaya
GitHub-hosted runner $0.008/menit (Linux). Untuk pipeline pendek (beberapa menit, beberapa kali sehari) itu murah. Tapi kalau kamu build Docker image besar atau jalankan test suite 40 menit tiap PR, biayanya naik. Self-hosted runner (VM sendiri) gratis dari sisi menit, tapi kamu bayar server + maintenance.
Kapan self-hosted masuk akal: - Butuh akses ke resource internal (DB on-prem, registry privat) yang tidak boleh keluar. - Build berat dan sering → lebih murah di hardware sendiri. - Butuh software khusus yang tidak ada di image hosted.
Kapan tidak: - Pipeline pendek dan sporadis → hosted lebih murah dan tanpa urusan patch. - Tim kecil tanpa orang yang mau rawat runner → hosted.
Risiko self-hosted: runner menjalankan kode dari PR eksternal. Kalau repo terbuka (public), attacker bisa buka PR yang exfiltrasi secret dari runner. Mitigasi: runs-on: self-hosted untuk PR dari anggota terpercaya saja, atau pakai ephemeral runner (tiap job VM baru, di-destroy setelahnya).
Secrets: jangan commit, jangan echo
Masalah paling sering: .env ke-commit ke repo (bahkan setelah di-delete, masih di git history). Atau pipeline echo $DB_PASSWORD untuk debug — masuk ke log publik.
Fix: - Secret di GitHub Secrets / GitLab CI Variables, bukan file di repo. - Jangan echo secret; kalau perlu debug, mask otomatis (GitHub menyensor variable yang didefinisikan sebagai secret). - Rotasi secret berkala + setelah insiden. - Jangan pakai personal token untuk deploy; buat deploy key / service account khusus dengan scope minimal.
# Salah: password di plaintext
env:
DB_PASSWORD: "admin123" # masuk ke repo, ke history
# Benar: reference secret
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
Why: secret di history repo = kompromi permanen, walau file di-hapus. Histori git tidak lupa.
Cache dengan benar, jangan cache sembarangan
Cache node_modules atau target/ mempercepat pipeline drastis. Tapi cache yang tidak di-invalidasi bisa sisa state — "lulus karena cache lama". Aturan: - Key cache pakai hash lockfile (package-lock.json), bukan latest. - Invalidate saat lockfile berubah. - Jangan cache .env atau artifact antar job yang seharusnya fresh.
- uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
Artifact dan immutability
Hasil build (Docker image, binary) harus immutable — build sekali, pakai di staging DAN prod dari artefak yang sama, bukan rebuild di tiap stage. Kalau kamu rebuild di deploy-prod, hasilnya bisa beda dari yang lulus test.
# Tag image dengan SHA commit, bukan 'latest'
docker build -t registry/app:${{ github.sha }} .
# deploy staging & prod pakai tag SHA yang sama
Alasan: reproducible. "Yang di-test" persis "yang di-prod". latest adalah anti-pattern karena ambigu.
Rollback: yang jarang disiapkan
Pipeline maju (deploy) jalan mulus, tapi saat prod error, tidak ada tombol mundur. Rollback harus otomatis atau satu perintah: - Blue-green: dua environment, switch traffic dalam detik. - Canary: rilis ke 5% user, observasi, lanjut atau stop. - Tag-based: git tag v1.2.3 → rollback = deploy tag sebelumnya.
# Kubernetes: rollback satu commandkubectl rollout undo deployment/app# atau deploy SHA lama
docker pull registry/app:<previous-sha>Why: MTTR (waktu pemulihan) ditentukan seberapa cepat kamu bisa balik, bukan seberapa cepat deploy maju.
Database migration dalam pipeline
Ini yang paling sering bikin outage. Migration dijalankan otomatis di pipeline, tapi tidak backward-compatible → rollback app gagal karena schema sudah berubah.
Fix: migration harus expand-contract: 1. Expand: tambah kolom baru (tanpa hapus yang lama), deploy app yang bisa baca dua-duanya. 2. Migrasi data ke kolom baru. 3. Contract: hapus kolom lama di rilis berikutnya.
Jangan jalankan DROP COLUMN di pipeline yang sama dengan deploy app yang masih butuh kolom itu. Alasan: migrasi breaking = tidak bisa rollback tanpa kehilangan data.
Observability pipeline
Pipeline butuh visibilitas: berapa lama tiap stage, berapa sering gagal, apa penyebab. GitHub menampilkan di tab Actions, tapi untuk tim banyak repo, aggregasi ke dashboard (Grafana) membantu. Alert ke Slack kalau prod deploy gagal — jangan biarkan gagal diam-diam.
# Contoh notifikasi
- name: Notify on failure
if: failure()
run: curl -X POST ${{ secrets.SLACK_WEBHOOK }} -d '{"text":"Deploy prod gagal"}'
Kesalahan yang sering lolos
CI ada, CD manual
scp→ tidak konsisten antar orang.Skip integration test → "hijau" tapi DB bug tetap lewat.
Versi Node/OS di CI beda prod → bug environment.
Self-hosted runner untuk PR publik → secret bocor.
Secret di-commit / di-echo ke log.
Cache tanpa hash lockfile → state usang.
Rebuild artefak tiap stage → tidak reproducible.
Rollback tidak disiapkan → MTTR jam, bukan detik.
Migration breaking dalam pipeline → tidak bisa balik.
Tidak ada notifikasi gagal → outage diam.
Takeaways
CI dan CD beda; tim kecil cukup CI + manual deploy (Delivery).
Stage wajib: lint, test, build, integration, staging, prod (approve).
Pin version CI = prod supaya tidak uji keberuntungan.
Self-hosted runner untuk build berat/internal; hosted untuk pendek.
Secret di vault, jangan commit/echo; pakai deploy key minimal.
Cache di-key hash lockfile; invalidate saat berubah.
Build sekali, deploy artefak immutable (tag SHA, bukan latest).
Siapkan rollback (blue-green / canary / tag) sebelum butuh.
Migration expand-contract supaya bisa balik.
Notifikasi gagal wajib; pipeline diam = outage tersembunyi.



