Active Health Check #

Memastikan penyeimbang beban (load balancer) hanya mengarahkan lalu lintas ke server yang benar-benar siap memproses data adalah prasyarat mutlak untuk menjaga stabilitas sistem. Caddy menyediakan fitur Active Health Check (pemantauan kesehatan aktif) yang bekerja secara proaktif menguji keandalan server backend sebelum pengguna asli mengirimkan permintaan mereka. Kita akan menguraikan arsitektur mesin pemantau internal Caddy, seluruh parameter konfigurasi (seperti validasi status HTTP dan substring body), metode pengamanan endpoint kesehatan dari eksploitasi luar, serta strategi perancangan detektor kesehatan multi-resource pada level aplikasi.


Cara Kerja Active Health Check #

Mekanisme Active Health Check bekerja dengan prinsip proaktif dan periodik. Caddy secara berkala mengirimkan permintaan HTTP buatan (probe) ke jalur khusus yang kita tentukan pada setiap server backend (upstream).

Proses pemantauan ini berjalan secara independen di latar belakang:

  1. Background Worker Goroutines: Caddy membuat goroutine khusus di latar belakang yang bertindak sebagai pemantau kesehatan (health poller). Goroutine ini berjalan secara asinkronus tanpa mengganggu jalannya pemrosesan lalu lintas pengguna riil.
  2. Kirim HTTP Probe: Setiap kali durasi interval terlewati (misalnya setiap 10 detik), poller mengirimkan request HTTP (biasanya berupa metode GET atau HEAD) ke backend.
  3. Validasi Respons: Poller menerima respons dari backend dan mengujinya berdasarkan kriteria kelayakan kesehatan yang kita tetapkan (kode status HTTP dan isi body response).
  4. Atomic Flag Update: Jika backend lulus kriteria, poller menandai status backend tersebut sebagai sehat (Healthy = true). Jika gagal (atau mengalami timeout koneksi), poller langsung menandainya sebagai sakit (Healthy = false). Pembaruan status ini dilakukan secara atomik di memori agar terbaca secara instan oleh goroutine penyeimbang beban utama.

Konsolidasi Upstream di Memori (De-duplication) #

Dalam arsitektur besar di mana kita mendefinisikan lusinan nama domain berbeda (virtual hosts) yang menunjuk ke klaster backend yang sama, Caddy secara cerdas mengkonsolidasikan registry penunjuk upstream di memorinya. Jika domain api.example.com dan app.example.com sama-sama memiliki backend 10.0.1.15:3000, Caddy tidak akan membuat dua goroutine poller terpisah untuk backend tersebut. Caddy membuang duplikasi (de-duplication) pendaftaran dan hanya menjalankan satu daemon poller tunggal untuk menguji kesehatan server tersebut. Hal ini menghemat penggunaan memori RAM server proxy dan mencegah pembanjiran log request di server backend kita.

Berikut adalah arsitektur internal mesin active health check Caddy:

flowchart TD
    subgraph Caddy_Proxy_Process["Proses Utama Caddy"]
        LB["Goroutine Load Balancer"]
        Poller["Goroutine Active Poller (Background)"]
    end

    subgraph Backend_Servers["Klaster Backend"]
        B1["Server 1 (Status: Healthy)"]
        B2["Server 2 (Status: Unhealthy)"]
    end

    Poller -->|"1. Kirim Probe HTTP Periodik"| B1
    Poller -->|"2. Kirim Probe HTTP Periodik"| B2
    
    B1 -->|"3. Kembalikan 200 OK"| Poller
    B2 -->|"4. Koneksi Timeout / 503"| Poller
    
    Poller -->|"5. Update atomic flag Healthy=true"| B1
    Poller -->|"6. Update atomic flag Healthy=false"| B2
    
    LB -->|"7. Hanya rute request ke Server 1"| B1

    style Poller stroke:#0288d1,stroke-width:2px
    style B1 stroke:#43a047,stroke-width:2px
    style B2 stroke:#e53935,stroke-dasharray: 5,5

Dengan arsitektur proaktif ini, jika Server 2 mengalami crash pada pukul 10.00.00, Caddy sudah mendeteksinya pada pukul 10.00.10. Ketika pengguna mengirimkan request pada pukul 10.00.11, Caddy langsung melewatkan Server 2 tanpa mencoba menghubunginya terlebih dahulu, menghindarkan pengguna dari pengalaman buruk mendapatkan error koneksi lambat.


Konfigurasi Parameter Active Health Check #

Caddyfile menyediakan sekumpulan parameter konfigurasi yang sangat fleksibel untuk menyesuaikan perilaku pemantauan aktif di dalam blok reverse_proxy:

# Konfigurasi lengkap pemantauan aktif
example.com {
    reverse_proxy backend-1:3000 backend-2:3000 {
        # ═══ Parameter Active Health Check ═══
        # Jalur URL kesehatan di backend
        health_uri /healthz
        
        # Jeda waktu antar pemeriksaan (default: 30 detik)
        health_interval 10s
        
        # Batas waktu tunggu respons probe
        health_timeout 3s
        
        # Kode status HTTP yang dianggap sehat
        health_status 200
        
        # Kirim header kustom saat melakukan probe
        health_headers {
            X-Health-Checker "Caddy-Gateway"
            Authorization "Bearer token-rahasia-pemantau-lokal"
        }
        
        # Tentukan port khusus untuk probe kesehatan (opsional)
        # health_port 8081
    }
}
  • health_uri: Endpoint backend tujuan pemeriksaan (misalnya /healthz, /status, atau /ping).
  • health_interval: Mengontrol seberapa sering poller mengirim probe. Menyetel interval terlalu cepat (seperti 1s) akan membebani log dan resource backend. Menyetel terlalu lambat (seperti 1m) memperlambat deteksi kegagalan. Nilai ideal adalah 5s hingga 15s.
  • health_timeout: Batas waktu toleransi respons socket. Jika backend sedang memproses beban ekstrim sehingga jabat tangan TCP/HTTP terhambat melewati 3 detik, Caddy menganggap backend tersebut sakit agar tidak memperlambat lalu lintas pengguna.
  • health_headers: Sangat berguna jika backend kita berada di belakang firewall aplikasi yang membutuhkan header otentikasi khusus, atau untuk mencatat identitas logger di backend.

Validasi Kesehatan Tingkat Lanjut (Response Body Matching) #

Menilai kesehatan server backend hanya berdasarkan kode status HTTP 200 OK rawan memicu kecolongan sistem. Sering kali, aplikasi web yang mengalami kerusakan koneksi database internal atau kehabisan memori RAM tetap memproses request HTTP secara normal dan mengembalikan kode status 200 OK, namun dengan payload data body berupa pesan kesalahan JSON:

// ANTI-PATTERN: Status HTTP 200 OK, tetapi aplikasi internal rusak!
{
  "status": "error",
  "error": "connection to PostgreSQL pool timed out"
}

Jika Caddy hanya memvalidasi status 200, Caddy mengira backend tersebut sehat dan terus mengirimkan request pengguna ke sana, menyajikan halaman error JSON ke pengunjung situs.

Untuk mengatasinya, kita dapat menggunakan subdirektif health_body untuk memvalidasi isi payload data yang dikembalikan oleh backend:

# Validasi substring respons body
example.com {
    reverse_proxy backend-1:3000 backend-2:3000 {
        health_uri /healthz
        health_status 200
        
        # Caddy hanya menyatakan backend sehat jika respons body mengandung string di bawah
        health_body "\"status\":\"UP\""
    }
}

Poller Caddy akan membaca stream byte data response body dan mencari kecocokan substring tersebut. Jika status HTTP bernilai 200 namun body yang dikembalikan berisi pesan error, backend langsung dinyatakan sakit dan dinonaktifkan secara otomatis.


Keamanan Endpoint Kesehatan (Health Endpoint Security) #

Membiarkan endpoint kesehatan seperti /healthz terbuka secara bebas untuk publik di internet mengandung risiko keamanan yang signifikan:

  1. Kebocoran Informasi Sensitif: Detail status database, versi framework, memori server, dan dependensi internal klaster dapat dibaca bebas oleh penyerang.
  2. Serangan Denial of Service (DDoS): Endpoint /healthz yang menjalankan kueri database berat dapat dimanfaatkan penyerang untuk membanjiri server dengan request berturut-turut guna membuat database kita macet.

Berikut adalah tiga strategi terbaik untuk mengamankan endpoint kesehatan:

1. Gunakan Port Terpisah (health_port) #

Konfigurasikan server backend kita untuk menyajikan rute lalu lintas aplikasi normal di port utama (misal :3000), dan menyajikan rute /healthz di port administratif khusus (misal :8081).

Kemudian, tutup port :8081 dari akses luar menggunakan aturan firewall sistem operasi (Security Group cloud atau UFW Linux), dan instruksikan Caddy untuk melakukan probe ke port tersebut:

# Mengarahkan probe ke port privat khusus
example.com {
    reverse_proxy backend-1:3000 backend-2:3000 {
        health_uri /healthz
        
        # Kirim probe kesehatan ke port 8081 privat backend
        health_port 8081
    }
}

2. Validasi Token Otentikasi (health_headers) #

Instruksikan Caddy untuk menyertakan token otentikasi unik di dalam header HTTP probe, dan konfigurasikan kode backend kita untuk menolak akses jika token tersebut tidak cocok:

# Mengirim token pemantau
example.com {
    reverse_proxy backend-1:3000 backend-2:3000 {
        health_uri /healthz
        health_headers {
            Authorization "Bearer token-pemantau-lokal-123"
        }
    }
}

3. Pengamanan Menggunakan Mutual TLS (mTLS) #

Jika backend TLS kita mewajibkan validasi sertifikat klien, Caddy secara otomatis menggunakan konfigurasi transport HTTP yang terdefinisi pada reverse proxy yang bersangkutan untuk melakukan probe kesehatan. Artinya, sertifikat mTLS yang kita daftarkan di dalam blok transport http akan dikirimkan secara otomatis oleh background poller Caddy saat melakukan pengujian /healthz ke backend HTTPS kita:

# Otentikasi mTLS terintegrasi untuk probe kesehatan
example.com {
    reverse_proxy https://secure-backend:8443 {
        health_uri /healthz
        
        transport http {
            tls
            tls_client_auth /etc/certs/caddy.crt /etc/certs/caddy.key
            tls_trusted_ca_certs /etc/certs/root-ca.crt
        }
    }
}

Bahaya Cascading Failure Akibat Salah Konfigurasi #

Cascading Failure (kegagalan beruntun) adalah momok menakutkan bagi arsitek infrastruktur. Kesalahan pengaturan waktu pemantauan aktif dapat secara tidak sengaja mempercepat matinya seluruh server dalam klaster kita saat terjadi lonjakan lalu lintas (traffic spike).

Skenario Insiden Runtuhnya Klaster (Cascading Outage) #

Bayangkan kita memiliki 3 backend server, dengan pengaturan pemantauan aktif yang sangat ketat: health_interval 2s dan health_timeout 1s.

1. Server 1 mengalami gangguan perangkat keras dan mati.
2. Beban lalu lintas yang dipikul Server 1 dialihkan seluruhnya ke Server 2 dan Server 3.
3. Beban Server 2 dan Server 3 melonjak drastis, menyebabkan utilisasi CPU mereka menyentuh 100%.
4. Akibat beban kerja CPU 100%, respons waktu Server 2 dan Server 3 melambat dari 50ms menjadi 1.2 detik.
5. Pada saat bersamaan, poller Caddy mengirim probe kesehatan. Karena respons waktu backend (1.2 detik) melebihi batas "health_timeout 1s", poller menganggap Server 2 dan Server 3 mati.
6. Caddy secara instan menutup seluruh aliran lalu lintas ke Server 2 dan Server 3.
7. Seluruh klaster padam total (503 Service Unavailable) bagi semua pengguna, meskipun Server 2 dan Server 3 sebenarnya masih hidup dan berupaya memproses request secara maksimal.

Rekomendasi Desain Aman #

Untuk mencegah bencana ini:

  • Jangan menyetel health_timeout terlalu ketat. Nilai timeout harus mencakup latensi terburuk backend di bawah beban tinggi (misal 3s atau 5s).
  • Setel health_interval secara rasional (misal 10s atau 15s) untuk mengurangi beban komputasi tambahan pada backend yang sedang kewalahan.

Strategi Pemantauan Kesehatan Multi-Resource (Backend Code) #

Sebuah endpoint kesehatan yang andal di tingkat aplikasi harus menguji kesiapan seluruh dependensi kritisnya. Perhatikan contoh implementasi endpoint /healthz tingkat lanjut menggunakan bahasa pemrograman Go yang memantau konektivitas database SQL, cache Redis, dan kapasitas penyimpanan penyimpanan lokal:

package main

import (
	"database/sql"
	"encoding/json"
	"net/http"
	"syscall"
	"time"

	"github.com/go-redis/redis/v8"
	_ "github.com/lib/pq"
)

type HealthResponse struct {
	Status    string            `json:"status"`
	Timestamp string            `json:"timestamp"`
	Resources map[string]string `json:"resources"`
}

var (
	db    *sql.DB
	rdb   *redis.Client
)

func healthHandler(w http.ResponseWriter, r *http.Request) {
	// Inisialisasi status default
	response := HealthResponse{
		Status:    "UP",
		Timestamp: time.Now().Format(time.RFC3339),
		Resources: make(map[string]string),
	}

	// 1. Cek Autentikasi token dari Caddy
	token := r.Header.Get("Authorization")
	if token != "Bearer token-pemantau-lokal-123" {
		http.Error(w, "Unauthorized Checker", http.StatusUnauthorized)
		return
	}

	// 2. Uji Konektivitas database SQL (PostgreSQL)
	// Kita menggunakan Ping() daripada SELECT 1 karena Ping() di Go telah
	// dioptimalkan untuk memvalidasi status pool koneksi internal secara cepat
	// tanpa memaksa kompilasi kueri sql di parser database.
	if err := db.Ping(); err != nil {
		response.Status = "DOWN"
		response.Resources["database"] = "DOWN: " + err.Error()
	} else {
		response.Resources["database"] = "UP"
	}

	// 3. Uji Konektivitas Cache Server (Redis)
	if err := rdb.Ping(r.Context()).Err(); err != nil {
		response.Status = "DOWN"
		response.Resources["redis"] = "DOWN: " + err.Error()
	} else {
		response.Resources["redis"] = "UP"
	}

	// 4. Uji Kapasitas Disk Space sistem operasi (Disk Space Check)
	// Syscall.Statfs membaca langsung metadata filesystem sistem operasi (Linux/macOS)
	// untuk menguji ketersediaan blok memori tersisa secara instant.
	var stat syscall.Statfs_t
	if err := syscall.Statfs("/", &stat); err == nil {
		// Hitung sisa ruang penyimpanan byte
		freeDiskBytes := stat.Bavail * uint64(stat.Bsize)
		if freeDiskBytes < 500*1024*1024 { // Batas minimal 500MB
			response.Status = "DOWN"
			response.Resources["disk_space"] = "CRITICAL: Free disk is less than 500MB"
		} else {
			response.Resources["disk_space"] = "OK"
		}
	}

	// Tentukan status HTTP berdasarkan kelulusan checks
	statusCode := http.StatusOK
	if response.Status == "DOWN" {
		statusCode = http.StatusServiceUnavailable
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(statusCode)
	json.NewEncoder(w).Encode(response)
}

func main() {
	// (Kode inisialisasi koneksi DB dan Redis diletakkan di sini)
	http.HandleFunc("/healthz", healthHandler)
	http.ListenAndServe(":8081", nil)
}

Aplikasi Go di atas mendengarkan probe kesehatan di port :8081 yang privat, mengamankan data dengan memvalidasi token otentikasi dari Caddy, melakukan pengujian mendalam pada seluruh resource kritis database, dan mengembalikan status HTTP 503 Service Unavailable secara aman jika terdeteksi kegagalan konektivitas database.


Ringkasan #

  • Definisi Utama: Fitur Active Health Check secara berkala mengirimkan request uji coba (probe) di latar belakang menggunakan goroutine asinkronus ke server backend.
  • Proaktif System: Mengisolasi server backend yang mati secara instan sebelum permintaan riil dari pengguna asli sempat dikirimkan ke server tersebut.
  • Deteksi Body Respon: Gunakan direktif health_body untuk memvalidasi isi string respons (seperti JSON status) guna menghindari lolosnya backend yang berstatus HTTP 200 namun mengalami kegagalan database internal.
  • Keamanan Endpoint: Lindungi endpoint kesehatan dari eksploitasi publik dengan memisahkannya ke port administratif tertutup (health_port) atau menggunakan verifikasi token otentikasi header (health_headers).
  • Mitigasi Bencana: Hindari menyetel health_timeout terlalu ketat untuk mencegah terjadinya kegagalan beruntun (cascading failure) pada saat server mengalami lonjakan beban.
  • Desain Validasi: Rancang pemantau kesehatan di tingkat aplikasi (Go handler) yang menguji kesiapan dependensi database, status redis, disk space, dan memori backend secara terstruktur.

← Sebelumnya: Weighted   Berikutnya: Passive Health Check →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact