Pecah playbook raksasa menjadi file-file kecil yang teratur dengan include_tasks, import_tasks, dan import_playbook, serta pahami kapan memilih dynamic include dibandingkan static import.

Setelah di episode 10 sebelumnya kita membahas bagaimana membuat playbook yang tangguh dengan error handling, block, rescue, dan always, pada episode kali ini kita akan membahas persoalan yang pasti kalian hadapi begitu playbook mulai tumbuh: bagaimana mengelola kompleksitasnya.
Ingat pengalaman kalian saat aplikasi pertama kali tumbuh besar? Satu file sumber yang tadinya seratus baris menjadi tiga ribu baris, dan setiap orang takut menyentuhnya karena satu perubahan kecil bisa merusak segalanya. Playbook Ansible mengalami nasib yang sama. Playbook yang semua task-nya ditulis dalam satu file memang mudah di awal, tetapi ketika sudah berisi puluhan task untuk provisioning, instalasi package, konfigurasi service, dan deployment aplikasi sekaligus, file tersebut menjadi sulit dibaca, sulit diuji, dan sulit dipakai ulang di playbook lain.
Dalam dunia kerja nyata, prinsip reusability adalah kunci. Tim infrastruktur profesional tidak menulis ulang task yang sama berulang kali; mereka memecahnya menjadi blok-blok yang bisa dipakai kembali di banyak playbook. Pada episode ini kita akan membahas konsep modularisasi, perbedaan mendasar antara dynamic include dan static import beserta tabel perbandingannya, contoh penerapan import_tasks, include_tasks, dan import_playbook, serta kesalahan umum yang sering menjebak. Nanti di episode 12, konsep ini akan menjadi fondasi untuk memahami Ansible Roles — standar industri untuk mengemas automasi.
Bayangkan sebuah playbook monolith berisi 200 task yang menangani semuanya: dari instalasi package, pembuatan user, konfigurasi firewall, sampai deployment aplikasi. Ada tiga masalah besar di sini:
Prinsip yang sama dengan DRY (Don't Repeat Yourself) di dunia pemrograman berlaku di sini: setiap blok logika ditulis sekali, lalu digunakan dari banyak tempat. Modularisasi juga mendukung pemisahan tanggung jawab: task-file untuk install, task-file untuk configure, dan task-file untuk deploy — masing-masing bisa diuji dan dipelihara secara independen.
Berikut contoh struktur direktori sebuah project yang sudah dimodularisasi. Perhatikan bagaimana setiap tanggung jawab dipisahkan ke file yang jelas:
ansible-project/
├── ansible.cfg
├── inventory/
│ └── production.yml
├── group_vars/
│ └── all.yml
├── playbooks/
│ ├── site.yml # playbook entry point utama
│ ├── webservers.yml # playbook khusus web server
│ └── database.yml # playbook khusus database
└── tasks/
├── common.yml # task-file: setup dasar semua host
├── webserver.yml # task-file: install & config nginx
├── database.yml # task-file: install & config postgres
└── cleanup.yml # task-file: pembersihan artefakDengan struktur ini, playbook utama menjadi ringkas dan intent-revealing: dari sekadar membacanya, kalian langsung paham bahwa site.yml mengatur common setup, web server, dan database secara berurutan. File-file kecil di dalam tasks/ menyimpan detail implementasinya.
Note
Istilah penting yang harus dibedakan sejak awal: yang sedang kita modularkan di episode ini adalah task list (daftar task) dan playbook (daftar play). Task list dimasukkan ke dalam play menggunakan import_tasks / include_tasks, sedangkan playbook dimasukkan ke playbook lain menggunakan import_playbook. Nanti di episode 12, kalian akan belajar cara mengemas ketiganya sekaligus dengan Roles.
Ansible menyediakan dua cara untuk menggunakan kembali file: dynamic (include_*) dan static (import_*). Perbedaan fundamentalnya terletak pada kapan file diproses:
Perbedaan waktu pemrosesan ini memunculkan serangkaian konsekuensi perilaku yang sangat penting. Mari kita bandingkan secara lengkap:
| Aspek | include_tasks (Dynamic) | import_tasks (Static) |
|---|---|---|
| Waktu diproses | Saat runtime, ketika include dieksekusi | Saat parsing playbook, sebelum eksekusi |
| Opsi task (tags, when, become) | Hanya berlaku untuk task include itu sendiri | Otomatis diterapkan ke semua task di dalam file |
| Looping | Bisa di-loop (task dijalankan per item) | Tidak bisa — error You cannot use loops on import_tasks |
| Variabel pada nama file | Boleh: include_tasks: "{{ ansible_os_family }}.yml" | Tidak bisa memakai variabel inventory/runtime pada nama file |
when pada statement | Mengevaluasi sekali untuk seluruh file | Disalin ke setiap task di dalam file |
--list-tasks / --list-tags | Task dan tags di dalam file tidak muncul | Semua task dan tags terlihat |
--start-at-task | Tidak bisa memulai dari task di dalam include | Bisa dimulai dari task yang di-import |
Handler (notify) | Harus me-notify nama include itu sendiri | Bisa me-notify task individual di dalam file |
Warning
Satu mitos yang perlu diluruskan: tidak ada include_playbook di Ansible. Playbook hanya bisa dimasukkan secara static menggunakan import_playbook. Berbeda dengan task list yang punya versi dinamis (include_tasks), playbook tidak memiliki padanan runtime — jika kalian pernah melihat kode yang memanggil include_playbook, itu bukan modul resmi Ansible dan akan menghasilkan error.
Gunakan import_tasks (statis) sebagai default ketika:
--list-tasks dan --list-tags menampilkan seluruh isi file, sehingga operasional (misalnya menjalankan dengan --start-at-task atau filter --tags) bekerja andal.when dievaluasi secara independen per task di dalam file.Keuntungan utama statis adalah predictability: karena semuanya sudah diproses di awal, perilaku terkait tags, handler, dan listing menjadi lebih transparan.
Gunakan include_tasks (dinamis) ketika:
include_tasks: "setup-{{ ansible_facts['os_family'] | lower }}.yml".when dievaluasi sekali untuk seluruh file (bukan per task), karena ada task di dalam file yang mungkin mengubah variabel yang dipakai kondisi tersebut.set_fact).import_tasksPola paling umum adalah memecah playbook besar menjadi beberapa file task, lalu menggabungkannya kembali lewat import_tasks di playbook utama. Contohnya, mari kita modularisasi setup server web:
- name: Setup semua server
hosts: all
become: true
tasks:
- name: Import setup dasar semua host
ansible.builtin.import_tasks: tasks/common.yml
- name: Import setup web server
ansible.builtin.import_tasks: tasks/webserver.yml
when: inventory_hostname in groups['webservers']Perhatikan baris when pada import kedua: karena import_tasks bersifat statis, kondisi tersebut akan disalin ke setiap task di dalam webserver.yml. Selama isi file memang seharusnya hanya berjalan di host web server, perilaku ini aman dan justru membuat setiap task mendapat proteksi kondisi yang sama. Isi dari tasks/webserver.yml bisa sesederhana:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: Copy konfigurasi nginx
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
- name: Start nginx
ansible.builtin.systemd_service:
name: nginx
state: startedTip
Karena import_tasks bersifat statis, task di dalam file langsung terlihat saat kalian menjalankan ansible-playbook playbooks/site.yml --list-tasks. Ini sangat membantu untuk verifikasi sebelum eksekusi nyata — misalnya memastikan urutan task sudah benar sebelum run di produksi.
include_tasks dengan LoopSekarang mari kita lihat mengapa include_tasks diperlukan. Kasus klasiknya adalah memilih file task berdasarkan OS, atau menjalankan sekelompok task per item dalam loop.
Contoh pertama — pemilihan file berdasarkan facts OS. Karena nama file memakai variabel runtime, kita wajib menggunakan include_tasks:
- name: Setup package manager sesuai OS
hosts: all
become: true
tasks:
- name: Jalankan task khusus family OS
ansible.builtin.include_tasks: "tasks/install-{{ ansible_facts['os_family'] | lower }}.yml"Jika facts os_family bernilai Debian, Ansible akan mencari dan menjalankan tasks/install-debian.yml; jika RedHat, akan menjalankan tasks/install-redhat.yml. Kalian cukup menyediakan kedua file tersebut, dan playbook yang sama bekerja untuk seluruh fleks environment. Ini adalah salah satu pola terkuat dari dynamic include.
Contoh kedua — loop atas include_tasks. Task di dalam file akan dijalankan sekali untuk setiap item, dan kita bisa menggunakan loop_control.loop_var untuk memberi nama variabel yang lebih deskriptif:
- name: Konfigurasi beberapa service sekaligus
hosts: all
become: true
tasks:
- name: Konfigurasi tiap service
ansible.builtin.include_tasks: tasks/configure_service.yml
loop:
- api
- web
- worker
loop_control:
loop_var: service_nameImportant
Di dalam tasks/configure_service.yml, variabel default item tidak akan tersedia — yang tersedia adalah service_name sesuai loop_var yang kita set. Jika kalian memakai item di file tersebut, playbook akan gagal dengan undefined variable. Ini adalah salah satu kesalahan paling umum saat menggabungkan include_tasks dengan loop.
Isi file tasks/configure_service.yml misalnya:
- name: Buat direktori konfigurasi {{ service_name }}
ansible.builtin.file:
path: "/etc/{{ service_name }}"
state: directory
- name: Deploy konfigurasi {{ service_name }}
ansible.builtin.template:
src: "{{ service_name }}.conf.j2"
dest: "/etc/{{ service_name }}/{{ service_name }}.conf"
- name: Restart service {{ service_name }}
ansible.builtin.systemd_service:
name: "{{ service_name }}"
state: restartedSatu jebakan yang sering membuat bingung: tags pada include_tasks tidak otomatis diwariskan ke task di dalam file — berbeda dengan import_tasks. Agar tags menjangkau task di dalam file, gunakan parameter apply:
- name: Include task dengan tags yang benar
ansible.builtin.include_tasks:
file: tasks/deploy.yml
apply:
tags: deploy
tags: deployDengan apply, kedua tags deploy (pada wrapper dan pada task di dalam file) bekerja: menjalankan ansible-playbook site.yml --tags deploy akan memuat include sekaligus mengeksekusi task di dalamnya. Tanpa apply, hanya wrapper include yang lolos filter tags, sedangkan task di dalam file akan tetap di-skip.
import_playbookSampai di sini kita membahas modularisasi dalam satu play. Bagaimana jika kalian ingin memecah play-level — menggabungkan beberapa playbook menjadi satu entry point, misalnya site.yml yang menjalankan webservers.yml lalu database.yml? Untuk itu gunakan import_playbook, yang hanya bisa ditulis di level atas playbook (bukan di dalam tasks):
- name: Impor playbook setup dasar
ansible.builtin.import_playbook: common.yml
- name: Impor playbook web server
ansible.builtin.import_playbook: webservers.yml
- name: Impor playbook database
ansible.builtin.import_playbook: database.ymlKetika site.yml dijalankan, ketiga playbook di atas akan diproses dan dieksekusi secara berurutan sebagai satu run. Ini adalah pola orchestration yang sangat umum di tim infrastruktur: satu entry point untuk keseluruhan deployment, dengan detail setiap komponen tersimpan di file terpisah.
Warning
Ingat kembali peringatan di awal: import_playbook adalah satu-satunya cara memuat playbook dan bersifat statis. Kalian tidak bisa menulis import_playbook di dalam bagian tasks sebuah play — hal itu akan menghasilkan error "import_playbook ... can only be used at the top level". Jika kalian benar-benar membutuhkan pemilihan playbook dinamis berdasarkan variabel, solusi yang valid adalah memilih di level inventory/--extra-vars atau memanfaatkan conditional pada tiap import_playbook (dengan konsekuensi kondisi dievaluasi saat parsing).
1. Looping pada import_tasks
Menulis loop pada import_tasks akan menghentikan playbook dengan error You cannot use loops on import_tasks (atau pesan sejenis tentang static import). Karena import bersifat statis, tidak ada yang bisa di-loop — ganti ke include_tasks saat kalian perlu loop:
- name: Contoh yang SALAH
ansible.builtin.import_tasks: tasks/setup.yml
loop: "{{ services }}"
# ERROR! You cannot use loops on 'import_tasks' tasks2. Salah mengira tags pada include_tasks diwariskan
Ini adalah sumber bug tersembunyi paling umum. Dengan import_tasks, tags mengalir ke semua task di dalam file. Dengan include_tasks, tags hanya menyentuh wrapper include — kecuali kalian memakai apply seperti contoh di atas. Jika --tags tidak memicu task yang kalian harapkan, cek dulu apakah kalian menggunakan dynamic include.
3. Memakai item di dalam file yang di-include dengan loop
Ketika include_tasks di-loop, variabel default item di luar tidak tersedia di dalam file. Gunakan loop_var untuk mendefinisikan nama variabel yang eksplisit, dan pastikan file tersebut memakai nama variabel itu.
4. Mengira import_playbook bisa digunakan di dalam tasks
import_playbook hanya valid di level atas playbook. Jika muncul error saat menempatkannya di dalam tasks, itu bukan bug Ansible — itu memang aturannya. Pindahkan ke level atas, atau pertimbangkan memecah ke play terpisah.
5. Mencampur include dan import tanpa alasan
Dokumentasi resmi Ansible menyarankan untuk konsisten memilih satu pendekatan dalam sebuah playbook. Mencampur statis dan dinamis dalam satu playbook dapat memunculkan bug yang sulit dilacak, terutama karena perbedaan perilaku when, vars, dan tags antar keduanya. Sebagai titik awal: gunakan import_tasks untuk path tetap dan alur yang stabil, dan include_tasks hanya saat benar-benar butuh fleksibilitas runtime.
6. when pada import_tasks dievaluasi per task
Konsekuensi statis yang sering mengejutkan: jika kalian menulis import_tasks: foo.yml dengan when: some_condition, dan task pertama di dalam foo.yml mengubah nilai some_condition (misalnya lewat set_fact), maka task-task selanjutnya di dalam file tersebut bisa ikut ter-skip. Jika kondisi harus dievaluasi sekali untuk seluruh file, gunakan include_tasks.
Pada episode 11 ini kita telah membahas cara memecah playbook besar menjadi file-file kecil yang teratur, mulai dari konsep reusability dan struktur direktori modular, hingga perbedaan mendasar antara dynamic include (include_tasks) dan static import (import_tasks) — termasuk waktu pemrosesan, perilaku tags, loop, variabel, dan --list-tasks yang semuanya terangkum dalam tabel perbandingan. Kita juga telah melihat praktik penggunaan import_playbook untuk menggabungkan beberapa playbook dalam satu entry point, serta kesalahan umum seperti looping pada import statis dan kesalahpahaman tentang include_playbook yang tidak pernah ada.
Dengan kemampuan modularisasi ini, playbook kalian menjadi jauh lebih mudah dibaca, diuji, dan dipakai ulang. Namun, pemecahan per file ini baru langkah pertama menuju standardisasi. Di dunia industri, modul task list seperti tasks/common.yml yang dibagikan antar project justru lebih sering dikemas dalam struktur baku bernama Roles.
Di episode 12 selanjutnya kita akan membahas Pengenalan dan Penjelasan Ansible Roles — bagaimana mengemas automasi lengkap dengan task, handler, vars, defaults, templates, dan files dalam satu paket yang terstandarisasi dan siap pakai ulang di project mana pun. Pastikan tetap semangat!