Role Anti Pattern #
Role adalah unit reusability dan modularitas utama di Ansible. Melalui role, kita bisa mengelompokkan task, template, file, handler, dan variabel ke dalam satu struktur folder terstandardisasi yang mudah didistribusikan. Namun, role yang ditulis dengan buruk atau terburu-buru justru bisa menjadi bumerang. Ia sering kali memiliki asumsi tersembunyi, ketergantungan yang tidak terdokumentasi, coupling ketat dengan role lain, atau perilaku tidak terduga yang merusak idempotensi sistem. Anti-pattern dalam role biasanya berkembang secara organik ketika sebuah playbook didekomposisi tanpa perencanaan arsitektur yang matang. Artikel ini membahas berbagai anti-pattern yang sering kita temukan saat menulis Ansible role serta bagaimana mendesain role yang bersih, modular, andal, dan mudah di-test.
Alur Keputusan Mendesain Role yang Modular #
Sebelum menulis sebuah role baru, kita harus menentukan batasan tanggung jawab dan struktur variabelnya. Diagram alur berikut membantu kita mengidentifikasi keputusan desain yang harus diambil agar role tetap modular dan mandiri.
flowchart TD
A["Mulai Mendesain Role Baru"] --> B{"Apakah role memiliki satu tanggung jawab saja?"}
B -- "Tidak" --> C["Pecah menjadi beberapa role mandiri"]
B -- "Ya" --> D{"Apakah ada variabel konfigurasi?"}
D -- "Ya" --> E["Letakkan default variabel di defaults/main.yml"]
D -- "Tidak" --> F{"Apakah butuh dependensi luar?"}
E --> G{"Apakah ada variabel konstanta internal?"}
G -- "Ya" --> H["Letakkan di vars/main.yml"]
G -- "Tidak" --> F
H --> F
F -- "Ya" --> I["Definisikan dependensi di meta/main.yml"]
F -- "Tidak" --> J["Buat task utama di tasks/main.yml"]
I --> J
J --> K["Selesai"]
1. Role Monolitik yang Melakukan Terlalu Banyak (God Role) #
Anti-pattern ini terjadi ketika sebuah role dirancang untuk menangani seluruh konfigurasi satu tipe server sekaligus, alih-alih membaginya berdasarkan komponen atau layanan spesifik.
Mengapa Ini Menyulitkan Kita? #
Ketika kita membuat role raksasa bernama setup-server atau common-prod yang menginstal nginx, memasang database, menyalin aplikasi Node.js, mengatur backup cron, dan mengonfigurasi firewall dalam satu folder tasks:
- Tidak Bisa Digunakan Kembali: Jika server database kita tidak membutuhkan nginx, kita tidak bisa menggunakan role ini karena nginx sudah menyatu di dalamnya.
- Sulit Di-test: Menjalankan pengujian otomatis (seperti menggunakan Molecule) pada role monolitik membutuhkan resource yang besar karena seluruh komponen harus dinyalakan sekaligus.
- Membingungkan Developer Lain: File
tasks/main.ymlyang berisi ratusan baris menyulitkan developer lain untuk melacak di mana konfigurasi firewall disetel atau di mana konfigurasi user dibuat.
Perbandingan Struktur Direktori #
# ANTI-PATTERN: satu role monolitik raksasa
roles/setup-server/
├── tasks/
│ └── main.yml ← (500+ baris task konfigurasi nginx, db, user, backup, firewall)
├── templates/
│ ├── nginx.conf.j2
│ ├── pg_hba.conf.j2
│ └── backup.sh.j2
└── defaults/
└── main.yml ← (100+ variabel untuk berbagai aplikasi)
# BENAR: memisahkan ke dalam beberapa role dengan tanggung jawab tunggal
roles/
├── common/ ← (Setup OS dasar: user, timezone, ssh)
├── nginx/ ← (Instalasi dan konfigurasi nginx)
├── postgresql/ ← (Instalasi dan konfigurasi PostgreSQL)
├── myapp/ ← (Deployment codebase aplikasi)
└── firewall/ ← (Aturan iptables/ufw)
Dengan struktur yang modular, playbook kita hanya perlu memanggil role yang dibutuhkan secara deklaratif di level play. Ini memberikan fleksibilitas penuh untuk menggabungkan komponen yang berbeda pada setiap tipe host server.
2. Variabel Wajib Tanpa Dokumentasi dan Validasi #
Ketika sebuah role membutuhkan variabel input dari luar (misalnya credential database atau API key) namun tidak mendokumentasikan atau memvalidasi keberadaan variabel tersebut sebelum task dijalankan.
Dampak Kegagalan #
Pengguna role kita (atau pipeline CI/CD) baru akan menyadari adanya variabel yang kurang setelah playbook berjalan di tengah-tengah proses deployment dan menghasilkan error template Jinja2. Hal ini merusak deployment dan bisa meninggalkan sistem target dalam kondisi tidak konsisten (partial state).
Perbandingan Kode #
# ANTI-PATTERN: role langsung menggunakan variabel tanpa cek atau dokumentasi
# roles/myapp/tasks/main.yml
- name: Deploy file konfigurasi database aplikasi
template:
src: config.json.j2
dest: /opt/myapp/config.json
# Task ini langsung memanggil {{ db_password }} dan {{ api_secret_key }}.
# Jika variabel ini tidak disediakan di inventory, task akan crash di tengah jalan.
# BENAR: gunakan argument_specs untuk validasi modern, atau assert sebagai fail-safe
# Pendekatan 1: Menggunakan roles/myapp/meta/argument_specs.yml (Ansible >= 2.11)
argument_specs:
main:
short_description: Deploy dan konfigurasi aplikasi backend
options:
db_password:
type: str
required: true
description: Password database utama. Gunakan Ansible Vault untuk mengamankannya.
no_log: true
api_secret_key:
type: str
required: true
description: Kunci API rahasia untuk integrasi payment gateway.
no_log: true
app_port:
type: int
required: false
default: 8080
description: Port HTTP yang digunakan oleh aplikasi backend.
# Pendekatan 2: Menggunakan assert di awal tasks untuk fail-safe (Semua versi Ansible)
# roles/myapp/tasks/main.yml
- name: Validasi input variabel wajib
assert:
that:
- db_password is defined and db_password | length > 0
- api_secret_key is defined and api_secret_key | length > 0
fail_msg: >
Error: Variabel 'db_password' dan 'api_secret_key' wajib didefinisikan!
Silakan periksa dokumentasi README.md pada role myapp.
- name: Deploy file konfigurasi database aplikasi
template:
src: config.json.j2
dest: /opt/myapp/config.json
Dengan melakukan validasi di awal, kita menghentikan proses eksekusi playbook secara aman dan memberikan pesan error yang jelas sebelum ada konfigurasi sistem target yang diubah.
3. Coupling Ketat Antar-Role (Cross-Role Coupling) #
Anti-pattern ini terjadi ketika sebuah role secara langsung mengakses variabel internal milik role lain secara implisit.
Mengapa Ini Berbahaya? #
Hal ini menciptakan ketergantungan tersembunyi (hidden coupling). Jika kita memindahkan role ke playbook lain yang tidak memanggil role database, role aplikasi kita akan gagal karena tidak dapat menemukan variabel internal database tersebut. Hal ini menghancurkan prinsip kemandirian (encapsulation) sebuah role.
Perbandingan Kode #
# ANTI-PATTERN: role myapp menggunakan variabel internal role postgresql secara langsung
# roles/myapp/tasks/main.yml
- name: Tunggu port database terbuka
wait_for:
host: "{{ postgresql_listen_addresses }}" # Variabel internal dari role postgresql!
port: "{{ postgresql_port }}" # Coupling tersembunyi!
timeout: 30
# BENAR: role mendefinisikan interface variabelnya sendiri
# roles/myapp/defaults/main.yml
# Kita mendefinisikan variabel default dengan namespace role myapp sendiri
myapp_db_host: "127.0.0.1"
myapp_db_port: 5432
# roles/myapp/tasks/main.yml
- name: Tunggu port database terbuka
wait_for:
host: "{{ myapp_db_host }}"
port: "{{ myapp_db_port | int }}"
timeout: 30
# Di level Playbook / Inventory, kita hubungkan secara eksplisit:
# group_vars/all.yml
myapp_db_host: "{{ postgresql_listen_addresses | default('127.0.0.1') }}"
myapp_db_port: "{{ postgresql_port | default(5432) }}"
Dengan memisahkan variabel, role myapp tidak perlu tahu bagaimana cara role postgresql mengelola variabelnya. Kita menyinkronkan kedua nilai tersebut di level konfigurasi global (seperti group_vars), bukan di dalam kode internal role.
4. Berkas defaults/main.yml Kosong atau Tidak Ada #
Meninggalkan folder defaults kosong dan memaksa pengguna untuk menyuplai setiap konfigurasi kecil secara manual adalah kesalahan desain yang menyulitkan kegunaan role.
Mengapa defaults Sangat Penting? #
Sebuah role yang dirancang dengan baik harus dapat langsung dijalankan hanya dengan konfigurasi minimal. File defaults/main.yml adalah tempat kita meletakkan nilai default dengan precedence terendah (Precedence Tingkat 1). Ini memberikan fleksibilitas kepada pengguna untuk meng-override variabel tersebut hanya jika mereka membutuhkannya. Jika kita tidak menyediakannya, pengguna harus menebak struktur dan nilai konfigurasi dasar yang ramah bagi aplikasi.
Perbandingan Kode #
# ANTI-PATTERN: file defaults/main.yml kosong
# Pengguna harus mendefinisikan path instalasi, user, group, log dir, dll.
# Jika lupa, task akan error karena variabel bernilai undefined.
# BENAR: sediakan defaults dengan nilai yang masuk akal dan aman
# roles/nginx/defaults/main.yml
nginx_user: www-data
nginx_group: www-data
nginx_worker_processes: "auto"
nginx_client_max_body_size: "10M"
nginx_keepalive_timeout: 65
nginx_conf_dir: /etc/nginx
nginx_log_dir: /var/log/nginx
nginx_enable_gzip: "on"
Dengan menyediakan defaults seperti di atas, pengguna role cukup menulis roles: [ nginx ] dalam playbook mereka untuk mendapatkan instalasi nginx standar yang aman tanpa perlu mendefinisikan satu pun variabel tambahan.
5. Menyimpan Konfigurasi Pengguna di vars/main.yml #
Kesalahan ini terjadi karena ketidakpahaman terhadap precedence variabel di Ansible. Banyak developer pemula meletakkan konfigurasi yang bisa diubah oleh pengguna di dalam berkas vars/main.yml alih-alih defaults/main.yml.
Perbedaan Precedence #
Variabel yang didefinisikan di vars/main.yml memiliki precedence yang sangat tinggi (Tingkat 15). Ini berarti variabel tersebut tidak bisa di-override melalui group_vars, host_vars, atau parameter vars di level playbook biasa. Satu-satunya cara untuk meng-override-nya adalah dengan menggunakan Extra Vars (-e), yang sangat tidak praktis untuk konfigurasi sehari-hari.
Perbandingan Kode #
# ANTI-PATTERN: menyimpan variabel konfigurasi pengguna di vars/main.yml
# roles/nginx/vars/main.yml
nginx_port: 80
nginx_max_connections: 1024
# Jika pengguna ingin mengubah port menjadi 8080 untuk server tertentu via host_vars,
# perubahan tersebut tidak akan berdampak karena vars/main.yml meng-override host_vars!
# BENAR: defaults/ untuk konfigurasi pengguna, vars/ untuk konstanta internal
# roles/nginx/defaults/main.yml
nginx_port: 80
nginx_max_connections: 1024
# Pengguna bisa meng-override nilai ini dengan mudah di host_vars/web-01.yml
# roles/nginx/vars/main.yml
# Hanya gunakan vars/ untuk variabel konstanta sistem yang tidak boleh diubah pengguna,
# misalnya daftar paket berdasarkan sistem operasi target.
nginx_os_packages:
Debian:
- nginx
- nginx-common
RedHat:
- nginx
- nginx-filesystem
6. Mengeksekusi Restart Layanan Langsung di tasks/main.yml #
Memaksa layanan untuk me-restart di tengah eksekusi tasks role, bukan menggunakan mekanisme handlers, adalah anti-pattern yang merusak performa dan stabilitas layanan.
Dampak Buruk #
Sama seperti pada playbook, memanggil modul systemd restart langsung di tasks menyebabkan layanan selalu mati-hidup setiap kali role dipanggil, meskipun tidak ada perubahan konfigurasi sama sekali. Hal ini menyebabkan downtime pada server produksi tanpa alasan yang valid.
Perbandingan Kode #
# ANTI-PATTERN: me-restart service secara manual di tasks
# roles/nginx/tasks/main.yml
- name: Deploy virtual host configuration
template:
src: vhost.conf.j2
dest: /etc/nginx/sites-enabled/app.conf
- name: Restart nginx service
systemd:
name: nginx
state: restarted
# Selalu me-restart nginx setiap kali role dijalankan!
# BENAR: gunakan notify ke handler yang terisolasi
# roles/nginx/tasks/main.yml
- name: Deploy virtual host configuration
template:
src: vhost.conf.j2
dest: /etc/nginx/sites-enabled/app.conf
notify: Reload nginx
# Hanya memicu reload jika konfigurasi berubah
# roles/nginx/handlers/main.yml
- name: Reload nginx
systemd:
name: nginx
state: reloaded
7. Penulisan Task yang Merusak Idempotensi Sistem #
Menulis task di dalam role yang selalu memodifikasi file atau menjalankan perintah luar secara permanen tanpa adanya pengecekan kondisi sistem saat ini.
Masalah #
Jika role kita dijalankan dalam nightly build atau scheduled automation, role yang tidak idempoten akan terus menambah baris teks, merusak file sistem, atau membuat resource duplikat yang dapat menyebabkan disfungsi server.
Perbandingan Kode #
# ANTI-PATTERN: generate SSL key tanpa perlindungan idempotensi
# roles/common/tasks/main.yml
- name: Generate SSL certificate key pair
command: openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /etc/ssl/private/app.key -out /etc/ssl/certs/app.crt -subj "/CN=myapp.internal"
# Menimpa sertifikat lama dan membuat sertifikat baru setiap run!
# BENAR: gunakan parameter creates untuk menjamin idempotensi
# roles/common/tasks/main.yml
- name: Generate SSL certificate key pair jika belum ada
command: openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /etc/ssl/private/app.key -out /etc/ssl/certs/app.crt -subj "/CN=myapp.internal"
args:
creates: /etc/ssl/private/app.key
# Task dilewati jika file app.key sudah ada di sistem target
8. Mengabaikan Pengujian Otomatis dengan Molecule #
Mengembangkan role tanpa menulis skenario pengujian otomatis (unit testing) adalah anti-pattern dalam siklus pengembangan modern (devops lifecycle).
Masalah yang Dihadapi #
Tanpa pengujian otomatis, kita tidak tahu apakah role kita masih bekerja dengan baik pada sistem operasi versi terbaru (misalnya migrasi dari Ubuntu 20.04 ke 22.04). Kita baru mengetahui adanya kegagalan saat production deployment dilakukan, yang merupakan situasi sangat berisiko.
Solusi: Gunakan Molecule #
Molecule adalah framework pengujian yang didesain khusus untuk Ansible role. Molecule mempermudah kita untuk spin up container Docker, menjalankan role pada container tersebut, memverifikasi status idempotensi, dan menjalankan pengujian kepatuhan (linting).
# Struktur dasar inisialisasi Molecule pada role
roles/myapp/
├── molecule/
│ └── default/
│ ├── molecule.yml ← Konfigurasi driver (Docker/Podman) dan verifier
│ ├── prepare.yml ← Menyiapkan environment test container
│ ├── converge.yml ← Playbook untuk menjalankan role yang di-test
│ └── verify.yml ← Skrip untuk memverifikasi kondisi akhir container
Dengan memanfaatkan Molecule, setiap perubahan kode pada role dapat diuji secara otomatis melalui pipeline CI/CD sebelum role di-merge ke branch utama.
9. Struktur Folder yang Tidak Standar dan File Berantakan #
Menolak menggunakan struktur folder Ansible Galaxy standar dan membuat layout file sendiri yang membingungkan bagi developer lain.
Dampak #
Ansible mengandalkan konvensi alih-alih konfigurasi (convention over configuration). Jika kita menaruh file handler di dalam folder tasks atau menyimpan template di folder files, Ansible tidak akan bisa memuatnya secara otomatis tanpa kita menulis path lengkap yang rumit. Ini mempersulit kolaborasi tim.
Solusi: Ikuti Standar Ansible Galaxy #
Pastikan kita selalu membuat role baru menggunakan perintah ansible-galaxy role init untuk menghasilkan kerangka folder standar:
roles/nama-role/
├── README.md ← Dokumentasi penggunaan, input variabel, dan contoh playbook
├── meta/
│ └── main.yml ← Informasi author, lisensi, dan dependensi role
├── defaults/
│ └── main.yml ← Variabel default precedence rendah
├── vars/
│ └── main.yml ← Variabel internal konstanta precedence tinggi
├── tasks/
│ └── main.yml ← Daftar task utama yang akan dieksekusi
├── handlers/
│ └── main.yml ← Handlers untuk merespon perubahan status task
├── templates/ ← Berkas template Jinja2 (.j2)
└── files/ ← Berkas statis yang disalin apa adanya (script, tar.gz)
Ringkasan #
- Bagi Tanggung Jawab (SRP) — Pecah role monolitik raksasa menjadi beberapa role kecil yang fokus pada satu komponen atau aplikasi tunggal.
- Validasi Variabel Wajib — Terapkan document argument specs menggunakan meta/argument_specs.yml atau assert di awal tasks untuk menghindari silent crash.
- Dekopel Ketergantungan Variabel — Gunakan namespace role sendiri pada defaults/main.yml dan hindari memanggil variabel internal milik role lain secara langsung.
- Terapkan Sensible Defaults — Pastikan defaults/main.yml terisi dengan nilai bawaan yang aman agar role langsung dapat digunakan tanpa setup manual yang berbelit.
- Pahami Precedence vars/ — Simpan hanya variabel konstanta sistem yang tidak boleh diganti di vars/main.yml, dan gunakan defaults/ untuk variabel konfigurasi.
- Pindahkan Restart ke Handlers — Delegasikan tugas restart/reload layanan ke handlers/main.yml agar downtime singkat pada server target tidak terjadi berkali-kali.
- Gunakan Molecule untuk Test — Integrasikan framework Molecule untuk melakukan unit testing otomatis pada role di platform Docker sebelum rilis ke production.
← Sebelumnya: Playbook Anti Pattern Berikutnya: Variable Anti Pattern →