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.

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:

bash
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.conf

Output yang Dihasilkan:

text
# 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:

  1. listen 80; dan listen [::]:80;: server block mendengarkan port 80 untuk IPv4 dan IPv6 dari semua interface. Nanti di Bagian 3, versi HTTPS menambahkan listen 443 ssl.
  2. server_name latihan.example.com;: nama yang dicocokkan dengan header Host — mekanisme name-based virtual host yang sudah kita saksikan di Chapter Web Server . Nama domain example.com dipakai karena memang dicadangkan untuk dokumentasi.
  3. root /var/www/site/current;: inilah baris yang mengikat konfigurasi ke pola deployment Bagian 1. nginx membaca isi situs dari tempat current menunjuk — bukan dari folder release tertentu — sehingga pergantian versi cukup menggeser symlink, tanpa menyentuh berkas konfigurasi ini sama sekali.
  4. index index.html;: permintaan ke sebuah folder otomatis mencoba index.html di dalamnya.
  5. server_tokens off;: menyembunyikan nomor versi nginx dari header Server dan halaman error. Versi yang bocor memudahkan penyerang mencari celah yang spesifik untuk versi itu.
  6. 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.
  7. add_header X-Content-Type-Options "nosniff" always;: melarang browser menebak tipe isi — berkas yang diklaim text/plain tidak akan diam-diam dieksekusi sebagai skrip.
  8. add_header X-Frame-Options "SAMEORIGIN" always; dan add_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. Kata always membuat header tetap terkirim pada balasan error, bukan hanya 200.
  9. location /assets/ { expires 7d; }: aset diberi header Expires tujuh 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 .
  10. location /api/ { proxy_pass http://127.0.0.1:8080/; ... }: seluruh sub-pohon /api/ diteruskan ke backend yang hanya mendengarkan loopback. Garis miring di ujung proxy_pass mengganti bagian /api/ dengan / — permintaan /api/halo tiba di backend sebagai /halo; tanpa garis miring, path dikirim utuh. Empat proxy_set_header meneruskan identitas asli: nama domain, IP klien, rantai alamat, dan skema asli — rincian masing-masing ada di dokumentasi ngx_http_proxy_module .
  11. 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:

bash
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:

text
== 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:

  1. json.dumps({"layanan": "backend-uji", "path": self.path}): balasan backend memuat path yang diterimanya sendiri — itulah yang membuat perbedaan rewrite proxy_pass bisa kita lihat mata, bukan sekadar dipercaya.
  2. 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 situs membuat folder situs sebagai akar dokumennya.
  3. Dua loop for i in 1 2 3 4 5: server butuh sekejap untuk mulai mendengarkan, jadi setiap server dites kesiapannya dengan curl yang diulang sampai berhasil. Pola yang sama nanti hidup di health check deploy.sh.
  4. Dua curl terakhir ke port 8080: inilah bagian pentingnya. curl http://127.0.0.1:8080/halo memperlihatkan apa yang diterima backend ketika nginx meneruskan /api/halo dengan garis miring di ujung proxy_pass, dan curl http://127.0.0.1:8080/api/halo memperlihatkan 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:

bash
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:

text
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=11

Tiga keputusan di dalamnya:

  1. grep -qF -- "$pola": opsi -F memperlakukan pola sebagai teks literal, bukan regex — karakter seperti $uri dan { dicari apa adanya. Daftar pola menjadi data murni: menambah directive wajib berarti menambah satu baris di daftar.
  2. 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 dalam run-tests.sh.
  3. Berkas rusak.conf: dua baris yang terlihat cukup ini gagal sebelas pemeriksaan. nginx -t mungkin 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
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

deploy/periksa-conf.sh

Unduh berkas
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"

deploy/rollback.sh

Unduh berkas
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

tests/run-tests.sh

Unduh berkas
bash
#!/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 ]