VPS yang baru dibuka masih menjadi milik siapa saja yang bisa menebak password root-nya — dan di internet publik, upaya menebak itu datang setiap menit. Pekerjaan pertama proyek ini bukan mengurus nginx, melainkan menutup pintu-pintu itu: mengganti login root dengan user kerja bernama deployer yang masuk lewat kunci SSH, membuka firewall hanya untuk tiga port yang memang dipakai, dan menyiapkan struktur webroot tempat seluruh situs nantinya hidup. Semua perintah di bagian ini kita tulis sebagaimana dijalankan di VPS sungguhan; sesudahnya, yang bisa diuji di lingkungan latihan — memvalidasi konfigurasi sshd dan aturan firewall dari wujud teksnya — kita jalankan sungguhan dengan fixture.
Checklist Persiapan VPS #
Enam langkah berikut mengubah VPS kosong menjadi fondasi yang bisa dipakai tiga bagian ke depan. Tabelnya dulu, perintahnya menyusul:
| # | Langkah | Perintah inti | Tujuan |
|---|---|---|---|
| 1 | perbarui paket | sudo apt update && sudo apt upgrade | tutup celah keamanan usang |
| 2 | pasang nginx | sudo apt install nginx | web server yang akan kita konfigurasi |
| 3 | buat user deployer | sudo useradd -m -s /bin/bash deployer | akun kerja tanpa hak root |
| 4 | kunci SSH | ssh-keygen lalu ssh-copy-id | login tanpa password |
| 5 | firewall | sudo ufw allow untuk 22/80/443, lalu enable | tutup semua port lain |
| 6 | zona waktu | sudo timedatectl set-timezone Asia/Jakarta | log ber-jam lokal |
Dua perintah pertama dijalankan sebagai root lewat sudo — di VPS Debian baru, login pertama biasanya memang sebagai root, dan itulah satu-satunya momen kita memakainya:
sudo apt update && sudo apt upgrade -y
sudo apt install nginxapt update menyegarkan daftar paket, apt upgrade menaikkan paket yang sudah terpasang ke versi terbaru, lalu apt install nginx memasang web server beserta unit systemd-nya — service langsung aktif dan mendengarkan port 80 dengan halaman selamat datang bawaan. Kita belum menyentuh konfigurasinya; itu pekerjaan Bagian 2. Perintah di atas membutuhkan root sehingga tidak dijalankan di lingkungan latihan ini — kita cukup percaya strukturnya, karena peta direktori konfigurasi Debian sudah dibedah di Chapter nginx
.
User Deployer dan Kunci SSH #
Mengelola server lewat akun root itu seperti mengemudi tanpa sabuk pengaman: cepat, tetapi satu salah ketik rm -rf berarti tamat. Kita buat akun kerja khusus, lalu taruh kunci publik kita ke akun itu:
sudo useradd -m -s /bin/bash deployer
sudo usermod -aG sudo deployerOpsi -m membuat home directory /home/deployer, -s /bin/bash memastikan shell login yang layak, dan usermod -aG sudo menempatkannya di grup sudo sehingga hak root tetap bisa dipakai bila benar-benar diperlukan — dengan password yang diminta setiap kali, bukan sesi permanen. Kuncinya dibuat di laptop kita, lalu dipindahkan:
ssh-keygen -t ed25519 -C "budi@laptop-budi"
ssh-copy-id [email protected]ssh-keygen membuat pasangan kunci privat-publik di ~/.ssh/ (kunci privat tidak pernah meninggalkan laptop), dan ssh-copy-id menempelkan kunci publik ke ~/.ssh/authorized_keys milik deployer di server. Sejak itu ssh [email protected] masuk tanpa password — alamat 203.0.113.10 di sini contoh dari blok dokumentasi; ganti dengan IP VPS Anda.
Setelah login lewat kunci terbukti berhasil, giliran pintu password ditutup. Tiga directive sshd inilah yang kita ubah di /etc/ssh/sshd_config:
sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl restart sshKetiganya berarti: root tidak boleh login SSH, password tidak lagi diterima, hanya kunci yang bicara. Konfigurasi seperti ini layak diperiksa ulang secara rutin — dan karena sshd membaca directive dari atas ke bawah (nilai terakhir yang menang), pemeriksaannya bisa kita latih di sini pada cuplikan berkas yang bentuknya sama. Blok berikut menulis fixture, membuat skrip pemeriksa, lalu mengujinya pada dua versi konfigurasi — yang taat aturan dan yang longgar:
mkdir -p latihan
cat > latihan/sshd_config <<'EOF'
# sshd_config — cuplikan untuk latihan (bukan berkas asli)
Port 22
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AllowUsers deployer
EOF
cat > periksa-sshd.sh <<'BASH'
#!/usr/bin/env bash
# periksa-sshd.sh — cocokkan aturan sshd_config dengan nilai harapan.
set -u
berkas=$1
gagal=0
while read -r kunci nilai; do
nyata=$(awk -v k="$kunci" '$1 == k { v = $2 } END { print v }' "$berkas")
if [ "$nyata" != "$nilai" ]; then
printf 'TIDAK COCOK %s: nyata %s, harapan %s\n' \
"$kunci" "$nyata" "$nilai"
gagal=$((gagal + 1))
fi
done <<'ATURAN'
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ATURAN
if [ "$gagal" -eq 0 ]; then
printf 'semua aturan sshd sesuai\n'
fi
exit "$gagal"
BASH
bash periksa-sshd.sh latihan/sshd_config; echo "exit=$?"
printf 'Port 22\nPermitRootLogin yes\nPasswordAuthentication yes\nPubkeyAuthentication yes\n' > longgar.conf
bash periksa-sshd.sh longgar.conf; echo "exit=$?"Output yang Dihasilkan:
semua aturan sshd sesuai
exit=0
TIDAK COCOK PermitRootLogin: nyata yes, harapan no
TIDAK COCOK PasswordAuthentication: nyata yes, harapan no
exit=2Mari kita bedah bagian-bagian kuncinya:
- Baris
awk -v k="$kunci" '$1 == k { v = $2 } END { print v }': untuk setiap baris yang kolom pertamanya cocok dengan nama directive, nilai kolom keduanya disimpan kev— dan karena penugasan berjalan terus sampai berkas habis, nilai yang tersimpan adalah yang muncul terakhir. Perilaku ini meniru carasshdsendiri menangani directive yang ditulis dua kali, dan tabel lengkap opsinya ada di sshd_config(5) . - Baris
while read -r kunci nilai ... done <<'ATURAN': daftar aturan harapan ditulis sebagai heredoc —read -r kunci nilaimemecah setiap baris menjadi nama directive dan nilai harapannya. Menambah aturan baru berarti menambah satu baris, bukan menyunting logika. - Baris
exit "$gagal": jumlah aturan yang dilanggar menjadi exit code — 0 berarti semua sesuai, 2 berarti dua pelanggaran. Angka itu nanti bisa dibaca test suite di Bagian 3 untuk memastikan pemeriksaan seperti ini tetap hijau. - Eksekusi kedua pada
longgar.conf: konfigurasi yang masih mengizinkan root login dan password tertangkap dengan dua barisTIDAK COCOK— dan nilainyatayang tercetak adalahyes, persis nilai berbahaya yang kita cari.
Firewall: Hanya Tiga Port #
Firewall yang baik itu pelit: semuanya ditolak kecuali yang kita izinkan. ufw menyusun aturan itu dengan tiga perintah, lalu satu kunci pengaktifan:
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enablePort 22 untuk SSH (termasuk channel SFTP bila kelak ingin mengunggah berkas), port 80 untuk HTTP, dan port 443 untuk HTTPS yang akan hidup di Bagian 3. ufw enable menyalakan aturan default-deny — sejak saat itu koneksi ke port lain ditolak di kernel. Hasil akhirnya bisa dibaca kembali lewat sudo ufw status, dan bentuk output-nya stabil sehingga layak diperiksa dengan awk. Blok berikut memakai fixture berisi output ufw status yang telah dijalankan di VPS, lalu mengurai aturan yang aktif:
mkdir -p latihan
cat > latihan/ufw-status.txt <<'EOF'
Status: active
To Action From
-- ------ ----
22/tcp ALLOW Anywhere
80/tcp ALLOW Anywhere
443 ALLOW Anywhere
22/tcp (v6) ALLOW Anywhere (v6)
80/tcp (v6) ALLOW Anywhere (v6)
443 (v6) ALLOW Anywhere (v6)
EOF
echo "== aturan ALLOW (IPv4 dan IPv6) =="
awk 'NR > 4 && $2 == "ALLOW" { print $1 }' latihan/ufw-status.txt | sort -u
echo "== port yang terbuka =="
port=$(awk 'NR > 4 && $2 == "ALLOW" { split($1, a, "/"); print a[1] }' \
latihan/ufw-status.txt | sort -n -u | paste -sd,)
echo "$port"
if [ "$port" = "22,80,443" ]; then
echo "firewall sesuai rencana: hanya port 22, 80, 443"
fiOutput yang Dihasilkan:
== aturan ALLOW (IPv4 dan IPv6) ==
22/tcp
443
80/tcp
== port yang terbuka ==
22,80,443
firewall sesuai rencana: hanya port 22, 80, 443Bedahnya singkat tetapi berisi:
NR > 4: empat baris pertama fixture adalah kepala — status, baris kosong, dan dua baris judul kolom. Data aturan baru dimulai baris kelima.$2 == "ALLOW": kolom kedua tiap baris aturan berisi aksinya; hanya barisALLOWyang berarti port terbuka. Bila suatu saat adaDENY, baris itu otomatis tidak ikut dihitung terbuka.split($1, a, "/"): kolom pertama berbentuk22/tcp, jadi nama port dipisahkan dari protokolnya dulu.sort -n -umengurutkan secara numerik sekaligus membuang duplikat — aturan IPv4 dan IPv6 untuk port yang sama hanya dihitung sekali. Perhatikan bahwasort -utanpa-ndi blok pertama mengurutkan443sebelum80/tcpkarena mengurutkan teks, bukan angka; itulah mengapa pengurutan port memakai-n.- Perbandingan
if [ "$port" = "22,80,443" ]: daftar port yang terbuka harus persis tiga port itu. Aturan yang tak terduga — misalnya8080yang lupa dihapus — membuat string berbeda dan pemeriksaan gagal, bukan lolos diam-diam.
Zona Waktu Server #
Log yang jamnya melenceng membuat investigasi menyakitkan: serangan dan kegagalan dikorelasikan dengan log lain di jaringan, dan itu hanya mungkin bila semuanya sepakat soal waktu. Satu perintah menyelesaikannya di VPS:
sudo timedatectl set-timezone Asia/Jakarta
timedatectltimedatectl tanpa argumen menampilkan status lengkap — zona waktu, sinkronisasi NTP, dan jam saat ini. Sinkronisasi waktu lewat NTP biasanya sudah aktif bawaan di Debian; bila belum, sudo timedatectl set-ntp true menyalakannya. Log nginx di Bagian 2 dan log deploy di Bagian 3 nantinya memakai jam server ini.
Rencana Struktur Webroot #
Keputusan struktural terakhir sebelum menyentuh konfigurasi: di mana situs hidup. Alih-alih menumpuk berkas langsung di /var/www/html dan menimpanya setiap deploy, kita menyiapkan pola release-folder yang nantinya digeser oleh skrip deployment. Bentuk akhirnya:
/var/www/site/
├── releases/
│ └── 20260115-0900/ ← satu folder = satu versi utuh situs
│ ├── index.html
│ └── assets/
│ └── gaya.css
├── shared/ ← berkas yang bertahan lintas versi
└── current -> releases/20260115-0900releases/ menampung setiap versi yang pernah dideploy, current adalah symlink yang menentukan versi mana yang dilayani nginx, dan shared disiapkan untuk berkas yang harus bertahan lintas versi — misalnya berkas yang diunggah pengguna. Di VPS, strukturnya dibuat dengan sudo mkdir -p /var/www/site/{releases,shared}. Di lingkungan latihan kita bangun bentuk yang sama di dalam folder kerja, lengkap dengan symlink pertamanya:
mkdir -p latihan/site/{releases,shared}
mkdir -p latihan/site/releases/20260115-0900/assets
printf '<h1>Situs v1</h1>\n' > latihan/site/releases/20260115-0900/index.html
ln -sfn releases/20260115-0900 latihan/site/current
tree latihan/siteOutput yang Dihasilkan:
latihan/site
├── current -> releases/20260115-0900
├── releases
│ └── 20260115-0900
│ ├── assets
│ └── index.html
└── shared
6 directories, 1 fileTampilan -> pada baris current adalah tanda tautan simbolik, dan nilai 4 directories, 1 file-nya mengikuti isi tree — versi persis di komputer Anda bisa sedikit berbeda bila foldernya lebih ramai. Dua hal penting sudah tampak di sini: ln -sfn membuat symlink secara atomik (bila sudah ada, diganti, bukan digandakan error), dan targetnya ditulis relatif (releases/20260115-0900, bukan jalur absolut) sehingga seluruh struktur bisa dipindah tanpa memutus tautan. Kedua sifat itu akan menjadi tulang punggung deploy.sh di Bagian 3.
Fondasi kini ada: user deployer dengan kunci SSH sebagai satu-satunya pintu masuk, firewall yang hanya membuka 22/80/443, dan webroot berpola release yang siap diisi. Yang belum ada adalah penjempolnya — nginx masih menyajikan halaman selamat datang bawaan. Pekerjaan Bagian 2: menulis server block yang menghubungkan nama domain, webroot, dan backend API sekaligus.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
deploy/deploy.sh
Unduh berkas#!/usr/bin/env bash
# deploy.sh — deployment dengan release folder, symlink current, dan
# health check.
#
# Alur satu eksekusi:
# 1. baca dan validasi konfigurasi (deploy.conf)
# 2. validasi artifact build (index.html wajib ada)
# 3. salin build ke releases/STAMP lalu verifikasi salinannya
# 4. geser symlink current ke release baru
# 5. health check dengan retry; gagal -> rollback otomatis ke
# release sebelumnya dan release baru dihapus
#
# Variabel lingkungan (per eksekusi):
# CONF berkas konfigurasi yang dibaca (default: deploy.conf)
# BUILD_DIR folder artifact yang dideploy (default: build)
# STAMP identitas release (default: waktu sekarang, YYYYmmdd-HHMMSS)
#
# Kode keluar:
# 0 sukses 1 konfigurasi tidak valid
# 2 artifact build tidak siap 3 release gagal disalin/diverifikasi
# 4 health check gagal (current sudah dikembalikan ke release lama)
set -u
CONF="${CONF:-deploy.conf}"
BUILD_DIR="${BUILD_DIR:-build}"
STAMP="${STAMP:-$(date +%Y%m%d-%H%M%S)}"
# log(): tulis satu baris beridentitas release ke stdout.
log() {
printf '[%s] %s\n' "$STAMP" "$1"
}
# die(): catat kegagalan lewat log, lalu keluar dengan kode tertentu.
die() {
log "GAGAL: $1 (kode $2)"
exit "$2"
}
# --- 1. Baca dan validasi konfigurasi ---------------------------------
# Pesan tahap ini keluar lewat stderr: gagal di sini berarti skrip
# belum boleh menyentuh folder site sama sekali.
if [ ! -f "$CONF" ]; then
printf 'deploy.sh: berkas konfigurasi %s tidak ditemukan\n' "$CONF" >&2
exit 1
fi
# shellcheck source=/dev/null
. "$CONF"
if [ -z "${SITE_DIR:-}" ]; then
printf 'deploy.sh: SITE_DIR belum diisi di %s\n' "$CONF" >&2
exit 1
fi
if [ -z "${HEALTH_URL:-}" ]; then
printf 'deploy.sh: HEALTH_URL belum diisi di %s\n' "$CONF" >&2
exit 1
fi
case ${HEALTH_RETRIES:-} in
''|*[!0-9]*|0)
printf 'deploy.sh: HEALTH_RETRIES harus angka 1 atau lebih\n' >&2
exit 1
;;
esac
case ${HEALTH_DELAY:-} in
''|*[!0-9]*)
printf 'deploy.sh: HEALTH_DELAY harus angka 0 atau lebih\n' >&2
exit 1
;;
esac
# --- 2. Validasi artifact build ----------------------------------------
[ -d "$BUILD_DIR" ] || die "folder build tidak ditemukan: $BUILD_DIR" 2
[ -f "$BUILD_DIR/index.html" ] || die "index.html tidak ada di $BUILD_DIR" 2
# --- 3. Siapkan struktur site dan release baru -------------------------
mkdir -p "$SITE_DIR/releases" || exit 1
RELEASE="$SITE_DIR/releases/$STAMP"
if [ -e "$RELEASE" ]; then
die "release $STAMP sudah ada; pakai STAMP yang berbeda" 1
fi
# Target symlink saat ini disimpan dulu: inilah tujuan rollback bila
# health check release baru gagal.
PREV=""
if [ -L "$SITE_DIR/current" ]; then
PREV=$(readlink "$SITE_DIR/current")
fi
# --- 4. Salin artifact lalu verifikasi salinannya -----------------------
if ! cp -a "$BUILD_DIR" "$RELEASE"; then
die "gagal menyalin $BUILD_DIR ke $RELEASE" 3
fi
if ! diff -r "$BUILD_DIR" "$RELEASE" > /dev/null; then
rm -rf "$RELEASE"
die "salinan release berbeda dari build" 3
fi
log "release tersalin: $RELEASE"
# --- 5. Geser symlink current ke release baru ---------------------------
# Target dibuat relatif terhadap folder site supaya struktur bisa
# dipindahkan tanpa memutus tautan.
ln -sfn "releases/$STAMP" "$SITE_DIR/current"
log "current -> releases/$STAMP"
# --- 6. Health check dengan retry ---------------------------------------
sehat=0
for percobaan in $(seq 1 "$HEALTH_RETRIES"); do
if curl -fsS -o /dev/null "$HEALTH_URL" 2> /dev/null; then
sehat=1
break
fi
sleep "$HEALTH_DELAY"
done
if [ "$sehat" -ne 0 ]; then
log "health check OK: $HEALTH_URL"
log "selesai tanpa error"
exit 0
fi
# --- 7. Health check gagal: rollback otomatis ---------------------------
# Release yang tidak sehat dihapus supaya rollback di masa depan
# tidak pernah mendarat di release rusak.
if [ -n "$PREV" ]; then
ln -sfn "$PREV" "$SITE_DIR/current"
log "health check gagal; current dikembalikan ke $PREV"
else
rm -f "$SITE_DIR/current"
log "health check gagal; tidak ada release sebelumnya; current dihapus"
fi
rm -rf "$RELEASE"
die "health check gagal setelah $HEALTH_RETRIES percobaan: $HEALTH_URL" 4
deploy/periksa-conf.sh
Unduh berkas#!/usr/bin/env bash
# periksa-conf.sh — pastikan directive wajib ada di berkas server block.
set -u
berkas=$1
gagal=0
for pola in \
'server {' \
'listen 80' \
'server_name latihan.example.com' \
'root /var/www/site/current' \
'index index.html' \
'try_files $uri $uri/ =404' \
'gzip on' \
'location /api/' \
'proxy_pass http://127.0.0.1:8080/' \
'server_tokens off' \
'X-Content-Type-Options' \
'X-Frame-Options' \
'Referrer-Policy'
do
if grep -qF -- "$pola" "$berkas"; then
printf 'OK %s\n' "$pola"
else
printf 'HILANG %s\n' "$pola"
gagal=$((gagal + 1))
fi
done
exit "$gagal"
deploy/rollback.sh
Unduh berkas#!/usr/bin/env bash
# rollback.sh — kembalikan symlink current ke release sebelumnya.
#
# Release diurutkan menurut nama: STAMP berformat YYYYmmdd-HHMMSS
# terurut sama dengan urutan waktunya. current dipindah ke release
# tepat di bawahnya, lalu health check memastikan release lama itu
# benar-benar sehat; bila tidak, current dikembalikan seperti semula.
#
# Variabel lingkungan:
# CONF berkas konfigurasi yang dibaca (default: deploy.conf)
#
# Kode keluar:
# 0 sukses 1 konfigurasi tidak valid
# 2 tidak ada release lebih tua 3 health check gagal
set -u
CONF="${CONF:-deploy.conf}"
# log(): tulis satu baris beridentitas rollback ke stdout.
log() {
printf '[rollback] %s\n' "$1"
}
# die(): catat kegagalan lewat log, lalu keluar dengan kode tertentu.
die() {
log "GAGAL: $1 (kode $2)"
exit "$2"
}
# --- 1. Baca dan validasi konfigurasi ---------------------------------
if [ ! -f "$CONF" ]; then
printf 'rollback.sh: berkas konfigurasi %s tidak ditemukan\n' "$CONF" >&2
exit 1
fi
# shellcheck source=/dev/null
. "$CONF"
if [ -z "${SITE_DIR:-}" ]; then
printf 'rollback.sh: SITE_DIR belum diisi di %s\n' "$CONF" >&2
exit 1
fi
if [ -z "${HEALTH_URL:-}" ]; then
printf 'rollback.sh: HEALTH_URL belum diisi di %s\n' "$CONF" >&2
exit 1
fi
case ${HEALTH_RETRIES:-} in
''|*[!0-9]*|0)
printf 'rollback.sh: HEALTH_RETRIES harus angka 1 atau lebih\n' >&2
exit 1
;;
esac
case ${HEALTH_DELAY:-} in
''|*[!0-9]*)
printf 'rollback.sh: HEALTH_DELAY harus angka 0 atau lebih\n' >&2
exit 1
;;
esac
# --- 2. Temukan release saat ini dan release di bawahnya ---------------
RELEASES="$SITE_DIR/releases"
[ -d "$RELEASES" ] || die "folder releases tidak ditemukan: $RELEASES" 1
KINI=""
if [ -L "$SITE_DIR/current" ]; then
KINI=$(basename "$(readlink "$SITE_DIR/current")")
fi
[ -n "$KINI" ] || die "current belum menunjuk release mana pun" 1
# awk menyimpan nama release terakhir yang lebih kecil dari current;
# karena daftar sudah terurut, itulah release tepat di bawahnya.
LAMA=$(ls -1 "$RELEASES" |
awk -v kini="$KINI" '$0 < kini { lama = $0 } END { print lama }')
[ -n "$LAMA" ] || die "tidak ada release lebih tua dari $KINI" 2
# --- 3. Geser current, lalu health check --------------------------------
ln -sfn "releases/$LAMA" "$SITE_DIR/current"
log "current -> releases/$LAMA"
sehat=0
for percobaan in $(seq 1 "$HEALTH_RETRIES"); do
if curl -fsS -o /dev/null "$HEALTH_URL" 2> /dev/null; then
sehat=1
break
fi
sleep "$HEALTH_DELAY"
done
if [ "$sehat" -eq 0 ]; then
ln -sfn "releases/$KINI" "$SITE_DIR/current"
log "health check gagal; current dikembalikan ke $KINI"
die "health check gagal setelah rollback ke $LAMA" 3
fi
log "health check OK: $HEALTH_URL"
log "selesai tanpa error"
exit 0
tests/run-tests.sh
Unduh berkas#!/usr/bin/env bash
# run-tests.sh — test suite untuk proyek server web.
# Suite menyiapkan folder kerja terisolasi (mktemp) beserta data
# contohnya, menjalankan python3 -m http.server sebagai server
# latihan pengganti nginx, lalu menguji: directive wajib pada berkas
# konfigurasi nginx, alur deploy (release + symlink + health check),
# rollback manual, dan rollback otomatis saat health check gagal.
# Jalankan dari mana saja: bash tests/run-tests.sh
set -u
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJ_DIR=$(dirname "$SCRIPT_DIR")
PASS=0
FAIL=0
WORK=""
SERVER=""
# assert_eq <harapan> <nyata> <pesan>: bandingkan dua nilai.
assert_eq() {
if [ "$1" = "$2" ]; then
PASS=$((PASS + 1))
printf 'OK %s\n' "$3"
else
FAIL=$((FAIL + 1))
printf 'GAGAL %s: harapan %s, nyata %s\n' "$3" "$1" "$2"
fi
}
# assert_ada <pola> <berkas> <pesan>: pola harus ditemukan di berkas.
assert_ada() {
if grep -qF -- "$1" "$2"; then
PASS=$((PASS + 1))
printf 'OK %s\n' "$3"
else
FAIL=$((FAIL + 1))
printf 'GAGAL %s: pola tidak ditemukan di %s\n' "$3" "$2"
fi
}
bersihkan() {
if [ -n "$SERVER" ]; then
kill "$SERVER" 2> /dev/null
fi
if [ -n "$WORK" ]; then
rm -rf "$WORK"
fi
}
trap bersihkan EXIT
# --- uji 1: konfigurasi nginx memuat directive wajib ---------------------
CONF_UTAMA="$PROJ_DIR/config/latihan.example.com.conf"
CONF_TLS="$PROJ_DIR/config/ssl-latihan.example.com.conf"
for pola in 'server {' 'listen 80' 'server_name latihan.example.com' \
'root /var/www/site/current' 'index index.html' \
'try_files $uri $uri/ =404' 'gzip on' 'location /api/' \
'proxy_pass http://127.0.0.1:8080/' 'server_tokens off' \
'X-Content-Type-Options' 'X-Frame-Options' 'Referrer-Policy'; do
assert_ada "$pola" "$CONF_UTAMA" "server block memuat: $pola"
done
for pola in 'listen 443 ssl' 'ssl_certificate ' \
'return 301 https://$host$request_uri' 'Strict-Transport-Security'; do
assert_ada "$pola" "$CONF_TLS" "server block TLS memuat: $pola"
done
for kunci in SITE_DIR HEALTH_URL HEALTH_RETRIES; do
assert_ada "$kunci" "$PROJ_DIR/config/deploy.conf" \
"deploy.conf memuat $kunci"
done
# --- siapkan folder kerja terisolasi --------------------------------------
WORK=$(mktemp -d)
cp "$PROJ_DIR/deploy/deploy.sh" "$PROJ_DIR/deploy/rollback.sh" "$WORK/"
cd "$WORK" || exit 1
# --- uji 2: server latihan siap -------------------------------------------
# Struktur site dibuat dulu: di VPS, nginx sudah berjalan sebelum
# deploy pertama — server latihan kita pun harus demikian.
mkdir -p site/releases
python3 -m http.server 8765 --bind 127.0.0.1 --directory site \
> /dev/null 2>&1 &
SERVER=$!
siap=0
for i in 1 2 3 4 5 6 7 8 9 10; do
if curl -fsS -o /dev/null http://127.0.0.1:8765/ 2> /dev/null; then
siap=1
break
fi
sleep 0.2
done
assert_eq 1 "$siap" "server latihan siap di port 8765"
# --- uji 3: deploy pertama sukses ------------------------------------------
cat > deploy.conf <<'CONF'
SITE_DIR="site"
HEALTH_URL="http://127.0.0.1:8765/current/"
HEALTH_RETRIES=3
HEALTH_DELAY=0
CONF
mkdir -p build-v1/assets build-v2/assets
printf '<h1>Situs v1</h1>\n' > build-v1/index.html
printf 'body { color: #333; }\n' > build-v1/assets/gaya.css
printf '<h1>Situs v2</h1>\n' > build-v2/index.html
printf 'body { color: #111; }\n' > build-v2/assets/gaya.css
BUILD_DIR=build-v1 STAMP=20260115-0900 bash deploy.sh > /dev/null 2>&1
assert_eq 0 "$?" "deploy v1 keluar 0"
[ -d site/releases/20260115-0900 ]
assert_eq 0 "$?" "release v1 dibuat"
assert_eq "releases/20260115-0900" "$(readlink site/current)" \
"current menunjuk release v1"
assert_eq '<h1>Situs v1</h1>' "$(curl -s http://127.0.0.1:8765/current/)" \
"konten tersaji: v1"
# --- uji 4: deploy kedua menggeser current --------------------------------
BUILD_DIR=build-v2 STAMP=20260115-0930 bash deploy.sh > /dev/null 2>&1
assert_eq 0 "$?" "deploy v2 keluar 0"
assert_eq "releases/20260115-0930" "$(readlink site/current)" \
"current bergeser ke v2"
assert_eq '<h1>Situs v2</h1>' "$(curl -s http://127.0.0.1:8765/current/)" \
"konten tersaji: v2"
[ -d site/releases/20260115-0900 ]
assert_eq 0 "$?" "release v1 tidak terhapus"
# --- uji 5: rollback manual memulihkan v1 ---------------------------------
bash rollback.sh > /dev/null 2>&1
assert_eq 0 "$?" "rollback keluar 0"
assert_eq "releases/20260115-0900" "$(readlink site/current)" \
"current kembali ke v1"
assert_eq '<h1>Situs v1</h1>' "$(curl -s http://127.0.0.1:8765/current/)" \
"konten kembali v1"
# --- uji 6: health check gagal -> rollback otomatis -----------------------
# Deploy ketiga agar ada dua release sehat; server lalu dimatikan
# supaya health check deploy keempat pasti gagal.
BUILD_DIR=build-v2 STAMP=20260115-1000 bash deploy.sh > /dev/null 2>&1
assert_eq 0 "$?" "deploy ketiga keluar 0"
kill "$SERVER" 2> /dev/null
wait "$SERVER" 2> /dev/null
SERVER=""
BUILD_DIR=build-v2 STAMP=20260115-1030 bash deploy.sh > /dev/null 2>&1
assert_eq 4 "$?" "health check gagal keluar 4"
assert_eq "releases/20260115-1000" "$(readlink site/current)" \
"current kembali ke release sehat"
[ ! -d site/releases/20260115-1030 ]
assert_eq 0 "$?" "release gagal dihapus"
# --- uji 7: exit code konfigurasi dan artifact ------------------------------
CONF=halang.conf bash deploy.sh > /dev/null 2>&1
assert_eq 1 "$?" "konfigurasi hilang keluar 1"
BUILD_DIR=tidak-ada STAMP=20260115-1100 bash deploy.sh > /dev/null 2>&1
assert_eq 2 "$?" "artifact hilang keluar 2"
# --- uji 8: rollback tanpa release lebih tua -------------------------------
mkdir -p site-satu/releases/20260115-0900
printf '<h1>Situs v1</h1>\n' > site-satu/releases/20260115-0900/index.html
ln -sfn releases/20260115-0900 site-satu/current
cat > satu.conf <<'CONF'
SITE_DIR="site-satu"
HEALTH_URL="http://127.0.0.1:8765/current/"
HEALTH_RETRIES=1
HEALTH_DELAY=0
CONF
CONF=satu.conf bash rollback.sh > /dev/null 2>&1
assert_eq 2 "$?" "rollback tanpa release lebih tua keluar 2"
# --- ringkasan ----------------------------------------------------------------
if [ "$FAIL" -eq 0 ]; then
printf 'PASSED: %d/%d test lulus\n' "$PASS" "$PASS"
else
printf 'FAILED: %d/%d test lulus\n' "$PASS" "$((PASS + FAIL))"
fi
[ "$FAIL" -eq 0 ]