Performance Anti Pattern

Performance Anti Pattern #

Kecepatan dan efisiensi eksekusi adalah kunci keberhasilan otomatisasi infrastruktur skala besar. Saat kita menulis playbook Ansible untuk pertama kalinya, fokus utama kita biasanya hanya pada kebenaran logika — memastikan bahwa server terkonfigurasi dengan benar. Namun, seiring bertambahnya jumlah server yang dikelola (managed nodes), efisiensi playbook mulai diuji. Playbook yang awalnya selesai dalam 2 menit untuk 3 server dapat membengkak menjadi 45 menit ketika dijalankan untuk 100 server. Lambatnya eksekusi ini sering kali bukan disebabkan oleh kapasitas server yang tidak memadai, melainkan oleh keputusan desain playbook yang memicu operasi berulang dan tidak efisien.

Feedback loop yang lambat akan memperlambat siklus pengembangan, menghambat proses deployment darurat, dan memperluas jendela risiko kegagalan (maintenance window). Sebagian besar bottleneck di Ansible dapat diidentifikasi dan diperbaiki dengan mudah tanpa harus mengubah logika akhir sistem. Dengan memahami bagaimana Ansible berinteraksi dengan server remote di balik layar, kita dapat mengoptimalkan playbook agar berjalan berkali-kali lipat lebih cepat. Dalam artikel ini, kita akan mengeksplorasi berbagai anti pattern performa, mengapa pola tersebut memperlambat kita, serta cara menerapkan solusi optimasi yang tepat.


1. Mengaktifkan Gathering Facts Tanpa Menggunakannya #

Secara default, Ansible akan menjalankan modul setup di awal setiap play untuk mengumpulkan fakta (facts) tentang server target, seperti detail sistem operasi, alamat IP, alokasi memori, hingga status disk. Proses ini dikenal sebagai gathering facts.

Mengapa Ini Berbahaya? #

Proses pengumpulan fakta membutuhkan waktu sekitar 1 hingga 3 detik per host. Ansible harus melakukan SSH handshake, mentransfer modul pengumpul fakta, mengeksekusinya di server remote, dan mengembalikan data JSON yang sangat besar ke control node. Jika playbook kita memiliki banyak play dan mengelola ratusan server, namun kita sama sekali tidak menggunakan variabel bawaan Ansible (ansible_*) di dalam logika task kita, maka proses ini membuang waktu dan resource komputasi yang sangat besar secara sia-sia.

# ANTI-PATTERN: Membiarkan gathering facts menyala di play yang tidak membutuhkannya
- name: Restart Layanan Aplikasi Web
  hosts: webservers
  # gather_facts secara default bernilai true jika tidak didefinisikan!
  tasks:
    - name: Restart layanan systemd nginx
      systemd:
        name: nginx
        state: restarted
      become: true
    # Playbook ini berjalan lambat di ratusan host karena mengumpulkan data hardware
    # yang sama sekali tidak dibaca oleh modul systemd di atas.

Solusi mendasar adalah dengan menonaktifkan gather_facts pada play yang hanya melakukan tugas-tugas operasional atau konfigurasi statis yang tidak bergantung pada kondisi hardware server remote.

# BENAR: Menonaktifkan gathering facts untuk tugas operasional sederhana
- name: Restart Layanan Aplikasi Web secara Cepat
  hosts: webservers
  gather_facts: false   # ✓ BENAR: Menghemat waktu bootstrap koneksi SSH awal
  tasks:
    - name: Restart layanan systemd nginx
      systemd:
        name: nginx
        state: restarted
      become: true

Jika playbook kita memang membutuhkan informasi fakta server tetapi tersebar di beberapa play yang berbeda, kita dapat mengaktifkan sistem fact caching. Dengan cara ini, fakta server hanya dikumpulkan sekali di awal, disimpan dalam cache lokal (seperti file JSON atau database Redis), dan digunakan kembali pada eksekusi play berikutnya tanpa perlu melakukan kueri ulang ke server remote.

# BENAR: Mengonfigurasi Fact Caching di ansible.cfg
# File: ansible.cfg
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_fact_cache
fact_caching_timeout = 86400  # Cache berlaku selama 24 jam

2. Menggunakan Loop untuk Task yang Mendukung Operasi Batch #

Ansible menyediakan directive loop (atau with_items pada versi lama) untuk menjalankan satu task secara berulang dengan parameter input yang berbeda-beda.

Mengapa Ini Berbahaya? #

Jika kita menggunakan loop pada modul-modul sistem paket manager seperti apt, yum, atau pip, Ansible akan mengeksekusi modul tersebut secara individual sebanyak jumlah item di dalam loop. Artinya, jika ada 10 paket yang ingin diinstal, Ansible akan memanggil perintah paket manager 10 kali secara terpisah. Setiap panggilan membutuhkan waktu penyelesaian (round-trip) SSH baru, inisialisasi modul, penguncian database paket (seperti apt lock), dan pemeriksaan dependensi. Hal ini membuat waktu instalasi menjadi sangat lambat.

# ANTI-PATTERN: Menggunakan loop untuk instalasi paket satu per satu
- name: Instalasi Paket Sistem Pemeliharaan
  apt:
    name: "{{ item }}"
    state: present
  loop:
    - nginx
    - postgresql-client
    - redis-tools
    - python3-pip
    - git
  become: true
  # ✗ JANGAN: Ansible akan memanggil perintah apt-get install sebanyak 5 kali terpisah!

Sebagai gantinya, kita harus mengirimkan daftar (list) paket secara langsung ke parameter modul. Sebagian besar modul bawaan Ansible seperti apt, yum, package, dan pip telah dioptimalkan untuk menerima list dan mengeksekusinya dalam satu operasi batch tunggal.

# BENAR: Melakukan instalasi massal (batch install) dalam satu pemanggilan modul
- name: Instalasi Paket Sistem Pemeliharaan secara Batch
  apt:
    name:
      - nginx
      - postgresql-client
      - redis-tools
      - python3-pip
      - git
    state: present
    update_cache: true
  become: true
  # ✓ BENAR: Hanya satu pemanggilan perintah apt-get install. Jauh lebih cepat dan efisien.

[!TIP] Pola optimasi batch ini juga berlaku untuk modul-modul lain. Misalnya, saat menyalin banyak file statis, alih-alih menggunakan loop dengan modul copy yang memakan waktu SSH berulang kali, kita bisa menggunakan modul synchronize yang memanfaatkan utilitas rsync di balik layar untuk mentransfer file secara massal dalam satu koneksi tunggal.


3. Membiarkan Nilai Forks pada Konfigurasi Bawaan (Default forks = 5) #

Secara default, Ansible dikonfigurasi untuk memproses tugas secara paralel pada maksimal 5 server sekaligus. Parameter ini diatur oleh opsi forks di konfigurasi ansible.cfg.

Mengapa Ini Berbahaya? #

Jika kita memiliki inventory yang berisi 100 server remote, Ansible hanya akan mengeksekusi task pada 5 server pertama, menunggu semuanya selesai, lalu melanjutkan ke 5 server berikutnya. Untuk menyelesaikan satu task di seluruh host, Ansible membutuhkan 20 batch antrean (100 / 5 = 20). Jika satu task membutuhkan waktu 10 detik, total waktu eksekusi task tersebut menjadi 200 detik. Ini menciptakan antrean bottleneck yang sangat panjang padahal resource CPU dan memori di control node kita masih sangat longgar.

# ANTI-PATTERN: Membiarkan batas forks default membatasi paralelisasi
# File: ansible.cfg (Atau tanpa konfigurasi kustom sama sekali)
[defaults]
# forks = 5   # ✗ JANGAN: Batas bawaan yang terlalu rendah untuk infrastruktur modern

Kita harus menaikkan nilai forks sesuai dengan kapasitas hardware control node (CPU dan RAM) serta bandwidth jaringan kita. Sebagai aturan praktis, kita dapat menaikkan nilai forks ke angka 20 hingga 50 untuk mengoptimalkan paralelisasi tanpa membebani control node secara berlebihan.

# BENAR: Meningkatkan nilai forks di ansible.cfg
# File: ansible.cfg
[defaults]
forks = 30   # ✓ BENAR: Memproses 30 server secara bersamaan, mempercepat eksekusi hingga 600%

Untuk menentukan angka forks yang optimal bagi infrastruktur kita, lakukan pengujian bertahap sambil memantau penggunaan resource sistem pada control node menggunakan perintah berikut:

# Monitor utilisasi resource CPU dan memory control node saat playbook berjalan
ansible-playbook -i inventory/production/ site.yml &
watch -n1 'ps aux | grep ansible | head -5; echo "---"; uptime'

4. Mengabaikan Optimasi Koneksi SSH (Pipelining dan Multiplexing) #

Secara default, proses eksekusi task Ansible pada target host melalui protokol SSH melibatkan beberapa tahapan: Ansible menghasilkan file script python sementara, membungkusnya, mentransfer file tersebut ke direktori sementara di host target menggunakan SFTP/SCP, memberikan izin eksekusi, menjalankan file tersebut, dan akhirnya menghapus file tersebut setelah selesai.

Mengapa Ini Berbahaya? #

Siklus salin-dan-jalankan file script ini terjadi untuk setiap task pada setiap host. Ini menghasilkan overhead transfer file dan inisialisasi koneksi SSH yang sangat tinggi, terutama jika playbook kita memiliki puluhan task pendek. Tanpa optimasi koneksi SSH, sebagian besar waktu eksekusi playbook kita dihabiskan untuk menegosiasikan enkripsi koneksi dan mentransfer file script kecil.

Berikut adalah diagram perbandingan antara proses eksekusi default (tanpa optimasi) dibandingkan dengan proses eksekusi yang telah dioptimalkan menggunakan Pipelining dan SSH Multiplexing (ControlPersist):

sequenceDiagram
    autonumber
    participant C as "Control Node (Ansible)"
    participant S as "Managed Node (Server)"
    Note over C,S: Tanpa Optimasi SSH (Default)
    C->>S: "Inisiasi TCP & Kriptografi Handshake SSH"
    S-->>C: "Sesi SSH Terbuka"
    C->>S: "Transfer file script modul python (.py)"
    S-->>C: "Transfer selesai"
    C->>S: "Jalankan script python remote"
    S-->>C: "Hasil eksekusi modul"
    C->>S: "Tutup Koneksi SSH"
    Note over C,S: Dengan Optimasi (Pipelining & ControlPersist)
    C->>S: "Inisiasi TCP & Kriptografi Handshake SSH (Sekali saja)"
    S-->>C: "Sesi Multiplexing Terbuka di socket lokal"
    C->>S: "Eksekusi langsung via stdin (Pipelined)"
    S-->>C: "Hasil eksekusi modul"
    C->>S: "Eksekusi task berikutnya via socket yang sama"
    S-->>C: "Hasil eksekusi modul"
    Note over C,S: Soket tetap terbuka selama ControlPersist (e.g. 60s)

Untuk mengatasi masalah overhead koneksi ini, kita harus mengaktifkan dua fitur penting di konfigurasi SSH Ansible: Pipelining dan SSH Multiplexing (ControlPersist).

# BENAR: Mengaktifkan Pipelining dan Multiplexing SSH di ansible.cfg
# File: ansible.cfg
[defaults]
forks = 20

[ssh_connection]
pipelining = True
# ✓ BENAR: Pipelining mengeksekusi modul python langsung melalui stdin SSH tanpa mentransfer file.
# Ini dapat mengurangi waktu eksekusi task hingga 30% - 50%.

ssh_args = -o ControlMaster=auto -o ControlPersist=120s -o ControlPath=/tmp/ansible-ssh-%h-%p-%r
# ✓ BENAR: Mengaktifkan multiplexing SSH. Koneksi kriptografi awal ke server remote akan disimpan
# sebagai file soket di /tmp/. Task-task berikutnya akan menggunakan kembali terowongan (tunnel) SSH
# yang sudah ada tanpa perlu melakukan handshake ulang. ControlPersist=120s menjaga terowongan tetap
# terbuka selama 2 menit setelah task terakhir selesai.

[!WARNING] Saat mengaktifkan pipelining = True, pastikan konfigurasi keamanan di managed node (server remote) tidak memiliki opsi requiretty di file /etc/sudoers. Jika opsi tersebut aktif, eksekusi privilege escalation via sudo akan gagal karena pipelining tidak menyediakan pseudo-TTY. Nonaktifkan requiretty untuk user ansible untuk memanfaatkan fitur ini.


5. Menjalankan Task Lokal Berulang Tanpa run_once #

Ada kalanya kita perlu menjalankan task yang berinteraksi dengan control node (localhost), seperti mengambil kredensial dari API eksternal (misalnya HashiCorp Vault), membuat direktori backup lokal, atau mencatat notifikasi ke Slack.

Mengapa Ini Berbahaya? #

Jika kita menggunakan directive delegate_to: localhost tanpa menyertakan run_once: true, Ansible akan mengeksekusi task tersebut secara berulang sebanyak jumlah target host yang ada di dalam play kita. Jika kita mengelola 50 server, Ansible akan mengirimkan 50 request API yang identik ke Vault atau Slack. Hal ini dapat memicu pemblokiran batas request (rate limiting) dari API target, membuang bandwidth, dan memperlambat playbook secara drastis karena pemrosesan antrean yang tidak perlu.

# ANTI-PATTERN: Menjalankan task delegasi lokal berulang kali untuk setiap host
- name: Ambil Token Otorisasi API dari Server Pusat
  hosts: appservers
  tasks:
    - name: Dapatkan token akses sementara
      uri:
        url: "https://vault.company.internal/v1/auth/token"
        method: POST
        body_format: json
        body:
          role_id: "app-role"
          secret_id: "app-secret"
      register: vault_response
      delegate_to: localhost
      # ✗ JANGAN: Task ini dipanggil berulang kali untuk setiap host di appservers!
      # Jika ada 100 host, 100 request HTTP akan dikirim ke Vault dengan hasil yang persis sama.

Solusinya adalah dengan menambahkan parameter run_once: true pada task delegasi lokal tersebut. Ansible hanya akan mengeksekusi task tersebut satu kali saja, lalu membagikan hasil variabel yang diregistrasikan ke seluruh host lain yang ada di dalam play.

# BENAR: Menggunakan run_once untuk membatasi eksekusi task lokal tunggal
- name: Ambil Token Otorisasi API secara Efisien
  hosts: appservers
  tasks:
    - name: Dapatkan token akses sementara (Cukup sekali saja)
      uri:
        url: "https://vault.company.internal/v1/auth/token"
        method: POST
        body_format: json
        body:
          role_id: "app-role"
          secret_id: "app-secret"
      register: vault_response
      delegate_to: localhost
      run_once: true              # ✓ BENAR: Dieksekusi 1 kali saja untuk seluruh play
      no_log: true

    - name: Terapkan token ke file konfigurasi aplikasi
      template:
        src: "config.json.j2"
        dest: "/etc/myapp/config.json"
      # Hasil 'vault_response' dari host pertama otomatis dapat diakses oleh host lain

6. Penjadwalan Serial Tanpa Paralelisasi Tugas (Serial vs Free Strategy) #

Secara default, Ansible menggunakan strategi eksekusi linear. Dalam strategi ini, Ansible akan menyelesaikan task pertama di seluruh host yang aktif sebelum beralih ke task kedua secara bersamaan.

Mengapa Ini Berbahaya? #

Jika salah satu host mengalami penundaan (delay) yang lama saat menjalankan suatu task (misalnya karena masalah performa disk atau koneksi jaringan lambat), seluruh antrean playbook akan tertahan. Server-server lain yang sudah menyelesaikan task pertama lebih cepat terpaksa menunggu server lambat tersebut selesai sebelum bisa melanjutkan ke task berikutnya.

Kita dapat mengoptimalkan alur kerja ini dengan menyesuaikan strategi eksekusi atau menggunakan parameter serial dengan bijak.

# ALTERNATIF: Menggunakan Strategi Free untuk Tugas Independen
- name: Pengumpulan Log dan Analisis Sistem
  hosts: database_servers
  strategy: free   # ✓ BENAR: Setiap host akan mengeksekusi semua task secepat mungkin
  # tanpa harus menunggu host lain menyelesaikan task yang sama. Cocok untuk play
  # yang bertugas melakukan pengumpulan data atau verifikasi status independen.
  tasks:
    - name: Kumpulkan statistik disk
      command: df -h
    - name: Kumpulkan status memory
      command: free -m

[!IMPORTANT] Jangan gunakan strategy: free jika task-task di dalam playbook kita memiliki dependensi urutan yang ketat di antara host yang berbeda (misalnya: mematikan load balancer -> update app server -> menyalakan load balancer). Untuk skenario deployment aplikasi tanpa downtime (rolling update), gunakan opsi serial dengan proporsi persentase yang seimbang agar tidak memperlambat rilis namun tetap membatasi blast radius kegagalan.

# BENAR: Rolling Update dengan Proporsi Serial yang Seimbang
- name: Deploy Aplikasi Web Cluster
  hosts: webservers
  serial: "25%"  # ✓ BENAR: Memproses 25% server sekaligus dalam satu batch.
  # Jika batch pertama berhasil, lanjut ke batch berikutnya. Ini memberikan keamanan
  # tanpa harus mengorbankan performa eksekusi seperti halnya 'serial: 1'.
  tasks:
    - name: Update source code
      git:
        repo: "https://github.com/app/web.git"
        dest: "/var/www/html"

7. Mengabaikan Profiling Task untuk Menganalisis Bottleneck #

Mengoptimalkan performa playbook tanpa melakukan pengukuran terlebih dahulu adalah tindakan spekulatif yang sering kali salah sasaran. Kita mungkin menghabiskan waktu berjam-jam untuk merestrukturisasi task yang sebenarnya hanya memakan waktu 2 detik dari total 15 menit waktu eksekusi.

Mengapa Ini Berbahaya? #

Tanpa visualisasi data durasi eksekusi, kita tidak tahu di mana letak hambatan sebenarnya. Apakah karena proses pembaruan repositori paket manager? Ataukah karena modul template yang lambat memproses logika Jinja2? Kita memerlukan sistem profiling otomatis untuk mengukur durasi setiap task.

Kita dapat mengaktifkan plugin callback bawaan Ansible untuk melakukan profiling waktu eksekusi tugas secara detail tanpa perlu menambahkan tool pihak ketiga.

# BENAR: Mengaktifkan Callback Profiling di ansible.cfg
# File: ansible.cfg
[defaults]
# Aktifkan callback bawaan untuk profiling tugas, role, dan timer total
callbacks_enabled = profile_tasks, profile_roles, timer

Setelah callback diaktifkan, setiap kali kita menjalankan playbook, Ansible akan menampilkan ringkasan durasi task terlama di akhir output terminal.

# Contoh Output Terminal Profiling setelah Playbook Selesai:
Thursday 18 June 2026  15:45:10 +0700 (0:00:03.245)       0:08:42.123
===================================================================
Install all packages in batch ------------------------------- 185.34s  ← Target Utama Optimasi!
Gathering Facts ---------------------------------------------- 72.15s  ← Aktifkan Fact Caching!
Compile application assets from source ----------------------- 54.12s
Verify database migration state ------------------------------ 12.04s
Template configuration file ----------------------------------- 8.11s

Dengan data ini, kita tahu persis task mana yang harus kita optimalkan terlebih dahulu. Fokuskan upaya optimasi kita pada task-task teratas yang memakan persentase waktu terbesar.


Ringkasan #

  • Matikan gather_facts: false — Untuk semua play operasional yang tidak membaca properti hardware target host. Gunakan fact caching jika fakta tetap dibutuhkan di beberapa play.
  • Hindari Loop Paket Manager — Kirim daftar paket sebagai list langsung ke argumen modul (apt, yum, pip) untuk mengeksekusinya dalam satu batch tunggal, alih-alih mengulangnya satu per satu.
  • Tingkatkan Batas forks — Jangan biarkan konfigurasi forks default di angka 5 membatasi proses paralelisasi pada infrastruktur skala besar. Set minimal di angka 20 hingga 50.
  • Aktifkan Pipelining dan ControlPersist — Konfigurasikan opsi SSH pada ansible.cfg untuk meminimalkan overhead cryptographic handshake dan proses transfer file script sementara.
  • Batasi Delegasi Lokal dengan run_once — Selalu sertakan run_once: true pada task yang menggunakan delegate_to: localhost untuk mencegah pengiriman request berulang yang identik ke API eksternal.
  • Pilih Strategi Eksekusi yang Sesuai — Gunakan strategy: free untuk tugas audit atau pengumpulan data independen, dan terapkan proporsi serial berbasis persentase untuk rolling update yang efisien.
  • Lakukan Profiling Durasi secara Teratur — Aktifkan callback profile_tasks dan profile_roles di ansible.cfg untuk mendapatkan visualisasi data durasi guna memandu prioritas optimasi performa.

← Sebelumnya: Security Anti Pattern   Berikutnya: CI/CD Anti Pattern →

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