Role Anti Pattern

Role Anti Pattern #

Role adalah unit reusability di Ansible. Tapi role yang ditulis dengan buruk bisa menjadi lebih sulit digunakan daripada tidak ada role sama sekali — ia punya asumsi tersembunyi, coupling dengan role lain, variabel yang wajib diisi tanpa dokumentasi, dan perilaku yang sulit diprediksi. Anti pattern dalam role sering muncul karena role “tumbuh organik” dari playbook yang difaktorisasi terburu-buru, tanpa desain yang jelas tentang batasan tanggung jawab dan kontrak antarmukanya.

Anti Pattern 1: Role yang Melakukan Terlalu Banyak #

# ANTI-PATTERN: role "setup-server" yang melakukan segalanya
roles/setup-server/
  └── tasks/
      └── main.yml   ← Install OS packages, konfigurasi nginx, setup database,
                        deploy aplikasi, konfigurasi monitoring, setup backup...
                        (500+ baris)
# BENAR: satu role, satu tanggung jawab
roles/
  ├── common/        ← Konfigurasi OS dasar (user, timezone, packages)
  ├── nginx/         ← Instalasi dan konfigurasi nginx saja
  ├── postgresql/    ← Instalasi dan konfigurasi PostgreSQL saja
  ├── myapp/         ← Deploy aplikasi saja
  └── monitoring/    ← Setup monitoring agent saja

# Playbook mengkomposisikan role:
- hosts: webservers
  roles:
    - common
    - nginx
    - myapp
    - monitoring

Role yang terlalu besar tidak bisa di-test secara terisolasi, sulit dipahami, dan tidak bisa digunakan ulang di konteks yang berbeda.


Anti Pattern 2: Variabel Wajib Tanpa Dokumentasi #

# ANTI-PATTERN: role yang akan gagal tanpa dokumentasi yang jelas
# roles/myapp/tasks/main.yml
- name: Deploy konfigurasi
  template:
    src: app.conf.j2
    dest: /etc/myapp/app.conf
  # Template ini menggunakan db_host, db_password, api_key
  # Tapi tidak ada dokumentasi bahwa variabel ini wajib!
  # Pengguna role hanya tahu setelah menjalankan dan mendapat error template
# BENAR: dokumentasikan dan validasi variabel wajib
# roles/myapp/meta/argument_specs.yml (Ansible >= 2.11)
argument_specs:
  main:
    short_description: Deploy dan konfigurasi myapp
    options:
      db_host:
        type: str
        required: true
        description: Hostname atau IP database server
      db_password:
        type: str
        required: true
        description: Password database — gunakan Ansible Vault
        no_log: true
      api_key:
        type: str
        required: true
        description: API key untuk autentikasi layanan eksternal
        no_log: true
      app_port:
        type: int
        required: false
        default: 8080
        description: Port yang digunakan aplikasi
# Alternatif untuk Ansible < 2.11: validasi manual di tasks
# roles/myapp/tasks/validate.yml
- name: Validasi variabel wajib
  assert:
    that:
      - db_host is defined and db_host | length > 0
      - db_password is defined
      - api_key is defined
    fail_msg: >
      Role myapp membutuhkan variabel: db_host, db_password, api_key.
      Lihat README role untuk dokumentasi lengkap.      

Anti Pattern 3: Menggunakan Variables dari Role Lain Secara Langsung #

# ANTI-PATTERN: role myapp menggunakan variabel internal dari role postgresql
# roles/myapp/tasks/main.yml
- name: Tunggu database siap
  wait_for:
    host: "{{ postgresql_listen_addresses }}"  # Variabel internal role postgresql!
    port: "{{ postgresql_port }}"              # Coupling tersembunyi!
    timeout: 30
# BENAR: role mendefinisikan variabelnya sendiri
# roles/myapp/defaults/main.yml
db_host: localhost    # Default yang masuk akal
db_port: 5432

# roles/myapp/tasks/main.yml
- name: Tunggu database siap
  wait_for:
    host: "{{ db_host }}"    # Variabel role myapp sendiri
    port: "{{ db_port }}"
    timeout: 30

# Pengguna yang menggunakan keduanya bisa menyinkronkan:
# group_vars/dbservers.yml
db_host: "{{ ansible_default_ipv4.address }}"
db_port: "{{ postgresql_port }}"  # Mapping eksplisit, bukan coupling tersembunyi

Anti Pattern 4: defaults/main.yml Kosong atau Tidak Ada #

# ANTI-PATTERN: role tanpa defaults
# Tidak ada roles/myapp/defaults/main.yml
# Semua variabel HARUS diset oleh pengguna role,
# tidak ada nilai default yang masuk akal

# roles/myapp/tasks/main.yml
- name: Deploy dengan variabel yang tidak punya default
  template:
    src: app.conf.j2
    dest: "{{ app_conf_dir }}/app.conf"  # Gagal jika app_conf_dir tidak diset
# BENAR: defaults yang komprehensif dengan nilai yang masuk akal
# roles/myapp/defaults/main.yml
app_user: myapp
app_group: myapp
app_dir: /opt/myapp
app_conf_dir: /etc/myapp
app_log_dir: /var/log/myapp
app_port: 8080
app_workers: "{{ ansible_processor_vcpus }}"
app_debug: false
app_log_level: info

# Nilai yang WAJIB diisi (tidak ada default yang masuk akal)
# Dokumentasikan dengan eksplisit bahwa tidak ada default:
# db_host: ""          # WAJIB — tidak ada default
# db_password: ""      # WAJIB — tidak ada default, gunakan vault

Anti Pattern 5: Task yang Seharusnya Jadi Handler #

# ANTI-PATTERN: restart service di setiap task yang mengubah konfigurasi
- name: Update nginx.conf
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf

- name: Restart nginx karena nginx.conf berubah
  systemd:
    name: nginx
    state: restarted

- name: Update virtual host config
  template:
    src: vhost.conf.j2
    dest: /etc/nginx/conf.d/app.conf

- name: Restart nginx lagi karena vhost berubah
  systemd:
    name: nginx
    state: restarted
# nginx di-restart 2 kali meski tidak diperlukan!
# BENAR: gunakan handler
# roles/nginx/tasks/main.yml
- name: Update nginx.conf
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: Reload nginx

- name: Update virtual host config
  template:
    src: vhost.conf.j2
    dest: /etc/nginx/conf.d/app.conf
  notify: Reload nginx

# roles/nginx/handlers/main.yml
handlers:
  - name: Reload nginx
    systemd:
      name: nginx
      state: reloaded   # reload lebih baik dari restart untuk nginx

Anti Pattern 6: Role Tidak Bisa Dijalankan Ulang Tanpa Side Effect #

# ANTI-PATTERN: role yang membuat perubahan permanen pada setiap run
- name: Tambahkan entri ke /etc/hosts
  shell: echo "10.0.1.5 db-primary.internal" >> /etc/hosts
  # Menambahkan duplikat setiap kali role dijalankan!

- name: Generate SSH key pair
  command: ssh-keygen -t ed25519 -f /home/appuser/.ssh/id_ed25519 -N ""
  # Menimpa key yang sudah ada setiap kali role dijalankan!
# BENAR: idempoten secara natural
- name: Tambahkan entri ke /etc/hosts
  lineinfile:
    path: /etc/hosts
    line: "10.0.1.5 db-primary.internal"
    state: present
    # Tidak menambahkan duplikat

- name: Generate SSH key pair jika belum ada
  command: ssh-keygen -t ed25519 -f /home/appuser/.ssh/id_ed25519 -N ""
  args:
    creates: /home/appuser/.ssh/id_ed25519   # Hanya jika file belum ada

Ringkasan #

  • Satu role, satu tanggung jawab — role yang melakukan terlalu banyak tidak bisa di-test terisolasi dan tidak bisa digunakan ulang secara bermakna.
  • Dokumentasikan semua variabel menggunakan meta/argument_specs.yml (Ansible ≥ 2.11) atau validasi eksplisit dengan assert — pengguna tidak boleh menebak variabel apa yang dibutuhkan.
  • Jangan gunakan variabel internal role lain secara langsung — ini menciptakan coupling tersembunyi yang rapuh. Definisikan variabel interface role sendiri.
  • defaults/main.yml harus komprehensif — berikan nilai default yang masuk akal untuk semua parameter yang bisa punya default; dokumentasikan yang wajib.
  • Handler untuk restart/reload service, bukan task langsung — handler hanya berjalan jika ada perubahan nyata dan hanya sekali meski di-trigger berkali-kali.
  • Role harus idempoten — bisa dijalankan berulang kali tanpa efek samping yang tidak diinginkan. Gunakan args: creates:, lineinfile, dan state: present secara konsisten.

← Sebelumnya: Playbook Anti Pattern   Berikutnya: Variable Anti Pattern →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact