Delegation & Local Action #
Saat menulis playbook Ansible, kita sering berasumsi bahwa setiap task di dalam play harus dieksekusi secara langsung pada managed node yang sedang diproses. Ini adalah perilaku bawaan (default behavior) Ansible yang sangat efisien untuk sebagian besar tugas administratif seperti menginstal paket, menyalin file konfigurasi, atau menyalakan service. Namun, dalam orkestrasi infrastruktur modern yang kompleks, kita sering kali perlu melakukan koordinasi antar-server. Misalnya, sebelum mengupgrade server web, kita harus menonaktifkannya terlebih dahulu di server load balancer, membuat entri DNS baru di server DNS utama, atau mencatat log deployment di control node lokal kita. Untuk skenario-skenario multi-host seperti ini, kita memerlukan fitur Delegation dan Local Action.
Konsep Dasar Delegasi Eksekusi #
Delegasi eksekusi di Ansible diatur menggunakan direktif delegate_to. Direktif ini memberi tahu Ansible untuk menjalankan task tertentu pada host lain yang kita tunjuk, alih-alih pada host target yang sedang diproses oleh antrian play saat itu.
Meskipun task tersebut dieksekusi di host lain (host delegasi), hal yang sangat penting untuk dipahami adalah bahwa konteks variabel dan facts tetap merujuk pada host target asli. Artinya, variabel seperti inventory_hostname, ansible_default_ipv4, atau variabel khusus host yang didefinisikan di host_vars tetap membawa data milik server web yang sedang kita upgrade, bukan milik server load balancer tempat perintah delegasi itu dieksekusi.
Berikut adalah diagram alir yang membandingkan aliran eksekusi tanpa delegasi vs aliran eksekusi dengan delegasi:
flowchart TD
subgraph Aliran Biasa ("Tanpa Delegasi")
C1["Control Node"] -->|"Kirim & Eksekusi Modul"| W1["Web Server 1"]
C1 -->|"Kirim & Eksekusi Modul"| W2["Web Server 2"]
end
subgraph Aliran Delegasi ("Dengan delegate_to: Load Balancer")
C2["Control Node"] -->|"Kirim Instruksi (Variabel Web Server 1)"| LB["Load Balancer Server"]
LB -->|"Eksekusi Aksi untuk Web Server 1"| LB
C2 -->|"Kirim Instruksi (Variabel Web Server 2)"| LB
LB -->|"Eksekusi Aksi untuk Web Server 2"| LB
end
Dengan delegasi, kita bisa dengan mudah membuat playbook yang menjembatani komunikasi antar-server secara dinamis selama proses deployment berlangsung.
local_action dan delegate_to: localhost #
Dalam banyak skenario automasi, kita membutuhkan control node (mesin tempat kita menjalankan perintah ansible-playbook) untuk melakukan suatu aksi. Contohnya meliputi:
- Mengirimkan notifikasi HTTP POST ke webhook Slack, Discord, atau Microsoft Teams.
- Mengunduh file konfigurasi sensitif dari Vault eksternal ke control node sebelum didistribusikan.
- Menunggu port port TCP tertentu menyala pada managed node dari perspektif control node (memastikan tidak ada blocking firewall).
- Mencatat riwayat deployment ke file log lokal di control node.
Ansible menyediakan dua cara untuk mengarahkan eksekusi ke control node: delegate_to: localhost dan local_action. Keduanya memiliki fungsi yang identik secara fungsional, namun memiliki format penulisan sintaksis yang berbeda.
Sintaks delegate_to: localhost vs local_action #
Sintaks delegate_to: localhost adalah pendekatan modern yang lebih direkomendasikan karena menjaga struktur penulisan task tetap konsisten dengan modul biasa. Sementara itu, local_action adalah sintaks warisan (legacy syntax) di mana nama modul ditempatkan sebagai argumen pertama dari direktif local_action.
Mari kita lihat perbedaan implementasi keduanya melalui contoh anti-pattern dan solusi penulisan berikut:
# ANTI-PATTERN: Menggunakan shell script curl lokal yang rumit didelegasikan secara tidak perlu
- name: Kirim notifikasi slack secara manual
hosts: app_servers
tasks:
- name: Kirim pesan slack via shell local
ansible.builtin.shell: "curl -X POST -H 'Content-type: application/json' --data '{\"text\":\"Deploying to {{ inventory_hostname }}\"}' https://hooks.slack.com/services/XXX"
delegate_to: localhost
# JANGAN: Menggunakan shell command mentah memecah idempodensi dan membuat penanganan error sulit.
# BENAR: Menggunakan modul 'uri' yang bersih dengan delegate_to: localhost
- name: Kirim notifikasi slack secara optimal
hosts: app_servers
tasks:
- name: Kirim pesan slack via modul uri
ansible.builtin.uri:
url: https://hooks.slack.com/services/XXX
method: POST
body_format: json
body:
text: "Ansible sukses melakukan konfigurasi pada server: {{ inventory_hostname }}"
delegate_to: localhost # ✓ Mengeksekusi modul uri secara lokal dari control node kita
Jika kita ingin menulis hal yang sama menggunakan local_action, formatnya akan tampak seperti ini:
# BENAR: Penggunaan sintaks local_action alternatif
- name: Catat log deployment lokal
hosts: app_servers
tasks:
- name: Tulis ke log file lokal
local_action:
module: ansible.builtin.lineinfile
path: /var/log/ansible_runs.log
line: "Host {{ inventory_hostname }} berhasil dikonfigurasi pada {{ ansible_date_time.iso8601 }}"
create: true
# ✓ Modul lineinfile dijalankan di localhost
Meskipun local_action masih didukung penuh, kita disarankan untuk menggunakan delegate_to: localhost demi kemudahan pembacaan kode (readability) oleh anggota tim lainnya.
delegate_facts untuk Menyebarkan Informasi Antar-Host #
Secara default, jika sebuah task didelegasikan ke host lain menggunakan delegate_to, facts yang dikumpulkan oleh task tersebut (misalnya jika kita menjalankan modul setup atau modul yang meregistrasi variabel baru) akan tetap disimpan di bawah nama host target asli di dalam variabel memori hostvars.
Namun, terkadang kita justru sengaja mendelegasikan task untuk mengambil informasi spesifik dari host delegasi dan kita ingin informasi tersebut disimpan sebagai facts milik host delegasi tersebut agar bisa digunakan oleh host lain nanti. Untuk mencapai skenario ini, kita menggunakan direktif delegate_facts: true.
Use Case delegate_facts #
Bayangkan kita sedang mengonfigurasi sekumpulan server web (web_servers) yang membutuhkan alamat IP internal dari database master yang berada di kelompok server lain (db_servers). Kita bisa mendelegasikan task pencarian IP ke server database tersebut, menyimpan facts-nya langsung di server database tersebut, lalu mengaksesnya dari server web kita.
Berikut adalah implementasi praktisnya:
# BENAR: Mengumpulkan IP dari server database dan menyebarkannya ke server web menggunakan delegate_facts
---
- name: Hubungkan Server Web dengan Database Cluster
hosts: web_servers
vars:
db_host_target: "db-master.kita.internal"
tasks:
- name: Kumpulkan facts dari server database secara terdelegasi
ansible.builtin.setup:
filter: ansible_default_ipv4
delegate_to: "{{ db_host_target }}"
delegate_facts: true # ✓ Menyimpan facts default_ipv4 ke db-master.kita.internal, bukan ke web_servers
run_once: true # Cukup jalankan sekali saja, tidak perlu diulangi untuk setiap web server
- name: Tulis file konfigurasi database di semua server web
ansible.builtin.template:
src: db_config.j2
dest: /var/www/html/config.php
mode: '0600'
vars:
# Kita mengakses facts milik db-master.kita.internal yang sudah diisi oleh task sebelumnya
database_ip: "{{ hostvars[db_host_target]['ansible_default_ipv4']['address'] }}"
Tanpa delegate_facts: true, facts IP database di atas akan disimpan di bawah host web pertama yang memproses task tersebut, yang akan membingungkan developer lain ketika membaca ulang kode templates kita karena variabelnya tidak teratur.
Studi Kasus Produksi: Cordon dan Uncordon Load Balancer #
Mari kita bahas studi kasus dunia nyata yang sangat sering ditemui di lingkungan produksi DevOps: Zero-Downtime Rolling Upgrade.
Kita memiliki kluster aplikasi web dengan 3 server di belakang sebuah load balancer HAProxy. Jika kita menghentikan service web dan melakukan deploy ke semua server secara bersamaan, pengguna aplikasi kita akan mengalami pemadaman layanan (downtime). Untuk mencegah hal ini, kita harus menggunakan teknik rolling upgrade:
- Cordon: Hubungi load balancer untuk menonaktifkan lalu lintas baru ke Server A.
- Connection Draining: Tunggu beberapa detik agar koneksi HTTP yang sedang aktif di Server A selesai diproses secara alami.
- Upgrade: Lakukan pembaruan kode aplikasi, migrasi, dan restart service di Server A.
- Health Check: Lakukan verifikasi lokal bahwa port aplikasi di Server A sudah merespons dengan benar.
- Uncordon: Aktifkan kembali Server A di load balancer untuk mulai menerima traffic baru.
- Ulangi langkah 1-5 untuk Server B dan Server C secara bergantian.
Proses rolling upgrade ini divisualisasikan melalui diagram urutan (sequence diagram) berikut:
sequenceDiagram
participant CN as Control Node
participant LB as HAProxy Load Balancer
participant WS as Web Server (Target)
CN->>LB: Cordon (Matikan traffic ke Web Server via delegate_to)
Note over LB: HAProxy mengubah status server target menjadi MAINT
CN->>CN: Connection Draining (Tunggu 10 detik)
CN->>WS: Deploy & Upgrade Aplikasi (Kode baru & Restart Service)
CN->>WS: Health Check Lokal (Pastikan port webapp merespons 200 OK)
WS-->>CN: Sehat (200 OK)
CN->>LB: Uncordon (Aktifkan kembali Web Server via delegate_to)
Note over LB: HAProxy mengubah status server target menjadi READY
Berikut adalah contoh playbook Ansible produksi yang lengkap dan fungsional untuk menjalankan skenario di atas dengan memanipulasi API statistik HAProxy secara terdelegasi:
# BENAR: Playbook rolling upgrade dengan cordon/uncordon HAProxy load balancer menggunakan delegate_to
---
- name: Rolling Upgrade Aplikasi Zero-Downtime
hosts: web_servers
serial: 1 # ✓ Sangat penting! Mengeksekusi server secara bergantian satu per satu
vars:
app_port: 8080
lb_control_host: "lb-prod.kita.internal"
lb_backend_name: "be_webapp"
app_dir: "/var/www/webapp"
tasks:
- name: 1. CORDON - Nonaktifkan server di HAProxy Load Balancer
ansible.builtin.uri:
url: "http://{{ lb_control_host }}:9000/stats"
method: POST
user: admin
password: securepassword123
force_basic_auth: true
body_format: form-urlencoded
body:
# Kita mengirim perintah backend/server agar masuk ke mode pemeliharaan (MAINT)
s: "{{ lb_backend_name }}/{{ inventory_hostname }}"
action: disable
delegate_to: "{{ lb_control_host }}" # ✓ Aksi API dipanggil dari server load balancer itu sendiri
changed_when: true
- name: 2. DRAINING - Berikan waktu bagi koneksi aktif untuk selesai
ansible.builtin.pause:
seconds: 15
# Langkah pause ini berjalan lokal di control node untuk menjeda proses transisi
- name: 3. UPGRADE - Tarik kode aplikasi terbaru dari repositori
ansible.builtin.git:
repo: "[email protected]:kita/webapp.git"
dest: "{{ app_dir }}"
version: "tags/v2.1.5"
- name: 4. UPGRADE - Jalankan instalasi dependensi internal
ansible.builtin.command: npm install --production
args:
chdir: "{{ app_dir }}"
- name: 5. UPGRADE - Restart daemon service aplikasi webapp
ansible.builtin.systemd:
name: webapp
state: restarted
- name: 6. HEALTH CHECK - Pastikan aplikasi lokal telah menyala dengan benar
ansible.builtin.uri:
url: "http://127.0.0.1:{{ app_port }}/health"
status_code: 200
register: local_health
until: local_health.status == 200
retries: 6
delay: 5
# Task ini memastikan kita tidak memasukkan kembali server yang rusak ke load balancer
- name: 7. UNCORDON - Aktifkan kembali server di HAProxy Load Balancer
ansible.builtin.uri:
url: "http://{{ lb_control_host }}:9000/stats"
method: POST
user: admin
password: securepassword123
force_basic_auth: true
body_format: form-urlencoded
body:
# Mengaktifkan kembali server agar keluar dari mode MAINT ke mode READY
s: "{{ lb_backend_name }}/{{ inventory_hostname }}"
action: enable
delegate_to: "{{ lb_control_host }}" # ✓ Dikirim kembali ke load balancer
changed_when: true
Playbook di atas menunjukkan kehebatan delegate_to yang berpadu dengan serial: 1. Setiap kali loop satu server dijalankan, ia akan menarik dirinya sendiri keluar dari sirkulasi traffic global, melakukan instalasi secara aman, memverifikasi kesehatan dirinya sendiri secara lokal, dan masuk kembali ke kluster load balancer tanpa pernah mengganggu pengguna aktif.
run_once untuk Efisiensi Eksekusi #
Secara default, jika sebuah play mencakup 20 server, setiap task di dalam play tersebut akan dijalankan sebanyak 20 kali (satu kali untuk setiap server). Namun, ada beberapa operasi yang bersifat global dan hanya perlu dieksekusi tepat satu kali saja untuk seluruh siklus play. Contoh klasik adalah:
- Menjalankan script migrasi database (misalnya
django-admin migrateatauflyway migrate). Kita hanya perlu menjalankannya dari salah satu server aplikasi, bukan dari 20 server secara bersamaan yang justru akan merusak integritas skema database. - Membuat folder rilis global di sistem penyimpanan terpusat (S3 atau NAS).
- Mengirimkan satu notifikasi email atau Slack di awal proses deployment untuk memberi tahu tim bahwa proses automasi telah dimulai.
Untuk menghemat waktu eksekusi dan mencegah crash akibat aksi duplikat, kita bisa menyematkan direktif run_once: true pada task tersebut.
Bagaimana run_once Memilih Host? #
Saat run_once: true dideklarasikan tanpa parameter tambahan, Ansible secara default akan memilih host pertama yang terdaftar di dalam inventory aktif untuk mengeksekusi task tersebut. Kita juga bisa menggabungkan run_once dengan delegate_to untuk memaksa eksekusi satu kali itu dilakukan di host tertentu yang spesifik, misalnya di server database master atau di localhost control node.
Berikut perbandingan anti-pattern penulisan tugas global tanpa run_once vs solusi yang benar:
# ANTI-PATTERN: Menjalankan migrasi database di semua host.
# Ini akan menyebabkan perlombaan penulisan (race condition) dan berisiko merusak database cluster!
- name: Deployment web server
hosts: app_servers
tasks:
- name: Jalankan migrasi skema database
ansible.builtin.command: /var/www/webapp/bin/db-migrate
# JANGAN: Tanpa run_once, task ini akan dipanggil oleh setiap server di app_servers secara paralel.
# BENAR: Menggunakan run_once untuk membatasi migrasi hanya sekali eksekusi
- name: Deployment web server optimal
hosts: app_servers
tasks:
- name: Jalankan migrasi skema database sekali saja
ansible.builtin.command: /var/www/webapp/bin/db-migrate
run_once: true # ✓ Menjamin hanya dijalankan oleh host pertama di app_servers
# Kita bisa juga menambahkan delegate_to jika ingin diproses dari node tertentu
Di bawah ini adalah contoh kombinasi run_once dan delegate_to untuk mencatat awal waktu rilis secara global di server monitoring pusat:
# BENAR: Menggabungkan run_once dengan delegate_to
- name: Deployment sistem besar
hosts: web_servers
tasks:
- name: Beritahu monitoring server bahwa deployment dimulai
ansible.builtin.uri:
url: "http://monitoring.kita.internal/api/events"
method: POST
body_format: json
body:
event: "Deployment started"
cluster: "web-prod"
run_once: true # ✓ Hanya kirim 1 event notifikasi, bukan 50 event!
delegate_to: localhost # ✓ Dikirim langsung dari control node
Ringkasan #
delegate_tomengarahkan eksekusi task ke host lain yang ditentukan, namun tetap membawa dan mempertahankan konteks variabel serta facts milik host target asli (inventory_hostname).local_actiondandelegate_to: localhostdigunakan untuk mengeksekusi task secara lokal di control node, seperti mengirim notifikasi Slack, menulis log audit lokal, atau menunggu port eksternal aktif.delegate_facts: truememaksa facts yang dikumpulkan dari task terdelegasi disimpan di bawah nama host pelaksana delegasi tersebut di dalam memorihostvars, bukan di bawah host target play.- Kombinasi
serial: 1dengandelegate_to: load_balanceradalah pola arsitektur standar industri untuk membangun deployment zero-downtime (Cordon, Upgrade, Health Check, Uncordon).run_once: truemembatasi eksekusi task agar hanya dijalankan satu kali saja untuk seluruh host di dalam play, sangat krusial untuk mencegah race condition pada task global seperti migrasi skema database.