Fondasi dari Bagian 1 belum tahu cara melayani siapa pun: nginx di VPS masih menyajikan halaman selamat datang bawaan, dan webroot kita masih kosong. Bagian ini mengisi keduanya. Kita menulis satu server block lengkap untuk latihan.example.com — melayani berkas statis dari webroot, meneruskan /api/ ke backend di loopback, mengaktifkan gzip, dan memasang header keamanan — lalu membedahnya directive demi directive. Karena nginx tidak terpasang di lingkungan latihan, validasi nginx -t milik VPS kita ganti sementara dengan skrip pemeriksa struktur yang bisa berjalan di mana saja, dan arah tujuan proxy_pass kita buktikan dengan backend sungguhan.
Menulis Server Block #
Satu server block berisi seluruh keputusan tentang satu situs: di port mana mendengarkan, nama apa yang dijawab, dari mana berkas dilayani, dan ke mana permintaan tertentu diteruskan. Berkas berikut kita tulis persis seperti yang akan ditempatkan di /etc/nginx/sites-available/ pada VPS — nama berkasnya mengikuti nama domain, komentar ikut serta karena konfigurasi juga dokumen:
mkdir -p nginx/sites-available
cat > nginx/sites-available/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 nginx/sites-available/latihan.example.com.confOutput yang Dihasilkan:
# 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;
}
}Mari kita bedah directive demi directive — baris-baris komentarnya di atas sudah merangkum, di sini kita uraikan alasannya:
listen 80;danlisten [::]:80;: server block mendengarkan port 80 untuk IPv4 dan IPv6 dari semua interface. Nanti di Bagian 3, versi HTTPS menambahkanlisten 443 ssl.server_name latihan.example.com;: nama yang dicocokkan dengan headerHost— mekanisme name-based virtual host yang sudah kita saksikan di Chapter Web Server . Nama domainexample.comdipakai karena memang dicadangkan untuk dokumentasi.root /var/www/site/current;: inilah baris yang mengikat konfigurasi ke pola deployment Bagian 1. nginx membaca isi situs dari tempatcurrentmenunjuk — bukan dari folder release tertentu — sehingga pergantian versi cukup menggeser symlink, tanpa menyentuh berkas konfigurasi ini sama sekali.index index.html;: permintaan ke sebuah folder otomatis mencobaindex.htmldi dalamnya.server_tokens off;: menyembunyikan nomor versi nginx dari headerServerdan halaman error. Versi yang bocor memudahkan penyerang mencari celah yang spesifik untuk versi itu.gzip on; gzip_types ...; gzip_min_length 1024;: balasan teks di atas 1 KB dikompresi. CSS, JavaScript, JSON, dan SVG adalah tipe yang paling diuntungkan; gambar JPEG/PNG sudah terkompresi sendiri sehingga sengaja tidak didaftar.add_header X-Content-Type-Options "nosniff" always;: melarang browser menebak tipe isi — berkas yang diklaimtext/plaintidak akan diam-diam dieksekusi sebagai skrip.add_header X-Frame-Options "SAMEORIGIN" always;danadd_header Referrer-Policy "strict-origin-when-cross-origin" always;: yang pertama mencegah situs lain membingkai halaman kita (bahan clickjacking), yang kedua membatasi informasi asal pengunjung yang ikut terkirim saat mengeklik tautan keluar. Kataalwaysmembuat header tetap terkirim pada balasan error, bukan hanya 200.location /assets/ { expires 7d; }: aset diberi headerExpirestujuh hari. Ini pas untuk berkas yang namanya membawa sidik jari versi (gaya-a3f2.css) karena nama baru berarti berkas baru; daftar lengkap opsinya ada di dokumentasi ngx_http_headers_module .location /api/ { proxy_pass http://127.0.0.1:8080/; ... }: seluruh sub-pohon/api/diteruskan ke backend yang hanya mendengarkan loopback. Garis miring di ujungproxy_passmengganti bagian/api/dengan/— permintaan/api/halotiba di backend sebagai/halo; tanpa garis miring, path dikirim utuh. Empatproxy_set_headermeneruskan identitas asli: nama domain, IP klien, rantai alamat, dan skema asli — rincian masing-masing ada di dokumentasi ngx_http_proxy_module .location / { try_files $uri $uri/ =404; }: penutup untuk semua permintaan lain — coba berkas dengan nama path itu, lalu folder, lalu jawab 404. Perilaku ketiga variabel ini diuraikan di dokumentasi ngx_http_core_module .
Backend Uji untuk /api/ #
proxy_pass butuh sesuatu di ujungnya. Kita pakai backend kecil berbasis Python: cukup http.server dari pustaka standar yang diubah agar menjawab JSON pada setiap GET. Ia mewakili aplikasi API sungguhan yang kelak berjalan sebagai service di VPS:
cat > backend-uji.py <<'PY'
#!/usr/bin/env python3
# backend-uji.py — backend API sederhana untuk uji reverse proxy.
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
isi = json.dumps({"layanan": "backend-uji", "path": self.path})
body = isi.encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass # akses tidak dicetak supaya output latihan bersih
HTTPServer(("127.0.0.1", 8080), Handler).serve_forever()
PY
mkdir -p situs
printf '<h1>Situs Latihan</h1>\n' > situs/index.html
python3 backend-uji.py & backend=$!
python3 -m http.server 8765 --bind 127.0.0.1 --directory situs >/dev/null 2>&1 & statik=$!
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
for i in 1 2 3 4 5; do curl -fsS -o /dev/null http://127.0.0.1:8080/halo 2>/dev/null && break; sleep 0.2; done
echo "== situs statis (port 8765) =="
curl -s http://127.0.0.1:8765/
echo "== backend: /halo (path yang nginx teruskan dari /api/halo) =="
curl -s http://127.0.0.1:8080/halo
echo
echo "== backend: /api/halo (path utuh, bila proxy_pass tanpa garis miring) =="
curl -s http://127.0.0.1:8080/api/halo
echo
kill "$backend" "$statik"Output yang Dihasilkan:
== situs statis (port 8765) ==
<h1>Situs Latihan</h1>
== backend: /halo (path yang nginx teruskan dari /api/halo) ==
{"layanan": "backend-uji", "path": "/halo"}
== backend: /api/halo (path utuh, bila proxy_pass tanpa garis miring) ==
{"layanan": "backend-uji", "path": "/api/halo"}Bedah bloknya:
json.dumps({"layanan": "backend-uji", "path": self.path}): balasan backend memuat path yang diterimanya sendiri — itulah yang membuat perbedaan rewriteproxy_passbisa kita lihat mata, bukan sekadar dipercaya.python3 -m http.server 8765 --bind 127.0.0.1 --directory situs: server statis bawaan Python berperan sebagai pengganti nginx di latihan ini — sama-sama melayani folder dari disk, hanya tanpa server block. Opsi--directory situsmembuat foldersitussebagai akar dokumennya.- Dua loop
for i in 1 2 3 4 5: server butuh sekejap untuk mulai mendengarkan, jadi setiap server dites kesiapannya dengancurlyang diulang sampai berhasil. Pola yang sama nanti hidup di health checkdeploy.sh. - Dua
curlterakhir ke port 8080: inilah bagian pentingnya.curl http://127.0.0.1:8080/halomemperlihatkan apa yang diterima backend ketika nginx meneruskan/api/halodengan garis miring di ujungproxy_pass, dancurl http://127.0.0.1:8080/api/halomemperlihatkan versi tanpa rewrite. Dua baris JSON itu adalah bukti kedua ujung yang kelak disambungkan.
Validasi Struktur Konfigurasi #
Di VPS, sudo nginx -t memeriksa seluruh konfigurasi dan sudo systemctl reload nginx menerapkannya tanpa memutus koneksi — keduanya sudah dibahas tuntas di Chapter nginx
. Tetapi ada pemeriksaan yang bisa — dan berguna — dijalankan sebelum berkas itu sampai ke server: apakah semua directive yang kita janjikan benar-benar ada? Skrip kecil berikut menjawabnya dengan grep, lalu menguji dirinya pada konfigurasi yang lengkap dan yang sengaja dibikin bolong:
cat > 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 > periksa-conf.sh <<'BASH'
#!/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"
BASH
bash periksa-conf.sh latihan.example.com.conf; echo "exit=$?"
printf 'server {\n listen 80;\n}\n' > rusak.conf
bash periksa-conf.sh rusak.conf; echo "exit=$?"Output yang Dihasilkan:
OK server {
OK listen 80
OK server_name latihan.example.com
OK root /var/www/site/current
OK index index.html
OK try_files $uri $uri/ =404
OK gzip on
OK location /api/
OK proxy_pass http://127.0.0.1:8080/
OK server_tokens off
OK X-Content-Type-Options
OK X-Frame-Options
OK Referrer-Policy
exit=0
OK server {
OK listen 80
HILANG server_name latihan.example.com
HILANG root /var/www/site/current
HILANG index index.html
HILANG try_files $uri $uri/ =404
HILANG gzip on
HILANG location /api/
HILANG proxy_pass http://127.0.0.1:8080/
HILANG server_tokens off
HILANG X-Content-Type-Options
HILANG X-Frame-Options
HILANG Referrer-Policy
exit=11Tiga keputusan di dalamnya:
grep -qF -- "$pola": opsi-Fmemperlakukan pola sebagai teks literal, bukan regex — karakter seperti$uridan{dicari apa adanya. Daftar pola menjadi data murni: menambah directive wajib berarti menambah satu baris di daftar.exit "$gagal": jumlah directive yang hilang menjadi exit code, sehingga pemeriksaan ini bisa ditanam di pipeline atau test suite — di Bagian 3 nanti pola yang sama hidup di dalamrun-tests.sh.- Berkas
rusak.conf: dua baris yang terlihat cukup ini gagal sebelas pemeriksaan.nginx -tmungkin masih meloloskannya (sintaksnya sah), tetapi situsnya tidak akan bekerja seperti yang kita rancang — root yang menunjuk ke mana-mana, proxy yang tidak ada, header keamanan yang hilang. Pemeriksaan struktur menjaga kontrak desain, bukan hanya sintaks.
Server block kini lengkap: ditulis, dibedah, dan lolos pemeriksa struktur. Yang belum kita miliki adalah kebiasaan operasionalnya — bagaimana versi situs diganti dengan aman di tengah trafik, dan bagaimana server mengabarkan bila pergantian itu gagal. Itu pekerjaan Bagian 3, tempat keamanan dinaikkan satu tingkat dan deployment menjadi satu perintah.
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 ]