Pipeline Design #
Pipeline CI/CD yang baik bukan sekadar otomasi langkah-langkah yang sebelumnya dilakukan manual. Ia adalah sistem yang memberikan umpan balik cepat saat ada masalah, memastikan hanya kode yang tervalidasi yang sampai ke production, dan membuat deployment menjadi kejadian yang membosankan — bukan momen yang penuh ketegangan. Artikel ini membahas prinsip desain pipeline yang mengintegrasikan Ansible sebagai engine deployment, dan menjadi landasan untuk dua artikel berikutnya yang membahas implementasi konkret di GitHub Actions dan GitLab CI.
Prinsip Desain Pipeline #
Lima prinsip ini harus ada di setiap pipeline yang kita rancang. Mengabaikan salah satunya akan menimbulkan masalah yang lebih mahal untuk diperbaiki nanti:
1. Fail Fast
Tempatkan pengecekan yang paling cepat di awal pipeline.
Jangan jalankan deployment ke staging jika lint saja sudah gagal.
Pipeline yang lambat membuat developer malas menunggu dan akhirnya
merge kode yang belum di-test dengan benar.
2. Environment Promotion, bukan Re-Build
Artifact yang sama (image Docker, package) harus dipromosikan
dari staging ke production — bukan di-build ulang untuk setiap
environment. Build ulang berarti kode yang sampai ke production
belum tentu sama dengan yang di-test di staging.
3. Idempoten di Setiap Tahap
Setiap tahap harus aman dijalankan berulang kali tanpa efek
samping. Re-run pipeline yang gagal tidak boleh merusak state.
Ansible sudah idempoten secara desain — manfaatkan ini.
4. Immutable Artifact
Setelah build, artifact tidak boleh berubah.
Tag image Docker dengan git SHA atau versi semantik, bukan
'latest'. Image yang sama harus persis sama di semua env.
5. Separation of Concern
Build pipeline ≠ Deployment pipeline.
Build menghasilkan artifact; deploy mendistribusikannya.
Campur keduanya dan kita tidak bisa mempromosikannya tanpa build ulang.
Prinsip “fail fast” bukan berarti semua step harus paralel. Yang penting adalah urutan eksekusi: langkah murah dan cepat (lint, syntax check) di depan; langkah mahal dan lambat (integration test, deploy ke multi-node) di belakang. Kalau ada langkah yang bisa selesai dalam 30 detik dan bisa mendeteksi 80% masalah, ia harus jadi gate pertama.
Alur Fail Fast dalam Pipeline #
stateDiagram-v2
[*] --> Lint
Lint --> UnitTest: lint pass
Lint --> Failed: lint gagal
UnitTest --> Build: test pass
UnitTest --> Failed: test gagal
Build --> Scan: image built
Build --> Failed: build error
Scan --> PushRegistry: scan bersih
Scan --> Failed: vulnerability tinggi
PushRegistry --> DeployStaging: tag immutable
DeployStaging --> IntegrationTest: deployed
IntegrationTest --> Gate: smoke test pass
IntegrationTest --> RollbackStaging: test gagal
Gate --> DeployProduction: approved
Gate --> Hold: menunggu approval
Hold --> DeployProduction: reviewer setujui
Hold --> RollbackStaging: reviewer tolak
DeployProduction --> VerifyProd: deployed
VerifyProd --> [*]: success
VerifyProd --> RollbackProd: health check gagal
RollbackStaging --> Failed
RollbackProd --> Failed
Failed --> [*]
Diagram state di atas menunjukkan keputusan yang terjadi di setiap stage. Perhatikan bahwa setiap jalur kegagalan (Failed) bisa di-rollback sebelum sempat merusak production. Ini adalah inti dari fail-fast: deteksi masalah sedekat mungkin dengan sumbernya.
Anatomi Pipeline CI dan CD #
Pipeline modern biasanya dibagi jadi dua bagian besar yang punya trigger, output, dan lifecycle berbeda. Mencampur keduanya dalam satu workflow adalah salah satu sumber masalah paling umum di pipeline CI/CD.
flowchart LR
subgraph CI["CI Pipeline — trigger: push/PR"]
A["Push kode"] --> B["Lint"]
B --> C["Unit Test"]
C --> D["Build Image"]
D --> E["Scan Vulnerability"]
E --> F["Push ke Registry"]
end
F -->|image:2.1.0-abc123| G[("Container Registry")]
subgraph CD["CD Pipeline — trigger: CI success / merge main"]
G --> H["Deploy Staging"]
H --> I["Integration Test"]
I --> J{"Gate Approval"}
J -->|setuju| K["Deploy Production"]
J -->|tolak| L["Hold"]
K --> M["Verify Prod"]
end
Bedanya signifikan. CI pipeline dipicu setiap ada perubahan kode dan fokus pada verifikasi: apakah kode ini benar? Apakah aman di-build? Apakah lolos test? Output CI adalah artifact yang sudah terjamin kualitasnya — biasanya image container yang sudah di-tag immutable. CD pipeline dipicu setelah CI sukses dan fokus pada distribusi: artifact yang sama dipasang di staging, diuji, lalu di-promote ke production. Ansible mengambil peran dominan di sisi CD: dari community.docker.docker_image untuk pull image, hingga task deployment ke target server.
Keuntungan utama pemisahan ini: satu build, banyak deploy. Image yang sama persis di-test di staging dan di-deploy ke production. Tidak ada lagi drama “kok di staging jalan, di production error?”.
Jangan pernah me-rebuild image di tiap environment. Jika kita memiliki jobbuildyang jalan di staging dan jobbuildyang jalan di production dengan kode yang sama, kita kehilangan jaminan bahwa production == staging. Build sekali, tag immutable (git SHAatausemver), promosikan di semua environment.
Branching Strategy dan Trigger Pipeline #
Strategi branching menentukan kapan dan apa yang dipicu oleh pipeline. Pilihan yang salah akan membuat pipeline kita lambat, tidak reliable, atau membingungkan developer. Tabel berikut membandingkan tiga strategi yang paling umum:
| Aspek | GitFlow | Trunk-Based | GitHub Flow |
|---|---|---|---|
| Cabang utama | main (releases) + develop (integrasi) |
main saja |
main saja |
| Feature branch | dari develop, merge balik ke develop |
dari main, merge cepat (≤1 hari) |
dari main, merge via PR |
| Release branch | ada, jangka panjang | tidak ada | tidak ada |
| Hotfix | dari main, merge ke main + develop |
langsung ke main |
langsung ke main |
| Pipeline trigger | banyak (per branch) | fokus ke main + short-lived |
fokus ke PR + main |
| Cocok untuk | produk dengan versioning ketat (library, API publik) | tim kecil hingga menengah, deploy sering | tim yang pakai GitHub, deploy via PR merge |
| Kompleksitas operasional | tinggi | rendah | rendah |
Untuk kebanyakan tim yang menjalankan Ansible sebagai deployment engine, trunk-based atau GitHub Flow adalah pilihan terbaik. Pipeline cukup trigger pada push ke main dan pull request; tidak perlu handle banyak branch berbeda dengan konfigurasi masing-masing.
Decision Tree Memilih Branching Strategy #
flowchart TD
A{"Apakah kita membutuhkan<br/>versi panjang<br/>untuk klien?"} -- Ya --> B["GitFlow"]
A -- Tidak --> C{"Deploy minimal<br/>1x per hari?"}
C -- Ya --> D["Trunk-Based Development"]
C -- Tidak --> E{"Apakah produk ini<br/>library/API publik<br/>dengan SemVer ketat?"}
E -- Ya --> F["GitFlow variant:<br/>release branch saja"]
E -- Tidak --> G["GitHub Flow"]
B --> H["Konfigurasi pipeline<br/>per branch"]
D --> I["Satu pipeline,<br/>trigger main + PR"]
F --> J["Pipeline fokus<br/>di release branch"]
G --> K["PR = preview deploy,<br/>merge = prod deploy"]
Jika kita baru memulai, pilih trunk-based development. Branch main selalu dalam kondisi deployable, feature branch berumur pendek (≤1 hari), dan pipeline cukup punya dua trigger: pull request (untuk validasi) dan push ke main (untuk deploy). Konfigurasi pipeline jadi sederhana dan developer tidak perlu pusing memikirkan branch mana yang harus di-merge duluan.
Memisahkan CI dan CD Pipeline #
Sekarang mari kita lihat implementasi konkret. Contoh di bawah ini memisahkan dua workflow di GitHub Actions — satu untuk build, satu untuk deploy dengan Ansible. Pola yang sama bisa diadopsi di GitLab CI dengan struktur stages yang berbeda.
# .github/workflows/ci.yml — Build dan test
name: CI
on:
push:
branches: ['**']
pull_request:
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: |
pip install -r requirements-dev.txt
pytest tests/
ansible-lint
build-image:
needs: lint-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
outputs:
image_tag: ${{ steps.meta.outputs.tags }}
image_digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: Generate image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: registry.company.com/myapp
tags: |
type=sha,prefix=,format=short
type=semver,pattern={{version}}
- name: Build dan push image
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# .github/workflows/cd.yml — Deploy dengan Ansible
name: CD
on:
workflow_run:
workflows: [CI]
types: [completed]
branches: [main]
jobs:
deploy-staging:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Setup Ansible
run: |
pip install ansible
ansible-galaxy install -r requirements.yml
- name: Ambil image tag dari CI run
id: get_tag
run: |
# Ambil tag dari output CI workflow
echo "IMAGE_TAG=${{ github.event.workflow_run.head_sha | truncate(7) }}" >> $GITHUB_ENV
- name: Deploy ke staging dengan Ansible
run: |
echo "${{ secrets.STAGING_SSH_KEY }}" > /tmp/id_ed25519
echo "${{ secrets.VAULT_PASS_STAGING }}" > /tmp/.vault_pass
chmod 600 /tmp/id_ed25519 /tmp/.vault_pass
ansible-playbook -i inventory/staging/ deploy.yml \
-e "app_version=${{ env.IMAGE_TAG }}" \
--vault-password-file /tmp/.vault_pass \
--private-key /tmp/id_ed25519
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production # Required reviewers diset di GitHub environment settings
steps:
- uses: actions/checkout@v4
- name: Deploy ke production
run: |
pip install ansible
ansible-galaxy install -r requirements.yml
echo "${{ secrets.PROD_SSH_KEY }}" > /tmp/id_ed25519
echo "${{ secrets.VAULT_PASS_PROD }}" > /tmp/.vault_pass
chmod 600 /tmp/id_ed25519 /tmp/.vault_pass
ansible-playbook -i inventory/production/ deploy.yml \
-e "app_version=${{ env.IMAGE_TAG }}" \
--vault-password-file /tmp/.vault_pass \
--private-key /tmp/id_ed25519
rm -f /tmp/id_ed25519 /tmp/.vault_pass
Perhatikan bagaimana CI workflow tidak tahu-menahu soal deployment. Ia hanya menghasilkan image dan menuliskan tag-nya di output. CD workflow menunggu CI sukses, lalu mengambil image yang sama persis dari registry untuk di-deploy ke staging, dan akhirnya ke production. Trigger workflow_run di CD adalah pola resmi dari GitHub untuk chain workflow tanpa coupling langsung.
Playbook Deploy yang Menerima Versi dari Pipeline #
Pipeline yang baik tidak akan pernah menjalankan playbook tanpa versi yang eksplisit. Ansible menyediakan filter mandatory yang bisa kita gunakan untuk memaksa variabel tertentu selalu ada:
# playbooks/deploy.yml
---
- name: Deploy aplikasi
hosts: appservers
vars:
# app_version HARUS dipass dari pipeline: -e "app_version=abc1234"
app_image: "registry.company.com/myapp:{{ app_version | mandatory }}"
pre_tasks:
- name: Verifikasi image tersedia di registry
command: "docker manifest inspect {{ app_image }}"
changed_when: false
delegate_to: localhost
tasks:
- name: Pull image ke setiap server
community.docker.docker_image:
name: "{{ app_image }}"
source: pull
force_source: true
- name: Deploy container baru
community.docker.docker_container:
name: myapp
image: "{{ app_image }}"
state: started
restart_policy: unless-stopped
recreate: true
pull: false # Sudah di-pull di atas
post_tasks:
- name: Verifikasi deployment berhasil
uri:
url: "http://localhost:{{ app_port }}/health"
status_code: 200
retries: 10
delay: 6
Tiga hal penting di playbook ini: pertama, app_version | mandatory memaksa pipeline mengirim versi eksplisit — kalau tidak ada, playbook langsung gagal dengan error yang jelas. Kedua, pre_tasks memverifikasi image benar-benar ada di registry sebelum mencoba pull, jadi kita tidak men-deploy placeholder. Ketiga, post_tasks menjalankan health check dengan retry — kalau container butuh waktu untuk start, deployment baru dianggap sukses setelah health endpoint benar-benar merespons 200.
Jangan pernah hardcodeapp_image: "registry.company.com/myapp:latest"di playbook. Imagelatestadalah tag mutable — bisa berubah antara waktu build dan waktu deploy. Kalau seseorang rebuild image di registry dengan tag yang sama (misalnya untuk hotfix), production tiba-tiba menjalankan kode yang tidak pernah di-test. Selalu tag dengan git SHA atau versi semver.
Pipeline Gate: Cek Sebelum Lanjut #
Gate adalah titik di pipeline yang harus dilewati sebelum lanjut ke stage berikutnya. Tanpa gate, deployment ke production akan terjadi otomatis setiap ada yang merge ke main — recipe untuk disaster.
# Tambahkan gate di antara staging dan production
validate-staging:
needs: deploy-staging
runs-on: ubuntu-latest
steps:
- name: Jalankan smoke test ke staging
run: |
# Test endpoint utama
curl -f https://staging.company.com/health
curl -f https://staging.company.com/api/version
- name: Cek error rate staging di Prometheus
run: |
ERROR_RATE=$(curl -s \
"https://prometheus.company.com/api/v1/query?query=rate(http_requests_total{status=~'5..',env='staging'}[5m])" \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(d['data']['result'][0]['value'][1] if d['data']['result'] else '0')")
if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
echo "Error rate staging terlalu tinggi: $ERROR_RATE"
exit 1
fi
Gate di atas punya dua lapis: smoke test untuk memastikan endpoint benar-benar hidup, dan Prometheus query untuk memastikan error rate tidak melonjak. Kalau salah satu gagal, pipeline mati di sini dan tidak lanjut ke production. Kita bisa tambahkan gate lebih ketat sesuai kebutuhan — security scan, performance test, atau approval manual dari reviewer.
Gate yang baik adalah gate yang bisa diotomasi dan di-repeat. Kalau gate-nya cuma “cek manual di Slack”, ia akan diabaikan saat sedang buru-buru. Kombinasikan automated check (Prometheus, test) dengan environment protection rule di GitHub atau GitLab untuk approval manual. Yang otomatis harus otomatis; yang manual harus eksplisit dan tercatat.
Observability Pipeline: Tahu Apa yang Terjadi #
Pipeline yang berjalan sukses tanpa terlihat prosesnya adalah pipeline yang membingungkan saat error. Kita perlu mengetahui: berapa lama setiap stage, di mana sering gagal, dan deployment mana yang baru saja selesai. Bagian ini cross-link ke section observability untuk detail lengkap tentang tooling-nya.
sequenceDiagram
participant Dev as Developer
participant GHA as GitHub Actions
participant Reg as Container Registry
participant Ans as Ansible
participant Mon as Monitoring
participant Slack as Slack/Alert
Dev->>GHA: push ke main
GHA->>GHA: lint + test (stage 1)
GHA->>GHA: build image (stage 2)
GHA->>Reg: push image tag=abc1234
GHA->>Mon: emit metric: ci_build_duration_seconds
GHA->>Mon: emit event: ci_build_success (image: abc1234)
GHA->>Ans: trigger deploy-staging
Ans->>Reg: pull image abc1234
Ans->>Mon: emit metric: deploy_duration_seconds (env: staging)
Ans->>Mon: emit event: deploy_complete (env: staging, version: abc1234)
Mon->>Slack: alert jika error_rate > threshold
Ans->>Mon: smoke test result
Mon-->>Dev: notifikasi deployment sukses/gagal
Sequence diagram di atas menunjukkan metadata apa saja yang harus di-emit di setiap titik kritis. Setidaknya kita membutuhkan:
- Metric
ci_build_duration_seconds— berapa lama setiap stage CI. Kalau tiba-tiba naik, ada bottleneck. - Metric
deploy_duration_seconds{env,version}— berapa lama deploy ke setiap environment. Anomali di sini bisa menandakan masalah jaringan atau konfigurasi. - Event
deploy_complete{env,version,timestamp}— catatan audit. Bisa dipakai untuk compliance (“siapa deploy apa kapan”) dan untuk correlating dengan insiden di production. - Alert otomatis saat error rate di staging melonjak setelah deploy — pipeline harusnya mati sebelum sempat ke production, tapi kalau lolos, monitoring adalah jaring pengaman terakhir.
Detail tentang cara setup metric collection, dashboard, dan alert ada di section observability — terutama artikel Metric Collection, Dashboard, dan Alerting. Hal yang perlu kita ingat di tahap desain pipeline: pipeline adalah sistem yang bisa diobservasi juga, bukan kotak hitam yang jalan lalu selesai.**, bukan kotak hitam yang jalan lalu selesai.
Anti-Pattern yang Harus Dihindari #
Berikut tiga anti-pattern yang paling sering muncul di pipeline CI/CD, lengkap dengan versi yang benar:
1. Re-Build Image di Setiap Environment #
# ANTI-PATTERN: image di-build ulang per environment
# Job ini build image yang sama di staging dan production
stages:
- build-staging
- build-production
- deploy-staging
- deploy-production
build-staging:
script:
- docker build -t myapp:$CI_COMMIT_SHA .
- docker push myapp:$CI_COMMIT_SHA-staging
stage: build-staging
build-production:
script:
- docker build -t myapp:$CI_COMMIT_SHA .
- docker push myapp:$CI_COMMIT_SHA-prod
stage: build-production
# ANTI-PATTERN: dua job build terpisah, dua tag berbeda untuk image
# yang isinya sama. Staging dan production tidak pernah benar-benar
# menjalankan kode yang sama persis.
# BENAR: build sekali, promote ke semua environment
stages:
- build
- deploy-staging
- deploy-production
build:
script:
- docker build -t myapp:$CI_COMMIT_SHA .
- docker push myapp:$CI_COMMIT_SHA
stage: build
deploy-staging:
script:
- ansible-playbook -i inventory/staging/ deploy.yml
-e "app_version=$CI_COMMIT_SHA"
stage: deploy-staging
deploy-production:
script:
- ansible-playbook -i inventory/production/ deploy.yml
-e "app_version=$CI_COMMIT_SHA"
stage: deploy-production
# BENAR: satu image dengan satu tag immutable dipromosikan ke
# staging dan production. Yang di-test di staging = yang jalan di
# production. Tidak ada kemungkinan drift.
2. Long-Running Pipeline yang Tidak Bisa Di-Resume #
# ANTI-PATTERN: satu workflow panjang yang harus restart dari awal
# kalau ada step yang gagal
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: ./build.sh # 10 menit
- run: ./test.sh # 5 menit
- run: ./deploy-staging.sh # 5 menit — gagal di sini
- run: ./deploy-prod.sh # Tidak pernah jalan, harus re-run semua
# ANTI-PATTERN: gagal di step 3 berarti 15 menit terbuang untuk
# build + test yang sebenarnya sudah sukses. Developer harus menunggu
# atau menghabiskan biaya runner dua kali.
# BENAR: pisahkan jadi job yang berdiri sendiri dengan cache
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ hashFiles('requirements.txt') }}
- run: ./build.sh
- run: ./test.sh
deploy-staging:
needs: test
runs-on: ubuntu-latest
steps:
- run: ./deploy-staging.sh
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
steps:
- run: ./deploy-prod.sh
# BENAR: kalau deploy-staging gagal, cukup re-run job itu saja.
# Test job yang sudah sukses tidak perlu diulang. Cache pip
# mempercepat iterasi.
3. Secret Hardcoded di Pipeline File #
# ANTI-PATTERN: secret ditulis langsung di workflow
jobs:
deploy:
steps:
- run: |
echo "ssh-rsa AAAAB3NzaC1yc2EAAAA..." > /tmp/deploy_key
echo "vaultpassword123" > /tmp/.vault_pass
ansible-playbook -i inventory/prod/ deploy.yml
# ANTI-PATTERN: secret di-commit ke git. Siapapun yang punya akses
# baca ke repository bisa lihat production credential. Rotasi
# credential juga harus re-commit, yang biasanya ditunda-tunda.
# BENAR: secret diambil dari secret manager dan di-cleanup otomatis
jobs:
deploy:
steps:
- name: Ambil SSH key dari secret
env:
SSH_KEY: ${{ secrets.PROD_SSH_KEY }}
run: |
echo "$SSH_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
- name: Ambil vault password dari secret
env:
VAULT: ${{ secrets.VAULT_PASS_PROD }}
run: |
echo "$VAULT" > /tmp/.vault_pass
chmod 600 /tmp/.vault_pass
- name: Deploy
run: ansible-playbook -i inventory/prod/ deploy.yml \
--vault-password-file /tmp/.vault_pass \
--private-key /tmp/deploy_key
- name: Cleanup
if: always()
run: rm -f /tmp/deploy_key /tmp/.vault_pass
# BENAR: secret hanya hidup di memory runner selama job berjalan.
# Setelah selesai (sukses atau gagal), file langsung dihapus.
# Repository tetap bersih, audit trail jelas.
Ringkasan #
- Pisahkan CI dan CD — CI menghasilkan artifact terverifikasi, CD mendistribusikannya. Deployment bukan bagian dari proses build.
- Artifact immutable: tag image Docker dengan git SHA atau versi semantik, bukan
latest. Image yang sama dipromosikan dari staging ke production.- Environment promotion: artifact yang sama digunakan di semua environment — ini membuktikan bahwa apa yang di-test di staging adalah persis apa yang di-deploy ke production.
app_version | mandatorydi playbook memastikan versi selalu dipass dari pipeline — deployment tidak bisa berjalan tanpa versi yang eksplisit.- Gate antara staging dan production: smoke test dan cek metrik sebelum promotion — otomatis menghentikan deployment jika ada masalah di staging.
- Branching strategy sederhana (trunk-based atau GitHub Flow) membuat pipeline lebih simpel dan reliable — banyak branch = banyak konfigurasi = banyak bug.
- Pipeline emits metrics dan events ke sistem monitoring — bukan kotak hitam. Setidaknya lacak durasi stage, success rate, dan event deploy per environment.
- Hapus credential dengan
rm -fdi setiap step setelah selesai, gunakanif: always()untuk cleanup yang berjalan meski pipeline gagal.