Bab 21.4.3 menutup studi kasus pipeline deploy dengan satu janji tertunda: pipeline deploy-pipeline.sh "masih dipicu manual", dan menyambungkannya ke sistem CI/CD supaya berjalan otomatis setiap kode baru di-push ke branch main "dibahas lebih lanjut di Bab 24.1". Bab 23 menutup pembahasan keamanan dengan peringatan serupa, bahwa seluruh prinsip menghindari command injection, memvalidasi input, dan menjaga idempotensi "jadi makin krusial di Bab 24, saat script Bash mulai dijalankan di dalam pipeline CI/CD tanpa pengawasan manusia sama sekali di setiap langkahnya". Bab 17.3.1 pun menunda satu hal lagi, pola useradd lengkap yang dipakai "untuk kebutuhan otomatisasi atau provisioning server baru (Bab 24)". Bab ini menepati ketiga janji itu sekaligus, karena ternyata ketiganya bermuara ke satu peran yang sama: Bash sebagai perekat yang menyatukan tool-tool DevOps modern, dijalankan otomatis oleh mesin, bukan lagi diketik manual satu per satu oleh Sysadmin atau Developer di depan terminal. Bab ini dibagi jadi empat bagian: menjalankan Bash di dalam pipeline CI/CD seperti GitHub Actions dan GitLab CI (24.1), memakai Bash sebagai glue script yang menyatukan Docker, Ansible, dan Terraform (24.2), membangun wrapper script supaya operasional tim lebih aman dan mudah diikuti (24.3), lalu ditutup studi kasus menyatukan semuanya jadi satu script provisioning server baru dari nol (24.4).
24.1 Bash dalam Pipeline CI/CD (GitHub Actions, GitLab CI)
Lima sub-bab ini membahas bagaimana script Bash yang selama ini dijalankan manual di terminal berubah peran menjadi bagian dari pipeline CI/CD (Continuous Integration/Continuous Deployment), rangkaian langkah otomatis yang berjalan setiap ada perubahan kode, tanpa Sysadmin atau Developer perlu menekan Enter sama sekali. Setiap pipeline tersusun dari satu atau lebih job, satuan pekerjaan yang dieksekusi runner dan biasanya berisi beberapa langkah berurutan; istilah job ini dipakai berulang kali di sisa bab ini persis dengan makna itu.
24.1.1 Bash Sebagai Shell Default CI/CD
Seluruh script di Bab 1 sampai 23 dijalankan langsung oleh Sysadmin atau User yang tahu persis shell apa yang aktif di terminalnya. Begitu script yang sama dijalankan sebagai bagian pipeline CI/CD, shell yang mengeksekusinya ditentukan oleh platform, bukan oleh siapa yang memicunya, dan asumsi yang salah soal shell mana yang aktif bisa membuat script yang mulus di laptop Developer tiba-tiba gagal di runner, yaitu mesin (biasanya container sekali pakai) yang benar-benar mengeksekusi setiap langkah pipeline.
Menurut dokumentasi resmi GitHub Actions, langkah run di runner Linux dan macOS memakai bash secara default kalau tersedia, dengan sh sebagai fallback. Detail yang lebih penting lagi: begitu shell: bash ditulis eksplisit, GitHub Actions menjalankannya persis lewat perintah bash --noprofile --norc -eo pipefail {0}, berbeda dari perilaku default tanpa shell yang cuma setara bash -e {0} tanpa opsi pipefail. Opsi pipefail ini konsep yang sama persis dengan set -o pipefail yang dibahas tuntas di Bab 25.1; kalau opsi itu tidak aktif, sebuah pipeline seperti curl ... | grep sukses yang gagal di tahap curl tetap dianggap "berhasil" oleh langkah CI, karena exit status yang diperiksa cuma milik grep di ujung pipe. Praktik yang lebih aman adalah menetapkan shell: bash secara eksplisit lewat blok defaults di level workflow atau job, bukan mengandalkan default yang tidak tertulis.
defaults:
run:
shell: bashGitLab CI punya perilaku serupa tapi diatur dari sisi yang berbeda. Menurut dokumentasi resmi GitLab Runner, bash/sh adalah shell default di seluruh sistem berbasis Unix, dengan bash dicoba lebih dulu dan sh jadi fallback kalau bash tidak tersedia. Bedanya dengan GitHub Actions, shell ini ditentukan lewat konfigurasi config.toml di sisi runner (untuk executor shell), bukan per baris di file pipeline; untuk executor Docker, shell yang benar-benar dipakai bergantung pada apa yang tersedia di dalam image container yang dipilih job. Implikasi praktisnya: kalau sebuah job GitLab CI memakai image minimal seperti alpine yang tidak menyertakan bash bawaan, script yang mengandalkan fitur khas Bash seperti array (Bab 4) atau [[ ]] (Bab 6.1.3) akan gagal dengan pesan command not found, bukan karena scriptnya salah, melainkan karena shell yang tersedia di image itu cuma sh murni. Kebiasaan yang wajar di lapangan adalah memastikan image yang dipakai job sudah menyertakan bash, atau menambahkan langkah instalasi bash di awal script kalau image dasarnya memang minimal.
24.1.2 Workflow GitHub Actions Memicu Deploy
Workflow berikut menepati janji Bab 21.4.3, menyambungkan deploy-pipeline.sh supaya terpicu otomatis setiap ada push ke branch main. File ini disimpan di .github/workflows/deploy.yml pada repository aplikasi, sesuai konvensi lokasi workflow yang diwajibkan GitHub Actions.
name: Deploy Aplikasi
on:
push:
branches: [main]
defaults:
run:
shell: bash
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout kode
uses: actions/checkout@v4
- name: Deploy lewat SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }}
run: |
set -euo pipefail
mkdir -p ~/.ssh
printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh -o StrictHostKeyChecking=accept-new \
-i ~/.ssh/deploy_key deploy@"$SSH_HOST"Detail yang mudah terlewat: baris ssh ... deploy@"$SSH_HOST" di atas tidak menyertakan perintah apa pun untuk dijalankan di sisi server, dan itu memang disengaja. Bab 23.3.4 memperkenalkan opsi restrict,command="..." di authorized_keys yang memaksa sshd selalu menjalankan perintah tertentu begitu autentikasi berhasil, mengabaikan perintah apa pun yang diminta client. Kunci SSH_DEPLOY_KEY yang dipakai workflow ini memang sengaja dipasangkan dengan baris authorized_keys yang dikunci ke /var/www/aplikasi/deploy-pipeline.sh saja (detail lengkapnya di 24.4.2), jadi sekalipun langkah di atas ditambah embel-embel perintah lain, sshd tetap cuma akan menjalankan deploy-pipeline.sh. Ini bukan cuma soal kerapian, tapi mitigasi nyata: kalau SSH_DEPLOY_KEY bocor dari log workflow atau runner yang disusupi, Attacker yang memegang kunci itu tetap cuma bisa memicu satu script spesifik, bukan membuka shell bebas ke server produksi. Opsi StrictHostKeyChecking=accept-new, tersedia sejak OpenSSH 7.6 menurut ssh_config(5), menambahkan host key baru secara otomatis ke known_hosts tapi tetap menolak koneksi kalau host key yang sudah tersimpan berubah, jauh lebih aman dibanding StrictHostKeyChecking=no yang menerima host key apa pun tanpa verifikasi sama sekali, celah yang membuka jalan serangan man-in-the-middle.
24.1.3 Pipeline Setara di GitLab CI
Konsep yang sama, diterjemahkan ke .gitlab-ci.yml di root repository, memakai predefined variable CI_COMMIT_BRANCH yang menurut dokumentasi resmi GitLab berisi nama branch komit saat ini, tersedia di pipeline branch termasuk branch default.
stages:
- deploy
deploy-produksi:
stage: deploy
rules:
- if: $CI_COMMIT_BRANCH == "main"
script:
- mkdir -p ~/.ssh
- printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
- chmod 600 ~/.ssh/deploy_key
- ssh -o StrictHostKeyChecking=accept-new
-i ~/.ssh/deploy_key deploy@"$SSH_HOST"SSH_PRIVATE_KEY dan SSH_HOST di sini bukan didefinisikan di dalam file, melainkan didaftarkan lewat halaman Settings > CI/CD > Variables pada project GitLab, ditandai sebagai masked sekaligus protected. Menurut dokumentasi resmi GitLab, variable yang ditandai protected cuma disuntikkan ke pipeline yang berjalan di branch atau tag yang juga berstatus protected, jadi kalau Attacker berhasil membuat branch baru sembarangan dan mencoba memicu pipeline dari situ, variable rahasia ini tetap tidak ikut terbawa. rules di atas membatasi job deploy-produksi supaya cuma berjalan kalau CI_COMMIT_BRANCH persis main, mencegah setiap push ke branch fitur ikut memicu deploy ke produksi.
24.1.4 Mengamankan Secret di CI/CD
Tabel berikut merangkum perbedaan istilah antara dua platform yang dibahas, berguna sebagai referensi cepat saat berpindah dari satu platform ke platform lain.
| Konsep | GitHub Actions | GitLab CI |
|---|---|---|
| File konfigurasi | .github/workflows/*.yml | .gitlab-ci.yml |
| Unit pekerjaan | job, berisi steps | job, berisi script |
| Batasi ke branch tertentu | on.push.branches | rules.if dengan $CI_COMMIT_BRANCH |
| Lokasi simpan secret | Settings > Secrets and variables > Actions | Settings > CI/CD > Variables |
| Cara memanggil secret | ${{ secrets.NAMA }} | $NAMA (langsung sebagai environment variable) |
Dua platform ini sama-sama menawarkan penyamaran otomatis nilai secret di log, tapi berdasarkan dokumentasi resmi masing-masing, penyamaran ini bukan jaminan mutlak. Dokumentasi GitHub Actions menyebutkan bahwa secret yang sudah "diubah bentuk" (di-encode ulang, dipotong, atau disisipi karakter tambahan) sebelum dicetak ke log bisa lolos dari penyamaran otomatis. Dokumentasi GitLab menyatakan hal serupa secara lebih eksplisit: "masking sebuah variable CI/CD bukan cara yang terjamin mencegah pengguna jahat mengakses nilai variable itu", dan variable yang mengandung spasi atau kurang dari delapan karakter bahkan tidak memenuhi syarat untuk di-mask sama sekali. Prinsip praktis yang aman dipegang: jangan pernah sengaja mencetak isi secret ke log lewat echo atau printf untuk keperluan debug, dan hindari mengaktifkan set -x (Bab 25.1) pada langkah yang memegang variable rahasia, karena set -x mencetak setiap baris perintah beserta nilai variabelnya persis sebelum dieksekusi, termasuk kalau baris itu kebetulan memakai $SSH_PRIVATE_KEY secara langsung. Kombinasi antara secret yang di-mask platform dan kunci SSH yang scope-nya sudah dipersempit lewat command= (24.1.2) membentuk dua lapis pertahanan; kalaupun satu lapis gagal, lapis satunya tetap membatasi kerusakan yang bisa ditimbulkan.
24.1.5 Menguji Pipeline dan Menelusuri Kegagalan
Cara paling aman menguji workflow di 24.1.2 pertama kali adalah lewat repository percobaan atau branch yang belum benar-benar dipantau produksi, sebelum baru dipasang ke repository aplikasi sungguhan. Setelah commit percobaan di-push, buka tab Actions (GitHub) atau Pipelines (GitLab) di repository, lalu amati log tiap langkahnya secara real-time; kedua platform menampilkan output stdout/stderr job apa adanya, termasuk baris mana yang gagal kalau set -euo pipefail menghentikan script sebelum baris terakhir.
Tiga kegagalan yang paling sering ditemui di lapangan: pertama, Permission denied (publickey), penyebabnya persis sama dengan troubleshooting Bab 21.2.1, entah authorized_keys di server belum benar berisi key yang sesuai, atau secret SSH_DEPLOY_KEY yang didaftarkan di 24.1.4 tidak lengkap tersalin (kasus umum: baris terakhir private key ikut terpotong saat disalin manual). Kedua, job gagal di langkah paling awal dengan pesan semacam bash: [[: command not found, tanda runner atau image yang dipakai ternyata cuma menyediakan sh, bukan bash, persis kondisi yang dibahas 24.1.1. Ketiga, workflow yang sama sekali tidak terpicu meski sudah di-push ke main, biasanya karena nama branch di on.push.branches atau kondisi rules.if tidak persis cocok dengan nama branch sungguhan (misalnya master dibandingkan main), atau file YAML-nya sendiri punya kesalahan indentasi yang membuat platform menolak seluruh workflow tanpa sempat menjalankannya sama sekali.
Satu catatan jujur soal StrictHostKeyChecking=accept-new dari 24.1.2 yang perlu disadari: opsi ini aman untuk mesin developer yang dipakai berulang-ulang dari waktu ke waktu, karena percobaan man-in-the-middle di percobaan koneksi berikutnya akan tertangkap begitu host key yang tersimpan ternyata berubah. Runner CI/CD kebanyakan bersifat sekali pakai, memulai dengan known_hosts yang selalu kosong di setiap job, sehingga perlindungan "tolak kalau berubah" itu tidak pernah sempat teruji lintas job, tiap job pada dasarnya mempercayai host key yang pertama kali diterimanya. Untuk kebutuhan yang benar-benar kritis, mitigasi yang lebih ketat adalah merekam baris known_hosts server tujuan satu kali secara manual lewat ssh-keyscan, menyimpannya sebagai secret terpisah, lalu menuliskannya ke known_hosts secara eksplisit sebelum ssh dipanggil, bukan mengandalkan trust-on-first-use di setiap job baru.
echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
ssh -o StrictHostKeyChecking=yes -i ~/.ssh/deploy_key deploy@"$SSH_HOST"24.2 Bash Sebagai Glue Script Antar Tools
Bab 1.2.3 sempat menyinggung istilah "glue script", script yang menyatukan berbagai tool dalam satu alur kerja tanpa jadi bahasa utama aplikasi itu sendiri. Tiga sub-bab ini membedah peran itu lebih konkret lewat tiga tool DevOps yang paling umum ditemui Developer di lapangan: Docker, Ansible, dan Terraform.
24.2.1 Wrapper Build dan Push Docker
Kebutuhan paling umum di seputar Docker adalah membangun image dan mengirimnya ke registry dengan skema tag yang konsisten, biasanya berdasarkan commit Git. Pola git rev-parse --short HEAD dari Bab 21.4.2 dipakai lagi di sini, dikombinasikan dengan pola array dari 23.1.3 untuk menyusun opsi -t secara dinamis.
#!/bin/bash
set -euo pipefail
IMAGE="registry.example.com/tim/aplikasi"
COMMIT=$(git rev-parse --short HEAD)
BRANCH=$(git rev-parse --abbrev-ref HEAD)
TAGS=(-t "$IMAGE:$COMMIT")
if [[ "$BRANCH" == "main" ]]; then
TAGS+=(-t "$IMAGE:latest")
fi
docker buildx build "${TAGS[@]}" --push .Sesuai dokumentasi resmi Docker, --tag/-t pada docker buildx build bisa dipakai berkali-kali dalam satu perintah untuk menempelkan beberapa tag sekaligus ke satu image hasil build yang sama, dan --push berfungsi sebagai jalan pintas yang otomatis mengirim hasil build langsung ke registry tanpa perlu perintah docker push terpisah. Array TAGS di sini menghindari godaan memakai eval atau menggabung string opsi secara manual, persis alasan yang sama dengan 23.1.3: tag latest cuma ditambahkan kalau memang sedang berada di branch main, dan logika kondisinya tetap berupa percabangan Bash biasa, bukan string yang harus dievaluasi ulang. Setiap image jadi bisa ditelusuri balik ke commit persis yang menghasilkannya lewat tag $COMMIT, jauh lebih berguna untuk investigasi insiden dibanding tag latest saja yang terus tertimpa setiap build baru. Verifikasi hasilnya lewat docker buildx imagetools inspect "$IMAGE:$COMMIT", yang menurut dokumentasi resmi Docker menampilkan detail manifest image langsung dari registry tanpa perlu mengunduh seluruh layer-nya; kalau perintah itu gagal menemukan manifest untuk tag tersebut, tandanya proses --push belum berhasil menaruh image itu ke registry, jauh lebih cepat diketahui lewat langkah ini dibanding baru sadar saat proses deploy di server tujuan gagal menarik image yang ternyata belum ada.
24.2.2 Ansible Sebagai Pemicu, Bukan Pengganti Idempotensi
Bab 23.2.3 mendefinisikan idempotent sebagai sifat aman dijalankan berkali-kali tanpa efek samping tak terduga, dan menunjukkan cara mencapainya di Bash secara manual lewat pengecekan kondisi sebelum mengubah apa pun. Menurut dokumentasi resmi Ansible, idempotensi didefinisikan sebagai "sebuah operasi bersifat idempotent kalau hasil menjalankannya sekali persis sama dengan hasil menjalankannya berulang kali tanpa ada tindakan lain di antaranya", dan modul-modul bawaan Ansible seperti user, apt, atau ufw sudah dirancang memenuhi sifat itu secara bawaan, tervalidasi lintas platform oleh komunitas dan tim Ansible sendiri. Konsekuensi praktisnya bagi Developer: begitu sebuah kebutuhan provisioning bisa dinyatakan lewat modul Ansible yang sudah ada, pilih itu, jangan menulis ulang logika "cek dulu sebelum ubah" secara manual di Bash seperti 23.2.3, karena Ansible sudah menjamin itu lebih matang dan lebih teruji.
Peran Bash di sekitar Ansible bergeser jadi lapisan pemicu yang menyederhanakan opsi baris perintah ansible-playbook yang cukup banyak, supaya anggota tim tidak perlu menghafal semuanya satu per satu.
#!/bin/bash
set -euo pipefail
MODE_UJI=false
LIMIT_HOST=""
while getopts ":cl:" opsi; do
case "$opsi" in
c) MODE_UJI=true ;;
l) LIMIT_HOST="$OPTARG" ;;
*) echo "Penggunaan: $0 [-c] [-l host]" >&2; exit 1 ;;
esac
done
cmd=(ansible-playbook -i inventory.ini site.yml)
if [[ "$MODE_UJI" == "true" ]]; then
cmd+=(--check --diff)
fi
if [[ -n "$LIMIT_HOST" ]]; then
cmd+=(--limit "$LIMIT_HOST")
fi
"${cmd[@]}"Pola getopts ini mengikuti persis Bab 12.2, dan pola array cmd+=(...) mengikuti persis 23.1.3, dipakai lagi di sini karena kombinasi keduanya memang jadi cara paling aman menyusun perintah eksternal dengan opsi yang berubah-ubah tanpa eval. Sesuai dokumentasi resmi ansible-playbook, opsi -C/--check menjalankan playbook dalam mode simulasi, mencoba memprediksi perubahan apa yang akan terjadi tanpa benar-benar mengeksekusinya, dan -D/--diff menampilkan perbedaan isi file yang akan berubah, "bekerja sangat baik dipakai bersama --check" menurut dokumentasi itu sendiri. Kombinasi -c di wrapper Bash ini jadi jalan cepat menjalankan ./jalankan-playbook.sh -c sebelum benar-benar menjalankan ./jalankan-playbook.sh tanpa opsi itu, kebiasaan yang layak jadi standar sebelum playbook apa pun benar-benar mengubah kondisi server produksi. Baca baris ringkasan di akhir output mode simulasi, formatnya changed=N untuk setiap host; kalau angka changed tidak sesuai perkiraan (misalnya jauh lebih besar dari task yang memang sengaja diubah), hentikan dulu dan telusuri lewat -D sebelum melanjutkan ke eksekusi sungguhan tanpa -c.
24.2.3 Wrapper Plan dan Apply Terraform
Terraform, tool infrastructure as code untuk mendefinisikan dan mengelola resource cloud lewat file konfigurasi deklaratif, punya alur kerja standar tiga langkah: init, plan, apply. Bash cocok jadi glue script di sini untuk memastikan urutan itu selalu konsisten dan menambahkan gerbang validasi sebelum apply benar-benar mengubah infrastruktur sungguhan.
#!/bin/bash
set -euo pipefail
cd infrastruktur/
echo "Memeriksa format konfigurasi..."
if ! terraform fmt -check -recursive; then
echo "Format Terraform tidak sesuai standar" >&2
echo "Jalankan 'terraform fmt -recursive' lalu commit ulang" >&2
exit 1
fi
terraform init -input=false
terraform plan -input=false -out=tfplan
if [[ "${TERRAFORM_AUTO_APPLY:-false}" == "true" ]]; then
terraform apply tfplan
fiMenurut dokumentasi resmi HashiCorp, terraform fmt -check memeriksa apakah file konfigurasi sudah mengikuti format kanonis tanpa mengubah isinya, keluar dengan exit status 0 kalau sudah rapi dan bukan nol kalau ada file yang belum, sementara opsi -recursive memperluas pemeriksaan itu ke seluruh subdirektori, bukan cuma direktori saat ini. Menjadikan langkah ini gerbang paling awal, sebelum init maupun plan dijalankan sama sekali, mencegah pipeline membuang waktu memproses konfigurasi yang bahkan belum rapi formatnya. Opsi -input=false pada init dan plan mematikan seluruh prompt interaktif Terraform, penting karena pipeline CI/CD tidak punya siapa pun yang bisa mengetik jawaban kalau Terraform tiba-tiba bertanya. -out=tfplan menyimpan hasil plan ke sebuah file; menurut dokumentasi resmi, begitu file plan itu diteruskan ke terraform apply, Terraform memperlakukan tindakan menyerahkan file itu sebagai bentuk persetujuan itu sendiri, sehingga apply tidak lagi menampilkan prompt konfirmasi "yes" yang biasa muncul kalau apply dipanggil langsung tanpa file plan. Variable TERRAFORM_AUTO_APPLY di baris terakhir sengaja jadi gerbang tambahan di level pipeline, bukan di level Terraform sendiri, supaya tim bisa memisahkan job "plan" yang boleh berjalan otomatis di setiap pull request dari job "apply" yang cuma boleh berjalan setelah ada review manusia atas hasil plan tersebut, pola pemisahan plan dan apply yang umum dipakai di banyak pipeline infrastruktur produksi supaya perubahan pada resource cloud sungguhan tidak pernah lolos tanpa ditinjau lebih dulu. Sebelum TERRAFORM_AUTO_APPLY diaktifkan, baca dulu ringkasan di baris terakhir output terraform plan, formatnya Plan: N to add, N to change, N to destroy; angka pada destroy yang tidak nol wajib diperiksa manual, karena resource yang dihapus Terraform tidak bisa dikembalikan lewat git reset seperti kode aplikasi di Bab 21.3.3.
24.3 Wrapper Script untuk Operasional Tim
Bab 12.4 sudah membahas dasar membuat CLI tool sederhana, dan Bab 17 sampai 22 menumpuk banyak perintah operasional yang masing-masing punya opsi dan urutan sendiri-sendiri. Sub-bab ini menyatukan semuanya jadi satu titik masuk tunggal yang lebih ramah dipakai anggota tim yang tidak menghafal seluruh detail itu.
24.3.1 Kenapa Tim Butuh Wrapper Script
Di lapangan, tidak semua anggota tim yang perlu men-deploy atau memeriksa status aplikasi adalah Sysadmin berpengalaman yang hafal systemctl, journalctl, atau alur rollback berbasis Git dari Bab 21.3.3. Seorang Developer yang cuma perlu tahu "apakah deploy tadi berhasil" seharusnya tidak perlu menghafal journalctl -u aplikasi-web.service -n 50 --no-pager, apalagi sampai salah ketik opsi -u jadi opsi lain yang berbahaya. Wrapper script, sudah disinggung sekilas di Bab 12.4.1 dan 17.1.3, menyediakan antarmuka yang lebih sempit dan lebih aman di atas perintah-perintah mentah itu, sekaligus jadi satu tempat untuk menegakkan kebiasaan aman seperti konfirmasi sebelum tindakan destruktif.
24.3.2 Dispatcher Subperintah dengan case
case statement dari Bab 6.3 cocok dipakai sebagai dispatcher, pola yang membaca argumen pertama sebagai nama subperintah lalu mengarahkan eksekusi ke fungsi yang sesuai, mirip cara git atau docker sendiri bekerja lewat subperintah seperti git commit atau docker build.
case "${1:-help}" in
deploy) cmd_deploy ;;
rollback) cmd_rollback ;;
status) cmd_status ;;
logs) cmd_logs "${2:-50}" ;;
help|*) tampilkan_bantuan ;;
esac"${1:-help}" memakai parameter expansion nilai default dari Bab 6.4.4 supaya ops.sh tanpa argumen sama sekali otomatis jatuh ke subperintah help, bukan berhenti dengan error yang membingungkan. Pola help|*) di baris terakhir menangkap dua kondisi sekaligus: subperintah help yang memang diminta eksplisit, dan subperintah apa pun yang tidak dikenali, keduanya berakhir menampilkan pesan bantuan yang sama.
24.3.3 Implementasi ops.sh
Versi lengkap ops.sh berikut menyatukan pola dispatcher di atas dengan pemanggilan SSH ke server, memakai kembali deploy-pipeline.sh dan aplikasi-web.service dari Bab 21.
#!/bin/bash
set -uo pipefail
HOST="app.example.com"
DEPLOY_USER="deploy"
tampilkan_bantuan() {
cat <<'USAGE'
Penggunaan: ops.sh <subperintah> [argumen]
Subperintah yang tersedia:
deploy Tarik kode terbaru dan restart service
rollback Kembalikan ke commit sebelumnya (minta konfirmasi)
status Tampilkan status service saat ini
logs [N] Tampilkan N baris log terakhir (default 50)
help Tampilkan pesan ini
USAGE
}
jalankan_jauh() {
ssh -o StrictHostKeyChecking=accept-new "$DEPLOY_USER@$HOST" -- "$@"
}
cmd_deploy() {
jalankan_jauh "/var/www/aplikasi/deploy-pipeline.sh"
}
cmd_rollback() {
read -rp "Yakin rollback $HOST ke commit sebelumnya? [y/N] " konfirmasi
if [[ "$konfirmasi" =~ ^[Yy]$ ]]; then
jalankan_jauh "cd /var/www/aplikasi && git reset --hard HEAD~1 && sudo systemctl restart aplikasi-web.service"
else
echo "Rollback dibatalkan"
fi
}
cmd_status() {
jalankan_jauh "systemctl status aplikasi-web.service --no-pager"
}
cmd_logs() {
local jumlah="$1"
jalankan_jauh "journalctl -u aplikasi-web.service -n $jumlah --no-pager"
}
case "${1:-help}" in
deploy) cmd_deploy ;;
rollback) cmd_rollback ;;
status) cmd_status ;;
logs) cmd_logs "${2:-50}" ;;
help|*) tampilkan_bantuan ;;
esacSatu detail penting yang membedakan kunci SSH di sini dari kunci CI/CD di 24.1.2: kunci yang dipakai ops.sh ini sengaja tidak dikunci lewat command= ke satu script saja, karena anggota tim memang perlu menjalankan beberapa subperintah berbeda (status, logs, rollback), bukan cuma satu. Sebagai gantinya, batasan keamanannya dipindah ke sisi sudo: baris deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart aplikasi-web.service dari Bab 23.3.4 memastikan perintah apa pun yang dikirim lewat ops.sh, sekalipun user yang login lewat SSH itu mencoba menyisipkan perintah lain, cuma systemctl restart aplikasi-web.service spesifik yang bisa berjalan sebagai root tanpa password; operasi git reset pada cmd_rollback() sendiri berjalan sebagai user deploy biasa karena memang deploy pemilik folder /var/www/aplikasi. cmd_rollback() juga sengaja memakai read -rp dari Bab 5.1 untuk meminta konfirmasi eksplisit sebelum benar-benar menjalankan git reset --hard, tindakan yang berpotensi membuang perubahan kalau salah ketik subperintah, konsisten dengan prinsip kejujuran soal risiko tindakan destruktif.
| Subperintah | Efek | Butuh Konfirmasi |
|---|---|---|
ops.sh deploy | Tarik kode terbaru, restart service, rollback otomatis kalau health check gagal | Tidak |
ops.sh rollback | Paksa kembali ke commit sebelumnya | Ya |
ops.sh status | Tampilkan status service (read-only) | Tidak |
ops.sh logs [N] | Tampilkan N baris log terakhir (read-only) | Tidak |
Uji dulu dua subperintah read-only sebelum mencoba deploy atau rollback, supaya kalau ada yang salah, penyebabnya jelas terpisah antara masalah konektivitas SSH dan masalah pada deploy-pipeline.sh itu sendiri.
chmod +x ops.sh
./ops.sh status
./ops.sh logs 20Kalau ops.sh status keluar dengan Permission denied (publickey), penyebabnya sama seperti yang dibahas troubleshooting Bab 21.2.1: permission ~/.ssh di sisi pemanggil atau authorized_keys di sisi server belum sesuai, atau key yang dipakai anggota tim itu belum sempat ditambahkan ke authorized_keys milik deploy. Kalau perintahnya berhasil terhubung tapi systemctl status melaporkan service dalam kondisi failed, lanjutkan penelusuran lewat ./ops.sh logs untuk membaca detail error dari aplikasi itu sendiri sebelum mencoba ./ops.sh rollback.
24.4 Studi Kasus: Provisioning Server Baru
Studi kasus penutup ini menepati janji terakhir yang tersisa, pola useradd lengkap dari Bab 17.3.1 untuk kebutuhan provisioning, proses menyiapkan server baru dari kondisi kosong sampai siap menerima trafik produksi. Skenarionya: sebuah VPS Ubuntu 22.04 LTS baru saja dipesan, masih polos, dan perlu disiapkan jadi server yang bisa langsung menerima deploy otomatis dari pipeline CI/CD di 24.1.
24.4.1 Rancangan Provisioning End-to-End
Urutan langkah provision-server.sh, dijalankan sekali sebagai root lewat SSH begitu VPS baru selesai dipesan: (1) pasang paket dasar (docker.io, git, ufw); (2) buat user deploy secara idempotent, dipersempit hak aksesnya dibanding contoh awal di Bab 17.3.1; (3) pasang kunci SSH dari CI/CD dengan pembatasan command= dari 23.3.4; (4) beri hak sudo yang dipersempit cuma untuk satu perintah restart; (5) clone repository aplikasi; (6) daftarkan aplikasi-web.service lewat systemd; (7) aktifkan firewall ufw. Berbeda dari deploy-pipeline.sh di Bab 21.4.2 yang sengaja memakai set -uo pipefail tanpa -e supaya logika rollback tetap sempat berjalan meski satu langkah gagal, script provisioning ini memakai set -euo pipefail penuh, karena kegagalan di tengah proses provisioning (misalnya paket gagal terpasang) seharusnya langsung menghentikan seluruh proses, bukan melanjutkan ke langkah berikutnya di atas fondasi yang sudah diketahui rusak.
24.4.2 Implementasi provision-server.sh
#!/bin/bash
set -euo pipefail
APP_DIR="/var/www/aplikasi"
SERVICE="aplikasi-web.service"
PUBKEY_FILE=""
REPO_URL=""
while getopts ":k:r:" opsi; do
case "$opsi" in
k) PUBKEY_FILE="$OPTARG" ;;
r) REPO_URL="$OPTARG" ;;
*) echo "Penggunaan: $0 -k pubkey_file -r repo_url" >&2; exit 1 ;;
esac
done
if [[ -z "$PUBKEY_FILE" || -z "$REPO_URL" ]]; then
echo "Opsi -k dan -r wajib diisi" >&2
exit 1
fi
echo "==> Memasang paket dasar"
export DEBIAN_FRONTEND=noninteractive
apt-get update -y
apt-get install -y docker.io git ufw
echo "==> Menyiapkan user deploy"
if id deploy &> /dev/null; then
echo "User deploy sudah ada, lewati"
else
useradd -m -s /bin/bash deploy
echo "User deploy dibuat"
fi
usermod -aG docker deploy
echo "==> Memasang kunci SSH terbatas milik CI/CD"
SSH_DIR="/home/deploy/.ssh"
AUTHORIZED_KEYS="$SSH_DIR/authorized_keys"
PUBKEY_ISI=$(cat "$PUBKEY_FILE")
BARIS_KEY="restrict,command=\"$APP_DIR/deploy-pipeline.sh\" $PUBKEY_ISI"
mkdir -p "$SSH_DIR"
touch "$AUTHORIZED_KEYS"
grep -qF -- "$PUBKEY_ISI" "$AUTHORIZED_KEYS" || echo "$BARIS_KEY" >> "$AUTHORIZED_KEYS"
chown -R deploy:deploy "$SSH_DIR"
chmod 700 "$SSH_DIR"
chmod 600 "$AUTHORIZED_KEYS"
echo "==> Membatasi hak sudo deploy"
SUDOERS_FILE="/etc/sudoers.d/deploy-aplikasi-web"
echo "deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart $SERVICE" > "$SUDOERS_FILE.tmp"
chmod 440 "$SUDOERS_FILE.tmp"
if visudo -c -f "$SUDOERS_FILE.tmp"; then
mv "$SUDOERS_FILE.tmp" "$SUDOERS_FILE"
else
echo "Sintaks sudoers tidak valid, batalkan provisioning" >&2
rm -f "$SUDOERS_FILE.tmp"
exit 1
fi
echo "==> Menarik kode aplikasi"
if [[ -d "$APP_DIR/.git" ]]; then
echo "Repository sudah ada di $APP_DIR, lewati clone"
else
mkdir -p "$APP_DIR"
chown deploy:deploy "$APP_DIR"
sudo -u deploy git clone "$REPO_URL" "$APP_DIR"
fi
echo "==> Mendaftarkan systemd service"
cat > "/etc/systemd/system/$SERVICE" <<UNIT
[Unit]
Description=Aplikasi web internal
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=$APP_DIR
ExecStart=$APP_DIR/jalankan.sh
Restart=on-failure
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable --now "$SERVICE"
echo "==> Mengaktifkan firewall"
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
echo "==> Provisioning selesai"Script di atas ditulis khusus untuk Ubuntu (dan turunan Debian lain), konsisten dengan skenario 24.4.1; untuk distro keluarga RHEL, CentOS, atau Fedora, ganti apt-get install dengan dnf install, dan ganti seluruh baris ufw dengan firewall-cmd mengikuti pola Bab 23.3.3. Paket docker.io yang dipasang juga sengaja dipilih ketimbang repository resmi Docker (docker-ce), karena docker.io tersedia langsung dari repository bawaan Ubuntu tanpa perlu menambah repository pihak ketiga dan kunci GPG tambahan, cocok untuk script provisioning yang mengutamakan kesederhanaan; trade-off-nya, versi Docker yang terpasang lewat docker.io biasanya tertinggal beberapa rilis dibanding docker-ce langsung dari Docker Inc., jadi untuk kebutuhan produksi yang butuh fitur Docker paling baru, pertimbangkan memasang lewat repository resmi Docker sesuai dokumentasinya sendiri.
Perbandingkan baris useradd -m -s /bin/bash deploy di atas dengan contoh Bab 17.3.1, sudo useradd -m -s /bin/bash -G docker,sudo deploy. Contoh Bab 17 sengaja langsung memasukkan deploy ke grup sudo penuh untuk kemudahan pemakaian umum; script provisioning ini justru menariknya kembali mengikuti prinsip hak akses minimal dari 23.3.4, deploy tidak masuk grup sudo sama sekali, cuma diberi izin NOPASSWD untuk satu perintah spesifik lewat /etc/sudoers.d/deploy-aplikasi-web. Grup docker tetap ditambahkan lewat usermod -aG docker deploy karena memang dibutuhkan untuk build image (24.2.1), tapi ini bukan tanpa risiko yang perlu disadari: menurut dokumentasi resmi Docker sendiri, "grup docker memberikan hak akses setara root ke user yang jadi anggotanya", karena siapa pun yang bisa bicara ke Docker daemon pada dasarnya bisa me-mount filesystem host manapun lewat container. Trade-off ini diterima secara sadar di sini, bukan diabaikan, dan sebagian dimitigasi lewat kunci SSH yang tetap dikunci ke satu script lewat command=, jadi sekalipun deploy punya hak akses docker penuh, jalur masuk dari CI/CD tetap tidak bisa dipakai bebas menjalankan perintah docker sembarangan.
visudo -c -f dipakai sebelum file sudoers baru benar-benar dipasang; menurut man visudo resmi (sudo.ws dan man7.org), opsi -c menjalankan mode periksa saja tanpa mengubah apa pun, dan -f menunjuk file alternatif yang diperiksa, keluar dengan exit status 0 kalau sintaksnya valid. Langkah verifikasi ini bukan formalitas, satu baris sudoers yang salah tulis bisa membuat mekanisme sudo di seluruh server rusak total, dan berbeda dari kebanyakan kesalahan konfigurasi lain, kesalahan sudoers seringkali cuma bisa dipulihkan lewat akses konsol langsung karena sudo itu sendiri yang rusak. ufw --force enable di baris-baris terakhir memakai opsi --force yang menurut manpage resmi ufw melewati prompt konfirmasi interaktif "Command may disrupt existing ssh connections", prompt yang kalau dibiarkan aktif akan membuat script ini menggantung menunggu input yang tidak akan pernah datang di lingkungan non-interaktif.
24.4.3 Verifikasi Hasil Provisioning
chmod +x provision-server.sh
sudo ./provision-server.sh -k id_ed25519_deploy.pub -r [email protected]:tim/aplikasi.git
systemctl status aplikasi-web.service --no-pager
sudo ufw status verbose
sudo -l -U deploy
sudo cat /home/deploy/.ssh/authorized_keysPerhatikan output sudo -l -U deploy, pola yang sama dipakai audit di Bab 23.3.4, seharusnya cuma menampilkan satu baris izin NOPASSWD untuk systemctl restart aplikasi-web.service, bukan akses ALL=(ALL) yang lebih luas. Isi authorized_keys seharusnya diawali restrict,command="..." persis seperti pola 23.3.4. Sebagai pengecekan tambahan yang layak dilakukan sekali, port yang dibuka ufw di sini (22, 80, 443) sengaja disamakan dengan PORT_DIIZINKAN pada toolkit-keamanan.sh di Bab 23.4.3; kalau toolkit-keamanan.sh ikut dipasang di server baru ini, fungsi audit_port()-nya seharusnya tidak melaporkan peringatan apa pun untuk ketiga port itu. Cara paling meyakinkan menguji jalur idempotensinya, sesuai kebiasaan 23.4.4, adalah menjalankan provision-server.sh sekali lagi dengan argumen yang sama dan memastikan pesan yang muncul semuanya "sudah ada, lewati", bukan mencoba membuat ulang apa pun yang sudah ada.
24.4.4 Menyambungkan ke Pipeline CI/CD
Langkah terakhir menutup lingkaran penuh yang dijanjikan sejak Bab 17, 21, dan 23: daftarkan alamat server baru ini sebagai secret SSH_HOST, dan isi private key pasangan dari id_ed25519_deploy.pub yang tadi dipasang sebagai SSH_DEPLOY_KEY, lewat halaman secret GitHub Actions atau variable GitLab CI dari 24.1.4. Push satu commit percobaan ke branch main, lalu amati tab Actions (GitHub) atau Pipelines (GitLab) di repository, memastikan job deploy berjalan, berhasil terhubung lewat SSH ke server baru, dan deploy-pipeline.sh benar-benar tereksekusi di sisi server, terlihat dari isi /var/log/deploy-pipeline.log yang bertambah baris baru. Server yang baru saja diprovisikan lewat satu perintah ini sekarang siap menerima deploy otomatis kapan pun ada kode baru masuk ke branch main, tanpa Sysadmin perlu login manual sama sekali untuk konfigurasi awal maupun untuk setiap rilis berikutnya.
Bab ini melengkapi kemampuan menjalankan Bash bukan cuma sebagai script yang dipanggil manual, tapi sebagai bagian pipeline otomatis yang menyatukan CI/CD, Docker, Ansible, Terraform, dan operasional tim sehari-hari, sekaligus jadi mesin yang benar-benar menyalakan server baru dari nol. Bagian 3 series ini pun ditutup di sini. Bagian 4 melanjutkan ke topik lanjutan dan praktik terbaik, dimulai dari Bab 25 yang membedah set -x, set -e, set -u, dan pipefail yang baru disinggung sekilas di 24.1.1 secara jauh lebih mendalam, lalu Bab 26 membahas cara mengorganisasi script yang jumlahnya sudah mulai menumpuk seperti di bab ini (deploy-pipeline.sh, ops.sh, provision-server.sh, dan beberapa wrapper lain) supaya tetap rapi dan mudah dikolaborasikan lewat Git, sebelum akhirnya semua kemampuan ini disatukan jadi proyek akhir di Bab 28.2.

