CI/CD Anti Pattern

CI/CD Anti Pattern #

Integrasi otomatisasi infrastruktur dengan jalur pipa Integrasi Berkelanjutan dan Pengiriman Berkelanjutan (CI/CD) adalah puncak kematangan operasional tim DevOps. Dengan menggabungkan Ansible ke dalam alat seperti GitLab CI, GitHub Actions, atau Jenkins, kita dapat mewujudkan model Infrastructure as Code (IaC) secara penuh, di mana setiap perubahan konfigurasi server dapat dideploy secara otomatis begitu kode digabungkan ke repositori utama. Namun, mengotomatiskan deployment tanpa disertai dengan pagar pengaman (guardrails) yang tepat justru dapat meningkatkan risiko kegagalan sistem.

Pipeline CI/CD yang dirancang dengan buruk bertindak seperti penguat kesalahan (error amplifier). Jika proses deployment manual yang lambat memberikan kita kesempatan untuk mendeteksi kesalahan di tengah jalan, pipeline otomatis tanpa validasi akan menyebarkan kesalahan konfigurasi atau bug keamanan ke seluruh server produksi dalam hitungan detik. Tanpa adanya pemisahan lingkungan yang jelas, validasi sintaksis otomatis, pengujian terisolasi, serta mekanisme penanganan kegagalan yang andal, kita hanya sedang membangun jalan tol bagi penyebaran insiden. Di dalam artikel ini, kita akan mengulas secara mendalam berbagai anti pattern dalam integrasi Ansible dengan CI/CD dan mempelajari cara mengatasinya.


1. Mendeploy Perubahan secara Langsung ke Production Tanpa Melalui Staging #

Salah satu kesalahan desain alur kerja CI/CD yang paling umum adalah memicu deployment langsung ke server produksi (production environment) begitu ada komit baru di branch utama (seperti main atau master), tanpa melewati proses verifikasi di lingkungan perantara (staging).

Mengapa Ini Berbahaya? #

Setiap kode playbook atau konfigurasi template baru berisiko membawa bug, seperti kesalahan penulisan nama variabel, dependensi paket yang bentrok, atau perubahan konfigurasi sistem yang tidak kompatibel. Jika perubahan ini langsung diaplikasikan ke server produksi tanpa diuji terlebih dahulu di lingkungan staging yang serupa, layanan pengguna dapat langsung lumpuh. Kita kehilangan kesempatan terakhir untuk melakukan pengujian fungsional dan smoke testing pada sistem yang berjalan secara nyata.

# ANTI-PATTERN: Pipeline yang langsung mendeploy ke production tanpa pengaman
# File: .github/workflows/deploy.yml
on:
  push:
    branches:
      - main  # Memicu eksekusi begitu ada merge ke branch main

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Jalankan deployment langsung ke server utama
        run: |
          ansible-playbook -i inventory/production/site.yml site.yml
          # ✗ JANGAN: Tidak ada lingkungan staging, tidak ada smoke test, langsung menyentuh production!          

Solusi untuk mengatasi anti pattern ini adalah dengan membangun pipeline multi-tahap (multi-stage pipeline). Perubahan harus dideploy ke lingkungan staging terlebih dahulu secara otomatis. Setelah deploy staging berhasil, kita wajib menjalankan skrip pengujian otomatis (smoke test). Terakhir, untuk merilis ke production, kita harus menerapkan mekanisme persetujuan manual (manual approval gate) untuk memastikan tim memiliki kendali penuh atas waktu rilis.

# BENAR: Alur pipeline terstruktur dengan Staging, Smoke Test, dan Approval
# File: .github/workflows/deploy.yml
on:
  push:
    branches:
      - main

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Deploy ke Lingkungan Staging
        run: |
          ansible-playbook -i inventory/staging/site.yml site.yml          

  verify-staging:
    runs-on: ubuntu-latest
    needs: deploy-staging
    steps:
      - name: Jalankan Smoke Test di Staging
        run: |
          # Melakukan kueri endpoint kesehatan aplikasi di staging
          curl -f --retry 5 --retry-delay 5 https://staging-app.company.internal/health
          # ✓ BENAR: Memastikan aplikasi menyala dan merespons dengan benar sebelum melangkah lebih jauh          

  deploy-production:
    runs-on: ubuntu-latest
    needs: verify-staging
    environment:
      name: production  # ✓ BENAR: Mengaktifkan opsi Manual Approval di setelan GitHub Actions
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Deploy ke Lingkungan Production
        run: |
          ansible-playbook -i inventory/production/site.yml site.yml          

Berikut adalah visualisasi alur pipa (pipeline workflow) yang aman dari komit hingga rilis ke produksi:

flowchart TD
    Trigger["Developer melakukan Push / Merge ke branch main"] --> Validate["Linting & Syntax Check: ansible-lint"]
    Validate -- "Gagal" --> NotifyFail["Kirim Notifikasi Kegagalan & Batalkan Pipeline"]
    Validate -- "Berhasil" --> Molecule["Molecule Test (Local Testing/Ephemeral VM)"]
    Molecule -- "Gagal" --> NotifyFail
    Molecule -- "Berhasil" --> DeployStaging["Deploy ke Lingkungan Staging"]
    DeployStaging --> SmokeTest["Smoke Test & Integration Test di Staging"]
    SmokeTest -- "Gagal" --> RollbackStaging["Rollback Staging & Investigasi Log"]
    RollbackStaging --> NotifyFail
    SmokeTest -- "Berhasil" --> ManualApproval{"Manual Approval (Prod Gate)?"}
    ManualApproval -- "Ditolak / Timeout" --> Cancel["Batalkan Rilis Production"]
    ManualApproval -- "Disetujui" --> DeployProd["Deploy ke Lingkungan Production (Rolling Update)"]
    DeployProd --> PostCheck["Post-Deployment Health Check di Production"]
    PostCheck -- "Gagal" --> RollbackProd["Rollback Otomatis di Production"]
    RollbackProd --> NotifyFail
    PostCheck -- "Berhasil" --> Complete["Deployment Selesai & Sukses!"]

2. Menyimpan Kredensial secara Keras (Hardcoded) dalam Definisi Pipeline #

Untuk menjalankan playbook Ansible pada target remote dari runner CI/CD, kita memerlukan berbagai kredensial, seperti SSH private key, kata sandi enkripsi Ansible Vault, token API untuk modul dynamic inventory, atau kredensial cloud provider.

Mengapa Ini Berbahaya? #

Menuliskan nilai kredensial ini secara plaintext di dalam file definisi pipeline (seperti .gitlab-ci.yml atau .github/workflows/deploy.yml) adalah pelanggaran keamanan yang sangat serius. Siapa pun yang memiliki akses baca ke repositori dapat melihat kredensial tersebut. Selain itu, kredensial tersebut juga akan terekam dalam log Git history secara permanen. Kesalahan fatal lainnya adalah membiarkan kredensial tercetak di log konsol runner CI/CD karena kita lupa menyembunyikan output perintah penulisan file sementara.

# ANTI-PATTERN: Menuliskan kredensial sensitif secara plaintext di file pipeline
# File: .gitlab-ci.yml
deploy-job:
  stage: deploy
  script:
    - echo "my-super-secret-vault-pass" > /tmp/.vault_pass  # ✗ JANGAN: Vault password terekspos di Git!
    - ansible-playbook site.yml --vault-password-file /tmp/.vault_pass \
        -e "ssh_key_content=-----BEGIN RSA PRIVATE KEY-----..."  # ✗ JANGAN: SSH key dimasukkan sebagai string inline

Untuk mengatasinya, kita harus memanfaatkan fitur variabel rahasia (Secret Variables / Masked Variables) yang disediakan oleh platform CI/CD kita. Kredensial tidak boleh ditulis di dalam repositori, melainkan disimpan di panel konfigurasi admin CI/CD, disetel sebagai Masked (agar otomatis disensor berupa bintang-bintang *** jika tidak sengaja tercetak di log), dan diakses di dalam pipeline sebagai variabel lingkungan (environment variables).

# BENAR: Menggunakan Masked CI/CD Variables dan membersihkan file kredensial secara aman
# File: .gitlab-ci.yml
variables:
  # Opsi di bawah ini merujuk ke variabel rahasia yang telah dikonfigurasi di panel GitLab
  # VAULT_PASSWORD_PROD disetel sebagai jenis "Variable" (Masked + Protected)
  # PROD_SSH_PRIVATE_KEY disetel sebagai jenis "File" (Protected)

deploy-job:
  stage: deploy
  script:
    # Buat file password vault dari variabel rahasia dengan izin ketat
    - echo "$VAULT_PASSWORD_PROD" > "${CI_PROJECT_DIR}/.vault_pass"
    - chmod 600 "${CI_PROJECT_DIR}/.vault_pass"
    
    # Amankan izin file SSH private key yang dilewatkan sebagai variabel jenis File
    - chmod 600 "$PROD_SSH_PRIVATE_KEY"
    
    # Jalankan playbook dengan referensi file kredensial yang aman
    - ansible-playbook -i inventory/production/site.yml site.yml \
        --vault-password-file "${CI_PROJECT_DIR}/.vault_pass" \
        --private-key "$PROD_SSH_PRIVATE_KEY"
        
  after_script:
    # ✓ BENAR: Pastikan file password vault lokal selalu dihapus dari runner setelah eksekusi selesai
    # untuk mencegah kebocoran sisa data pada runner yang digunakan bersama (shared runner)
    - rm -f "${CI_PROJECT_DIR}/.vault_pass"

3. Merancang Pipeline yang Tidak Aman Di-Retry (Idempotensi Rendah) #

Runner CI/CD dapat mengalami kegagalan di tengah proses eksekusi karena berbagai alasan yang tidak terduga, seperti pemutusan koneksi jaringan, kehabisan resource memori pada runner, atau crash pada sistem target host saat proses deployment sedang berjalan.

Mengapa Ini Berbahaya? #

Jika playbook Ansible kita tidak dirancang secara idempoten (yaitu tidak aman dijalankan berulang kali dengan input yang sama), maka menekan tombol Retry pada pipeline yang gagal dapat memperparah kerusakan sistem. Misalnya, task yang menambahkan konfigurasi baris baru menggunakan modul shell dengan perintah append polos (echo "config" >> file.conf) akan menduplikasi baris tersebut setiap kali pipeline dijalankan ulang. Hal ini membuat file konfigurasi menjadi rusak dan aplikasi gagal memuat ulang data.

# ANTI-PATTERN: Task yang tidak idempoten dan berbahaya jika di-run ulang
- name: Setup Konfigurasi Aplikasi Web
  hosts: appservers
  tasks:
    - name: Unduh source code versi baru (Hapus folder lama terlebih dahulu)
      shell: "rm -rf /var/www/html/* && wget -O /tmp/src.tar.gz https://api.com/src.tar.gz && tar -xzf /tmp/src.tar.gz -C /var/www/html"
      # ✗ JANGAN: Task ini tidak idempoten. Jika koneksi wget terputus di tengah jalan,
      # server remote dibiarkan dalam kondisi folder /var/www/html kosong (downtime total)!

Untuk menjamin keamanan eksekusi ulang, kita harus memastikan setiap task di dalam playbook kita bersifat idempoten — hanya melakukan perubahan jika status sistem saat ini belum sesuai dengan target yang dideklarasikan. Selain itu, kita harus memanfaatkan blok penanganan error (block dan rescue) untuk melakukan rollback otomatis jika terjadi kegagalan di tengah jalan.

# BENAR: Menggunakan modul deklaratif idempoten dan mengimplementasikan rollback otomatis
- name: Setup Konfigurasi Aplikasi Web secara Aman dan Idempoten
  hosts: appservers
  serial: 2                    # Jalankan deployment pada maksimal 2 server sekaligus
  max_fail_percentage: 0       # Segera batalkan seluruh play jika ada 1 server saja yang gagal
  tasks:
    - block:
        - name: Unduh source code versi baru secara aman
          git:
            repo: "https://git.company.internal/app/web.git"
            dest: "/var/www/html"
            version: "{{ deploy_version }}"
          # ✓ BENAR: Modul git secara bawaan bersifat idempoten. Ia membandingkan commit hash
          # dan hanya mengunduh perbedaan data, tanpa menghapus folder tujuan secara kasar.

        - name: Jalankan uji kesehatan lokal (smoke check)
          uri:
            url: "http://localhost:8080/health"
            status_code: 200
          retries: 5
          delay: 5
          register: health_check
          # Verifikasi kesehatan aplikasi sebelum melanjutkan ke batch server berikutnya

      rescue:
        - name: Rollback ke versi stabil jika deployment mengalami error
          git:
            repo: "https://git.company.internal/app/web.git"
            dest: "/var/www/html"
            version: "{{ rollback_version }}"
          # ✓ BENAR: Secara otomatis mengembalikan kode ke commit stabil sebelumnya jika block di atas gagal

        - name: Hentikan jalannya pipeline dengan pesan error jelas
          fail:
            msg: "Deployment gagal pada host {{ inventory_hostname }}. Rollback otomatis ke versi {{ rollback_version }} telah selesai dilakukan."

4. Mengabaikan Pengujian Otomatis (Testing) di dalam Pipeline #

Banyak tim DevOps mengonfigurasi pipeline CI/CD mereka hanya sebagai eksekutor perintah deploy polos. Begitu ada perubahan kode, pipeline langsung menjalankan perintah ansible-playbook.

Mengapa Ini Berbahaya? #

Mengabaikan pengujian otomatis sebelum deployment sama saja dengan membiarkan kesalahan ketik sintaksis (syntax error) atau kesalahan penulisan spasi (indentation error) menyabotase server produksi kita. Kesalahan indentasi YAML yang sepele dapat menyebabkan Ansible salah menafsirkan parameter dan merusak logika eksekusi. Selain itu, tanpa adanya pengujian otomatis untuk memvalidasi kepatuhan gaya penulisan, playbook kita akan dipenuhi oleh utang teknis (technical debt) yang menyulitkan kolaborasi tim.

Kita wajib menyertakan tahap pengujian (test stage) di awal pipeline sebelum playbook diizinkan menyentuh lingkungan mana pun. Tahap ini minimal harus melakukan pemeriksaan sintaksis (--syntax-check) dan analisis statis menggunakan ansible-lint. Untuk pengujian tingkat lanjut yang lebih andal, kita harus mengintegrasikan pengujian unit (unit testing) menggunakan Molecule.

# BENAR: Menjalankan validasi sintaksis dan linting secara otomatis di awal pipeline
# File: .github/workflows/deploy.yml
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'

      - name: Instalasi Ansible dan Ansible Lint
        run: |
          pip install ansible ansible-lint          

      - name: Jalankan Pemeriksaan Sintaksis Playbook
        run: |
          ansible-playbook site.yml --syntax-check -i inventory/staging/site.yml
          # ✓ BENAR: Memastikan file YAML dapat diparsing dengan benar oleh mesin Ansible          

      - name: Jalankan Analisis Statis Kode (Linter)
        run: |
          ansible-lint site.yml
          # ✓ BENAR: Memeriksa kepatuhan kode terhadap aturan best practice Ansible secara otomatis          

5. Kompleksitas Pipeline yang Berlebihan dan Sulit Didokumentasikan #

Dalam upaya membuat sistem otomatisasi yang sangat dinamis, tidak jarang tim membangun arsitektur pipeline yang sangat kompleks. Mereka menggunakan puluhan job yang saling bergantung, logika kondisional yang berbelit-belit, generator file dinamis di dalam pipeline, serta pemanggilan sub-pipeline yang berlapis-lapis.

Mengapa Ini Berbahaya? #

Pipeline yang terlalu rumit dan over-engineered menjadi sangat sulit dipahami oleh anggota tim lainnya. Ketika terjadi kegagalan eksekusi, proses debugging membutuhkan waktu yang sangat lama karena kita harus menelusuri rantai dependensi job yang rumit. Selain itu, kompleksitas ini menjadi utang teknis yang menakutkan — tidak ada anggota tim yang berani memodifikasi atau memperbarui file konfigurasi pipeline karena takut merusak alur kerja keseluruhan yang tidak terdokumentasi dengan baik.

Prinsip utama dalam merancang pipeline CI/CD untuk Ansible adalah kesederhanaan dan kejelasan alur. Setiap langkah di dalam pipeline harus dapat dipahami oleh engineer baru dalam waktu 5 menit tanpa memerlukan dokumentasi eksternal yang tebal.

# BENAR: Alur pipeline yang sederhana, bersih, dan mudah dipahami
# Setiap job memiliki tujuan yang jelas, berjalan pada kondisi yang eksplisit,
# dan tidak memiliki rantai dependensi yang berbelit-belit.
# File: .github/workflows/deploy.yml
jobs:
  # Job 1: Pengujian kode secara statis (Selalu berjalan pada Pull Request)
  test-and-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run linter
        run: pip install ansible-lint && ansible-lint site.yml

  # Job 2: Deploy ke Staging (Hanya berjalan setelah merge ke branch main dan Job 1 sukses)
  deploy-staging:
    runs-on: ubuntu-latest
    needs: test-and-lint
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - name: Deploy Staging
        run: ansible-playbook -i inventory/staging/site.yml site.yml

  # Job 3: Deploy ke Production (Hanya berjalan setelah deploy Staging sukses & butuh approval)
  deploy-production:
    runs-on: ubuntu-latest
    needs: deploy-staging
    environment: production  # Memicu persetujuan manual di environment production
    steps:
      - uses: actions/checkout@v4
      - name: Deploy Production
        run: ansible-playbook -i inventory/production/site.yml site.yml

6. Pengelolaan Penyimpanan Artifact Pipeline yang Tidak Teratur #

Ansible menghasilkan berbagai output eksekusi yang berharga, seperti file laporan perubahan (deployment-report.txt), status audit keamanan, file konfigurasi terkompilasi, atau file database cache sementara.

Mengapa Ini Berbahaya? #

Ada dua ekstrem kesalahan dalam mengelola artifact di pipeline:

  1. Tidak menyimpan artifact sama sekali: Jika pipeline gagal atau terjadi masalah setelah deployment selesai, kita tidak memiliki rekaman log rinci atau status sistem saat kegagalan terjadi untuk diinvestigasi. Kita kehilangan jejak audit penting.
  2. Menyimpan semua artifact selamanya: Menyimpan folder build berukuran besar atau log eksekusi lengkap secara permanen tanpa batas waktu kedaluwarsa akan membuat kapasitas penyimpanan (storage) server CI/CD atau cloud bucket membengkak dengan cepat. Ini menghasilkan tagihan biaya penyimpanan yang tidak perlu.

Kita harus mengonfigurasi penyimpanan artifact dengan menentukan batas waktu kedaluwarsa (expire_in atau retention-days) secara jelas. Simpan file-file penting yang membantu proses debugging saja, dan atur waktu retensi yang wajar (misalnya 7 hingga 30 hari).

# BENAR: Mengonfigurasi penyimpanan artifact dengan retensi yang tepat
# File: .gitlab-ci.yml
build-assets:
  stage: build
  script:
    - mkdir -p build/
    - ansible-playbook -i inventory/staging/site.yml build-assets.yml
    - echo "Build selesai pada $(date)" > build/build-report.txt
  artifacts:
    paths:
      - build/
      - .fact_cache/
    expire_in: 14 days    # ✓ BENAR: Artifact otomatis dihapus setelah 14 hari untuk menghemat storage
    when: always          # ✓ BENAR: Simpan artifact bahkan jika job gagal untuk mempermudah debugging

Ringkasan #

  • Wajibkan Jalur Pengujian Staging — Jangan pernah membiarkan perubahan playbook langsung diaplikasikan ke server produksi tanpa divalidasi terlebih dahulu di lingkungan staging yang identik secara struktural.
  • Amankan Kredensial via Secret Variables — Hilangkan semua bentuk hardcode kredensial di repositori Git. Gunakan variabel rahasia berjenis masked/file yang disediakan platform CI/CD, dan bersihkan file penampung setelah digunakan.
  • Terapkan Prinsip Idempotensi untuk Tombol Retry — Pastikan setiap task aman dijalankan berulang kali tanpa merusak state sistem. Gunakan blok rescue untuk menangani kegagalan dan memicu rollback otomatis.
  • Integrasikan Pengujian Otomatis — Jadikan pemeriksaan sintaksis (--syntax-check) dan analisis statis menggunakan ansible-lint sebagai prasyarat utama yang harus lolos sebelum deployment diizinkan berjalan.
  • Sederhanakan Alur Kerja Pipeline — Hindari kompleksitas berlebih pada definisi pipeline. Buat alur kerja yang linier, intuitif, dan dapat dipahami dengan cepat oleh seluruh anggota tim DevOps.
  • Atur Batas Waktu Retensi Artifact — Konfigurasikan opsi kedaluwarsa pada penyimpanan artifact pipeline (seperti expire_in: 14 days) agar log debugging tetap tersedia saat dibutuhkan tanpa menghabiskan kuota penyimpanan server.

← Sebelumnya: Performance Anti Pattern   Berikutnya: Project Structure →

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