Pernah nggak, harus update package di lima server? SSH ke server pertama, apt update, apt upgrade, keluar. SSH ke server kedua, ulangi lagi. Begitu sampai server kelima, sudah lupa tadi server ketiga sudah di-restart nginx-nya atau belum. 😅

Kalau servernya cuma satu, cara manual masih oke. Tapi begitu jumlahnya nambah, capeknya nambah, dan yang paling bahaya: hasilnya nggak konsisten. Di sinilah Ansible jadi penyelamat.

Di tulisan ini saya ceritain pengalaman ngoprek Ansible di lab berisi satu control node dan tiga server Ubuntu 24.04. Semua perintah dan playbook di sini beneran saya jalankan, termasuk error-error yang muncul di tengah jalan. Jadi kalau kamu nemu error yang sama, semoga solusinya ada di sini juga.

📌 Buat siapa tulisan ini? Kamu yang sudah paham dasar Linux dan jaringan (SSH, apt, systemctl), tapi belum pernah pakai Ansible sama sekali.

Ansible itu apa sih?

Singkatnya: Ansible itu alat buat menjalankan tugas administrasi ke banyak server sekaligus, dari satu tempat.

Yang bikin Ansible ramah buat pemula:

  • Nggak perlu install agent di server target. Cukup SSH dan Python, yang biasanya sudah ada di Ubuntu.
  • Tugasnya ditulis dalam YAML, yang relatif gampang dibaca.
  • Tugas yang sama bisa dijalankan berkali-kali dengan hasil yang sama (nanti ada istilahnya: idempotent).
  • Playbook-nya sekaligus jadi dokumentasi. Nggak perlu lagi nyatet “kemarin server ini saya apain aja ya?”

Istilah yang bakal sering muncul

Sebelum praktik, kenalan dulu sama istilahnya:

IstilahArtinya
Control nodeKomputer tempat kamu menjalankan Ansible
Managed nodeServer yang dikelola
InventoryDaftar server, bisa dikelompokkan
PlaybookFile YAML berisi urutan tugas
TaskSatu langkah di dalam playbook
Module“Alat” untuk tugas spesifik, misalnya apt, service, copy, template
HandlerTask yang cuma jalan kalau ada task lain yang bikin perubahan
BecomeJalanin tugas sebagai root (mirip sudo)
IdempotentKalau kondisi sudah sesuai, task nggak ngubah apa-apa

Tenang, nggak perlu hafal sekarang. Nanti kelihatan sendiri waktu praktik.

Lab yang saya pakai

PeranOSPythonKeterangan
Control nodeUbuntu 24.04.4 LTS3.11.15Ansible core 2.16.3
Managed node 1Ubuntu 24.04.5 LTS3.12.3user dengan sudo
Managed node 2Ubuntu 24.04.5 LTS3.12.3user dengan sudo
Managed node 3Ubuntu 24.04.5 LTS3.12.3user dengan sudo

Kalau kamu belum punya server, bikin aja VM di VirtualBox. Caranya pernah saya tulis di Konfigurasi SSH di Mesin Virtual Linux dan Panduan Port Forwarding VirtualBox.

Install Ansible

Di control node, cukup install dari repository Ubuntu:

sudo apt update
sudo apt install ansible -y
ansible --version

Di lab saya hasilnya ansible [core 2.16.3].

⚠️ Versi Ansible di repo Ubuntu bisa berubah dari waktu ke waktu. Cek versi di mesin kamu sendiri, karena beberapa parameter module bisa beda antar versi.

Siapin akses SSH dulu

Ansible ngobrol sama server lewat SSH. Biar nggak ditanya password terus, pakai SSH key.

Bikin key khusus buat lab:

ssh-keygen -t ed25519 -f lab_key -N ''

Kirim public key-nya ke ketiga server:

ssh-copy-id -i lab_key.pub engineer@10.45.1.200
ssh-copy-id -i lab_key.pub engineer@10.45.1.201
ssh-copy-id -i lab_key.pub engineer@10.45.1.202

Tes, harusnya langsung masuk tanpa ditanya password:

ssh -i lab_key engineer@10.45.1.200 hostname

Dengan cara ini, password nggak perlu ditulis di mana pun. Aman dan praktis.

Bikin inventory

Inventory itu daftar server yang mau diurus. Bikin file bernama hosts:

[webserver]
node1 ansible_host=10.45.1.200 ansible_user=engineer
node2 ansible_host=10.45.1.201 ansible_user=engineer
node3 ansible_host=10.45.1.202 ansible_user=engineer

[database]
node3 ansible_host=10.45.1.202 ansible_user=engineer

Perhatikan node3 muncul di dua grup sekaligus, webserver dan database. Itu boleh kok.

Cek apakah Ansible membaca inventory-nya dengan benar:

ansible-inventory --graph
@all:
  |--@ungrouped:
  |--@webserver:
  |  |--node1
  |  |--node2
  |  |--node3
  |--@database:
  |  |--node3

Bikin ansible.cfg biar perintahnya nggak panjang

Daripada tiap kali nulis -i hosts -u engineer --private-key ..., simpan semua setting di ansible.cfg di folder kerja:

[defaults]
inventory = ./hosts
host_key_checking = False
remote_user = engineer
private_key_file = ./lab_key
interpreter_python = auto_silent

[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False

⚠️ host_key_checking = False cuma biar lab nggak tersendat konfirmasi fingerprint. Jangan dipakai di produksi.

Tes koneksi: ping-pong

Sebelum ngapa-ngapain, pastikan Ansible bisa menjangkau semua server:

ansible all -m ping
node3 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
node1 | SUCCESS => { ... "ping": "pong" }
node2 | SUCCESS => { ... "ping": "pong" }

Ketiganya jawab pong. Artinya SSH dan Python di server sudah siap. 🎉

Playbook pertama: update package

Bikin file update_servers.yml:

---
- name: Update dan upgrade package di server web
  hosts: webserver
  become: yes

  tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600
      register: apt_cache
      retries: 5
      delay: 5
      until: apt_cache is not failed

    - name: Upgrade package saat ini
      apt:
        upgrade: dist
        autoremove: yes

Jalankan:

ansible-playbook update_servers.yml
TASK [Gathering Facts] ***
ok: [node1]   ok: [node3]   ok: [node2]

TASK [Update apt cache] ***
changed: [node1]   changed: [node3]   changed: [node2]

TASK [Upgrade package saat ini] ***
ok: [node2]   ok: [node3]   changed: [node1]

PLAY RECAP ***
node1 : ok=3  changed=2  unreachable=0  failed=0
node2 : ok=3  changed=1  unreachable=0  failed=0
node3 : ok=3  changed=1  unreachable=0  failed=0

Bagian yang paling penting dibaca adalah PLAY RECAP. Di situ kelihatan per server: berapa task yang aman (ok), berapa yang ngubah sesuatu (changed), dan ada yang gagal atau nggak (failed).

Drama #1: gagal acak, kadang node1, kadang node2

Eh tapi, playbook di atas itu versi yang sudah diperbaiki. Versi awal saya cuma pakai update_cache: yes tanpa tambahan apa-apa. Hasilnya? Gagal secara acak:

TASK [Update apt cache] ***
changed: [node1]
fatal: [node2]: FAILED! => {"changed": false, "msg": "Failed to update apt cache: unknown reason"}

Saya jalankan lima kali berturut-turut, yang gagal ganti-ganti node. Pesannya juga nggak membantu: unknown reason. 🙃

Ternyata penyebabnya unattended-upgrades, layanan bawaan Ubuntu yang diam-diam update di belakang layar. Kalau dia lagi pegang lock apt pas Ansible jalan, ya tabrakan.

Solusinya dua lapis:

  1. cache_valid_time: 3600. Kalau cache apt umurnya masih di bawah satu jam, Ansible nggak usah jalanin apt-get update lagi. Lebih jarang tabrakan.
  2. retries + until. Kalau tetap gagal, coba lagi sampai 5 kali dengan jeda 5 detik.

Fun fact dari ansible-doc apt: module apt sebenarnya sudah punya retry bawaan buat update cache.

update_cache_retries          default: 5
update_cache_retry_max_delay  default: 12
cache_valid_time              default: 0

Perhatikan cache_valid_time default-nya 0. Artinya kalau nggak diatur, cache selalu di-update.

Kenapa update_cache selalu changed?

Ini sempat bikin saya bingung. Task update_cache: yes selalu lapor changed, padahal nggak ada package baru. Itu karena apt-get update memang selalu memperbarui daftar package.

Jadi tenang, changed di situ bukan tanda masalah. Task ini baru bisa ok (nggak berubah) kalau cache dianggap masih segar lewat cache_valid_time.

Playbook kedua: install nginx

Bikin setup_nginx.yml:

---
- name: Install dan kelola Nginx
  hosts: webserver
  become: yes

  tasks:
    - name: Install nginx
      apt:
        name: nginx
        state: present
        update_cache: yes
        cache_valid_time: 3600
      register: apt_nginx
      retries: 5
      delay: 5
      until: apt_nginx is not failed

    - name: Pastikan nginx berjalan
      service:
        name: nginx
        state: started
        enabled: yes
ansible-playbook setup_nginx.yml

Yang bikin playbook ini aman dijalankan berulang:

  • state: present → kalau nginx sudah ada, nggak diinstall ulang
  • state: started → kalau sudah jalan, nggak di-restart
  • enabled: yes → nginx tetap nyala setelah server reboot

Jangan cuma percaya output Ansible, cek langsung ke server:

ansible webserver -m command -a "systemctl is-active nginx"
ansible webserver -m command -a "systemctl is-enabled nginx"
node1 | rc=0 >> active
node2 | rc=0 >> active
node3 | rc=0 >> active

node1 | rc=0 >> enabled
node2 | rc=0 >> enabled
node3 | rc=0 >> enabled

Drama #2: copy vs template, kelihatan mirip tapi beda jauh

Ini jebakan yang saya sendiri kena. 😅

Saya mau setiap server punya halaman index.html yang nampilin nama node-nya. Awalnya saya pakai module copy:

- name: Copy halaman index
  copy:
    src: files/index.html
    dest: /var/www/html/index.html
    owner: www-data
    group: www-data
    mode: '0644'

Isi files/index.html:

<p>Node: {{ inventory_hostname }}</p>

Halamannya kebuka, HTTP 200, aman. Tapi yang tampil di browser:

Node: {{ inventory_hostname }}

Lho, variabelnya nggak diganti! Ternyata copy itu memindahkan file apa adanya, nggak memproses Jinja2.

Solusinya: pakai template

Untuk file yang berisi variabel, pakai module template. Simpan file-nya dengan akhiran .j2, misalnya templates/index.html.j2:

<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="utf-8">
  <title>Lab Ansible</title>
</head>
<body>
  <h1>Halaman ini dikelola otomatis oleh Ansible</h1>
  <p>Node: {{ inventory_hostname }}</p>
  <p>Hostname: {{ ansible_hostname }}</p>
  <p>Distribusi: {{ ansible_distribution }} {{ ansible_distribution_version }}</p>
</body>
</html>

Playbook deploy_index.yml:

---
- name: Deploy halaman index dari template Jinja2
  hosts: webserver
  become: yes

  tasks:
    - name: Render template index.html
      template:
        src: templates/index.html.j2
        dest: /var/www/html/index.html
        owner: www-data
        group: www-data
        mode: '0644'
      notify: reload nginx

  handlers:
    - name: reload nginx
      service:
        name: nginx
        state: reloaded

Hasil di node2:

<p>Node: node2</p>
<p>Hostname: dwp-mng-2</p>
<p>Distribusi: Ubuntu 24.04</p>

Nah, sekarang tiap server dapat nilai sesuai dirinya sendiri. ✅

ModuleProses Jinja2?Cocok untuk
copy❌ NggakFile statis, sertifikat, binary
template✅ YaFile konfigurasi atau halaman yang ada variabelnya

Handler: restart cuma kalau perlu

Di playbook tadi ada notify: reload nginx. Itu manggil handler.

Handler cuma jalan kalau task yang manggilnya lapor changed. Jadi nginx nggak di-reload tiap kali playbook dijalankan, cuma kalau halamannya beneran berubah.

Jalan pertama (ada perubahan, handler ikut jalan):

TASK [Render template index.html] ***
changed: [node1]   changed: [node2]   changed: [node3]

RUNNING HANDLER [reload nginx] ***
changed: [node1]   changed: [node2]   changed: [node3]

Jalan kedua (nggak ada perubahan, handler dilewati):

TASK [Render template index.html] ***
ok: [node1]   ok: [node2]   ok: [node3]

PLAY RECAP ***
node1 : ok=2  changed=0  failed=0
node2 : ok=2  changed=0  failed=0
node3 : ok=2  changed=0  failed=0

changed=0 di ketiga node, itulah bukti idempotent yang sebenarnya.

Ubah satu baris config dengan lineinfile

Kadang kita nggak mau kirim file utuh, cuma mau ganti satu baris. Misalnya ganti server_name _; jadi server_name lab.local; di site default nginx. Untuk ini ada lineinfile.

Playbook configure_server_name.yml:

---
- name: Konfigurasi server_name pada nginx
  hosts: webserver
  become: yes

  tasks:
    - name: Pastikan server_name diatur pada site default
      lineinfile:
        path: /etc/nginx/sites-available/default
        regexp: '^\s*server_name\s+'
        line: '	server_name lab.local;'
        state: present
      notify: validate nginx config

  handlers:
    - name: validate nginx config
      command: nginx -t
      register: nginx_test
      failed_when: nginx_test.rc != 0
  • regexp → baris mana yang dicari (yang diawali server_name)
  • line → isi penggantinya
  • state: present → pastikan baris ini ada

Intip dulu pakai --check --diff

Sebelum ngubah config beneran, biasakan lihat dulu apa yang bakal berubah:

ansible-playbook configure_server_name.yml --check --diff
TASK [Pastikan server_name diatur pada site default] ***
--- before: /etc/nginx/sites-available/default (content)
+++ after: /etc/nginx/sites-available/default (content)
@@ -43,7 +43,7 @@
 	index index.html index.htm index.nginx-debian.html;

-	server_name _;
+	server_name lab.local;

--check itu mode simulasi, nggak ada yang diubah. --diff nunjukin perbandingan sebelum dan sesudah. Menariknya, handler-nya ke-trigger tapi dilewati:

RUNNING HANDLER [validate nginx config] ***
skipping: [node1]
skipping: [node2]
skipping: [node3]

Masuk akal, file-nya kan belum beneran berubah, jadi nginx -t juga nggak perlu jalan.

Jalankan beneran

ansible-playbook configure_server_name.yml
TASK [Pastikan server_name diatur pada site default] ***
changed: [node1]
changed: [node3]
changed: [node2]

RUNNING HANDLER [validate nginx config] ***
changed: [node1]
changed: [node2]
changed: [node3]

PLAY RECAP ***
node1 : ok=3  changed=2  unreachable=0  failed=0
node2 : ok=3  changed=2  unreachable=0  failed=0
node3 : ok=3  changed=2  unreachable=0  failed=0

Run kedua? changed=0 di semua node, handler nggak jalan. Idempotent lagi. ✅

Cek langsung isi file-nya:

ansible webserver -m command -a "grep -n server_name /etc/nginx/sites-available/default"
node1 | CHANGED | rc=0 >>
46:	server_name lab.local;
83:#	server_name example.com;

Baris 46 berubah, baris 83 yang berupa komentar tetap aman. regexp-nya tepat sasaran.

Drama #3: nginx -t sukses, tapi kok ada warning?

Setelah perubahan itu, nginx -t ngeluarin ini:

[warn] conflicting server name "lab.local" on 0.0.0.0:80, ignored
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Ternyata di /etc/nginx/sites-enabled/ ada dua file yang sama-sama pakai server_name lab.local: default dan lab-site.conf sisa latihan saya sebelumnya. 🤦

Pelajarannya: nginx -t bisa sukses tapi tetap kasih warning. Config-nya valid, tapi salah satu diabaikan nginx. Kalau ketemu kasus begini, cek dulu:

ls -l /etc/nginx/sites-enabled/

Saya bersihkan vhost latihan itu pakai module file:

- name: Hapus symlink sites-enabled/lab-site.conf
  file:
    path: /etc/nginx/sites-enabled/lab-site.conf
    state: absent

Warning hilang, dan tes HTTP-nya:

--- 10.45.1.200 ---
default Host  -> HTTP 200
Host lab.local -> HTTP 200
<p>Node: node1</p>
<p>Hostname: dwp-mng-1</p>

Request dengan header Host: lab.local juga jalan, berarti server_name baru sudah berfungsi.

Ringkasan perilaku lineinfile:

KondisiHasil
Baris nggak ketemuDitambahkan di akhir file
Baris ketemu, isinya bedaDiganti
Baris ketemu, isinya samaNggak ada perubahan (ok)
state: absentBarisnya dihapus

Kalau mau ubah banyak baris sekaligus, lineinfile kurang cocok. Pakai template atau blockinfile.

Tes dari control node

curl -I http://10.45.1.200/
HTTP/1.1 200 OK

Ketiga node jawab HTTP 200, dan masing-masing nampilin identitasnya sendiri dari hasil render template tadi.

Dua opsi penyelamat: --limit dan --check

Begitu servernya makin banyak, dua opsi ini wajib dikenal.

Coba di satu server dulu:

ansible-playbook deploy_index.yml --limit node1

Simulasi tanpa ngubah apa pun:

ansible-playbook deploy_index.yml --check

Atau gabungin keduanya:

ansible-playbook deploy_index.yml --limit node1 --check

Bayangin punya 20 server dan ternyata playbook-nya salah. Mending ketahuan di satu server, kan?

Troubleshooting: error yang beneran saya temui

Semua di bawah ini muncul waktu ngoprek lab ini, bukan contoh karangan.

1. E: dpkg was interrupted

fatal: [node1]: FAILED! => {
  "msg": "'/usr/bin/apt-get dist-upgrade --auto-remove' failed:
  E: dpkg was interrupted, you must manually run
  'sudo dpkg --configure -a' to correct the problem.",
  "rc": 100
}

Artinya ada instalasi package yang nggak selesai. Sering kejadian di VM yang baru dibuat atau sempat kehabisan resource. Solusinya persis seperti saran di pesan error:

sudo dpkg --configure -a
sudo dpkg --audit

Di lab saya, dpkg --audit masih nunjukin apport bermasalah, jadi saya install ulang:

sudo apt-get install --reinstall -y apport

Setelah itu dpkg --audit bersih dan playbook jalan normal.

💡 Tips: sebelum jalanin playbook pertama di server baru, cek dulu sudo dpkg --audit. Lebih baik ketahuan di awal.

2. Failed to update apt cache: unknown reason

Ini Drama #1 tadi. Biasanya karena lock apt lagi dipegang unattended-upgrades. Cek prosesnya:

sudo fuser -v /var/lib/dpkg/lock-frontend
pgrep -a "apt|dpkg|unattended"

Solusinya ya cache_valid_time + retries/until seperti di playbook pertama.

3. UNREACHABLE: Ansible nggak bisa masuk

fatal: [node3]: UNREACHABLE! => {
  "msg": "Failed to connect to the host via ssh:
  kex_exchange_identification: read: Connection reset by peer"
}

Urutan ngeceknya:

  1. Server hidup? ping -c2 10.45.1.202
  2. SSH bisa? ssh -i lab_key engineer@10.45.1.202 hostname
  3. Kalau SSH ditolak, cek status layanan SSH di server itu
  4. Kalau server baru reboot, tunggu sampai SSH-nya benar-benar siap

Di kasus saya, ternyata node3 lagi reboot. Begitu nyala lagi, playbook jalan normal tanpa diubah apa-apa.

4. Variabel muncul mentah {{ ... }}

Hampir pasti kamu pakai copy untuk file yang ada variabelnya. Ganti ke template. (Lihat Drama #2.)

5. Error YAML

ERROR! Syntax Error while loading YAML

YAML itu sensitif banget sama indentasi. Cek:

  • Jangan campur tab dan spasi
  • Setiap item di bawah tasks: diawali - yang sejajar
  • Baris pertama ---

Cek sintaks tanpa menjalankan playbook:

ansible-playbook update_servers.yml --syntax-check

6. Playbook nge-hang lama pas update package

Ini yang sering disangka “Ansible-nya lemot”, padahal masalahnya di server. Gejalanya: playbook nggak selesai-selesai, nggak ada error. Kalau dipaksa berhenti, run berikutnya malah kena Could not get lock.

Kuncinya: pisahkan masalah Ansible dari masalah server.

# 1. Ada proses lain yang pegang lock apt?
ansible node1 -m shell -a "sudo fuser -v /var/lib/apt/lists/lock; pgrep -a 'apt|dpkg'"

# 2. Server bisa nyampe ke repo package?
ansible node1 -m shell -a "getent hosts archive.ubuntu.com; curl -s -o /dev/null -w '%{http_code}\n' --max-time 10 http://archive.ubuntu.com/ubuntu/dists/noble/InRelease"

Terus coba jalankan langsung di server, tanpa Ansible:

sudo apt-get update

Kalau ini juga nge-hang, berarti bukan salah playbook. Mulai cek dari koneksi server ke repository.

Kalau playbook sudah terlanjur dihentikan paksa, bisa ada proses yatim yang masih pegang lock:

# lihat proses sisa
ansible webserver -m shell -a "pgrep -af '[A]nsiballZ'"

# kalau sudah yakin itu proses yatim, bersihkan
ansible webserver -m shell -a "pkill -9 -f '[A]nsiballZ_apt.py'" --become

⚠️ Jalankan pkill hanya kalau kamu yakin nggak ada playbook lain yang lagi jalan. Trik [A] di pola itu supaya pgrep/pkill nggak ikut nyocokin proses shell-nya sendiri.

7. Error-nya terlalu singkat? Pakai -vvv

ansible-playbook setup_nginx.yml -vvv

Output-nya jadi jauh lebih panjang, termasuk perintah asli yang dikirim ke server. Sering kali penyebabnya langsung kelihatan dari situ.

Struktur folder yang saya pakai

project-ansible/
├── ansible.cfg
├── hosts
├── update_servers.yml
├── setup_nginx.yml
├── deploy_index.yml
├── configure_server_name.yml
├── files/
│   └── index.html
└── templates/
    └── index.html.j2

files/ buat file statis, templates/ buat file yang ada variabelnya. Dipisah begini, playbook jadi lebih gampang dibaca.

Status akhir lab

Yang diujiStatus
ansible all -m ping ke 3 node✅
Inventory dengan grup✅
update_servers.yml✅ (setelah dpkg dibereskan)
setup_nginx.yml (install + service)✅
copy untuk file statis✅
template render variabel✅
lineinfile ubah server_name✅
file hapus symlink vhost✅
Handler reload nginx dan nginx -t✅ cuma jalan saat ada perubahan
--check --diff, --limit✅
HTTP test (termasuk Host: lab.local)✅ HTTP 200 di 3 node
Idempotent (changed=0 di run ulang)✅ update_servers, deploy_index, configure_server_name
Idempotent setup_nginx.yml⏳ belum sempat diverifikasi

Soal yang terakhir, jujur aja: waktu mau run ulang setup_nginx.yml, server lab lagi nggak bisa nyampe ke repository package (kasus troubleshooting nomor 6). Jadi statusnya belum diverifikasi, bukan gagal. Nanti saya update kalau sudah dites ulang.

Pelajaran yang saya dapat

  1. copy dan template itu beda. Kelihatan mirip, tapi cuma template yang memproses variabel.
  2. changed belum tentu masalah. update_cache memang selalu lapor changed kalau nggak pakai cache_valid_time.
  3. Error di Ansible belum tentu salah playbook. Coba perintah yang sama manual di server. Kalau sama-sama gagal, masalahnya di server atau jaringan.
  4. nginx -t sukses ≠ bersih. Baca juga warning-nya.
  5. --check --diff dan --limit itu sabuk pengaman. Biasakan dipakai sebelum ngubah server penting.
  6. Cek dpkg --audit di server baru. Hemat waktu banget.

Langkah berikutnya

Kalau sudah nyaman sama dasar-dasar ini, topik lanjutan yang layak dioprek:

  • Role, biar playbook bisa dipecah dan dipakai ulang
  • Ansible Vault, buat nyimpan data sensitif seperti password
  • Integrasi ke CI/CD, biar playbook jalan otomatis tiap ada perubahan

Oh iya, simpan playbook dan inventory kamu di Git. Riwayat perubahannya kecatat rapi dan gampang kalau mau kerja bareng tim.

Seru sih. Begitu ketiga node jawab changed=0 di run kedua, rasanya kayak ngurus satu server padahal ada tiga. 😄

Kalau kamu nyoba dan mentok di error yang nggak ada di sini, kabari saya lewat halaman Contact. Siapa tahu bisa jadi bahan tulisan berikutnya.

Sources

[1] Ansible Documentation: Getting started with Ansible

[2] ansible.builtin.apt module

[3] ansible.builtin.template module

[4] ansible.builtin.lineinfile module