Skip to content

Dua Pembunuh Senyap dalam Satu Deploy Ansible

Adityo Guni Waluyo

Playbook Ansible mati karena apostrof di dalam block scalar, lalu host unreachable karena urutan penawaran key SSH. Dua pelajaran dari satu exit code 4.

Ringkasan

Cron collector-nya mati gara-gara dua bug sekaligus: komentar dalam block scalar YAML ternyata bukan komentar beneran, dan apostrof di kata "today's" bikin Ansible gagal split argumen. Abis itu SSH kena "Too many authentication failures" karena key ed25519 ditolak duluan, jadi dia nambahin IdentitiesOnly=yes di ansible.cfg. Sekarang tiap exit code 4, dia cek dua itu duluan sebelum nyalahin jaringan.

Pukul 16:47 UTC, cron collector saya mati. Log cuma meninggalkan satu baris: ansible-playbook keluar dengan exit code 4, tanpa satu pun output playbook. Tebakan pertama saya: jaringan. Tebakan kedua: SSH key-nya hilang. Dua-duanya meleset, dan yang saya temukan malah dua bug sekaligus yang sama-sama nggak bersuara.

Apostrof yang Bukan Komentar

Saya mulai dari error paling curiga: playbook gagal jalan sama sekali. Ternyata ada satu komentar di dalam task shell yang ditulis sebagai block scalar, dan komentarnya mengandung kata today's, lengkap dengan apostrof.

- name: ssh auth probe
  ansible.builtin.shell: |
    LOG=/var/log/auth.log
    # both units unknown -> re-check today's file lines
    f24=$(journalctl -u ssh --since -24h | grep -c Failed)

Yang nggak saya sadari: di dalam block scalar, baris # itu bukan komentar YAML. YAML melihat seluruh blok sebagai string bebas, lalu Ansible memecahnya lewat argumen splitter-nya. Apostrof tunggal membuat splitter menganggap kutipannya nggak seimbang, dan Ansible melempar pesan "failed at splitting arguments, either an unbalanced jinja2 block or quotes" [4]. Satu karakter, playbook mati total, dan pesan errornya menunjuk ke baris task, bukan ke file YAML-nya.

Bonus kebingungannya: exit code 4. Saya kira itu kode khusus parser error. Faktanya di kode sumber ansible-core, HOST_UNREACHABLE = 4 dan PARSER_ERROR = 4 memakai angka yang sama, bahkan ada komentar pengembang di kodenya yang mengakui bentroknya [3]. Jadi exit code 4 itu bukan diagnosis, cuma petunjuk arah.

Kunci yang Ditolak Sebelum Sempat Dicoba

Setelah komentar itu saya bersihkan, playbook jalan, tapi host tetap unreachable. Penyebabnya di luar playbook: container collector menjalankan ssh tanpa pin identitas, dan ssh menawarkan id_ed25519 lebih dulu. Server menolaknya. Setiap penolakan membakar jatah autentikasi, dan MaxAuthTries default-nya cuma 6 percobaan per koneksi [5]. Sebelum giliran key RSA yang beneran diterima sampai, koneksi sudah diputus dengan "Too many authentication failures".

Perbaikannya di ansible.cfg: IdentitiesOnly=yes supaya ssh hanya menawarkan identitas yang eksplisit dikonfigurasi [2], plus daftar IdentityFile terurut yang meniru urutan di ~/.ssh/config host.

[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s -o IdentitiesOnly=yes -o IdentityFile=/root/.ssh/id_rsa1 -o IdentityFile=/root/.ssh/id_rsa2 -o IdentityFile=/root/.ssh/id_rsa3 -o IdentityFile=/root/.ssh/id_rsa4

Satu jebakan yang hampir kena lagi: menimpa ssh_args berarti mengganti seluruh string, dan defaultnya berisi -C -o ControlMaster=auto -o ControlPersist=60s [1]. Jadi opsi multiplexing ikut saya tulis ulang, soalnya kalau nggak playbook jadi lambat karena buka-tutup koneksi terus-menerus.

Yang Saya Lakukan Biar Nggak Kejadian Lagi

Dua lapis pencegahan yang saya adopsi. Pertama, untuk task shell/command, saya pindah ke bentuk dictionary (cmd: + args:); ansible-lint punya rule no-free-form justru karena free-form "can produce subtle bugs" [6]. Kedua, komentar di dalam blok skrip panjang sekarang saya tulis tanpa apostrof, atau skripnya dipindah ke file terpisah. Klien ssh juga punya aturan first-value-wins per direktif [2], jadi urutan baris di config itu bukan kosmetik, itu semantik.

Detail sekecil apapun, komentar atau urutan key bisa menentukan deploy malam itu sukses atau saya begadang. Lima menit memeriksa config lebih murah daripada satu jam debugging di production. Sekarang setiap kali playbook saya gagal dengan exit code 4, saya cek dua hal itu lebih dulu sebelum curiga ke jaringan.

Sources

[1] Ansible ssh connection plugin docs

[2] ssh_config(5), OpenBSD

[3] ansible-core ExitCode constants

[4] ansible-core split_args source

[5] sshd_config(5), OpenBSD

[6] ansible-lint no-free-form rule

Artikel terkait