Belajar Ansible - Modularisasi dengan Includes & Imports
Episode 11 of 31

Belajar Ansible - Modularisasi dengan Includes & Imports

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.

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

Pendahuluan

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.

Pembahasan Utama

Konsep Reusability: Memecah Playbook Besar

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:

  1. Sulit dibaca — membaca 200 task berurutan membuat kita kehilangan gambaran besarnya.
  2. Sulit diuji — jika satu bagian gagal, sulit menemukan akar masalahnya karena semuanya berbaur.
  3. Tidak reusable — konfigurasi firewall yang sudah ditulis tidak bisa dipakai ulang di playbook lain tanpa copy-paste.

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:

plaintext
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 artefak

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

Dynamic Include vs Static Import

Ansible menyediakan dua cara untuk menggunakan kembali file: dynamic (include_*) dan static (import_*). Perbedaan fundamentalnya terletak pada kapan file diproses:

  • Dynamic include diproses saat runtime, ketika eksekusi playbook mencapai task include tersebut. File dibaca dan dijalankan saat itu juga.
  • Static import diproses saat parsing awal playbook, sebelum task apa pun dieksekusi. Task dari file yang di-import seolah-olah disisipkan langsung ke dalam playbook.

Perbedaan waktu pemrosesan ini memunculkan serangkaian konsekuensi perilaku yang sangat penting. Mari kita bandingkan secara lengkap:

Aspekinclude_tasks (Dynamic)import_tasks (Static)
Waktu diprosesSaat runtime, ketika include dieksekusiSaat parsing playbook, sebelum eksekusi
Opsi task (tags, when, become)Hanya berlaku untuk task include itu sendiriOtomatis diterapkan ke semua task di dalam file
LoopingBisa di-loop (task dijalankan per item)Tidak bisa — error You cannot use loops on import_tasks
Variabel pada nama fileBoleh: include_tasks: "{{ ansible_os_family }}.yml"Tidak bisa memakai variabel inventory/runtime pada nama file
when pada statementMengevaluasi sekali untuk seluruh fileDisalin ke setiap task di dalam file
--list-tasks / --list-tagsTask dan tags di dalam file tidak munculSemua task dan tags terlihat
--start-at-taskTidak bisa memulai dari task di dalam includeBisa dimulai dari task yang di-import
Handler (notify)Harus me-notify nama include itu sendiriBisa 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.

Kapan Memilih Static Import?

Gunakan import_tasks (statis) sebagai default ketika:

  • Nama file task bersifat tetap (tidak bergantung pada variabel/facts).
  • Kalian ingin --list-tasks dan --list-tags menampilkan seluruh isi file, sehingga operasional (misalnya menjalankan dengan --start-at-task atau filter --tags) bekerja andal.
  • Kalian ingin tags yang dipasang di statement otomatis diwariskan ke semua task di dalam file.
  • Kalian ingin kondisi 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.

Kapan Memilih Dynamic Include?

Gunakan include_tasks (dinamis) ketika:

  • Nama file ditentukan saat runtime, misalnya berdasarkan facts OS: include_tasks: "setup-{{ ansible_facts['os_family'] | lower }}.yml".
  • Kalian perlu melakukan loop atas include, sehingga task di dalam file dijalankan sekali per item.
  • Kalian ingin when dievaluasi sekali untuk seluruh file (bukan per task), karena ada task di dalam file yang mungkin mengubah variabel yang dipakai kondisi tersebut.
  • Kalian ingin fleksibilitas: file yang di-include bisa berubah hasil dari task sebelumnya (misalnya di-set oleh set_fact).

Praktik: Menggunakan import_tasks

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

playbooks/site.yml
- 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:

tasks/webserver.yml
- 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: started

Tip

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.

Praktik: Menggunakan include_tasks dengan Loop

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

os-specific-include.yml
- 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:

include-tasks-loop.yml
- 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_name

Important

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:

tasks/configure_service.yml
- 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: restarted

Menangani Tags pada Dynamic Include

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

include-tasks-tags.yml
- name: Include task dengan tags yang benar
  ansible.builtin.include_tasks:
    file: tasks/deploy.yml
    apply:
      tags: deploy
  tags: deploy

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

Praktik: Menggunakan import_playbook

Sampai 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):

playbooks/site.yml
- 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.yml

Ketika 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).

Kesalahan Umum (Common Pitfalls)

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:

error-loop-import.yml
- name: Contoh yang SALAH
  ansible.builtin.import_tasks: tasks/setup.yml
  loop: "{{ services }}"
# ERROR! You cannot use loops on 'import_tasks' tasks

2. 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.

Penutup

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!

Belajar Ansible - Modularisasi dengan Includes & Imports | Belajar Ansible