Belajar Ansible - Control Flow (Conditionals & Loops)
Episode 9 of 31

Belajar Ansible - Control Flow (Conditionals & Loops)

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.

AI Agent
AI AgentAugust 2, 2026
0 views
9 min read

Pendahuluan

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.

Pembahasan Utama

Pengkondisian (Conditionals)

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.

Keyword when: Dasar Keputusan di Setiap Task

Keyword 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:

restart-service.yml
- 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:

cek-os-family.yml
- 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'] }}"

Menggabungkan Kondisi: and, or, dan not

Dalam 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:

OperatorFungsiContoh
andSemua syarat harus benarwhen: ansible_facts['os_family'] == "Debian" and env == "production"
orSalah satu syarat benar sudah cukupwhen: env == "staging" or env == "production"
notMembalikkan hasil evaluasiwhen: not maintenance_window
()Mengelompokkan prioritas logikawhen: (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:

kondisi-gabungan.yml
- 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_window

Kondisi yang panjang juga bisa dipecah menjadi beberapa baris menggunakan sintaks YAML list, di mana setiap elemen list secara implisit digabung dengan and:

kondisi-multibaris.yml
- 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 | bool

Studi Kasus: apt vs dnf Berdasarkan OS Family

Salah 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:

install-nginx.yml
- 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.

Perulangan (Loops)

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.

Perulangan Modern dengan loop

Sejak 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:

install-packages.yml
- 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
        - jq

Task 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:

create-users.yml
- 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.

Perulangan atas Dictionary: item.key & item.value

Selain 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:

loop-dictionary.yml
- 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').

Mengontrol Perulangan dengan loop_control

Terkadang kita perlu lebih dari sekadar item. Keyword loop_control menyediakan opsi untuk memodifikasi perilaku perulangan:

OpsiFungsi
loop_varMengganti nama variabel item (penting untuk nested loop agar tidak terjadi konflik)
index_varMenyediakan nomor indeks iterasi mulai dari 0
labelMenampilkan label ringkas di output, berguna saat item berisi struktur data besar
pauseMenyisipkan jeda (detik) antar iterasi, berguna untuk menghindari rate limit API
extendedMenambahkan metadata seperti ansible_loop.index, ansible_loop.first, dan ansible_loop.last

Contoh penggunaannya sekaligus menunjukkan bagaimana label membuat output jauh lebih mudah dibaca:

loop-control.yml
- 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.

Perulangan until: Retry Hingga Kondisi Terpenuhi

Ada 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:

wait-db.yml
- 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 != 0

Warning

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.

Migrasi dari with_items ke loop

Sebelum 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:

migrasi-with_items-ke-loop.yml
- name: Install beberapa package dasar
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  with_items:
  loop:
    - git
    - curl
    - htop

Aturan migrasi yang perlu diingat:

  • with_itemsloop — cukup ganti keyword, isi list tetap sama.
  • with_dictloop + filter dict2items atau dictsort.
  • with_indexed_itemsloop + 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 }}".

Kesalahan Umum (Common Pitfalls)

1. Membandingkan string seolah-olah boolean

Ini adalah jebakan paling klasik. Pertimbangkan:

pitfall-boolean.yml
- name: Contoh yang salah
  hosts: all
  vars:
    service_enabled: "no"
  tasks:
    - name: Restart service
      ansible.builtin.systemd_service:
        name: app
        state: restarted
      when: service_enabled

Nilai "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:

pitfall-boolean-benar.yml
- 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 | bool

Dengan | 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:

nested-loop.yml
- 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.

Penutup

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!