Best Practice #
Setelah membahas semua komponen CI/CD — pipeline design, GitHub Actions, GitLab CI, environment management, rollback, artifact, dan notifikasi — artikel ini merangkum prinsip dan pola yang membedakan pipeline yang baik dari yang sekadar berfungsi. Banyak tim membangun pipeline yang bisa melakukan deploy, tetapi lebih sedikit yang membangun pipeline yang dipercayai dan diandalkan oleh tim setiap hari. Berikut delapan prinsip yang menjadi pondasi, ditambah daftar anti-pattern yang paling sering menghambat kematangan CI/CD, serta checklist review yang bisa langsung kita gunakan sebelum menyatakan pipeline “production-ready”.
stateDiagram-v2
[*] --> Manual
Manual: "Deploy manual<br/>oleh developer"
Scripted: "Script bash<br/>jalan di laptop"
CI: "CI server<br/>build + test"
PipelineAsCode: "Pipeline di Git<br/>+ testing"
GitOps: "Git sebagai<br/>source of truth"
AI: "AI-assisted<br/>pipeline tuning"
Manual --> Scripted: "Bikin script"
Scripted --> CI: "Pindah ke CI server"
CI --> PipelineAsCode: "Versi di Git"
PipelineAsCode --> GitOps: "Auto-sync ke cluster"
GitOps --> AI: "Optimize test & deploy"
AI --> [*]: "Continuous improvement"
Lifecycle di atas menunjukkan lima level kematangan yang umum ditemui. Manual (deploy via SSH satu per satu), Scripted (menggunakan bash script tetapi masih dari laptop), CI (adanya CI server yang men-trigger otomatis), PipelineAsCode (pipeline sendiri di-version-control di Git), dan GitOps (Git menjadi satu-satunya source of truth untuk desired state). Best practice yang dibahas di artikel ini membantu kita bergerak minimal ke level PipelineAsCode — di mana semua orang percaya bahwa pipeline akan menghasilkan hasil yang sama setiap kali dijalankan.
1. Pipeline Harus Idempotent dan Bisa Di-retry #
Pipeline yang sama harus bisa dijalankan dua kali, tiga kali, atau dua puluh kali dan menghasilkan state akhir yang identik. Ini bukan kemewahan — ini adalah pondasi dari setiap automation yang serius. Tanpa idempotency, setiap retry bisa menghasilkan efek samping (deploy dua kali, migrasi database dua kali, atau file konfigurasi yang ter-overwrite secara tidak konsisten).
# ANTI-PATTERN: task yang tidak idempotent
- name: Tambah baris ke crontab
shell: (crontab -l; echo "*/5 * * * * /opt/app/sync.sh") | crontab -
# Kalau playbook di-retry, baris yang sama akan ditambahkan LAGI
# Setelah 5 retry, crontab punya 5 baris identik
# → 5x eksekusi per 5 menit, bukan 1x
# BENAR: gunakan module idempotent Ansible
- name: Tambah cron job sync
ansible.builtin.cron:
name: "app-sync"
minute: "*/5"
job: "/opt/app/sync.sh"
user: app
# Ansible detect cron job dengan name yang sama → hanya satu entry
# Retry aman, tidak ada duplikasi
Idempotency bukan hanya soal module yang dipakai — ini juga soal logika. Jika playbook membuat file konfigurasi baru, jalankan task template: (yang meng-overwrite file secara bersih), bukan lineinfile: dengan pattern yang mungkin terduplikasi. Jika playbook men-deploy Docker image, gunakan module docker_container dengan tag versi yang eksplisit (bukan latest yang bisa berubah sewaktu-waktu). Jika playbook melakukan restart service, gunakan service: dengan state: restarted (idempotent), bukan command: systemctl restart (yang bisa error jika service belum berjalan).
Retry safety juga harus dikonfigurasi secara eksplisit. Tambahkan parameter retries dan delay di task yang rentan terhadap kegagalan sementara (HTTP call, git clone, atau Docker pull). Namun, pastikan task yang di-retry benar-benar aman untuk diulang — jika tidak, retry justru bisa menjadi sumber bug baru.
2. Build Artifact Sekali, Promote ke Semua Environment #
Pola yang sering salah adalah: setiap environment (dev, staging, production) di-build ulang dari source code. Masalahnya, binary yang sampai di production tidak pernah benar-benar sama dengan yang di-test di staging — versi dependency bisa berbeda, konfigurasi compiler bisa berbeda, atau timestamp yang tertanam bisa berbeda. Hasilnya, bug yang lolos testing di staging muncul di production bukan karena kodenya berubah, melainkan karena proses build-nya berbeda.
# ANTI-PATTERN: build per environment
# job build-staging:
- name: Build image untuk staging
docker_image:
name: myapp
tag: staging-{{ ansible_date_time.epoch }}
build:
path: .
dockerfile: Dockerfile
# job build-production:
- name: Build image untuk production
docker_image:
name: myapp
tag: production-{{ ansible_date_time.epoch }}
build:
path: .
dockerfile: Dockerfile
# Masalah: dua build, dua binary, dua set dependency
# → tidak ada jaminan image staging == image production
# BENAR: build sekali, promote ke semua env
# job build (sekali):
- name: Build image sekali
docker_image:
name: myapp
tag: "{{ git_sha }}" # Tag immutable
build:
path: .
dockerfile: Dockerfile
push: yes
# job deploy-staging (pakai artifact yang sama):
- name: Deploy image hasil build ke staging
command: kubectl set image deployment/myapp myapp=myapp:{{ git_sha }}
# job deploy-production (PROMOTE artifact yang sama):
- name: Deploy image yang sama ke production
command: kubectl set image deployment/myapp myapp=myapp:{{ git_sha }}
# Image myapp:{{ git_sha }} persis sama di staging dan production
# Yang berbeda hanya konfigurasi runtime (env vars, secrets)
Tag image harus immutable — gunakan git_sha atau semver, jangan gunakan latest atau timestamp. Tag latest di environment yang berbeda bisa di-resolve ke image yang berbeda (jika ada push baru di tengah jalan). Tag timestamp menggoda karena kelihatan unik, tetapi membuat audit lebih sulit (“kapan image ini di-build?” jawabnya harus cross-reference ke log CI).
Konfigurasi yang environment-specific (database URL, API key, feature flag) harus di-inject saat runtime via environment variable, Kubernetes ConfigMap, atau Vault — bukan dimasukkan secara permanen (bake) ke dalam image. Ini memungkinkan satu image dipromosikan ke staging dengan konfigurasi A, lalu ke production dengan konfigurasi B, tanpa perlu proses rebuild.
3. Test di Setiap Stage — Unit, Integration, dan End-to-End #
Pipeline yang tidak memiliki testing di setiap stage adalah pipeline yang hanya mencoba deploy, bukan memvalidasi deploy. Bug yang seharusnya terdeteksi di unit test lolos ke staging, lalu ke production. Setiap stage harus memiliki jenis testing yang sesuai dengan confidence level yang dibutuhkan.
flowchart LR
A["Commit"] --> B["Stage 1:<br/>Unit Test<br/>(< 2 menit)"]
B --> C["Stage 2:<br/>Lint + Static Analysis<br/>(< 1 menit)"]
C --> D["Stage 3:<br/>Build Artifact<br/>(< 3 menit)"]
D --> E["Stage 4:<br/>Integration Test<br/>(< 5 menit)"]
E --> F["Stage 5:<br/>Deploy ke Staging"]
F --> G["Stage 6:<br/>Smoke + E2E Test<br/>(< 10 menit)"]
G --> H["Stage 7:<br/>Deploy ke Production"]
H --> I["Stage 8:<br/>Post-deploy<br/>health check"]
B -. "gagal" .-> X["Pipeline stop<br/>fix dulu"]
E -. "gagal" .-> X
G -. "gagal" .-> X
I -. "gagal" .-> Y["Trigger<br/>rollback"]
Alur di atas menggambarkan testing progression — semakin ke kanan, testing semakin mahal (waktu, resource) tetapi memberikan confidence yang semakin tinggi. Unit test murah dan cepat, sedangkan e2e test mahal dan lambat. Idealnya, sebagian besar bug terdeteksi di stage awal (unit + integration), dan stage akhir (e2e) hanya memvalidasi happy path.
# ANTI-PATTERN: pipeline tanpa testing berlapis
- name: Deploy ke production
command: kubectl apply -f deployment.yml
# Kalau tidak ada test sebelumnya, kita men-deploy "harapan" bukan "validasi"
# Bug baru ketahuan saat user komplain
# BENAR: setiap stage punya validasi yang sesuai
- name: Unit test (stage 1)
command: pytest tests/unit/
# harus selesai < 2 menit, kalau tidak pecah jadi lebih kecil
- name: Integration test (stage 4)
command: pytest tests/integration/
# butuh service dependency (database, redis), pakai docker-compose test harness
- name: End-to-end test (stage 6) di staging
uri:
url: "https://staging.example.com/health"
status_code: 200
retries: 3
delay: 5
register: health_check
failed_when: false
# Kalau health check gagal, pipeline stop di stage 6
# Production tidak akan ter-deploy
- name: Post-deploy health check (stage 8)
uri:
url: "https://example.com/health"
status_code: 200
register: prod_health
failed_when: false
# Kalau gagal, trigger rollback otomatis
Prinsip fast feedback: bug yang ditemukan di unit test memakan biaya rendah (1 menit waktu developer), bug yang ditemukan di staging memakan biaya menengah (rollback + redeploy), sedangkan bug yang ditemukan di production memakan biaya tertinggi (insiden, post-mortem, dan reputasi). Pyramid testing yang baik menggeser deteksi ke kiri — sebanyak mungkin bug terdeteksi di stage paling awal.
4. Secrets Di-inject Saat Runtime, Bukan Di-bake ke Image #
Salah satu kesalahan keamanan yang paling umum dan paling berbahaya adalah: secret di-bake ke Docker image atau Ansible artifact. Image yang di-push ke Docker Hub public (atau registry internal yang bocor) akan berisi database password, API key, atau certificate. Begitu image tersebar, secret ikut tersebar — dan rotasinya akan menjadi mimpi buruk.
# ANTI-PATTERN: secret di-bake ke image
# Dockerfile:
# FROM python:3.11
# ENV DB_PASSWORD=hunter2
# ENV API_KEY=sk-abc123
# COPY app.py /app/
# → Image di-push ke registry = secret bocor
# BENAR: secret di-inject saat runtime
# Dockerfile:
# FROM python:3.11
# COPY app.py /app/
# ENV DB_PASSWORD_FILE=/run/secrets/db_password
# ENV API_KEY_FILE=/run/secrets/api_key
# → Image tidak punya secret, hanya referensi ke file/env yang di-inject saat deploy
# Deployment:
- name: Deploy dengan secret dari Vault
kubernetes.core.k8s:
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
image: "registry.company.com/myapp:{{ git_sha }}"
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secrets
key: db_password
- name: API_KEY
valueFrom:
secretKeyRef:
name: myapp-secrets
key: api_key
Untuk Ansible sendiri, secret dikelola via Ansible Vault (file terenkripsi) atau external secret manager (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager). Saat playbook dijalankan, secret diambil (fetch) dari source dan di-pass ke task sebagai variabel. Pastikan menggunakan parameter no_log: true di setiap task yang menggunakan secret, dan jalankan perintah ansible-vault encrypt pada file yang berisi credential.
Jangan pernah menampilkan (echo) nilai secret di command atau debug message, meskipun hanya untuk keperluan testing. Sekali secret masuk ke log CI atau terminal history, pertimbangkan secret tersebut compromised. Gunakanlookup('community.general.vault', ...)atau environment variable denganno_log: trueuntuk semua akses secret.
5. Fast Feedback — Pipeline Kurang dari 10 Menit #
Pipeline yang lambat cenderung akan di-bypass. Ini bukan hipotesis, melainkan pola yang konsisten di semua survei CI/CD: pipeline yang membutuhkan waktu > 15 menit akan membuat developer mulai mencari jalan pintas. Mereka akan melakukan commit langsung ke main (skip CI), melakukan rebase untuk me-run ulang stage yang gagal (bukan memperbaikinya), atau men-deploy manual dengan alasan “tidak mau menunggu pipeline”. Semua perilaku ini menghancurkan disiplin CI/CD.
# ANTI-PATTERN: pipeline yang lambat karena tidak parallel
stages:
- test-unit-python
- test-unit-javascript
- test-integration
- build
- deploy
# Total: 5 + 3 + 8 + 4 + 6 = 26 menit (sequential)
# Developer malas menunggu → skip pipeline
# BENAR: parallel di stage awal, sequential di akhir
stages:
# Parallel: tiga job berjalan bersamaan
- name: test-unit-python
- name: test-unit-javascript
- name: lint
# Sequential: butuh artifact dari job sebelumnya
- name: build
needs: [test-unit-python, test-unit-javascript, lint]
- name: deploy-staging
needs: [build]
- name: e2e-test
needs: [deploy-staging]
- name: deploy-production
needs: [e2e-test]
# Total parallel: 5 menit (max dari job)
# Total sequential: 5 + 4 + 6 + 3 = 18 menit (5 + build + staging + e2e + prod)
# → 18 menit total, tapi feedback "apakah test passed" dalam 5 menit
Strategi untuk menjaga pipeline tetap cepat:
- Parallel di stage awal — unit test, lint, dan static analysis bisa berjalan bersamaan. Tugas-tugas ini independen dan tidak membutuhkan shared state.
- Cache dependency —
pip cache,npm cache, ataumaven cachebisa di-reuse antar-run. Download dependency sering kali memakan waktu >30% dari total waktu build. - Incremental build — build hanya layer yang berubah, bukan full rebuild. Gunakan Docker layer cache, Bazel incremental build, atau
make -juntuk kompilasi paralel. - Fail fast — letakkan test yang paling sering gagal di bagian paling awal. Pipeline yang gagal di stage 1 selesai dalam 2 menit; pipeline yang gagal di stage 5 selesai dalam 12 menit.
- Conditional stage — e2e test di production hanya berjalan jika ada tag release, bukan di setiap commit.
6. Pipeline Itu Sendiri Harus Diobservasi #
Ironi yang sering terjadi adalah: tim memonitor aplikasi production dengan cermat, tetapi pipeline yang men-deploy aplikasi tidak pernah di-monitor. Pipeline gagal tanpa adanya alerting, retry dilakukan tanpa alasan yang jelas, atau stuck job menggantung pada inventory semaphore — semua ini terjadi tanpa ada yang tahu. Saat ada insiden, tim tidak memiliki data untuk forensik.
flowchart TD
A["Pipeline run"] --> B["CI Server<br/>(GitLab/GitHub Actions)"]
B --> C{"Logging"}
C --> D["Loki/ELK<br/>(log pipeline run)"]
B --> E{"Metrics"}
E --> F["Prometheus<br/>(durasi, success rate,<br/>queue time)"]
B --> G{"Alerting"}
G --> H["Alertmanager<br/>(pipeline gagal 3x berturut-turut,<br/>durasi > 15 menit)"]
B --> I{"Trace"}
I --> J["OpenTelemetry<br/>(trace job-to-job<br/>di multi-stage)"]
B --> K{"Dashboard"}
K --> L["Grafana<br/>(success rate 7 hari,<br/>duration p95, flakiness)"]
Empat hal yang harus diamati dari pipeline itu sendiri:
- Success rate per stage — stage mana yang paling sering gagal? Apakah ada regresi (misalnya success rate turun drastis minggu ini)?
- Duration p50 dan p95 — apakah pipeline makin lama makin lambat? Ini merupakan early warning untuk masalah dependency atau resource.
- Flakiness — test/job yang mengalami intermittent failure (sekali sukses, sekali gagal tanpa perubahan kode) harus di-flag dan diperbaiki, bukan di-retry diam-diam sampai terlupakan.
- Queue time — berapa lama job menunggu di antrean? Queue time yang panjang menandakan resource CI tidak mencukupi.
Alert minimum yang harus disiapkan: pipeline gagal 3x berturut-turut (mungkin ada masalah struktural), durasi pipeline > 2x rata-rata (regresi performa), queue time > 10 menit (kapasitas kurang), dan job tertentu stuck > 30 menit (mengalami hang).
7. Rollback Plan HARUS Ada Sebelum Deploy #
Pola yang membedakan tim senior dari tim junior adalah: rollback plan ditulis dan ditest sebelum deployment, bukan sesudahnya. Tim junior baru berpikir “kalau gagal ya rollback” saat deployment sudah berjalan dan production sedang down. Pada titik itu, rollback dilakukan dengan panik, ketidakpastian, dan sering kali justru menambah masalah baru.
# ANTI-PATTERN: rollback plan yang tidak pernah disiapkan
- name: Deploy ke production
command: kubectl apply -f deployment.yml
# Deployment selesai, kemudian:
# 1. Aplikasi down
# 2. "Coba rollback!"
# 3. "Ke versi mana? Cek git log dulu..."
# 4. "Image tag sebelumnya apa?"
# 5. "kubectl rollout undo? Atau apply file lama?"
# 6. 15 menit berlalu, production masih down
# BENAR: rollback plan terstruktur, teruji, dan otomatis
- name: Simpan state pre-deployment
set_fact:
previous_image_tag: "{{ lookup('pipe', 'kubectl get deployment myapp -o jsonpath=\"{.spec.template.spec.containers[0].image}\"') }}"
previous_replicas: "{{ lookup('pipe', 'kubectl get deployment myapp -o jsonpath=\"{.spec.replicas}\"') }}"
- name: Deploy image baru
command: kubectl set image deployment/myapp myapp=myapp:{{ git_sha }}
- name: Tunggu rollout selesai
kubernetes.core.k8s_info:
kind: Deployment
name: myapp
wait_condition:
type: Complete
status: "True"
wait_timeout: 300
register: rollout_status
- name: Verifikasi post-deploy
uri:
url: "https://example.com/health"
status_code: 200
register: health_check
until: health_check.status == 200
retries: 5
delay: 10
- name: Rollback otomatis jika health check gagal
command: >
kubectl set image deployment/myapp myapp={{ previous_image_tag }}
&& kubectl scale deployment/myapp --replicas={{ previous_replicas }}
when: health_check.failed
# Rollback dalam 30 detik, tanpa campur tangan manusia
# previous_image_tag sudah disimpan sebelum deploy
Tiga komponen rollback plan yang harus ada:
- State sebelumnya tersimpan — image tag, replica count, dan konfigurasi. Tanpa ini, rollback dilakukan ke “kira-kira versi sebelumnya”.
- Verifikasi otomatis — health check atau smoke test yang menentukan sukses atau gagalnya rilis. Tanpa ini, kita tidak tahu kapan harus rollback.
- Prosedur rollback otomatis — script atau playbook yang bisa dijalankan tanpa intervensi manusia. Melakukan rollback manual saat insiden rentan terhadap kesalahan tambahan.
Rollback plan juga harus pernah ditest, bukan hanya ditulis di atas kertas. Jalankan simulasi rollback di staging, ukur berapa lama waktu yang dibutuhkan, dan identifikasi hambatannya. Perbarui rencana berdasarkan temuan nyata.
8. Pipeline as Code, Versioned di Git #
Pipeline configuration adalah kode — ia harus di-review, di-test, dan di-version-control seperti halnya kode aplikasi. Tidak boleh ada konfigurasi pipeline yang diubah via UI CI server tanpa meninggalkan jejak. Tidak boleh ada tindakan seperti “kita menambahkan step baru di GitHub Actions langsung dari web, tapi lupa melakukan commit”.
# Struktur direktori CI/CD as code
cicd/
├── pipelines/
│ ├── main.yml # Pipeline untuk branch main
│ ├── pr.yml # Pipeline untuk pull request
│ └── release.yml # Pipeline untuk tag release
├── tasks/
│ ├── build.yml # Task build image
│ ├── test.yml # Task unit test
│ ├── deploy.yml # Task deploy ke env
│ ├── rollback.yml # Task rollback
│ └── notify.yml # Task notifikasi
├── vars/
│ ├── common.yml # Variabel shared
│ ├── prod.yml # Variabel production
│ └── staging.yml # Variabel staging
├── templates/
│ ├── deployment.yml.j2 # Template Kubernetes manifest
│ └── notification.json.j2 # Template payload Slack
└── tests/
├── test-pipeline-syntax.yml # Validasi YAML
└── test-deploy.yml # Test deploy ke environment ephemeral
Pola penting dari struktur di atas adalah: pipeline tasks bersifat reusable (bukan copy-paste). File deploy.yml dipakai oleh main, pr, dan release pipeline dengan parameter yang berbeda. Langkah ini mencegah terjadinya drift — jika kita memperbarui cara deploy di main pipeline tetapi lupa memperbarui di release pipeline, perilakunya akan menjadi berbeda.
# ANTI-PATTERN: pipeline configuration diubah via UI CI
# Cerita nyata:
# 1. Developer A menambahkan step di GitHub Actions via web
# 2. Pipeline berjalan 3 minggu tanpa ada yang sadar step itu tidak di-commit
# 3. Developer B menghapus branch, konfigurasi ikut hilang
# 4. Pipeline tiba-tiba gagal tanpa adanya perubahan kode
# 5. "Kok bisa? Tidak ada yang berubah!"
# BENAR: semua perubahan via PR, di-review, di-test
# .github/workflows/deploy.yml (atau .gitlab-ci.yml)
# ↑ di-commit di branch fitur
# ↑ PR di-review oleh minimal 1 orang
# ↑ CI menjalankan test pipeline syntax + deploy ke staging
# ↑ setelah merge, pipeline baru aktif
Konsekuensi dari penerapan pipeline as code: pipeline itu sendiri bisa memiliki bug. Task build.yml bisa salah urutan, atau deploy.yml bisa salah memanggil environment. Oleh karena itu, pipeline configuration membutuhkan pengujian yang sama seperti kode aplikasi. Lakukan linting YAML (yamllint), schema validation, dan test deployment ke environment ephemeral (jalankan cluster kecil, eksekusi pipeline, lalu verifikasi hasilnya).
CI vs CD: Pisah atau Gabung? #
Istilah “CI/CD” sering dipakai sebagai satu konsep utuh, tetapi sebenarnya terdiri atas dua hal berbeda dengan trade-off yang berbeda pula. CI (Continuous Integration) berfokus pada integrasi kode — menggabungkan perubahan dari banyak developer, menjalankan test, dan memastikan tidak ada regresi. CD bisa berarti dua hal: Continuous Delivery (otomatis melakukan build dan test, tetapi deployment ke production memerlukan approval manusia) atau Continuous Deployment (otomatis melakukan deploy ke production setelah seluruh test dinyatakan pass).
flowchart LR
A["CI<br/>(Continuous Integration)"] --> A1["Merge kode"]
A1 --> A2["Build"]
A2 --> A3["Unit + Integration test"]
A3 --> A4["Artifact published"]
B["CD - Continuous Delivery"] --> A4
B1["Deploy ke staging otomatis"] --> B2["Manual approval"]
B2 --> B3["Deploy ke production"]
C["CD - Continuous Deployment"] --> A4
C1["Deploy ke staging"] --> C2["E2E test"]
C2 --> C3["Deploy ke production<br/>otomatis"]
style A stroke:#4caf50,stroke-width:2px
style B stroke:#ff9800,stroke-width:2px
style C stroke:#f44336,stroke-width:2px
Kapan memilih pendekatan tersebut:
- CI only — untuk aplikasi internal dengan frekuensi deployment yang jarang, aturan compliance yang sangat ketat, atau tim yang baru mulai belajar. Ini adalah langkah pertama yang realistis.
- Continuous Delivery — untuk tim yang sudah memiliki CI solid, ingin melakukan deploy lebih sering, tetapi deployment ke production masih memerlukan kendali manusia. Ini merupakan sweet spot untuk kebanyakan tim.
- Continuous Deployment — untuk aplikasi yang sudah mature, test coverage tinggi, error budget mencukupi, dan memiliki tim on-call yang selalu siap. Hanya disarankan untuk tim yang sudah menerapkan Delivery dengan lancar selama berbulan-bulan.
Untuk Ansible dan infrastructure-as-code, Continuous Delivery adalah sweet spot — setiap perubahan di-merge ke main, build artifact, deploy ke staging secara otomatis, kemudian deploy ke production dengan manual approval. Adanya manual approval di production bukan bentuk birokrasi, melainkan pengakuan bahwa ada kalanya kita membutuhkan manusia untuk membuat keputusan strategis (misalnya: menunda deploy karena sedang berlangsung event penting dengan traffic tinggi).
flowchart TD
Start{"Pipeline atau<br/>workflow manual?"} -- "Pipeline" --> P1{"Frekuensi deployment<br/>> 1x per minggu?"}
P1 -- "Ya" --> P2{"Butuh rollback<br/>otomatis?"}
P2 -- "Ya" --> Pipeline["Pipeline as code"]
P2 -- "Tidak" --> Workflow["Workflow dengan approval"]
P1 -- "Tidak" --> Workflow
Start -- "Workflow" --> W1{"Compliance mengharuskan<br/>review manusia?"}
W1 -- "Ya" --> Workflow
W1 -- "Tidak" --> Pipeline
Decision tree ini membantu memutuskan apakah suatu proses harus diotomatisasikan penuh menjadi pipeline atau tetap mempertahankan alur workflow dengan approval. Trade-off: pipeline otomatis memberikan kecepatan tinggi tetapi kurang fleksibel, sedangkan workflow lebih lambat tetapi menempatkan manusia di dalam loop pengawasan. Untuk perubahan rutin (seperti deploy aplikasi atau restart service), pilih opsi pipeline. Untuk perubahan besar yang sifatnya satu kali (seperti migrasi database besar), alur dengan approval lebih disarankan.
Anti-Pattern yang Harus Dihindari #
Berikut anti-pattern yang paling sering ditemukan di pipeline CI/CD pada proyek Ansible. Setiap anti-pattern disertai dengan solusi ringkas yang bisa langsung diadopsi.
# ✗ Anti-pattern 1: Pipeline yang bypass-able
# Ada tombol "Skip CI" atau "Force merge" yang sering dipakai
# Situasi: Developer skip CI karena "testnya flaky, nanti saja"
# Konsekuensi: bug lolos, quality menurun, trust terhadap CI hilang
# ✓ Solusi: Hapus tombol skip, atau gunakan hanya untuk situasi yang
# terdokumentasi (dependency update otomatis). Flaky test
# harus diperbaiki, bukan di-skip.
# ✗ Anti-pattern 2: Pipeline yang hanya build, tidak validasi
# Pipeline selesai sukses tapi tidak ada yang tahu apakah artifact
# benar-benar bekerja
# ✓ Solusi: tambahkan stage verifikasi — smoke test, contract test,
# atau deployment ke environment ephemeral + run e2e
# ✗ Anti-pattern 3: Deploy ke production dari laptop
# "Tinggal push manual ke production, ga papa kok"
# ✓ Solusi: production deploy SELALU lewat pipeline. Tidak ada
# shortcut. Kalau pipeline terlalu lambat, perbaiki pipeline
# (bukan bypass).
# ✗ Anti-pattern 4: Secret hardcoded di pipeline file
# .github/workflows/deploy.yml:
- name: Deploy
run: ansible-playbook -e "db_pass=hunter2" site.yml
# db_pass masuk ke log CI = bocor
# ✓ Solusi: gunakan secret manager (Vault, GitHub Secrets, GitLab CI
# variables). Pass ke playbook via environment variable
# dengan no_log: true
# ✗ Anti-pattern 5: Test yang selalu di-skip atau di-comment
# "Test ini kadang gagal, skip aja dulu"
# Masalahnya: "sementara" = selamanya. Test yang di-skip jadi
# sumber bug yang lolos.
# ✓ Solusi: fix test dalam waktu 1 sprint. Kalau tidak bisa fix,
# delete test tersebut. Test yang di-skip lebih buruk dari
# tidak ada test, karena memberi ilusi validasi yang semu.
# ✗ Anti-pattern 6: Pipeline tanpa timeout atau resource limit
# Job yang hang menggantung resource CI selamanya
# Concurrency tanpa batas bisa exhaust runner
# ✓ Solusi: set timeout per job (misal 30 menit). Set resource limit
# (memory, CPU) untuk job berat. Gunakan cancel-in-progress
# untuk superseded job.
# .github/workflows/deploy.yml:
# jobs:
# deploy:
# timeout-minutes: 30
# steps: ...
# resources:
# limits:
# memory: 4Gi
# cpu: "2"
# ✗ Anti-pattern 7: Pipeline yang tidak idempotent
# Re-run menghasilkan efek samping (data duplikat, file duplikat)
# ✓ Solusi: audit setiap task. Pakai module Ansible idempotent
# (template, copy, lineinfile dengan state: present).
# Test: jalankan pipeline 2x dan bandingkan hasil.
Checklist Review CI/CD #
Gunakan checklist ini setiap kali setup pipeline baru, atau setiap kali melakukan review terhadap pipeline yang sudah berjalan. Pastikan semua kriteria terpenuhi sebelum menyatakan pipeline “production-ready” dan tim bisa percaya bahwa melakukan deploy lewat pipeline sama amannya dengan melakukan deploy secara manual (bahkan jauh lebih aman).
BUILD
□ Artifact di-build sekali dan di-promote (build-once-promote-everywhere)
□ Tag artifact immutable (git_sha / semver), bukan latest / timestamp
□ Build reproducible — build di mesin lain menghasilkan binary identik
□ Dependency di-pin ke versi eksak (bukan range), lock file di-commit
□ Build cache diaktifkan (pip/npm/maven cache) untuk percepat
□ Image scan untuk vulnerability (Trivy, Snyk, Grype)
□ Image size diminimalkan (multi-stage build, alpine base image)
TEST
□ Unit test jalan < 2 menit per modul
□ Integration test jalan < 5 menit per service
□ End-to-end test jalan di staging sebelum production deploy
□ Test coverage diukur dan threshold minimum didefinisikan
□ Flaky test dimonitor dan di-fix (bukan di-retry secara diam-diam)
□ Linting (yaml, ansible-lint, shellcheck) di pipeline
□ Secret scanning (gitleaks, trufflehog) di pipeline
□ Static analysis untuk security (bandit untuk Python, dll)
DEPLOY
□ Deploy ke staging otomatis setelah build sukses
□ Deploy ke production lewat approval (Continuous Delivery)
atau otomatis (Continuous Deployment)
□ Deployment strategy: rolling / blue-green / canary, bukan recreate
□ Health check post-deploy (bukan hanya "pod running")
□ Smoke test post-deploy di production
□ Deployment ke env ephemeral untuk test pipeline (bukan hanya staging)
□ Konfigurasi environment-specific via env var / ConfigMap / Vault
ROLLBACK
□ State pre-deployment tersimpan (image tag, config version)
□ Rollback otomatis jika health check gagal (timeout: 5-10 menit)
□ Rollback pernah ditest di staging (bukan hanya ditulis)
□ Database migration punya backward-compatible (expand-contract pattern)
□ Feature flag digunakan untuk pisahkan deploy code dari release fitur
□ Runbook untuk rollback manual tersedia (kalau otomatis gagal)
SECRETS
□ Tidak ada secret di repository (Vault, .env.example, .gitignore)
□ Secret di-inject saat runtime, bukan di-bake ke image
□ Ansible Vault atau external secret manager untuk semua credential
□ no_log: true di semua task yang akses secret
□ CI secret di-rotasi berkala (quarterly minimal)
□ Service account punya least privilege (bukan admin)
□ Audit log untuk akses secret (siapa akses apa kapan)
OBSERVABILITY
□ Pipeline run di-log ke central log (Loki/ELK)
□ Metrics: success rate, duration p50/p95, queue time
□ Alert: pipeline gagal 3x berturut-turut
□ Alert: pipeline duration > 2x rata-rata
□ Alert: job stuck > 30 menit
□ Dashboard pipeline tersedia di Grafana
□ Notification sukses/gagal ke channel yang tepat (lihat
artikel Notification & Reporting)
□ Runbook untuk setiap alert pipeline
SECURITY
□ SBOM (Software Bill of Materials) di-generate per release
□ Image signature verification (cosign, Notary)
□ Dependency vulnerability scanning di setiap build
□ Compliance check (CIS benchmark) di pipeline untuk infra changes
□ Approval gate untuk production yang sensitif (data deletion, dll)
□ Audit trail siapa approve apa kapan
DOCUMENTATION
□ README menjelaskan cara trigger pipeline (manual + otomatis)
□ Setiap variabel pipeline didokumentasikan (deskripsi, default, range)
□ Runbook untuk "pipeline gagal karena X" tersedia
□ Architecture diagram pipeline di-update saat ada perubahan
□ On-call tahu cara cancel pipeline yang stuck
□ Pipeline configuration di-review seperti kode aplikasi
□ Post-mortem untuk setiap insiden yang diakibatkan pipeline
Membangun Budaya CI/CD yang Sehat #
Pipeline yang baik tidak akan memberikan hasil yang optimal tanpa adanya budaya tim yang mendukung. Tiga pilar budaya yang harus dibangun:
"Deploy sering, deploy kecil, deploy dengan keyakinan"
Deploy sering:
Deployment yang jarang = perubahan yang besar = risiko yang tinggi.
Deploy setiap hari (atau lebih sering) memaksa kita untuk membuat
setiap deployment kecil dan mudah di-rollback.
Deploy kecil:
Semakin kecil perubahan per deployment, semakin mudah mencari penyebab
masalah jika ada yang salah. Feature flags memungkinkan deploy kode
tanpa mengaktifkan fitur.
Deploy dengan keyakinan:
Keyakinan datang dari:
- Test coverage yang baik
- Staging yang mencerminkan production
- Observability yang memadai
- Rollback yang sudah pernah dicoba dan terbukti bekerja
Budaya “deploy sering” bukan tentang mengejar kecepatan semata — ini tentang mengurangi risiko. Perubahan kecil lebih mudah di-debug, lebih mudah di-rollback, dan lebih mudah di-review. Tim yang takut melakukan deploy adalah tim yang akhirnya melakukan deploy sebulan sekali dengan bundle perubahan yang sangat besar dan tidak bisa di-rollback dengan aman.
Feature flags adalah enabler utama dari pola “deploy kecil”. Dengan feature flag, kita bisa men-deploy kode yang berisi fitur baru tetapi fitur tersebut tidak aktif sampai flag di-enable. Ini memisahkan kapan kode di-deploy (sering, kecil) dari kapan fitur dirilis ke user (lebih jarang, terkoordinasi). Rollback fitur pun tinggal dilakukan dengan men-disable flag — tanpa perlu melakukan redeploy.
Ringkasan #
- Idempotency dan retry safety adalah fondasi — pipeline harus bisa di-retry tanpa efek samping. Pakai module Ansible idempotent, tag artifact secara immutable, dan definisikan
retries/delaydi task yang transient.- Build-once-promote-everywhere: artifact yang sampai di production harus identik dengan yang di-test di staging. Tag dengan
git_sha/semver, lalu inject konfigurasi per-environment saat runtime.- Testing berlapis: unit test (< 2 menit), integration test (< 5 menit), e2e test (di staging), dan post-deploy health check. Semakin ke kanan semakin mahal, semakin ke kiri semakin banyak bug harus tertangkap.
- Secrets di runtime, bukan di-bake: image harus bebas dari secret. Gunakan Vault atau Ansible Vault untuk source, serta environment variable atau ConfigMap untuk injeksi runtime. Set
no_log: truedi semua task secret.- Fast feedback: pertahankan durasi pipeline < 10 menit. Jalankan secara paralel di stage awal, manfaatkan cache dependency, dan terapkan fail-fast. Pipeline yang lambat cenderung di-bypass oleh developer.
- Pipeline yang observable: monitor success rate, duration p95, flakiness, dan queue time. Pipeline yang tidak di-monitor akan melahirkan silent failure.
- Rollback plan sebelum deploy: simpan state pre-deployment, lakukan verifikasi otomatis, dan buat prosedur rollback otomatis. Rollback harus pernah ditest, bukan hanya ditulis di dokumentasi.
- Pipeline as code: simpan konfigurasi pipeline di Git, review di PR, dan uji seperti halnya kode aplikasi. Hindari melakukan perubahan konfigurasi via UI CI server.
- CI vs CD: mulailah dari CI, lalu Continuous Delivery (deploy ke production dengan approval), dan Continuous Deployment (deploy otomatis) hanya untuk tim yang sudah sangat mature.
- Budaya “deploy sering, kecil, dengan keyakinan”: gunakan feature flags, perluas test coverage, terapkan observability, dan uji mekanisme rollback. Ini memungkinkan tim melakukan deploy setiap hari tanpa drama.
← Sebelumnya: Notification & Reporting Berikutnya: Playbook Anti Pattern →