Toolkit yang menumpuk sepanjang Bagian 3, lib.sh, ops.sh, deploy-pipeline.sh, provision-server.sh, toolkit-keamanan.sh, dan alert-notifikasi.sh, sudah dirapikan strukturnya lewat sourcing dan README di Bab 26.1 sampai 26.2, diversikan lewat Git di Bab 26.3, dan diuji lewat bats di Bab 26.4. Seluruh proses itu diam-diam mengasumsikan satu hal: Bash memang bahasa yang tepat untuk seluruh toolkit itu. Asumsi ini sebenarnya sudah beberapa kali retak sepanjang series ini tanpa pernah dibahas terus terang. Bab 20.2.3 memperkenalkan jq justru karena menyusun JSON secara manual lewat penyambungan string di Bash "gampang rusak"; Bab 4.2.1 menegaskan associative array Bash cuma menyimpan pasangan key-value satu tingkat, tidak ada struktur bersarang seperti objek di dalam objek; dan Bab 26.4.2 sengaja menguji die() lewat proses yang benar-benar dijalankan (run), bukan lewat pemanggilan fungsi langsung dengan data tiruan seperti lazimnya unit test di bahasa lain. Bab ini membahas retakan-retakan itu secara eksplisit: kapan sinyal itu cukup kuat untuk pindah dari Bash ke Python, Go, atau bahasa lain (27.1), bagaimana Bash tetap berperan bahkan setelah sebagian logika pindah ke bahasa lain (27.2), dan studi kasus menerapkan kriteria itu ke toolkit-ops yang sudah dibangun sepanjang series ini (27.3), sebelum seluruh kemampuan yang terkumpul disatukan jadi proyek akhir di Bab 28.
27.1 Kapan Pindah dari Bash ke Bahasa Lain
Pertanyaan "kapan sebaiknya pindah dari Bash" tidak punya jawaban tunggal berupa jumlah baris kode atau jumlah fungsi tertentu. Lima sub-bagian berikut membedah sinyal yang lebih bisa diandalkan: kekuatan Bash yang memang tidak tergantikan (27.1.1), tiga kategori sinyal yang menunjukkan Bash mulai kurang cocok, dari sisi kompleksitas data dan logika (27.1.2), performa dan concurrency (27.1.3), sampai portabilitas dan distribusi (27.1.4), ditutup biaya nyata yang perlu ditimbang sebelum benar-benar memutuskan pindah (27.1.5).
27.1.1 Kekuatan Bash yang Tetap Relevan
Bash tersedia bawaan atau mudah dipasang di hampir seluruh distribusi Linux, macOS, dan WSL (Bab 1.2.1), tanpa perlu proses kompilasi maupun runtime tambahan; cukup chmod +x (Bab 1.4.1) dan script langsung berjalan. Sifat inilah yang membuat Bash jadi shell default nyaris semua runner CI/CD (Bab 24.1.1) dan bahasa paling wajar untuk peran "glue script" yang pertama kali disinggung di Bab 1.2.3, menyatukan tool-tool command-line yang sudah ada tanpa perlu menulis ulang logikanya dari nol. Sebagian besar pekerjaan yang sudah dibahas sepanjang Bagian 2 dan 3, otomatisasi tugas harian (Bab 13), monitoring (Bab 19), deployment (Bab 21), sampai provisioning server (Bab 24.4), pada dasarnya cuma urutan pemanggilan tool eksternal (systemctl, rsync, curl, git) yang hasilnya dievaluasi lewat exit status atau output teksnya, dan itu persis kekuatan inti Bash sejak dirancang.
Kekuatan ini bukan cuma soal kenyamanan sintaks. Tool Python atau Go yang setara ops.sh (Bab 24.3.3) tetap butuh interpreter atau binary terpasang lebih dulu di setiap mesin yang menjalankannya, sementara script Bash cukup mengandalkan shell yang memang sudah ada di hampir semua sistem sejak awal. Selama kebutuhan sebuah script masih sebatas menjalankan dan menggabungkan perintah yang sudah ada, menulis ulang ke bahasa lain justru menambah lapisan dependency baru tanpa manfaat yang sepadan.
27.1.2 Sinyal dari Kompleksitas Data dan Logika
Associative array Bash cuma menyimpan struktur flat satu tingkat (Bab 4.2.1): satu key menunjuk satu value, tidak ada objek di dalam objek. jq (Bab 20.2.3, 22.2.3) mengisi sebagian celah ini untuk membaca dan menyusun JSON bersarang, tapi begitu logika di baliknya makin rumit, menggabungkan data dari beberapa sumber berdasarkan key yang sama, menghitung agregat bertingkat, atau bercabang berdasarkan kombinasi banyak kondisi sekaligus, filter jq yang ditulis dalam satu baris panjang atau heredoc jadi sulit dibaca dan sulit diuji, dibanding logika yang sama ditulis sebagai dict dan list bersarang biasa di Python.
Pola trap ... EXIT dan die()/error() (Bab 25.2, 26.1.2) cukup untuk kebanyakan script linear yang cuma perlu berhenti rapi begitu satu langkah gagal. Begitu sebuah fungsi perlu menangani beberapa jenis kegagalan berbeda dengan penanganan yang juga berbeda-beda, misalnya timeout jaringan yang layak dicoba ulang, dibanding data tidak valid yang harus langsung dihentikan, exception bertipe di Python (setiap jenis error punya class sendiri yang bisa ditangkap spesifik lewat blok except) jauh lebih ekspresif dibanding memeriksa $? atau isi $output satu per satu lewat if/case bertingkat.
Sinyal ketiga datang dari cara toolkit ini diuji. bats (Bab 26.4) menguji lewat helper run, yang berarti tiap test case sesungguhnya membuka proses shell baru dan menjalankan sebuah fungsi atau perintah sebagai proses eksternal, cara pengujian yang lazim disebut black-box test karena cuma mengamati exit status dan output dari luar tanpa masuk ke detail internal fungsinya. Ini pas untuk memverifikasi perilaku keseluruhan script seperti die(), tapi jadi tidak praktis begitu yang ingin diuji adalah satu fungsi kalkulasi murni dengan puluhan kombinasi input, tanpa perlu menyiapkan file atau koneksi jaringan tiruan untuk tiap skenario. Modul unittest bawaan Python, atau pytest yang lebih populer di lapangan, memanggil fungsi langsung di dalam proses test yang sama, memungkinkan assertion granular dan mocking dependency eksternal tanpa benar-benar menjalankan proses, file, atau jaringan sungguhan.
27.1.3 Sinyal dari Performa dan Concurrency
Bash sendiri jarang jadi bottleneck untuk kebanyakan tugas administrasi, karena sebagian besar waktu eksekusinya dihabiskan oleh tool eksternal seperti rsync atau curl yang sudah compiled dan cepat. Masalah performa biasanya muncul dari pola pemakaiannya, bukan dari Bash itu sendiri: loop yang memanggil proses eksternal baru di setiap iterasi, misalnya memanggil curl di dalam for untuk ribuan endpoint satu per satu, jadi lambat karena overhead fork/exec proses baru tiap kali, bukan karena logika Bash-nya sendiri lambat dieksekusi.
Paralelisme paling praktis di Bash sebatas menjalankan proses di background lewat operator & atau memecah pekerjaan lewat xargs -P, cukup untuk kebutuhan seperti memicu beberapa health check bersamaan (Bab 22). Bash tidak menyediakan primitif sinkronisasi bawaan seperti channel atau mutex untuk mengoordinasikan banyak worker yang saling bertukar data. Goroutine, unit eksekusi ringan yang dikelola langsung oleh runtime Go (bukan thread sistem operasi secara langsung), bersama channel sebagai mekanisme resminya untuk komunikasi antar goroutine, jadi bagian inti bahasa Go menurut dokumentasi resminya, mengikuti salah satu Go Proverb yang dipopulerkan tim Go sendiri: "Do not communicate by sharing memory; instead, share memory by communicating." Perlu diperhatikan juga, implementasi resmi CPython, interpreter Python yang paling umum dipakai, punya mekanisme Global Interpreter Lock (GIL) yang menurut dokumentasi resminya membatasi hanya satu thread yang benar-benar mengeksekusi bytecode Python dalam satu waktu, sehingga thread di Python kurang cocok untuk pekerjaan CPU-bound dibanding goroutine di Go.
27.1.4 Sinyal dari Portabilitas dan Distribusi
Bab 1.3.2 sudah menyinggung, macOS membawa Bash bawaan versi 3.2 karena alasan lisensi GPLv3, dan Bab 4.2.1 menegaskan konsekuensinya: associative array sama sekali tidak berfungsi kecuali Bash diperbarui lewat Homebrew lebih dulu. Bab 10.3.2 menambah satu jebakan portabilitas lagi, opsi -i pada BSD sed bawaan macOS mewajibkan argumen suffix ditulis eksplisit, berbeda dari GNU sed di Linux yang menganggapnya opsional. Untuk server yang kita kuasai penuh, di mana versi Bash dan tool GNU yang terpasang sudah diketahui pasti, perbedaan semacam ini bukan masalah besar. Begitu tool yang sama perlu didistribusikan ke banyak mesin dengan environment yang tidak seragam, laptop pribadi anggota tim yang macam-macam sistem operasinya, atau dibagikan sebagai tool open source ke publik, ketergantungan pada versi Bash atau varian GNU tertentu jadi liabilitas nyata.
Menurut dokumentasi resmi Go, program Go pada umumnya dikompilasi jadi satu static binary yang menyertakan seluruh dependency-nya secara statis, terutama kalau tidak mengaktifkan cgo, sehingga binary itu bisa langsung disalin dan dijalankan di mesin lain dengan arsitektur dan sistem operasi yang sama tanpa perlu memasang runtime atau library tambahan apa pun lebih dulu. Python di sisi lain tetap butuh interpreter Python terpasang di setiap mesin tujuan, dan begitu script-nya memakai library di luar standard library bawaan, virtual environment serta daftar dependency-nya juga perlu ikut disiapkan di sana. Untuk tool internal yang cuma dipakai satu-dua Sysadmin di server yang sudah dikuasai, perbedaan ini tidak terlalu terasa; untuk tool yang perlu didistribusikan luas ke banyak mesin tak terkontrol, satu binary Go yang tinggal disalin jelas jauh lebih sederhana dioperasikan.
ldd ./laporan-agregatPerintah di atas jadi cara cepat memverifikasi klaim static binary ini di Linux: binary Go yang benar-benar statis biasanya membalas not a dynamic executable, tanda tidak ada shared library eksternal yang perlu dicari sistem saat binary itu dijalankan. Kalau ldd justru menampilkan daftar .so yang dibutuhkan, tandanya binary itu dikompilasi dengan cgo aktif (biasa terjadi kalau kode Go-nya memanggil library C tertentu), dan klaim "tinggal disalin ke mesin manapun" perlu dicek ulang untuk kasus spesifik itu.
27.1.5 Biaya Tersembunyi Berpindah Bahasa
Sinyal-sinyal di 27.1.2 sampai 27.1.4 gampang dibaca sebagai alasan untuk segera pindah bahasa, padahal keputusan itu sendiri punya biaya yang jarang disadari sebelum benar-benar dijalankan. Menambahkan python3 atau binary Go ke toolkit yang selama ini murni Bash bukan cuma soal menulis kode baru, tapi juga menambah satu toolchain lagi yang harus tersedia dan konsisten versinya di setiap tempat script itu dijalankan, termasuk di runner CI/CD (Bab 24.1.1) yang sebelumnya cukup memastikan bash tersedia, sekarang juga perlu memastikan python3 atau hasil build Go tersedia dengan versi yang cocok.
Biaya yang lebih sering luput justru di sisi manusia, bukan mesin. toolkit-ops/ (Bab 26.1.4) sejauh ini bisa dirawat siapa pun di tim yang paham Bash dasar, modal yang sudah dibangun sejak Bab 1. Begitu satu bagian pindah ke Python atau Go, anggota tim yang cuma paham Bash kehilangan kemampuan membaca dan memperbaiki bagian itu sendiri, harus menunggu rekan yang paham bahasa itu, kebalikan dari tujuan awal ops.sh sebagai satu titik masuk yang mudah dipakai siapa saja (Bab 24.3.1). Dependency Python di luar standard library juga menambah kewajiban baru yang tidak pernah ada di Bash murni: virtual environment dan daftar versi library-nya (27.1.4) perlu terus dirawat dan diperbarui dari waktu ke waktu, sesuatu yang sama seriusnya dengan kebiasaan menjaga sistem tetap mutakhir yang sudah dibahas untuk hardening server di Bab 23.
Ini bukan alasan untuk tidak pernah pindah bahasa, cuma pengingat supaya keputusan itu diambil sengaja setelah menimbang biayanya, bukan refleks begitu satu sinyal di 27.1.2 sampai 27.1.4 muncul. Studi kasus di 27.3.2 sengaja memindahkan sebagian kecil saja, satu file laporan.py, bukan menulis ulang seluruh toolkit-ops/, tepat karena mempertimbangkan biaya ini.
| Aspek | Bash | Python | Go |
|---|---|---|---|
| Startup & dependency | Bawaan hampir semua sistem Unix-like (1.2.1) | Butuh interpreter, kadang virtual environment | Compile jadi binary statis, tanpa runtime terpisah |
| Struktur data | Flat, associative array satu tingkat (4.2.1) | dict & list bersarang native | struct & slice bertipe statis |
| Error handling | Exit code, trap, pola die() (25.2, 26.1.2) | Exception bertipe class | Error value dikembalikan eksplisit tiap fungsi |
| Testing | Black-box lewat proses (bats, 26.4) | Unit test memanggil fungsi langsung (unittest/pytest) | Unit test bawaan (go test) |
| Concurrency | Background job (&), xargs -P | Thread dibatasi GIL untuk kode CPU-bound | Goroutine & channel native |
| Paling cocok untuk | Automasi CLI, glue script, orchestration (1.2.3, 24.2) | Logika data-heavy, tooling internal tim | CLI tool performa tinggi, didistribusikan luas |
27.2 Mengombinasikan Bash dengan Bahasa Lain
Sinyal-sinyal di 27.1 jarang berarti seluruh toolkit harus ditulis ulang dari nol. Yang lebih umum terjadi di lapangan: sebagian kecil logika yang benar-benar butuh Python atau Go dipisah ke bahasa itu sendiri, sementara Bash tetap memegang peran yang sudah terbukti cocok untuknya sejak Bab 24, menjalankan lewat cron/systemd timer (Bab 18), terpicu dari pipeline CI/CD (Bab 24.1), dan menyatukan hasil akhirnya lewat logging serta alert yang konsisten (Bab 19, 20, 26.1.2).
27.2.1 Bash sebagai Orchestrator, Bukan Segalanya
Orchestrator, peran yang mengatur kapan dan bagaimana komponen lain dijalankan tanpa harus jadi tempat seluruh logika hidup, sebenarnya bukan konsep baru di series ini. Bab 24.2 sudah menunjukkan Bash memegang peran ini untuk Docker, Ansible, dan Terraform, tiga tool yang sama sekali bukan ditulis dalam Bash (Ansible berbasis Python, Terraform dan Docker berbasis Go), tanpa ada yang mempermasalahkannya. Menerapkan pola orchestrator yang sama ke script Python atau binary Go yang ditulis sendiri oleh tim bukan hal baru, cuma memperluas pola yang sudah dipraktikkan ke kode internal, bukan cuma ke tool pihak ketiga. ops.sh (Bab 24.3.3) tetap jadi satu titik masuk yang sama bagi anggota tim, tinggal sebagian cmd_*() di dalamnya yang isinya memanggil script bahasa lain, bukan lagi murni logika Bash.
27.2.2 Kontrak Data Lewat Exit Code dan JSON
Begitu logika berpindah bahasa, Bash dan proses yang dipanggilnya butuh data contract yang konsisten, kesepakatan format tentang bagaimana keduanya saling bertukar informasi. Dua konvensi yang sudah berulang kali dipraktikkan sepanjang series ini tetap berlaku persis sama lintas bahasa: exit status sebagai penanda berhasil-tidaknya sebuah proses (Lampiran B merangkum arti tiap kode), dan pemisahan stdout untuk data hasil dari stderr untuk log atau pesan diagnostik (Bab 5.2). Untuk data yang lebih terstruktur dari sekadar teks polos, JSON di stdout jadi format kontrak yang paling praktis, karena Bash sudah punya jq (Bab 20.2.3) untuk membacanya kembali tanpa perlu parser tambahan.
#!/bin/bash
set -euo pipefail
source "${SCRIPT_DIR}/lib.sh"
if ! hasil=$(python3 "${SCRIPT_DIR}/cek_kuota.py" --host "$HOST"); then
die "cek_kuota.py gagal dijalankan untuk host $HOST"
fi
sisa_persen=$(echo "$hasil" | jq -r '.sisa_persen')
if (( sisa_persen < 10 )); then
kirim_alert "Kuota disk $HOST tersisa ${sisa_persen}%"
fiPola if ! hasil=$(...); then die ...; fi di atas persis mengikuti kebiasaan if ! git fetch ...; then di deploy-pipeline.sh (Bab 21.4.2), cuma perintah yang dipanggilnya sekarang python3. Selama cek_kuota.py konsisten mencetak satu objek JSON ke stdout dan keluar dengan exit status bukan nol kalau gagal, sisi Bash-nya tidak perlu tahu apa pun soal isi kode Python di baliknya, persis prinsip orchestrator di 27.2.1.
Jebakan yang paling sering ditemui di lapangan justru ada di sisi Python-nya: satu baris print() sisa debugging yang lupa dihapus, tercetak sebelum json.dumps(...), membuat stdout tidak lagi murni satu objek JSON. jq di sisi Bash lantas gagal mem-parsing seluruh output, biasanya dengan pesan parse error yang menunjuk isi output gabungan itu, bukan menunjuk baris print() yang sebenarnya jadi biang keladinya. Disiplin memisahkan stdout untuk data dari stderr untuk log (Bab 5.2) berlaku sama ketatnya lintas bahasa: pesan debug dari script Python wajib ditulis lewat print(..., file=sys.stderr), bukan print() polos, supaya stdout tetap murni data yang bisa dipercaya wrapper Bash-nya. Sebelum menyambungkan script Python ke wrapper, uji dulu keluarannya berdiri sendiri.
python3 cek_kuota.py --host app.example.com | jq -e . > /dev/null && echo "JSON valid"Opsi -e pada jq membuatnya keluar dengan exit status bukan nol kalau output-nya bukan JSON valid atau hasil filternya null/false, sesuai dokumentasi resmi jq, cara cepat memastikan kontrak data di atas benar-benar terpenuhi sebelum wrapper Bash-nya ikut dipercaya dalam alur otomatis.
27.2.3 Memanggil Script Python dari Bash
Sebelum memanggil python3, wrapper Bash sebaiknya memverifikasi lebih dulu interpreter itu memang tersedia, konsisten dengan kebiasaan validasi dependency yang sudah dipraktikkan di komentar header script (Bab 26.2.2). Wrapper berikut disimpan sebagai cek-config.sh, sengaja diberi nama berbeda dari validasi_config.py yang dipanggilnya supaya jelas mana lapisan orchestrator dan mana lapisan logika.
#!/bin/bash
set -euo pipefail
source "${SCRIPT_DIR}/lib.sh"
command -v python3 &> /dev/null || die "python3 tidak ditemukan, wajib terpasang untuk validasi config"
if ! python3 "${SCRIPT_DIR}/validasi_config.py" "$1"; then
die "Config $1 tidak valid, lihat detail error di atas"
fi
log INFO "Config $1 valid, lanjut provisioning"command -v memeriksa keberadaan sebuah perintah tanpa benar-benar menjalankannya, cara yang lebih portable dibanding mengandalkan which yang menurut POSIX bukan builtin shell dan perilakunya bisa berbeda-beda antar sistem. Kombinasi ini menjaga provision-server.sh (Bab 24.4.2) tetap gagal cepat dengan pesan yang jelas kalau dijalankan di server yang belum sempat memasang python3, dibanding gagal belakangan dengan pesan command not found yang membingungkan.
Uji dulu kedua jalurnya secara terpisah sebelum cek-config.sh disambungkan ke provision-server.sh yang sesungguhnya: jalankan sekali dengan config yang valid, lalu sekali lagi dengan config yang sengaja dirusak.
./cek-config.sh config-valid.yaml
echo "Exit code: $?"
./cek-config.sh config-rusak.yaml
echo "Exit code: $?"Percobaan pertama seharusnya mencetak Config config-valid.yaml valid, lanjut provisioning lewat log() dari lib.sh (Bab 26.1.2) dan keluar dengan exit code 0. Percobaan kedua seharusnya berhenti lewat die() dengan exit code default 1 (Bab 26.1.2), disertai detail error dari validasi_config.py sendiri di baris sebelumnya. Kalau exit code percobaan kedua ternyata tetap 0, tandanya validasi_config.py keluar dengan status nol meski validasinya gagal, bug di sisi Python yang wajib diperbaiki lebih dulu, karena wrapper Bash di atas sepenuhnya mempercayai exit status itu sebagai satu-satunya sinyal berhasil-tidaknya validasi.
27.2.4 Memanggil Binary Go dari Bash
Pola pemanggilan binary Go mirip dengan Python di 27.2.3, cuma pengecekan dependency-nya lebih sederhana karena tidak ada interpreter terpisah yang perlu dicek, cukup memastikan file binary-nya ada dan bisa dieksekusi.
#!/bin/bash
set -euo pipefail
source "${SCRIPT_DIR}/lib.sh"
GOTOOL="/usr/local/bin/laporan-agregat"
[[ -x "$GOTOOL" ]] || die "$GOTOOL tidak ditemukan atau tidak executable"
if ! output=$("$GOTOOL" --minggu-ini); then
die "laporan-agregat gagal dijalankan"
fi
echo "$output" | jq -r '.ringkasan'Perbedaan paling terasa dibanding memanggil script Python justru di tahap distribusi: laporan-agregat di atas cukup disalin sebagai satu file ke /usr/local/bin/ di server manapun yang butuh, tanpa perlu memasang apa pun lebih dulu, konsekuensi langsung dari sifat static binary yang dibahas di 27.1.4. validasi_config.py di 27.2.3 sebaliknya tetap butuh python3 sudah terpasang di server tujuan sebelum wrapper Bash-nya bisa berjalan sama sekali.
Uji binary Go-nya berdiri sendiri dulu sebelum disambungkan ke ops.sh, kebiasaan yang sama seperti script Python di 27.2.3.
./laporan-agregat --minggu-ini
echo "Exit code: $?"
file ./laporan-agregatfile di atas menampilkan tipe binary yang terdeteksi, biasanya berupa keterangan seperti ELF 64-bit LSB executable, statically linked di Linux untuk binary Go tanpa cgo, konfirmasi cepat sebelum menjalankan ldd (27.1.4) yang lebih detail. Kalau laporan-agregat ternyata belum diberi izin eksekusi setelah disalin dari mesin build, wrapper di atas berhenti lebih awal lewat pengecekan [[ -x "$GOTOOL" ]], bukan gagal samar-samar di tengah pemanggilan "$GOTOOL".
27.3 Studi Kasus: Bash Cukup atau Naik Kelas?
Sub-bagian penutup ini menerapkan sinyal-sinyal di 27.1 langsung ke toolkit-ops yang sudah dibangun sepanjang Bagian 3 dan Bab 26, dimulai dari audit jujur apakah toolkit yang ada sekarang benar-benar sudah perlu "naik kelas" (27.3.1), lalu skenario kebutuhan baru yang benar-benar melewati ambang batas itu (27.3.2), ditutup checklist ringkas yang bisa dipakai ulang untuk keputusan serupa di proyek lain (27.3.3).
27.3.1 Audit Toolkit-Ops: Masih Cukup Bash
Menelusuri isi toolkit-ops/ (Bab 26.1.4) satu per satu memakai kriteria 27.1 menunjukkan toolkit ini ternyata belum melewati ambang batas mana pun. ops.sh (Bab 24.3.3) murni dispatcher yang meneruskan argumen ke pemanggilan SSH, datanya sebatas string host dan nama service, tidak ada struktur bersarang. deploy-pipeline.sh (Bab 21.4.2) berurutan dan linear: tarik kode, restart service, satu health check lewat curl -f, rollback kalau gagal, tanpa cabang error yang perlu penanganan berbeda-beda. toolkit-keamanan.sh (Bab 23.4.3) mengaudit lewat grep/awk atas output iptables dan ss, pekerjaan pemrosesan teks baris demi baris yang justru jadi kekuatan inti Bash sejak Bab 9 sampai 11. alert-notifikasi.sh (Bab 20.4.2) menyusun JSON lewat jq -n --arg, tapi bentuknya tetap satu tingkat, sepasang status dan text, jauh dari kebutuhan struktur bersarang yang jadi sinyal di 27.1.2.
Kesimpulan ini bukan kebetulan. Sebagian besar automasi Sysadmin dan Developer sehari-hari memang berbentuk urutan pemanggilan tool eksternal dengan data sederhana, persis kekuatan yang dibahas 27.1.1, dan itu sebabnya toolkit yang sama bisa bertahan sepenuhnya di Bash sejak Bab 17 sampai Bab 26 tanpa pernah benar-benar butuh bahasa lain.
27.3.2 Kebutuhan Baru: Laporan Agregat Mingguan
Skenario yang mengubah kesimpulan itu: tim meminta laporan mingguan yang merangkum kondisi produksi dari tiga sumber sekaligus, log health check (Bab 22.2), riwayat alert yang selama ini cuma tercatat lewat kirim_alert() (Bab 20.4.2), dan deploy-pipeline.log (Bab 21.4.2). Laporan itu perlu menghitung total durasi tidak sehat per service, lima jenis alert paling sering muncul beserta rata-rata waktu respons, dan rasio deploy sukses dibanding rollback dalam seminggu terakhir, lalu digabung jadi satu ringkasan per service.
Kebutuhan ini melewati ambang batas 27.1.2 di beberapa titik sekaligus: data dari tiga sumber log berbeda perlu digabung berdasarkan nama service dan rentang waktu, hasilnya berupa struktur bersarang (per service, lalu per hari, lalu per metrik), dan agregasinya sendiri, rata-rata, pengurutan top-5, rasio, bukan pekerjaan yang natural ditulis lewat associative array flat Bash (Bab 4.2.1). Menguji logika perhitungan rata-rata durasi untuk kombinasi data tertentu juga jauh lebih praktis lewat fungsi murni yang dipanggil langsung dengan data tiruan, dibanding bats yang perlu menyiapkan file log tiruan sungguhan di disk untuk tiap skenario (Bab 26.4.2).
#!/usr/bin/env python3
import argparse
import json
import statistics
def hitung_ringkasan(data_alert):
per_service = {}
for alert in data_alert:
svc = alert["service"]
info = per_service.setdefault(svc, {"jumlah_alert": 0, "durasi_menit": []})
info["jumlah_alert"] += 1
info["durasi_menit"].append(alert["durasi_menit"])
for info in per_service.values():
info["rata_rata_durasi"] = statistics.mean(info["durasi_menit"])
del info["durasi_menit"]
return per_service
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--minggu-ini", action="store_true")
args = parser.parse_args()
# ...memuat data_alert dari alert-notifikasi.log...
ringkasan = hitung_ringkasan(data_alert)
print(json.dumps({"ringkasan": ringkasan}))hitung_ringkasan() di atas cuma fungsi murni: menerima list data alert, mengembalikan dict bersarang, tanpa efek samping IO sama sekali. Struktur inilah yang sulit ditulis rapi di Bash tapi natural di Python lewat dict dan list comprehension bawaan. Modul argparse dan json yang dipakai di atas sama-sama bagian standard library Python, tidak butuh virtual environment tambahan untuk kasus sesederhana ini, sesuai dokumentasi resmi Python. Fungsi hitung_ringkasan() sendiri juga langsung bisa diuji lewat unittest dengan data tiruan tanpa perlu menyentuh file log sungguhan, persis sinyal yang dibahas 27.1.2.
Integrasinya ke ops.sh cukup satu baris tambahan di dispatcher (Bab 24.3.2), mengikuti persis pola orchestrator di 27.2.1.
case "${1:-help}" in
deploy) cmd_deploy ;;
rollback) cmd_rollback ;;
status) cmd_status ;;
logs) cmd_logs "${2:-50}" ;;
report) python3 "${SCRIPT_DIR}/laporan.py" --minggu-ini | jq -r '.ringkasan' ;;
help|*) tampilkan_bantuan ;;
esacAnggota tim yang menjalankan ops.sh report tidak perlu tahu subperintah itu sebenarnya memanggil Python di baliknya, persis seperti mereka tidak perlu tahu ops.sh deploy memanggil deploy-pipeline.sh lewat SSH. Titik masuknya tetap satu, cuma sebagian isi di baliknya sekarang ditulis di bahasa yang lebih cocok untuk kebutuhan spesifik itu.
Verifikasi subperintah baru ini sama seperti subperintah lain di ops.sh (Bab 24.3.3): coba dulu secara langsung sebelum dipakai tim.
chmod +x ops.sh
./ops.sh reportOutput-nya seharusnya satu paragraf ringkasan per service, hasil field ringkasan yang diekstrak jq dari JSON yang dicetak laporan.py. Kalau yang muncul justru pesan error dari jq seperti parse error, penyebab paling umum persis seperti pitfall di 27.2.2, ada baris debug tambahan yang lolos tercetak ke stdout sebelum json.dumps(...) di laporan.py. Logika agregasi di hitung_ringkasan() sendiri, terpisah dari bagian yang mencetak JSON, bisa diuji lebih granular lewat unittest tanpa perlu menjalankan ops.sh report sama sekali.
python3 -m unittest test_laporan.py -vPerintah di atas persis sinyal yang dibahas 27.1.2: pengujian granular pada fungsi murni hitung_ringkasan() jauh lebih praktis ditulis lewat unittest dibanding bats, karena tidak perlu menyiapkan file log tiruan sungguhan di disk untuk tiap kombinasi data yang ingin diuji.
27.3.3 Checklist Keputusan Naik Kelas
Checklist berikut merangkum sinyal-sinyal dari 27.1, supaya bisa dipakai ulang setiap kali muncul kebutuhan baru di toolkit manapun:
- Data yang diolah punya struktur bersarang lebih dari satu tingkat, atau perlu digabung dari beberapa sumber sekaligus (27.1.2)
- Logika error butuh penanganan berbeda-beda per jenis kegagalan, bukan sekadar berhenti atau lanjut (27.1.2)
- Dibutuhkan unit test granular atas logika murni, bukan cuma black-box test lewat proses (27.1.2, 26.4)
- Ada perhitungan berat atau loop besar yang sudah terbukti jadi bottleneck nyata, bukan sekadar dugaan (27.1.3)
- Tool perlu didistribusikan ke banyak mesin dengan environment yang tidak dikontrol penuh (27.1.4)
- Kalaupun salah satu sinyal di atas muncul, biaya toolchain baru dan onboarding tim (27.1.5) tetap sepadan dengan manfaatnya
Kalau tidak satu pun dari lima sinyal pertama muncul, seperti hasil audit toolkit-ops/ di 27.3.1, biarkan tetap Bash tanpa perlu menimbang butir keenam sama sekali. Begitu salah satu dari lima sinyal itu benar-benar muncul, seperti kebutuhan laporan agregat di 27.3.2, butir keenam yang menentukan langkah berikutnya: migrasi cuma pantas dijalankan kalau biayanya memang sepadan dengan manfaatnya, bukan sekadar karena satu sinyal sudah muncul. Kalaupun kelak satu bagian kecil butuh bahasa lain seperti laporan.py di 27.3.2, itu bukan alasan menulis ulang seluruh toolkit dari nol; cukup pisahkan bagian yang benar-benar butuh naik kelas, panggil lewat pola orchestrator di 27.2, dan biarkan Bash tetap memegang peran yang memang paling cocok untuknya, menjalankan, menjadwalkan, dan menyatukan semuanya lewat log dan alert yang konsisten.
Bab ini menutup pembahasan kapan Bash cukup dan kapan perlu bahasa lain, dari sinyal kompleksitas data, performa, sampai portabilitas (27.1), cara Bash tetap berperan sebagai orchestrator lewat kontrak data berbasis exit code dan JSON (27.2), sampai penerapannya langsung ke toolkit-ops yang sudah dibangun sepanjang series ini (27.3). Bab 28 berikutnya, penutup series ini, menyatukan seluruh kemampuan dari Bagian 1 sampai 4 jadi dua proyek akhir: automation suite untuk desktop pribadi (28.1) dan toolkit administrasi server mini (28.2) yang mengikutsertakan logging, alerting, dan keamanan sekaligus, sebelum ditutup checklist "siap produksi" (28.3) dan sumber belajar lanjutan (28.4) bagi yang ingin memperdalam arah yang dibuka bab ini, termasuk kapan mulai menulis tool operasional dalam Python atau Go.

