Struktur

Struktur #

Membuat sebuah playbook yang sekadar berfungsi (working playbook) sangatlah mudah. Selama struktur indentasi YAML-nya valid dan parameter modulnya cocok, Ansible akan mengeksekusi instruksi tersebut. Namun, membuat playbook yang dirancang dengan baik (well-structured playbook) adalah sebuah seni rekayasa infrastruktur yang berbeda. Playbook yang terstruktur dengan baik tidak hanya berhasil menjalankan tugasnya saat ini, tetapi juga mudah dibaca oleh anggota tim pengembang lain, tidak membingungkan saat ditelusuri kesalahannya (debugging), dan tidak berubah menjadi mimpi buruk pemeliharaan enam bulan kemudian. Artikel ini membedah prinsip pemisahan tanggung jawab berkas playbook, manajemen orchestrator master, urutan eksekusi elemen internal, perbandingan pengimporan statis vs dinamis, hingga penanganan handlers tingkat lanjut.

Prinsip Pemisahan Tanggung Jawab #

Salah satu kesalahan desain (anti-pattern) yang paling sering kita temui di lapangan adalah penyusunan playbook monolitik tunggal yang mengurusi seluruh siklus hidup server. Berkas tersebut berisi ratusan tugas campuran: menginstal Apache, mengonfigurasi database MySQL, menarik kode repositori, hingga memasang agen monitoring.

# ANTI-PATTERN: Berkas monolitik tunggal yang mengurusi semua hal
# playbooks/site-monolithic.yml
---
- name: Setup dan Konfigurasi Seluruh Infrastruktur
  hosts: all
  become: true
  tasks:
    - name: Update apt cache
      apt: update_cache=yes
    - name: Pasang Nginx
      apt: name=nginx state=present
    - name: Pasang PostgreSQL
      apt: name=postgresql state=present
    - name: Salin kode aplikasi
      git: repo=https://github.com/org/app.git dest=/var/www
    - name: Jalankan backup cronjob
      cron: name="backup" hour="2" job="/opt/backup.sh"

Mengapa Pendekatan Monolitik Ini Buruk? #

  1. Hambatan Pengujian Parsial: Kita tidak dapat menjalankan satu bagian tugas saja secara mandiri (misalnya kita hanya ingin men-deploy ulang kode aplikasi) tanpa harus mengevaluasi ulang seluruh tugas instalasi database dan sistem monitoring yang memakan waktu.
  2. Tingkat Kerumitan Tinggi: Berkas yang terlalu panjang membuat pengembang kesulitan melacak di mana sebuah alur kerja dimulai dan berakhir.
  3. Resiko Konflik Tim: Beberapa pengembang yang bekerja pada bagian infrastruktur berbeda (misalnya database administrator dan frontend developer) akan sering mengalami konflik penggabungan kode (git merge conflicts) pada satu berkas yang sama.

Master Orchestrator site.yml #

Pola desain terbaik adalah menerapkan prinsip Satu Berkas, Satu Tanggung Jawab. Kita memisahkan playbook berdasarkan perannya, lalu membuat satu berkas playbook master bernama site.yml yang bertindak sebagai dirigen (orchestrator) utama untuk memanggil sub-playbook lainnya menggunakan modul pengimpor statis import_playbook.

Berikut tata letak folder yang rapi:

playbooks/
  ├── site.yml                 # Master playbook (Orchestrator)
  ├── 01-common-setup.yml      # Setup dasar semua server
  ├── 02-webservers.yml        # Setup khusus web server Nginx
  ├── 03-dbservers.yml         # Setup khusus database PostgreSQL
  └── 04-app-deployment.yml    # Alur rilis deploy aplikasi

Dan berikut isi dari berkas master site.yml:

# File: playbooks/site.yml
---
# Master Orchestrator Playbook

# 1. Panggil konfigurasi dasar (SSH hardening, firewall, ntp)
- import_playbook: 01-common-setup.yml

# 2. Panggil konfigurasi server database
- import_playbook: 03-dbservers.yml

# 3. Panggil konfigurasi server web proxy
- import_playbook: 02-webservers.yml

# 4. Panggil alur rilis aplikasi (paling akhir karena database & proxy harus siap dahulu)
- import_playbook: 04-app-deployment.yml

Setiap file sub-playbook (seperti 02-webservers.yml) adalah berkas independen lengkap yang berisi blok play tersendiri dengan target hosts: yang spesifik.


Urutan Eksekusi Elemen Internal #

Di dalam satu blok Play, kita dapat mendeklarasikan beberapa elemen kerja selain blok tasks: utama. Memahami urutan eksekusi internal sangat krusial agar kita tidak salah meletakkan tugas prasyarat.

Berikut adalah urutan kronologis jalannya elemen di dalam satu Play:

1. Pengecekan Koneksi & Pemuatan Variabel
                     │
2. Pengumpulan Fakta Target (Gather Facts)
                     │
3. Eksekusi blok pre_tasks
                     │
4. Evaluasi & Eksekusi Handlers (jika dipicu oleh pre_tasks)
                     │
5. Eksekusi blok roles (Modular paket tugas)
                     │
6. Eksekusi blok tasks (Tugas utama playbook)
                     │
7. Eksekusi blok post_tasks
                     │
8. Evaluasi & Eksekusi Handlers (pemicu akhir dari roles/tasks/post_tasks)

Alur eksekusi elemen play di atas divisualisasikan secara lengkap dalam diagram alir berikut:

flowchart TD
    A["Mulai Eksekusi Play"] --> B["1. Kumpulkan Data (Gather Facts)"]
    B --> C["2. Jalankan pre_tasks"]
    C --> D{"Apakah pre_tasks memicu Notify?"}
    D -- "Ya" --> E["Jalankan Handlers (Pre-tasks scope)"]
    D -- "Tidak" --> F["3. Jalankan Roles"]
    E --> F
    F --> G["4. Jalankan tasks Utama"]
    G --> H["5. Jalankan post_tasks"]
    H --> I{"Apakah tasks/roles/post_tasks memicu Notify?"}
    I -- "Ya" --> J["6. Jalankan Handlers (Akhir Play)"]
    I -- "Tidak" --> K["Selesai Play"]
    J --> K

Pentingnya pre_tasks dan post_tasks: #

  • pre_tasks: Sangat berguna untuk melakukan pemeriksaan kesehatan pra-koneksi atau melepas server dari pool Load Balancer sebelum role instalasi utama berjalan.
  • post_tasks: Digunakan untuk memasukkan kembali server ke Load Balancer atau memicu verifikasi kesehatan aplikasi setelah seluruh proses instalasi selesai.

Perbandingan static import vs dynamic include #

Ansible menyediakan dua cara untuk membagi tugas-tugas ke dalam file terpisah agar kode kita dapat digunakan kembali: Static Import (import_*) dan **Dynamic Include** (include_*`).

Perbedaan fundamental keduanya terletak pada kapan Ansible membaca berkas eksternal tersebut:

1. Static Import (Waktu Kompilasi) #

Menggunakan modul import_tasks membuat Ansible membaca dan menyuntikkan seluruh isi berkas eksternal ke dalam memori utama pada saat playbook pertama kali dimuat (compilation time), sebelum ada tugas apa pun yang dijalankan.

  • Sifat: Statis dan terprediksi.
  • Batasan: Kita tidak dapat menggunakan variabel dinamis (variabel yang dihasilkan dari tugas sebelumnya) untuk menentukan nama file yang akan di-import.
  • Contoh:
    tasks:
      # Berkas install.yml wajib ada sebelum eksekusi dimulai
      - import_tasks: tasks/install.yml
    

2. Dynamic Include (Waktu Eksekusi) #

Menggunakan modul include_tasks membuat Ansible baru akan membaca berkas eksternal saat baris tugas tersebut dieksekusi secara linear (runtime).

  • Sifat: Dinamis dan kondisional.
  • Keunggulan: Kita dapat menggunakan variabel dinamis untuk menentukan file mana yang akan dimuat. Sangat berguna saat kita merancang tugas multi-OS.
  • Contoh:
    tasks:
      # Memuat file installasi secara dinamis berdasarkan jenis OS server target
      - include_tasks: "tasks/install-{{ ansible_os_family | lower }}.yml"
    

Tabel perbandingan taktis antara Import dan Include:

Dimensi Evaluasi Static Import (import_tasks) Dynamic Include (include_tasks)
Waktu Pemuatan Awal playbook (Compile Time) Saat tugas dieksekusi (Runtime)
Kinerja Kecepatan Lebih cepat saat eksekusi berjalan Memiliki overhead baca disk di tengah jalan
Penggunaan Variabel Hanya mendukung variabel statis Mendukung variabel dinamis & facts
Penerapan Tags Tag di-inherit ke seluruh sub-tasks Tag hanya menempel pada baris include
Pencegahan Error Syntax error langsung terdeteksi di awal Syntax error baru terdeteksi saat baris dibaca

Standardisasi Penamaan Task #

Nama tugas (- name:) adalah bagian terpenting dari aspek keterbacaan sebuah playbook. Nama tugas yang baik harus menjelaskan tujuan bisnis (state) yang ingin dicapai, bukan mekanisme teknis perintah yang dijalankan.

# ANTI-PATTERN: Menuliskan mekanisme perintah CLI
- name: apt install nginx
  apt:
    name: nginx
    state: present

- name: cp nginx.conf /etc/nginx/
  copy:
    src: files/nginx.conf
    dest: /etc/nginx/nginx.conf

# BENAR: Menjelaskan tujuan fungsional dari tugas
- name: Pastikan mesin web server Nginx terpasang
  apt:
    name: nginx
    state: present

- name: Salin konfigurasi utama Nginx dari repositori lokal
  copy:
    src: files/nginx.conf
    dest: /etc/nginx/nginx.conf

Menuliskan nama tugas yang deskriptif membantu kita melakukan penelusuran logs secara visual di terminal, mempermudah pelacakan error saat integrasi CI/CD, serta mempermudah pencarian titik mulai menggunakan flag --start-at-task.


Penggunaan Tags #

Tags adalah fitur yang sangat kuat untuk mengontrol jalannya eksekusi playbook secara granular tanpa merubah kode fisik. Dengan menandai tugas-tugas kita menggunakan tag tertentu, kita dapat memilih untuk hanya mengeksekusi subset tugas, atau melewati tugas tertentu.

Contoh penulisan tag pada berkas playbook:

# File: playbooks/webservers.yml
---
- name: Konfigurasikan Web Server
  hosts: webservers
  become: true
  
  tasks:
    - name: Pasang paket sistem Nginx
      apt:
        name: nginx
        state: present
      tags:
        - install
        - nginx

    - name: Salin berkas sertifikat SSL
      copy:
        src: files/domain.crt
        dest: /etc/ssl/certs/domain.crt
      tags:
        - configure
        - ssl
        - nginx

    - name: Tarik kode aplikasi terbaru dari Git
      git:
        repo: https://github.com/org/app.git
        dest: /var/www/html
      tags:
        - deploy
        - app

Cara memanfaatkan tag dari baris perintah CLI:

# Skenario 1: Hanya jalankan tugas yang memiliki tag 'deploy'
ansible-playbook -i inventory/hosts.ini site.yml --tags "deploy"

# Skenario 2: Jalankan semua tugas KECUALI tugas yang berkaitan dengan 'ssl'
ansible-playbook -i inventory/hosts.ini site.yml --skip-tags "ssl"

# Skenario 3: Hanya jalankan tugas untuk konfigurasi SSL di server web
ansible-playbook -i inventory/hosts.ini site.yml --tags "configure,ssl"

Mekanisme Kerja Handlers #

Handlers adalah tugas khusus yang bertindak sebagai event listener. Mereka dirancang untuk menangani operasi pasca-perubahan seperti memicu muat ulang layanan (reload service), restart database, atau flush cache memory.

Karakteristik Penting Handlers: #

  1. Hanya Dijalankan Saat Terjadi Perubahan: Handler hanya akan dieksekusi jika tugas yang mengirimkan notify memiliki status keluaran changed. Jika tugas tersebut bernilai ok (karena status akhir server sudah sesuai), handler tidak akan dipicu.
  2. Eksekusi Tunggal di Akhir: Meskipun ada 10 tugas berbeda yang mengirimkan notify ke handler yang sama (misalnya: tugas salin config, tugas update SSL, dan tugas edit virtual host semuanya mengirimkan notify Restart Layanan Nginx), Ansible hanya akan mengeksekusi handler Restart Layanan Nginx satu kali saja di akhir play setelah seluruh tugas utama selesai.
  3. Diabaikan Jika Playbook Gagal: Jika eksekusi playbook kita mengalami kegagalan (failed task) di tengah jalan sebelum play selesai, seluruh handler yang sudah terpicu di atas tidak akan pernah dijalankan demi menjaga keamanan sistem.
# Ilustrasi pemicu ganda handler
tasks:
  - name: Perbarui konfigurasi virtual host web
    template:
      src: vhost.conf.j2
      dest: /etc/nginx/sites-available/app
    notify: Restart Layanan Nginx

  - name: Salin file sertifikat SSL produksi
    copy:
      src: files/prod.crt
      dest: /etc/ssl/certs/prod.crt
    notify: Restart Layanan Nginx

handlers:
  - name: Restart Layanan Nginx
    systemd:
      name: nginx
      state: restarted

Eksekusi Instan via flush_handlers #

Dalam skenario tertentu, kita tidak ingin menunggu sampai seluruh tasks utama selesai untuk menjalankan handler. Misalnya, kita mengubah port database pada task pertama dan memicu restart service database. Task-task berikutnya perlu segera terhubung ke database tersebut untuk melakukan migrasi skema. Jika restart database ditunda sampai akhir play, task migrasi di tengah jalan akan mengalami kegagalan (failed connection).

Ansible menyediakan modul meta bernama flush_handlers untuk memaksa pengeksekusian seluruh handler yang sedang antri saat itu juga secara instan:

# Skenario memaksa pengeksekusian handler secara instan
tasks:
  - name: Ubah port default database PostgreSQL
    template:
      src: postgresql.conf.j2
      dest: /etc/postgresql/15/main/postgresql.conf
    notify: Restart Layanan PostgreSQL

  # Memaksa eksekusi handler yang antri berjalan di baris ini
  - name: Jalankan antrian handler seketika
    meta: flush_handlers

  # Task ini aman dieksekusi karena layanan PostgreSQL sudah menyala di port baru
  - name: Jalankan migrasi skema database aplikasi
    command: python manage.py migrate
    changed_when: true

handlers:
  - name: Restart Layanan PostgreSQL
    systemd:
      name: postgresql
      state: restarted

Rekursif Handlers (Memicu Handler Lain) #

Sebuah handler juga dapat mengirimkan notify ke handler lainnya. Misalnya, ketika kita merestart layanan web server Nginx, kita juga ingin memicu modul penilai kesehatan koneksi (health check tester). Hal ini diizinkan oleh Ansible dan akan diproses secara berantai di akhir play.


Ringkasan #

  • Orchestrator site.yml — Pisahkan playbook berdasarkan area tanggung jawab, lalu gunakan satu file master site.yml sebagai pengendali utama menggunakan import_playbook.
  • Urutan Elemen Play — Pahami dan ikuti urutan kronologis jalannya elemen: pre_tasks -> roles -> tasks -> post_tasks -> handlers demi meminimalkan kesalahan prasyarat.
  • Kriteria Pemilihan Import vs Include — Gunakan import_tasks (Compile Time) untuk stabilitas tugas umum; gunakan include_tasks (Runtime) jika nama berkas dipengaruhi variabel dinamis.
  • Deklarasi Nama Deskriptif — Tulis parameter nama tugas dengan fokus pada tujuan bisnis (state) yang ingin diraih, bukan sekadar perintah perintah shell.
  • Tuning Eksekusi via Tags — Terapkan tagging taktis pada setiap tugas untuk memungkinkan pemeliharaan cepat seperti skip install sistem saat hanya ingin deploy kode.
  • Efisiensi Kerja Handlers — Manfaatkan handler untuk memicu restart/reload service; fitur eksekusi tunggal di akhir play menghemat waktu negosiasi status server target.
  • Perlindungan Handler — Ingat bahwa kegagalan tugas utama di tengah jalan akan membatalkan eksekusi handler demi menjaga server dari status inkonsisten setengah jalan.

← Sebelumnya: Apa itu Playbook?   Berikutnya: Task & Module Execution →

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