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.

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 | 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.
  • 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