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.
Prinsip Desain Pipeline #
1. Fail Fast
Tempatkan pengecekan yang paling cepat di awal pipeline.
Jangan jalankan deployment ke staging jika lint saja sudah gagal.
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.
3. Idempoten di Setiap Tahap
Setiap tahap harus aman dijalankan berulang kali tanpa efek samping.
Re-run pipeline yang gagal tidak boleh merusak state.
4. Immutable Artifact
Setelah build, artifact tidak boleh berubah.
Tag image Docker dengan versi spesifik, bukan 'latest'.
5. Separation of Concern
Build pipeline ≠ Deployment pipeline.
Build menghasilkan artifact; deploy mendistribusikannya.
Struktur Pipeline yang Direkomendasikan #
┌─────────────────────────────────────────────────────────────┐
│ CI Pipeline (dipicu oleh push/PR) │
│ │
│ Lint → Unit Test → Build Image → Push to Registry │
│ │
└─────────────────────────────────────────────────────────────┘
│
image:2.1.0-abc123
│
┌─────────────────────────────────────────────────────────────┐
│ CD Pipeline (dipicu oleh merge ke main) │
│ │
│ Deploy Staging → Integration Test → Approval → Deploy Prod │
│ ↑ │
│ (Ansible mengambil image yang SAMA dari registry) │
│ │
└─────────────────────────────────────────────────────────────┘
Memisahkan CI dan CD Pipeline #
# .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
Playbook Deploy yang Menerima Versi dari Pipeline #
# 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
Pipeline Gate: Cek Sebelum Lanjut #
# 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
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.
- Hapus credential dengan
rm -fdi setiap step setelah selesai, gunakanif: always()untuk cleanup yang berjalan meski pipeline gagal.