Pipeline Design

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 job build yang jalan di staging dan job build yang jalan di production dengan kode yang sama, kita kehilangan jaminan bahwa production == staging. Build sekali, tag immutable (git SHA atau semver), 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 hardcode app_image: "registry.company.com/myapp:latest" di playbook. Image latest adalah 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 | mandatory di 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 -f di setiap step setelah selesai, gunakan if: always() untuk cleanup yang berjalan meski pipeline gagal.

← Sebelumnya: AWX & Tower   Berikutnya: GitHub Actions →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact