Server block dari Bagian 2 sudah melayani situs, tetapi dua pekerjaan besar masih tersisa. Pertama, keamanan: trafik masih berjalan polos di HTTP, dan beberapa lapisan pelindung layak ditambahkan sebelum situs menghadap publik. Kedua, operasional: selama mengganti versi situs masih berarti menyalin berkas satu per satu ke webroot, kita selalu satu salah ketik menjauh dari situs rusak di tengah pengunjung. Bagian ini menuntaskan keduanya — sertifikat TLS lewat certbot, server block HTTPS, lalu deploy.sh dan rollback.sh yang membuat pergantian versi menjadi satu perintah yang bisa dipercaya, lengkap dengan health check dan jalan pulang otomatis.
Header Keamanan dan Sidik Jari Versi #
Empat lapisan hardening sudah tertanam di server block Bagian 2; tabel berikut merangkumannya sebelum kita lanjut ke TLS:
| Lapisan | Directive | Yang dicegahnya |
|---|---|---|
| Sembunyikan versi | server_tokens off | penyerang memetakan celah spesifik versi nginx |
| Anti MIME-sniffing | X-Content-Type-Options: nosniff | berkas dieksekusi sebagai tipe lain |
| Anti framing | X-Frame-Options: SAMEORIGIN | halaman dibingkai situs lain (clickjacking) |
| Privasi asal | Referrer-Policy: strict-origin-when-cross-origin | URL lengkap kita bocor ke situs pihak ketiga |
Dua pasang keputusan caching ikut memperkuat: gzip mengecilkan ongkir dokumen teks, dan expires 7d di location /assets/ mengizinkan browser menyimpan aset selama tujuh hari. Amannya selama nama aset membawa sidik jari versi — gaya-a3f2.css — karena setiap rilis baru otomatis menjadi berkas baru yang belum pernah ada di cache siapa pun.
Sertifikat TLS dengan Certbot #
HTTPS membutuhkan sertifikat dari Certificate Authority, dan Let’s Encrypt memberikannya gratis lewat tool certbot. Di VPS, pasang toolnya lalu minta sertifikat dengan metode webroot — certbot menaruh berkas tantangan ke webroot kita dan membuktikan kendali atas domain:
sudo apt install certbot
sudo certbot certonly --webroot -w /var/www/site/current \
-d latihan.example.com --email [email protected] --agree-tosOpsi -w /var/www/site/current menunjuk webroot yang sama dengan root nginx, sehingga berkas tantangan terlayani sebagai berkas statis biasa. Berikut contoh tampilan hasilnya di VPS — jam dan tanggal masa berlaku akan mengikuti eksekusi Anda:
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for latihan.example.com
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/latihan.example.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/latihan.example.com/privkey.pem
This certificate expires on 2026-04-15.
These files will be updated when the certificate renews.Pembaruan otomatis ditangani timer systemd bawaan paket (certbot.timer) — cukup pastikan systemctl list-timers | grep certbot menunjukkannya aktif. Detail alur sertifikat, ACME, dan pembaruan dibahas tuntas di Chapter TLS dan SSL Certificates
.
Server Block HTTPS #
Dengan sertifikat di tempatnya, giliran konfigurasi naik kelas. Versi HTTPS memakai dua server block: port 80 yang hanya mengalihkan, dan port 443 yang melayani. Berkas lengkapnya ada sebagai config/ssl-latihan.example.com.conf pada paket proyek:
server {
listen 80;
listen [::]:80;
server_name latihan.example.com;
# Seluruh trafik HTTP digiring ke HTTPS.
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name latihan.example.com;
# Sertifikat dan kunci dari certbot.
ssl_certificate /etc/letsencrypt/live/latihan.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/latihan.example.com/privkey.pem;
# Hanya protokol yang masih dianggap aman.
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
root /var/www/site/current;
index index.html;
server_tokens off;
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location /assets/ {
expires 7d;
}
location /api/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
try_files $uri $uri/ =404;
}
}Bagian-bagian barunya:
return 301 https://$host$request_uri;: server block port 80 hanya punya satu tugas — mengalihkan permanen ke HTTPS dengan nama host dan path yang sama.returnberhenti di situ, jadi tidak ada konfigurasi lain yang perlu dijaga di blok ini.listen 443 ssl;danhttp2 on;: port HTTPS dengan TLS aktif, lalu HTTP/2 untuk klien yang mendukungnya.ssl_certificatedanssl_certificate_key: jalur persis ke output certbot —fullchain.pemberisi sertifikat beserta rantai perantara,privkey.pemkunci privatnya.ssl_protocols TLSv1.2 TLSv1.3;: menutup pintu protokol usang; TLS 1.2 dipertahankan demi klien lama.add_header Strict-Transport-Security "max-age=31536000" always;: HSTS — perintah agar browser menolak HTTP polos selama setahun. Mulailah dengan nilai kecil di situs baru bila belum yakin; keputusan ini sulit dibatalkan dari sisi browser.
Pemasangannya mengikuti pola yang sama dengan Bagian 2: salin ke sites-available, tautkan dari sites-enabled, lalu pasangan sudo nginx -t && sudo systemctl reload nginx.
Pola Deployment: Release dan Symlink #
Sekarang persoalan operasionalnya. Menimpa berkas situs langsung di webroot itu cepat tetapi berbahaya: saat rsync berjalan setengah jalan, pengunjung mendapat campuran berkas versi lama dan baru — dan bila ternyata rusak, versi lama sudah tidak ada lagi untuk kembali. Pola release-folder menghapus kedua masalah itu:
/var/www/site/
├── releases/
│ ├── 20260115-0900/ ← versi lama: utuh, tidak pernah diubah lagi
│ │ ├── index.html
│ │ └── assets/
│ └── 20260115-0930/ ← versi baru: salinan artifact yang sudah diverifikasi
│ ├── index.html
│ └── assets/
├── shared/
└── current -> releases/20260115-0930Setiap deployment menyalin artifact build ke folder releases/STAMP yang baru — versi lama tidak pernah disentuh. Nama STAMP berformat YYYYmmdd-HHMMSS membuat urutan nama sama dengan urutan waktu, fakta yang nanti dipakai rollback.sh. Versi yang dilayani ditentukan oleh satu symlink: current. Menggesernya adalah operasi ln -sfn yang selesai dalam satu tarikan napas, dan mengembalikannya sama mudahnya — inilah “rollback instan” yang dijanjikan arsitektur ini. nginx tidak pernah reload pun tidak: baginya root menunjuk ke current, dan symlink dibaca ulang pada setiap permintaan.
deploy.sh dan rollback.sh #
Dua skrip berikut menjahit pola itu menjadi satu perintah. Blok besar di bawah memuat keduanya lengkap (identik dengan deploy/deploy.sh dan deploy/rollback.sh pada paket proyek), menyiapkan konfigurasi lokal deploy.conf untuk server latihan di port 8765, lalu memainkan seluruh siklusnya sungguhan: deploy v1, deploy v2, verifikasi current bergeser, rollback manual, dan deploy yang gagal health check hingga rollback otomatisnya:
mkdir -p site/releases
cat > deploy.conf <<'CONF'
SITE_DIR="site"
HEALTH_URL="http://127.0.0.1:8765/current/"
HEALTH_RETRIES=3
HEALTH_DELAY=1
CONF
cat > deploy.sh <<'BASH'
#!/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
BASH
cat > rollback.sh <<'BASH'
#!/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
BASH
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
python3 -m http.server 8765 --bind 127.0.0.1 --directory site >/dev/null 2>&1 &
server=$!
for i in 1 2 3 4 5; do curl -fsS -o /dev/null http://127.0.0.1:8765/ 2>/dev/null && break; sleep 0.2; done
echo "== deploy v1 =="
BUILD_DIR=build-v1 STAMP=20260115-0900 bash deploy.sh
echo "current -> $(readlink site/current)"
curl -s http://127.0.0.1:8765/current/
echo "== deploy v2 =="
BUILD_DIR=build-v2 STAMP=20260115-0930 bash deploy.sh
echo "current -> $(readlink site/current)"
curl -s http://127.0.0.1:8765/current/
echo "== rollback manual =="
bash rollback.sh
echo "current -> $(readlink site/current)"
curl -s http://127.0.0.1:8765/current/
echo "== deploy v3 saat server mati: health check gagal =="
kill "$server"
wait "$server" 2>/dev/null
BUILD_DIR=build-v2 STAMP=20260115-1030 bash deploy.sh
echo "exit=$?"
echo "current -> $(readlink site/current)"
echo "== release yang tersisa =="
ls -1 site/releasesOutput yang Dihasilkan:
== deploy v1 ==
[20260115-0900] release tersalin: site/releases/20260115-0900
[20260115-0900] current -> releases/20260115-0900
[20260115-0900] health check OK: http://127.0.0.1:8765/current/
[20260115-0900] selesai tanpa error
current -> releases/20260115-0900
<h1>Situs v1</h1>
== deploy v2 ==
[20260115-0930] release tersalin: site/releases/20260115-0930
[20260115-0930] current -> releases/20260115-0930
[20260115-0930] health check OK: http://127.0.0.1:8765/current/
[20260115-0930] selesai tanpa error
current -> releases/20260115-0930
<h1>Situs v2</h1>
== rollback manual ==
[rollback] current -> releases/20260115-0900
[rollback] health check OK: http://127.0.0.1:8765/current/
[rollback] selesai tanpa error
current -> releases/20260115-0900
<h1>Situs v1</h1>
== deploy v3 saat server mati: health check gagal ==
[20260115-1030] release tersalin: site/releases/20260115-1030
[20260115-1030] current -> releases/20260115-1030
[20260115-1030] health check gagal; current dikembalikan ke releases/20260115-0900
[20260115-1030] GAGAL: health check gagal setelah 3 percobaan: http://127.0.0.1:8765/current/ (kode 4)
exit=4
current -> releases/20260115-0900
== release yang tersisa ==
20260115-0900
20260115-0930Bacaan outputnya dulu, barulah bedah skripnya. Empat babak == == itu masing-masing meninggalkan jejak: setelah deploy v1 dan v2, readlink site/current (paman ls -l site/current menampilkan hal yang sama, lengkap dengan waktu pembuatan symlink) melaporkan releases/20260115-0900 lalu releases/20260115-0930, dan curl menyajikan <h1>Situs v1</h1> lalu <h1>Situs v2</h1> — bukti bahwa yang dilayani server ikut bergeser. Rollback manual memutar semuanya balik. Babak terakhir sengaja membunuh server latihan lebih dulu: deploy v3 menyalin berkasnya, menggeser current, lalu health check gagal tiga kali — dan skrip mengembalikan current ke releases/20260115-0900 sendirian, keluar kode 4, dan menghapus release yang tidak sehat sehingga ls -1 site/releases hanya menampilkan dua release yang terbukti hidup.
Keputusan-keputusan penting di dalam deploy.sh:
- Tahap 1 — pemisahan parameter: nilai yang berubah tiap eksekusi (
BUILD_DIR,STAMP) datang lewat variabel lingkungan dengan nilai bawaan, sedangkan sifat site (SITE_DIR,HEALTH_URL,HEALTH_RETRIES,HEALTH_DELAY) tinggal dideploy.conf. Pola${VAR:-bawaan}inilah yang membuatBUILD_DIR=build-v2 STAMP=20260115-0930 bash deploy.shbisa men-deploy versi mana pun tanpa menyunting apa pun. - Tahap 1 juga — urutan gagal yang aman: seluruh validasi konfigurasi selesai sebelum satu berkas pun tersentuh, dan pesannya keluar lewat stderr.
- Tahap 3 —
PREV=$(readlink ...): target symlink lama disimpan sebelum apa pun digeser. Tanpa potongan ini, saat health check gagal kita tidak tahu harus kembali ke mana. - Tahap 4 —
cp -alaludiff -r: artifact disalin dengan atributnya, lalu salinannya dibandingkan utuh dengan sumbernya. Release yang gagal diverifikasi langsung dihapus — tidak ada setengah-release direleases/. - Tahap 5 —
ln -sfn "releases/$STAMP": penggeseran satu atom. Opsi-npenting saatcurrentsudah ada: tanpanyaln -smembuat tautan di dalam folder tujuan, bukan menggantikannya. - Tahap 6 —
curl -fsSdengan retry: opsi-fmembuat kode HTTP 4xx/5xx dianggap gagal, bukan hanya error jaringan; jedaHEALTH_DELAYmemberi server napas untuk bangun. Health check menanyaicurrentmelalui HTTP sungguhan — persis pertanyaan yang akan diajukan nginx di VPS. - Tahap 7 — hapus release yang tidak sehat: setelah
currentdikembalikan kePREV, release baru dihapus. Keputusan ini menjaga janji penting: folder direleases/hanya berisi versi yang pernah terbukti menjawab 200, sehingga rollback tidak pernah mendarat di versi rusak.
rollback.sh sendiri memegang satu trik: ls -1 "$RELEASES" | awk -v kini="$KINI" '$0 < kini { lama = $0 } END { print lama }' memilih release tepat di bawah current — setiap nama yang lebih kecil dari KINI menimpa lama, dan karena daftar ls terurut, nilai akhirnya adalah nama terbesar yang masih lebih kecil, alias satu tingkat di bawahnya. Health check tetap dijalankan sesudah menggeser; bila release lama ternyata juga bermasalah, current dikembalikan ke KINI dan skrip keluar kode 3 — kondisi terburuk yang dilaporkannya jujur, bukan didiamkan.
Test Suite #
Seluruh klaim di atas kini dibungkus menjadi test suite bash — skrip yang menyiapkan folder kerja terisolasinya sendiri, menjalankan server latihan, lalu memeriksa assertion demi assertion. Blok berikut merakit tata letak proyek lengkap (config/, deploy/, tests/) dengan berkas-berkas yang sama persis dengan paket proyek, lalu menjalankan suitenya:
mkdir -p proyek/config proyek/deploy proyek/tests proyek/docs
cat > proyek/config/latihan.example.com.conf <<'NGINX'
# latihan.example.com.conf — server block nginx untuk situs latihan.
# Situs statis (root + try_files) + reverse proxy /api/ ke backend
# lokal + kompresi gzip + header keamanan + server_tokens off.
#
# Penempatan di VPS:
# sudo cp latihan.example.com.conf /etc/nginx/sites-available/
# sudo ln -s /etc/nginx/sites-available/latihan.example.com.conf \
# /etc/nginx/sites-enabled/
# sudo nginx -t && sudo systemctl reload nginx
server {
listen 80;
listen [::]:80;
server_name latihan.example.com;
# Akar dokumen menunjuk ke symlink "current" yang digeser oleh
# deploy.sh — pergantian release tidak menyentuh konfigurasi ini.
root /var/www/site/current;
index index.html;
# Jangan umumkan versi nginx di header balasan dan halaman error.
server_tokens off;
# Kompresi untuk tipe dokumen yang paling sering diminta.
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
# Header keamanan dasar untuk semua balasan.
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Aset membawa sidik jari versi pada namanya: aman di-cache lama.
location /assets/ {
expires 7d;
}
# /api/ diteruskan ke backend di loopback. Garis miring di ujung
# proxy_pass membuat /api/halo tiba di backend sebagai /halo.
location /api/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Permintaan lain: coba berkas, lalu folder, lalu 404.
location / {
try_files $uri $uri/ =404;
}
}
NGINX
cat > proyek/config/ssl-latihan.example.com.conf <<'NGINX'
# ssl-latihan.example.com.conf — varian HTTPS untuk server block.
# Sertifikat dari certbot (Let's Encrypt); port 80 hanya mengalihkan.
# Gantikan latihan.example.com.conf yang aktif dengan berkas ini
# setelah sertifikat terpasang.
server {
listen 80;
listen [::]:80;
server_name latihan.example.com;
# Seluruh trafik HTTP digiring ke HTTPS.
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name latihan.example.com;
# Sertifikat dan kunci dari certbot.
ssl_certificate /etc/letsencrypt/live/latihan.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/latihan.example.com/privkey.pem;
# Hanya protokol yang masih dianggap aman.
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
root /var/www/site/current;
index index.html;
server_tokens off;
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
# Header keamanan: HSTS khusus situs HTTPS, sisanya sama dengan
# versi HTTP.
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location /assets/ {
expires 7d;
}
location /api/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
try_files $uri $uri/ =404;
}
}
NGINX
cat > proyek/config/deploy.conf <<'CONF'
# deploy.conf — konfigurasi skrip deployment.
# Sunting nilai di bawah ini; deploy.sh dan rollback.sh tidak perlu
# disentuh. Jalur relatif dihitung dari direktori kerja saat skrip
# dijalankan; jalur absolut (mis. /var/www/site) juga boleh dipakai.
# Folder site: berisi releases/ dan symlink current.
SITE_DIR="/var/www/site"
# URL health check: harus menjawab 200 setelah release aktif.
# Di VPS nyata (nginx di port 80): http://localhost/
# Saat latihan lokal (server latihan): http://127.0.0.1:8765/current/
HEALTH_URL="http://localhost/"
# Berapa kali health check dicoba sebelum dinyatakan gagal.
HEALTH_RETRIES=3
# Jeda antar percobaan health check, dalam detik.
HEALTH_DELAY=1
# Catatan: folder artifact dibawa lewat variabel lingkungan BUILD_DIR
# (default: build) karena berbeda pada tiap eksekusi, bukan sifat site.
CONF
cat > proyek/deploy/deploy.sh <<'BASH'
#!/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
BASH
cat > proyek/deploy/rollback.sh <<'BASH'
#!/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
BASH
cat > proyek/tests/run-tests.sh <<'TESTS'
#!/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 ]
TESTS
bash proyek/tests/run-tests.shOutput yang Dihasilkan:
OK server block memuat: server {
OK server block memuat: listen 80
OK server block memuat: server_name latihan.example.com
OK server block memuat: root /var/www/site/current
OK server block memuat: index index.html
OK server block memuat: try_files $uri $uri/ =404
OK server block memuat: gzip on
OK server block memuat: location /api/
OK server block memuat: proxy_pass http://127.0.0.1:8080/
OK server block memuat: server_tokens off
OK server block memuat: X-Content-Type-Options
OK server block memuat: X-Frame-Options
OK server block memuat: Referrer-Policy
OK server block TLS memuat: listen 443 ssl
OK server block TLS memuat: ssl_certificate
OK server block TLS memuat: return 301 https://$host$request_uri
OK server block TLS memuat: Strict-Transport-Security
OK deploy.conf memuat SITE_DIR
OK deploy.conf memuat HEALTH_URL
OK deploy.conf memuat HEALTH_RETRIES
OK server latihan siap di port 8765
OK deploy v1 keluar 0
OK release v1 dibuat
OK current menunjuk release v1
OK konten tersaji: v1
OK deploy v2 keluar 0
OK current bergeser ke v2
OK konten tersaji: v2
OK release v1 tidak terhapus
OK rollback keluar 0
OK current kembali ke v1
OK konten kembali v1
OK deploy ketiga keluar 0
OK health check gagal keluar 4
OK current kembali ke release sehat
OK release gagal dihapus
OK konfigurasi hilang keluar 1
OK artifact hilang keluar 2
OK rollback tanpa release lebih tua keluar 2
PASSED: 39/39 test lulusPola-pola penting di dalam suitenya:
trap bersihkan EXIT: server latihan dimatikan dan folder kerja dihapus apa pun akhir eksekusinya — lulus maupun gagal di tengah jalan. DenganWORKdanSERVERdiinisialisasi kosong sejak awal, trap aman dijalankan kapan pun.mktemp -dplus salin dua skrip: tiap eksekusi suite mendapat dunia sendiri;deploy.shdanrollback.shdisalin ke sana sehingga suite tidak pernah mengotori paket aslinya. Berhasil dijalankan dua kali berturut-turut dengan hasil sama adalah syaratnya.- Server dimulai sebelum deploy pertama: meniru VPS, tempat nginx sudah hidup sebelum
deploy.shpertama tiba.HEALTH_DELAY=0membuat suite cepat tanpa mengubah perilaku yang diuji. - Assertion isi, bukan cuma status:
assert_eq '<h1>Situs v2</h1>' "$(curl -s ...)"membuktikan bahwa setelah deploy kedua, isi yang dilayani memang berubah — bukan sekadar exit code 0 dan symlink yang berpindah. - Uji kegagalan yang disengaja: server dibunuh sebelum deploy keempat, sehingga health check pasti gagal dan rollback otomatis bisa diuji ulang setiap saat tanpa menunggu kecelakaan sungguhan. Begitu pula
site-satudengan satu-satunya release, yang membuatrollback.shkeluar kode 2 secara deterministik.
Dari Latihan ke VPS #
Di VPS, siklus yang sama berjalan dengan tiga penyesuaian: deploy.conf memakai SITE_DIR="/var/www/site" dan HEALTH_URL="http://localhost/" (nginx sudah berjalan di port 80), artifact diunggah dengan rsync -a build/ deployer@SERVER:/tmp/build-baru/, lalu BUILD_DIR=/tmp/build-baru bash deploy.sh dijalankan dari dalam server. Panduan ringkas lengkapnya — termasuk tabel kode keluar kedua skrip — tersedia di docs/README.txt pada paket proyek.
Server web kita selesai: fondasi yang terkunci sejak Bagian 1, konfigurasi yang dibedah dan diperiksa strukturnya di Bagian 2, dan di bagian ini keamanan yang dinaikkan tingkat sertifikat serta deployment yang menjadi satu perintah dengan jaminan jalan pulang. Pola yang sama — folder ber-versi, symlink sebagai penunjuk aktif, health check sebagai penjaga gerbang — akan muncul kembali dalam wujud lain pada proyek berikutnya, Server File Lokal , tempat yang dipindahkan bukan lagi versi situs melainkan berkas-berkas yang dibagikan.
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 ]