SSD Nodes Learn
Panduan Matt ConnorOleh Matt Connor · Diperbarui 2026-07-24

Tutorial Ansible: Membuat Playbook di VPS

Pelajari cara instalasi Ansible via pipx di Ubuntu 24.04, pembuatan inventory, serta solusi error Permission denied dan sudo saat hardening VPS baru.

Apa yang Anda bangun

Satu mesin kontrol dengan Ansible terinstal, dan satu atau lebih VPS Ubuntu 24.04 baru yang hanya berisi citra standar (stock image). Pada akhirnya, Anda akan memiliki file inventaris yang menamai server Anda, perintah ad-hoc ping yang membuktikan autentikasi berhasil secara menyeluruh, dan sebuah playbook yang menjalankan seluruh daftar periksa VPS baru sebagai kode: pengguna deploy dengan kunci SSH Anda, sshd yang diperkuat, fail2ban, unattended upgrades, dan firewall yang mengizinkan OpenSSH sebelum memblokir semua koneksi lainnya. Gunakan untuk satu server atau dua puluh server. Jalankan dua kali dan eksekusi kedua tidak akan mengubah apa pun — itulah tujuan utamanya.

Setelah lima belas tahun melakukan provisi VPS, saya dapat memberi tahu Anda pola yang nyata: semua orang menyiapkan lima server pertama secara manual, lalu membuang waktu satu akhir pekan pada server keenam karena tidak ada yang ingat apa yang telah dilakukan pada lima server pertama. Panduan ini memperdalam pembahasan dalam mengelola banyak server Linux — bacalah saat Anda mendapati diri Anda mengetik apt install yang sama ke dalam tiga terminal.

Apa itu Ansible dalam satu paragraf

Ansible bersifat agentless. Tidak ada daemon yang perlu diinstal pada server yang dikelola: mesin kontrol terhubung melalui SSH biasa, menyalin modul Python kecil ke target, mengeksekusinya, membaca output JSON, lalu menghapusnya. Satu-satunya hal yang dibutuhkan target adalah python3, yang sudah tersedia pada setiap citra Ubuntu standar. Kata kuncinya adalah idempotent, yang memiliki arti sederhana: sebuah tugas mendeskripsikan sebuah state, bukan sebuah tindakan. state: present untuk sebuah paket berarti "pastikan paket ini terinstal", bukan "jalankan installer". Jika state sudah terpenuhi, Ansible tidak melakukan apa pun dan melaporkannya sebagai ok alih-alih changed. Properti tersebut adalah inti dari produk ini — hal inilah yang membuat menjalankan ulang playbook menjadi aman, dan pengulangan yang aman adalah hal yang mengubah shell script menjadi infrastruktur.

Prasyarat, dan kendala awal

  • Satu mesin kontrol: laptop Anda atau VPS kecil. Saya mengasumsikan Ubuntu 24.04; macOS bekerja secara identik setelah pipx diinstal melalui Homebrew.
  • Satu atau lebih VPS target yang menjalankan Ubuntu 24.04 pada KVM, dapat diakses sebagai root. Tidak ada perangkat lunak yang diinstal pada VPS tersebut.
  • Autentikasi key SSH ke setiap target. Ansible menggunakan tingkat autentikasi yang sama dengan perintah ssh Anda — jika ssh root@host meminta kata sandi, Ansible akan gagal.
  • Pada Ubuntu 24.04, pip install ansible gagal karena error: externally-managed-environment. Ini adalah kebijakan distro yang disengaja, bukan kerusakan sistem. Gunakan pipx.
  • Spasi pada YAML adalah sintaksis. Indentasi yang salah menghasilkan mapping values are not allowed in this context, dan penggunaan karakter tab di mana pun akan menyebabkan kegagalan fatal.
  • Tetap buka sesi SSH yang aktif pada setiap target saat playbook melakukan pengerasan (hardening) sshd. Setiap pemulihan akses yang saya bantu lakukan melibatkan penutupan sesi terakhir "untuk pengujian bersih".

Step 1: instal Ansible pada control machine menggunakan pipx, bukan pip

Insting umum adalah menggunakan pip3 install ansible. Pada image 24.04 yang benar-benar baru, proses akan gagal pada tahap awal — Command 'pip3' not found, but can be installed with: sudo apt install python3-pip — dan menginstal pip hanya akan memicu masalah serius:

pip3 install ansible
error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.

Ubuntu 24.04 menetapkan Python sistem sebagai externally managed (PEP 668) sehingga pip tidak dapat bertabrakan dengan apt pada file yang sama. Jangan gunakan --break-system-packages; nama flag tersebut sudah menjelaskan fungsinya. Solusi yang tepat adalah pipx, yang memberikan Ansible virtualenv terisolasi sendiri dan memasukkan binary ke dalam PATH Anda:

sudo apt update && sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansible

Buka shell baru setelah pipx ensurepath agar perubahan PATH diterapkan. --include-deps bukan sekadar hiasan: paket ansible tidak memiliki console script sendiri — ansible, ansible-playbook, dan lainnya adalah entry point dari dependency ansible-core — sehingga tanpa flag tersebut, pipx akan menolak instalasi dengan pesan No apps associated with package ansible or its dependencies. Dan instal paket ansible, bukan ansible-core saja — paket lengkap sudah menyertakan community collections, dan playbook ini menggunakan modul dari dua koleksi tersebut (ansible.posix dan community.general).

ansible --version

Hasil yang benar dimulai dengan baris seperti ansible [core 2.19.x] dan menyebutkan versi Python yang digunakan; versi core saat ini sudah cukup untuk semua kebutuhan di sini. ansible: command not found berarti ~/.local/bin belum ada di PATH Anda — gunakan shell baru, atau source ~/.bashrc.

Itu adalah seluruh proses instalasi. Target tidak menerima apa pun.

Step 2: Akses kunci SSH ke setiap target

ssh-keygen -t ed25519 -C "ansible control"
ssh-copy-id root@10.0.0.10
ssh-copy-id root@10.0.0.20

Kemudian buktikan, satu kali untuk setiap host:

ssh root@10.0.0.10 true && echo ok

Satu baris tersebut menjalankan dua fungsi: mengonfirmasi autentikasi kunci berhasil tanpa kata sandi, dan mencatat host key ke dalam known_hosts. Lakukan sekarang, karena Ansible akan menampilkan host key yang belum tercatat sebagai prompt interaktif di tengah proses eksekusi, yang terlihat seperti sistem macet.

Step 3: inventory — Gunakan INI terlebih dahulu, YAML saat sudah besar

Inventory adalah berkas teks yang berisi daftar mesin yang dapat diakses oleh Ansible. Buat inventory.ini di dalam direktori proyek baru:

[vps]
web1 ansible_host=10.0.0.10
web2 ansible_host=10.0.0.20

[vps:vars]
ansible_user=root

web1 adalah alias pilihan Anda — alias ini muncul pada output dan menjadi target saat menggunakan --limit web1. ansible_host adalah alamat asli. [vps] adalah sebuah grup, dan [vps:vars] menetapkan variabel untuk setiap host di dalamnya; ansible_user adalah user yang digunakan Ansible untuk login. Di sampingnya, terdapat ansible.cfg agar Anda tidak perlu mengetik -i lagi:

[defaults]
inventory = inventory.ini

Ansible membaca ansible.cfg dari direktori saat ini. Format inventory yang sama dalam YAML — simpan sebagai inventory.yml dan arahkan ansible.cfg ke nama tersebut — adalah format yang akan Anda gunakan setelah setiap host memiliki banyak variabel:

vps:
  hosts:
    web1:
      ansible_host: 10.0.0.10
    web2:
      ansible_host: 10.0.0.20
  vars:
    ansible_user: root

Keduanya setara. Format INI lebih mudah dibaca untuk dua server; format YAML lebih skalabel untuk dua puluh server. Pilih salah satu dan jangan memikirkannya lagi.

Langkah 4: perintah ad-hoc — indikator keberhasilan sistem

ansible all -m ping

Ini bukan ICMP. Modul ping adalah uji coba lengkap: login SSH, penyalinan modul, eksekusi Python pada target, dan pembersihan. Hasil yang benar berwarna hijau, satu blok untuk setiap host:

web1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

Warna hijau SUCCESS berarti autentikasi, interpreter Python, dan transport berfungsi — playbook juga akan berfungsi. Warna merah UNREACHABLE! berarti transport gagal sebelum modul dijalankan; detail pesan kesalahan dan solusinya ada di bagian mode kegagalan di bawah. Dua perintah ad-hoc lainnya yang penting untuk diketahui:

ansible all -a "uptime"
ansible all -m apt -a "update_cache=true upgrade=dist" --become

Ad-hoc digunakan untuk tugas satu kali dan pemeriksaan. Segala sesuatu yang akan Anda jalankan dua kali harus dimasukkan ke dalam playbook.

Step 5: playbook pertama — checklist new-VPS sebagai kode

Ini adalah semua hal yang akan Anda lakukan secara manual pada sepuluh menit pertama di server baru. Simpan sebagai site.yml:

---
- name: Baseline a fresh Ubuntu VPS
  hosts: vps
  become: true

  vars:
    deploy_user: deploy
    deploy_pubkey: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
    baseline_packages:
      - fail2ban
      - unattended-upgrades
      - ufw
    baseline_services:
      - fail2ban
      - unattended-upgrades

  tasks:
    - name: Create the deploy user
      ansible.builtin.user:
        name: "{{ deploy_user }}"
        groups: sudo
        append: true
        shell: /bin/bash

    - name: Install the deploy user's SSH key
      ansible.posix.authorized_key:
        user: "{{ deploy_user }}"
        key: "{{ deploy_pubkey }}"

    - name: Passwordless sudo for the deploy user
      ansible.builtin.copy:
        dest: /etc/sudoers.d/deploy
        content: "{{ deploy_user }} ALL=(ALL) NOPASSWD:ALL\n"
        mode: "0440"
        validate: /usr/sbin/visudo -cf %s

    - name: Install baseline packages
      ansible.builtin.apt:
        name: "{{ baseline_packages }}"
        state: present
        update_cache: true

    - name: Enable and start baseline services
      ansible.builtin.service:
        name: "{{ item }}"
        state: started
        enabled: true
      loop: "{{ baseline_services }}"

    - name: Harden sshd with a drop-in
      ansible.builtin.copy:
        dest: /etc/ssh/sshd_config.d/00-hardening.conf
        content: |
          PasswordAuthentication no
          KbdInteractiveAuthentication no
          PermitRootLogin prohibit-password
          X11Forwarding no
        mode: "0644"
        validate: /usr/sbin/sshd -t -f %s
      notify: Restart ssh

    - name: Allow OpenSSH through ufw
      community.general.ufw:
        rule: allow
        name: OpenSSH

    - name: Enable ufw with default deny
      community.general.ufw:
        state: enabled
        policy: deny

  handlers:
    - name: Restart ssh
      ansible.builtin.service:
        name: ssh
        state: restarted

Baris yang perlu dipahami, bukan sekadar disalin:

Variables berada di bawah vars: dan dirujuk dengan "{{ deploy_user }}" — gunakan tanda kutip pada seluruh ekspresi jika nilai dimulai dengan kurung kurawal, agar parser YAML tidak salah membaca. lookup('file', ...) membaca kunci publik Anda dari mesin control saat runtime, sehingga playbook tidak membawa materi kunci apa pun.

The loop. loop: "{{ baseline_services }}" menjalankan tugas layanan satu kali untuk setiap item, dan output akan menampilkan setiap item pada baris terpisah. Perhatikan bahwa tugas apt mengambil seluruh daftar paket sekaligus — satu transaksi apt lebih cepat dan merupakan pola yang disarankan untuk paket; loop digunakan untuk modul yang benar-benar bekerja pada satu hal dalam satu waktu.

The handler adalah konsep yang harus dipahami. notify: Restart ssh tidak berarti "restart ssh sekarang". Ini memasukkan handler ke dalam antrean, yang akan berjalan satu kali di akhir play, dan hanya jika tugas yang memanggilnya melaporkan changed. Jalankan ulang playbook besok: file drop-in sudah benar, tugas copy melaporkan ok, dan sshd tidak akan di-restart. Baris validate: adalah pengaman pemicu — sshd memeriksa file tersebut sebelum mengganti file lama, sehingga kesalahan ketik akan membatalkan tugas alih-alih merusak daemon.

PermitRootLogin prohibit-password, bukan no — secara sengaja. Playbook ini masuk sebagai root menggunakan kunci. prohibit-password mematikan login root dengan kata sandi sambil tetap menjaga akses Anda tetap aktif. Setelah user deploy terbukti (ssh deploy@10.0.0.10 sudo true — alamat biasa, karena web1 hanyalah alias yang hanya diketahui Ansible), ubah ansible_user=deploy di dalam inventory dan perketat menjadi no pada eksekusi berikutnya. Lakukan pengerasan (hardening) dengan urutan yang tidak akan membuat Anda terkunci dari sistem.

Prefix 00- sangat penting. Untuk sebagian besar kata kunci, sshd mematuhi kemunculan pertama yang diproses, dan sshd_config milik Ubuntu menyertakan sshd_config.d/*.conf dalam urutan leksikal sebelum isi utamanya. Cloud images Ubuntu 24.04 sudah menyertakan 60-cloudimg-settings.conf di direktori tersebut, dan penyedia layanan yang mengaktifkan login kata sandi melalui cloud-init menambahkan 50-cloud-init.conf dengan PasswordAuthentication yes; menamai milik kita 00-hardening.conf membuatnya diurutkan pertama dan mengungguli keduanya.

Urutan tugas adalah pengaman firewall. Allow OpenSSH berjalan sebelum Enable ufw dengan kebijakan deny — Ansible mengeksekusi tugas secara ketat sesuai urutan yang terdaftar, sehingga celah keamanan terbuka sebelum dinding dibangun. fail2ban tidak memerlukan konfigurasi tambahan untuk berguna di sini; default Ubuntu sudah memantau sshd secara langsung, dan apa yang sebenarnya dilakukan oleh jail — serta apa yang perlu diatur — dibahas dalam panduan fail2ban di Ubuntu 24.04.

Step 6: dry run dengan --check, lalu jalankan secara nyata

ansible-playbook site.yml --check

Mode check melakukan koneksi, menghitung apa yang akan dilakukan, dan tidak mengubah apa pun. Periksa jumlah changed= di PLAY RECAP pada bagian bawah — itu adalah jumlah tugas yang akan memodifikasi setiap host. Satu catatan penting: mode check memiliki batasan struktural jika tugas selanjutnya bergantung pada perubahan dari tugas sebelumnya. Citra server standar Ubuntu sudah menyertakan ufw, sehingga playbook ini berjalan dry-run dengan lancar — namun pada citra minimal tanpa ufw, tugas ufw akan gagal dalam mode check, karena mode check tidak benar-benar menginstal paket tersebut dan modul tidak memiliki objek untuk dipanggil. Itu adalah batasan dari dry run, bukan bug pada playbook Anda. Jika rencana sudah terlihat benar:

ansible-playbook site.yml

Setiap tugas mencetak satu baris per host — kuning changed, hijau ok — dan ringkasannya harus terbaca:

PLAY RECAP *********************************************************************
web1 : ok=10  changed=9  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
web2 : ok=10  changed=9  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

Sepuluh ok adalah pengumpulan data ditambah delapan tugas ditambah handler. changed Anda boleh berbeda satu atau dua angka dari milik saya: citra standar Ubuntu sudah menyertakan ufw dan unattended-upgrades, dan fail2ban akan berjalan otomatis segera setelah apt menginstalnya, sehingga sebuah tugas dapat melaporkan ok pada saat pertama kali dijalankan — karena status yang dinyatakan sudah terpenuhi. Angka yang harus bernilai nol adalah unreachable dan failed. Catatan tentang become: true: ini hanyalah formalitas saat Anda terhubung sebagai root, tetapi saat Anda mengubah ansible_user ke deploy, sudo akan aktif — dan file sudoers NOPASSWD yang diinstal playbook ini adalah alasan mengapa -K tidak muncul di command line Anda. Tanpa itu, Anda akan mendapatkan Missing sudo password, yang dijelaskan di bawah.

Step 7: jalankan dua kali — contoh idempotensi

Jalankan perintah yang sama segera setelahnya:

web1 : ok=9  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

changed=0, dan ok berkurang satu karena handler yang tidak terpicu tidak pernah berjalan. Tidak ada yang diinstal ulang, sshd tidak dimulai ulang, dan ufw tidak diubah. Hal ini membuat playbook berfungsi sebagai audit sekaligus penyedia (provisioner): tambahkan web3 ke dalam inventory bulan depan lalu jalankan kembali — server baru akan dibangun, sedangkan server lama akan diverifikasi. Nilai changed yang tidak nol pada server yang tidak Anda sentuh menunjukkan adanya drift, yang menandakan seseorang telah mengedit secara manual apa yang seharusnya diedit melalui playbook.

Pola ini akan terus berkembang. Playbook berikutnya yang perlu dibuat adalah memasang WireGuard VPN pada VPS yang sama dan memperketat aturan ufw agar SSH hanya merespons melalui tunnel; setelah itu, buat playbook untuk menginstal Docker dan Compose pada setiap app server. Jika site.yml sudah melewati tiga layar, pecahlah menjadi roles — tetapi jangan lakukan itu sebelum tahap tersebut.

Mode kegagalan, beserta pesan kesalahan yang akan Anda lihat

UNREACHABLE with Permission denied.

web1 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: root@10.0.0.10: Permission denied (publickey).",
    "unreachable": true
}

Transport SSH gagal sebelum modul dijalankan: ansible_user salah, kunci belum disalin ke host tersebut, atau kunci yang digunakan salah. Reproduksi dengan ssh root@10.0.0.10 biasa, lalu gunakan ssh -v untuk melihat kunci mana yang ditawarkan. Jika SSH dengan kata sandi berhasil tetapi Ansible gagal, Anda melewatkan ssh-copy-id.

Missing sudo password.

web1 | FAILED! => {
    "msg": "Missing sudo password"
}

Anda mengatur become: true, terhubung sebagai pengguna non-root, dan pengguna tersebut memerlukan kata sandi untuk sudo. Tambahkan -K (--ask-become-pass) ke baris perintah, atau berikan entri sudoers NOPASSWD kepada pengguna tersebut — inilah alasan mengapa playbook menginstal entri tersebut untuk deploy sebelum Anda beralih ke pengguna tersebut.

error: externally-managed-environment. Anda menjalankan pip pada Python sistem di Ubuntu 24.04. Dibahas pada langkah 1: gunakan pipx, bukan pip, dan bukan --break-system-packages.

mapping values are not allowed in this context.

ERROR! Syntax Error while loading YAML.
  mapping values are not allowed in this context

Hampir selalu karena indentasi: kunci berada pada kedalaman yang salah, atau kurang spasi setelah titik dua. Nomor baris yang dilaporkan menunjuk di dekat kesalahan, bukan tepat pada kesalahan tersebut — periksa juga baris di atasnya. Kesalahan serupa found character '\t' that cannot start any token berarti ada tab yang masuk; YAML melarang penggunaan tab. Jadikan ansible-playbook site.yml --syntax-check sebagai kebiasaan sebelum setiap menjalankan perintah, dan atur editor Anda ke indentasi dua spasi untuk YAML.

/usr/bin/python3: not found. Jarang terjadi pada image standar Ubuntu 24.04, namun umum pada image minimal atau netboot: eksekusi modul gagal karena target tidak memiliki Python. Lakukan bootstrap dengan modul raw, satu-satunya modul yang tidak memerlukan apa pun di sisi target: ansible all -m raw -a "apt-get update && apt-get install -y python3" --become, lalu jalankan ulang playbook.

FAQ

Apakah saya perlu menginstal Ansible pada server yang dikelola?

Tidak. Ansible bersifat agentless: mesin kontrol mengirimkan modul Python kecil melalui SSH, menjalankannya, lalu menghapusnya. Target hanya memerlukan python3 dan akses SSH, yang sudah tersedia pada citra standar Ubuntu. Satu-satunya instalasi dalam panduan ini hanya dilakukan pada mesin kontrol Anda.

Mengapa Ansible memunculkan pesan "Permission denied (publickey)"?

Blok UNREACHABLE! dengan Permission denied (publickey) berarti autentikasi SSH gagal sebelum Ansible menjalankan perintah apa pun. Pastikan ansible_user dalam inventory sesuai dengan akun yang Anda buat, pastikan Anda telah menjalankan ssh-copy-id ke host tersebut, dan pastikan ssh user@host dapat masuk tanpa kata sandi. Solusi apa pun yang memperbaiki perintah ssh biasa juga akan memperbaiki Ansible, karena keduanya menggunakan protokol transportasi yang sama.

Apa arti idempotensi dalam Ansible?

Sebuah tugas (task) menyatakan keadaan yang diinginkan — "paket ini terpasang", "baris ini ada di dalam file ini" — alih-alih sebuah tindakan untuk dilakukan. Jika keadaan tersebut sudah terpenuhi, Ansible tidak melakukan apa pun dan melaporkan ok alih-alih changed. Itulah sebabnya menjalankan playbook dua kali akan menunjukkan changed=0 pada kali kedua, dan mengapa menjalankan ulang adalah audit yang aman, bukan instalasi ulang yang berisiko.

Haruskah saya menggunakan pip atau pipx untuk menginstal Ansible di Ubuntu 24.04?

pipx. Ubuntu 24.04 menandai Python sistem sebagai dikelola secara eksternal, sehingga pip install ansible gagal dengan error: externally-managed-environment secara desain. pipx install --include-deps ansible menempatkan Ansible dalam virtualenv yang terisolasi dan menyediakan ansible, ansible-playbook, dan sisanya pada PATH Anda secara bersih.

Apa perbedaan antara paket ansible dan ansible-core?

ansible-core adalah mesin utama ditambah hanya dengan modul ansible.builtin. Paket ansible menggabungkan core dengan koleksi komunitas yang telah dikurasi — termasuk ansible.posix (modul authorized_key) dan community.general (modul ufw), yang keduanya digunakan dalam panduan ini. Mulailah dengan paket lengkap; gunakan core ditambah koleksi pilihan hanya jika Anda memiliki alasan khusus.