Pelajari percabangan logika dengan when, perulangan modern dengan loop, hingga retry loop with until agar playbook Ansible adaptif terhadap kondisi server yang beragam di dunia nyata.

Setelah di episode 8 sebelumnya kita membahas bagaimana menghasilkan file konfigurasi dinamis menggunakan Jinja2 templating dan filters, pada episode kali ini kita akan naik satu level: membuat logika di dalam playbook itu sendiri. Kalau di episode sebelumnya kita membuat data yang dinamis, sekarang kita akan membuat perilaku yang dinamis.
Kalian mungkin pernah mengalami situasi seperti ini di dunia kerja nyata: satu playbook yang sama ternyata harus berjalan di puluhan server dengan sistem operasi berbeda-beda — sebagian Ubuntu, sebagian Rocky Linux, bahkan sebagian lagi di environment staging dan production. Kalau playbook ditulis secara kaku, kalian akan berakhir membuat beberapa versi playbook yang hampir sama, saling menggandakan (duplicate), dan sulit dipelihara. Di sinilah conditionals dan loops berperan: keduanya memungkinkan satu playbook yang sama beradaptasi dengan kondisi setiap server dan menjalankan banyak task secara berulang tanpa menulis ulang kode.
Pada episode ini kita akan membahas dua kemampuan inti control flow di Ansible, yaitu pengkondisian menggunakan keyword when untuk percabangan logika, dan perulangan menggunakan keyword loop beserta varian lanjutannya seperti until untuk retry. Kita juga akan membahas kesalahan umum yang sering menjebak pemula, sehingga kalian bisa menulis playbook yang benar sejak awal.
Bayangkan kalian sedang memasak dengan resep yang berisi langkah "jika tidak punya saus tomat, gunakan saus sambal". Resep yang baik adalah resep yang beradaptasi dengan bahan yang tersedia. Begitu juga playbook yang baik: playbook harus beradaptasi dengan kondisi server yang dituju. Di Ansible, percabangan logika ini ditangani oleh keyword when yang bisa dipasang di level task, block, maupun play.
when: Dasar Keputusan di Setiap TaskKeyword when menerima sebuah ekspresi Jinja2 yang hasil evaluasinya harus bernilai boolean. Jika ekspresi tersebut bernilai true, task akan dijalankan; jika false, task akan di-skip dan ditampilkan sebagai SKIPPED pada output. Perhatikan contoh sederhana berikut:
- name: Restart service aplikasi
hosts: all
become: true
tasks:
- name: Restart service hanya di production
ansible.builtin.systemd_service:
name: app
state: restarted
when: env == "production"Jika kalian menjalankan playbook ini dengan --extra-vars "env=staging" atau variabel env bernilai selain production, maka task di atas tidak akan dieksekusi sama sekali. Nilai yang diuji oleh when tidak melulu harus berasal dari variabel — kalian juga bisa menguji facts (data sistem yang di-gather Ansible, yang sudah kita bahas di episode 7), hasil registrasi task lain, atau atribut dari inventory. Contohnya, menguji family sistem operasi:
- name: Cek family OS semua server
hosts: all
tasks:
- name: Tampilkan family OS
ansible.builtin.debug:
msg: "OS family server ini adalah {{ ansible_facts['os_family'] }}"and, or, dan notDalam sistem nyata, sebuah keputusan jarang hanya bergantung pada satu syarat. Ansible menyediakan operator logika Jinja2 yang bisa kalian kombinasikan sesuka hati. Berikut ringkasan operator yang paling sering dipakai:
| Operator | Fungsi | Contoh |
|---|---|---|
and | Semua syarat harus benar | when: ansible_facts['os_family'] == "Debian" and env == "production" |
or | Salah satu syarat benar sudah cukup | when: env == "staging" or env == "production" |
not | Membalikkan hasil evaluasi | when: not maintenance_window |
() | Mengelompokkan prioritas logika | when: (env == "staging" or env == "production") and not maintenance_window |
Sama seperti matematika, tanda kurung sangat penting saat kondisi sudah kompleks. Tanpa tanda kurung, kombinasi and dan or bisa dievaluasi dengan cara yang tidak kalian harapkan, karena and memiliki prioritas lebih tinggi daripada or. Contoh kombinasi tiga operator:
- name: Reboot server untuk maintenance
hosts: all
become: true
tasks:
- name: Reboot hanya di staging/production, kecuali sedang maintenance
ansible.builtin.reboot:
when: (env == "staging" or env == "production") and not maintenance_windowKondisi yang panjang juga bisa dipecah menjadi beberapa baris menggunakan sintaks YAML list, di mana setiap elemen list secara implisit digabung dengan and:
- name: Skenario multi-kondisi
hosts: all
tasks:
- name: Jalankan migrasi database
ansible.builtin.command: bundle exec rails db:migrate
when:
- env == "production"
- ansible_facts['os_family'] == "Debian"
- deploy_needed | boolapt vs dnf Berdasarkan OS FamilySalah satu skenario klasik di dunia nyata adalah meng-install package yang sama pada server dengan OS yang berbeda. Package manager di Debian/Ubuntu (apt) berbeda dengan RedHat/Rocky/Fedora (dnf). Tanpa kondisional, kalian harus membuat playbook terpisah. Dengan when, satu playbook bisa menangani keduanya:
- name: Install nginx sesuai family OS
hosts: all
become: true
gather_facts: true
tasks:
- name: Install nginx via apt (Debian / Ubuntu)
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
when: ansible_facts['os_family'] == "Debian"
- name: Install nginx via dnf (RedHat / Rocky / Fedora)
ansible.builtin.dnf:
name: nginx
state: present
when: ansible_facts['os_family'] == "RedHat"Tip
Fakta ansible_facts['os_family'] di-generate oleh modul setup saat gather_facts aktif. Untuk melihat seluruh facts yang tersedia, jalankan ansible <host> -m ansible.builtin.setup di terminal kalian. Di Ubuntu hasilnya Debian, di Rocky Linux hasilnya RedHat, sehingga pengecekan di atas bekerja secara otomatis tanpa perlu hardcode nama OS.
Sekarang bayangkan kalian harus meng-install sepuluh package di setiap server. Kalian bisa menulis sepuluh task apt yang identik hanya berbeda nama package-nya — tetapi itu artinya sepuluh baris yang sama, rawan salah ketik, dan sulit dipelihara. Analoginya seperti fotokopi resep sepuluh kali padahal intinya satu: "install setiap package dalam daftar ini". Di sinilah loop mengambil alih.
loopSejak Ansible 2.5, sintaks perulangan yang direkomendasikan adalah keyword loop. Sintaks ini lebih sederhana, lebih mudah dibaca, dan menjadi standar ke depan. Variabel default yang mewakili setiap elemen perulangan adalah item.
Contoh paling dasar — perulangan atas sebuah list package:
- name: Install package dasar
hosts: all
become: true
tasks:
- name: Install beberapa package sekaligus
ansible.builtin.apt:
name: "{{ item }}"
state: present
update_cache: true
loop:
- git
- curl
- htop
- jqTask di atas akan dieksekusi empat kali, masing-masing dengan nilai item yang berbeda (git, lalu curl, dan seterusnya). Dalam output eksekusi, setiap iterasi akan tampil sebagai apt: name=git, apt: name=curl, dan seterusnya, sehingga kalian bisa melacak iterasi mana yang berhasil atau gagal.
List juga bisa berisi dictionary, sehingga setiap iterasi bisa membawa beberapa data sekaligus — misalnya untuk membuat user secara massal:
- name: Buat user untuk tim engineering
hosts: all
become: true
tasks:
- name: Buat user tiap developer
ansible.builtin.user:
name: "{{ item.name }}"
shell: "{{ item.shell | default('/bin/bash') }}"
groups: "{{ item.groups | default('devs') }}"
state: present
loop:
- { name: budi, shell: /bin/bash, groups: "devs,sudo" }
- { name: siti, groups: "devs" }
- { name: agus, shell: /bin/zsh, groups: "ops" }Perhatikan bagaimana kita memanfaatkan filter default dari episode 8: field shell dan groups menjadi opsional, dan siti secara otomatis mendapatkan nilai default. Kombinasi loop dengan item.field dan filter inilah yang membuat playbook tetap ringkas meskipun datanya beragam.
item.key & item.valueSelain list, kalian juga sering berurusan dengan dictionary. Untuk melakukan loop atas dictionary, Ansible merekomendasikan penggunaan filter dict2items yang mengubah dictionary menjadi list of {key, value}. Setelah itu, setiap item akan memiliki atribut item.key dan item.value:
- name: Tulis konfigurasi environment variable
hosts: all
become: true
vars:
app_config:
LOG_LEVEL: info
PORT: "8080"
DATABASE_URL: postgres://app:pass@db.example.com/app
tasks:
- name: Tulis setiap key-value ke file konfigurasi
ansible.builtin.lineinfile:
path: /etc/app/app.conf
line: "{{ item.key }}={{ item.value }}"
create: true
loop: "{{ app_config | dict2items }}"Important
Jangan lupa filter dict2items saat melakukan loop atas dictionary dengan loop. Tanpa filter tersebut, perilakunya tidak konsisten dan sulit ditebak. Kalian juga bisa menyesuaikan nama atribut hasil konversi dengan parameter key_name dan value_name, misalnya dict2items(key_name='file', value_name='path').
loop_controlTerkadang kita perlu lebih dari sekadar item. Keyword loop_control menyediakan opsi untuk memodifikasi perilaku perulangan:
| Opsi | Fungsi |
|---|---|
loop_var | Mengganti nama variabel item (penting untuk nested loop agar tidak terjadi konflik) |
index_var | Menyediakan nomor indeks iterasi mulai dari 0 |
label | Menampilkan label ringkas di output, berguna saat item berisi struktur data besar |
pause | Menyisipkan jeda (detik) antar iterasi, berguna untuk menghindari rate limit API |
extended | Menambahkan metadata seperti ansible_loop.index, ansible_loop.first, dan ansible_loop.last |
Contoh penggunaannya sekaligus menunjukkan bagaimana label membuat output jauh lebih mudah dibaca:
- name: Deploy konfigurasi untuk setiap service
hosts: all
become: true
tasks:
- name: Buat direktori konfigurasi per service
ansible.builtin.file:
path: "/etc/app/{{ item.name }}"
state: directory
loop:
- { name: api, port: 8080 }
- { name: web, port: 3000 }
- { name: worker, port: 5000 }
loop_control:
label: "service {{ item.name }} (port {{ item.port }})"Tanpa label, output akan mencetak seluruh dictionary {name: api, port: 8080} untuk setiap iterasi — berisik di layar. Dengan label, setiap iterasi tampil sebagai service api (port 8080) yang langsung dimengerti manusia.
until: Retry Hingga Kondisi TerpenuhiAda satu jenis perulangan yang sering disalahpahami: until. Ini bukan perulangan untuk mengolah data, melainkan retry loop — mengulangi eksekusi task yang sama berulang kali sampai sebuah kondisi terpenuhi atau batas percobaan tercapai. Ini sangat berguna untuk menangani keadaan sementara (transient), misalnya menunggu service database benar-benar siap sebelum aplikasi mencoba terhubung.
Contoh nyata: menunggu PostgreSQL primary menerima koneksi sebelum menjalankan migrasi database:
- name: Tunggu PostgreSQL primary siap
ansible.builtin.command: pg_isready -h db-primary -p 5432 -U app
register: db_ready
until: db_ready.rc == 0
retries: 12
delay: 5
failed_when: db_ready.rc != 0Warning
Keyword until wajib dikombinasikan dengan register dan retries. Ansible akan mengevaluasi ekspresi until terhadap hasil task yang ter-register, menjalankan ulang task setiap delay detik, dan berhenti setelah retries kali percobaan. Jika semua percobaan gagal, task akan ditandai sebagai FAILED dengan pesan yang menampilkan hasil percobaan terakhir. Catatan lain: until tidak didukung pada keyword include_tasks.
Pola until sangat umum dipakai untuk menunggu port terbuka, menunggu status container healthy, atau menunggu aplikasi selesai restart. Ini adalah fondasi dari strategi resilience yang akan kita perdalam di episode berikutnya.
with_items ke loopSebelum Ansible 2.5, perulangan ditulis dengan sintaks with_* seperti with_items, with_dict, with_sequence, dan with_fileglob. Sintaks ini sekarang deprecated dan akan dikeluarkan dari Ansible core, sehingga playbook modern wajib bermigrasi ke loop. Kabar baiknya, migrasi ini hampir selalu hanya mengganti nama keyword-nya saja:
- name: Install beberapa package dasar
ansible.builtin.apt:
name: "{{ item }}"
state: present
with_items:
loop:
- git
- curl
- htopAturan migrasi yang perlu diingat:
with_items → loop — cukup ganti keyword, isi list tetap sama.with_dict → loop + filter dict2items atau dictsort.with_indexed_items → loop + loop_control.index_var dan filter dict2items.with_fileglob, with_sequence, dan lookup with_* lainnya → loop + lookup plugin, misalnya loop: "{{ query('ansible.builtin.fileglob', 'files/*.conf') }}".Tip
Perbedaan penting: with_items mem-flatten list satu level (misalnya list di dalam list digabung menjadi satu), sedangkan loop tidak melakukan flatten otomatis — item yang berupa list akan diperlakukan sebagai satu elemen list yang utuh. Jika playbook lama kalian bergantung pada perilaku flatten ini, tambahkan filter flatten secara eksplisit: loop: "{{ list_of_lists | flatten }}".
1. Membandingkan string seolah-olah boolean
Ini adalah jebakan paling klasik. Pertimbangkan:
- name: Contoh yang salah
hosts: all
vars:
service_enabled: "no"
tasks:
- name: Restart service
ansible.builtin.systemd_service:
name: app
state: restarted
when: service_enabledNilai "no" adalah sebuah string, bukan boolean. Dalam aturan truthiness Jinja2, string non-kosong selalu dianggap true — bahkan "no", "false", dan "0" sekalipun. Akibatnya, task di atas akan selalu dijalankan, meskipun kalian berniat menonaktifkannya. Solusinya: pastikan variabel benar-benar bertipe boolean, atau konversi secara eksplisit dengan filter | bool:
- name: Contoh yang benar
hosts: all
vars:
service_enabled: "no"
tasks:
- name: Restart service hanya jika diaktifkan
ansible.builtin.systemd_service:
name: app
state: restarted
when: service_enabled | boolDengan | bool, string "no" dievaluasi menjadi false dan task tidak dijalankan. Selalu tanyakan pada diri sendiri: apakah variabel yang saya uji benar-benar bertipe boolean?
2. Salah memahami output SKIPPED
Ketika sebuah task di-skip karena when bernilai false, Ansible tetap melanjutkan eksekusi playbook — ini adalah perilaku yang diharapkan. Namun banyak pemula mengira playbook berhenti atau menganggap skip sebagai error. Sebaliknya, SKIPPED adalah status normal dan sehat. Barulah FAILED yang harus diperhatikan, dan penanganannya akan kita bahas di episode berikutnya.
3. Konflik nama item pada nested loop
Jika kalian melakukan loop di dalam loop (nested loop), keduanya menggunakan variabel item yang sama — dan yang inner akan menimpa yang outer. Gunakan loop_control.loop_var untuk memberi nama berbeda:
- name: Deploy multi-environment
hosts: localhost
vars:
environments: [dev, staging]
services: [api, web]
tasks:
- name: Kombinasi environment dan service
ansible.builtin.debug:
msg: "{{ env }}-{{ svc }}"
loop: "{{ environments }}"
loop_control:
loop_var: env
# perulangan kedua harus bersarang di dalam task dengan include_tasks,
# atau kombinasikan kedua list terlebih dahulu dengan filter product()Pada episode 11 kita akan melihat bagaimana nested loop ditangani dengan benar menggunakan include_tasks dan loop_var.
4. Lupa menambahkan register pada until
Tanpa register, ekspresi until tidak punya hasil task untuk dievaluasi dan playbook langsung gagal dengan pesan error yang membingungkan. Selalu daftarkan hasilnya terlebih dahulu.
Pada episode 9 ini kita telah mempelajari dua pilar control flow di Ansible: pengkondisian dengan when yang memungkinkan satu playbook beradaptasi dengan kondisi tiap server (termasuk studi kasus apt vs dnf berdasarkan OS family), serta perulangan dengan loop yang mencakup loop atas list, loop atas dictionary dengan item.key dan item.value, pengendalian iterasi dengan loop_control, retry loop until, hingga migrasi dari sintaks with_items yang sudah deprecated.
Kemampuan ini mengubah playbook kalian dari sekadar daftar instruksi statis menjadi program yang benar-benar berpikir: memutuskan apakah sebuah task perlu dijalankan, dan menjalankannya berulang kali dengan efisien. Namun, playbook yang cerdas belum tentu tangguh — apa jadinya jika salah satu task gagal di tengah jalan? Bagaimana caranya memastikan sistem tetap konsisten saat terjadi kesalahan?
Di episode 10 selanjutnya kita akan membahas Error Handling & Resilience Strategies, mulai dari ignore_errors, failed_when, changed_when, hingga struktur block, rescue, dan always untuk membangun playbook yang tidak mudah menyerah di tengah kegagalan. Pastikan tetap semangat!