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? #
- 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.
- Tingkat Kerumitan Tinggi: Berkas yang terlalu panjang membuat pengembang kesulitan melacak di mana sebuah alur kerja dimulai dan berakhir.
- 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: #
- Hanya Dijalankan Saat Terjadi Perubahan: Handler hanya akan dieksekusi jika tugas yang mengirimkan
notifymemiliki status keluaranchanged. Jika tugas tersebut bernilaiok(karena status akhir server sudah sesuai), handler tidak akan dipicu. - Eksekusi Tunggal di Akhir: Meskipun ada 10 tugas berbeda yang mengirimkan
notifyke handler yang sama (misalnya: tugas salin config, tugas update SSL, dan tugas edit virtual host semuanya mengirimkan notifyRestart Layanan Nginx), Ansible hanya akan mengeksekusi handlerRestart Layanan Nginxsatu kali saja di akhir play setelah seluruh tugas utama selesai. - 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.ymlsebagai pengendali utama menggunakanimport_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; gunakaninclude_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 →