Belajar Axum - Configuration & Environment
Series/Belajar Axum/Episode 16
Episode 16 of 28

Belajar Axum - Configuration & Environment

Merapikan konfigurasi aplikasi: env vars sebagai satu sumber kebenaran, struktur Config bertipe dengan validation, plus graceful shutdown lewat signal handling agar server berhenti tanpa memutus request.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

Sampai episode 15, DATABASE_URL di-expect langsung di main, dan JWT secret dari episode 13 masih hardcoded. Ini baik untuk prototipe, buruk untuk produksi. Episode ini merapikan semuanya: konfigurasi terpusat berbasis env vars yang tervalidasi saat start, plus graceful shutdown — server berhenti tanpa memutus request yang sedang berjalan.

Mengapa config layak episode sendiri? Karena kesalahan config — secret bocor ke repo, nilai salah, shutdown yang memutus koneksi WebSocket — adalah penyebab insiden produksi yang paling memalukan. Konfigurasi yang benar adalah fondasi dari deployment (episode 24) dan security (episode 18).

Env Vars: Satu Sumber Kebenaran

Prinsip utama: semua nilai yang berubah antar environment (dev/staging/prod) berasal dari environment variable, bukan dari konstanta di kode. Nilai yang sama di semua environment boleh tetap di kode — tetapi secret dan koneksi tidak boleh.

Untuk development, .env file dibaca saat start. Tambahkan dotenvy:

Tambah dotenvy
cargo add dotenvy
Muat .env di main
use dotenvy::dotenv;
 
fn main() {
    dotenv().ok();  // memuat .env jika ada; abaikan jika tidak
    // ...
}

File .env:

.env.example — template config
# Salin ke .env untuk development. JANGAN commit .env.
DATABASE_URL=postgres://postgres:axum-dev@localhost:5432/axum_blog
JWT_SECRET=ganti-dengan-secret-panjang-acak-minimal-32-karakter
BIND_ADDR=127.0.0.1:3000
RUST_LOG=axum_api=debug,tower_http=debug
MAX_CONNECTIONS=10

Warning

.env berisi secret dan tidak boleh masuk git. Komit .env.example (versi template tanpa secret) dan tambahkan .env ke .gitignore. Jangan pernah mengalihkan secret ke kode — sekalinya masuk git history, sulit dihapus.

Struct Config Bertipe

Semua env vars dibaca dan divalidasi di satu tempat: struct Config. Ini membuat startup gagal cepat (fail fast) jika config salah:

src/config.rs
use std::env;
 
#[derive(Debug, Clone)]
pub struct Config {
    pub database_url: String,
    pub jwt_secret: String,
    pub bind_addr: String,
    pub max_connections: u32,
}
 
impl Config {
    pub fn from_env() -> Self {
        let database_url = env::var("DATABASE_URL").expect("DATABASE_URL wajib disetel");
        let jwt_secret = env::var("JWT_SECRET").expect("JWT_SECRET wajib disetel");
        let bind_addr = env::var("BIND_ADDR").unwrap_or_else(|_| "127.0.0.1:3000".into());
        let max_connections = env::var("MAX_CONNECTIONS")
            .ok()
            .and_then(|v| v.parse().ok())
            .unwrap_or(10);
 
        if jwt_secret.len() < 32 {
            panic!("JWT_SECRET harus minimal 32 karakter");
        }
 
        Self {
            database_url,
            jwt_secret,
            bind_addr,
            max_connections,
        }
    }
}

Pola ini penting:

  • .expect("... wajib disetel") — nilai yang wajib membuat aplikasi berhenti dengan pesan jelas saat start, bukan gagal misterius di tengah jalan.
  • .unwrap_or_else(...) — nilai opsional punya default yang didokumentasikan.
  • Validasi di constructor — misal panjang JWT secret — menolak config buruk sejak awal.
  • Struct config bisa di-Clone dan dimasukkan ke AppState, jadi handler mana pun bisa membacanya.

Alternatif yang lebih ketat: crate figment/config untuk file config + env vars. Untuk scope series ini, struct manual + env vars sudah cukup dan lebih transparan.

Memasukkan Config ke Aplikasi

Config menjadi bagian AppState (dari episode 7) dan JWT secret dibaca dari config, bukan konstanta:

Config dalam state
#[derive(Clone)]
pub struct AppState {
    pub db: sqlx::PgPool,
    pub config: Arc<Config>,
}
 
#[tokio::main]
async fn main() {
    dotenv().ok();
    tracing_subscriber::fmt()
        .with_env_filter(env::var("RUST_LOG").unwrap_or_else(|_| "info".into()))
        .init();
 
    let config = Config::from_env();
    let db = sqlx::postgres::PgPoolOptions::new()
        .max_connections(config.max_connections)
        .connect(&config.database_url)
        .await
        .expect("gagal konek database");
 
    let state = AppState {
        db,
        config: Arc::new(config.clone()),
    };
 
    // ... router dibangun dari state
    let listener = tokio::net::TcpListener::bind(&config.bind_addr).await.unwrap();
    axum::serve(listener, app).with_graceful_shutdown(shutdown_signal()).await.unwrap();
}

Sekarang main tidak lagi berisi nilai acak — semuanya bermuara ke satu Config. JWT verification (episode 13) diubah memakai state.config.jwt_secret:

JWT secret dari config
pub fn verify_token(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
    jsonwebtoken::decode::<Claims>(
        token,
        &jsonwebtoken::DecodingKey::from_secret(secret.as_bytes()),
        &jsonwebtoken::Validation::default(),
    )
    .map(|data| data.claims)
}

Graceful Shutdown: Berhenti Tanpa Memutus

axum::serve punya .with_graceful_shutdown(future) — ketika future selesai, server berhenti menerima request baru, menyelesaikan yang sedang berjalan, lalu tutup. Tanpa ini, proses di-terminate paksa dan request yang sedang diproses hilang.

Shutdown signal
use tokio::signal;
 
async fn shutdown_signal() {
    let ctrl_c = async {
        signal::ctrl_c().await.expect("gagal mendengarkan Ctrl+C");
    };
 
    #[cfg(unix)]
    let terminate = async {
        signal::unix::signal(signal::unix::SignalKind::terminate())
            .expect("gagal mendaftarkan signal handler")
            .recv()
            .await;
    };
 
    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();
 
    tokio::select! {
        _ = ctrl_c => tracing::info!("Ctrl+C diterima, shutdown..."),
        _ = terminate => tracing::info!("SIGTERM diterima, shutdown..."),
    }
}

Fungsi ini menunggu dua sinyal:

  • SIGINT (Ctrl+C) — dipakai saat development.
  • SIGTERM — dikirim oleh container runtime (Docker/K8s) saat scale-down atau deploy. Ini sinyal yang harus ditangani untuk deployment (episode 24).

Note

Kenapa signal terminate harus ada? Ketika Docker menghentikan container, ia mengirim SIGTERM lalu menunggu beberapa detik sebelum SIGKILL. Dengan graceful shutdown, request in-flight selesai dalam jendela itu — tanpanya, koneksi WebSocket terputus dan transaksi terpotong. Ini perbedaan antara "restart mulus" dan "crash".

Graceful shutdown juga saat yang tepat menutup sumber daya — terutama pool database:

Bersihkan resource saat shutdown
let state = AppState { db, config: Arc::new(config) };
let state_for_shutdown = state.clone();
 
axum::serve(listener, app)
    .with_graceful_shutdown(async move {
        shutdown_signal().await;
        // koneksi DB yang belum dipakai dikembalikan & ditutup
        state_for_shutdown.db.close().await;
    })
    .await
    .unwrap();

PgPool::close() menutup koneksi idle; koneksi yang sedang dipinjam handler tetap berfungsi sampai selesai. Ini mencegah "zombie connection" yang menyumbat database saat restart cepat.

Verifikasi Config dan Shutdown

Uji bahwa semuanya bekerja:

Uji config dan shutdown
# Config salah -> fail fast dengan pesan jelas
DATABASE_URL= cargo run
# Harus panic: "DATABASE_URL wajib disetel"
 
# Shutdown halus
cargo run &
sleep 2
kill -TERM %1
# Log: "SIGTERM diterima, shutdown..." lalu proses keluar bersih

Melihat log shutdown muncul dan proses keluar dengan code 0 adalah tanda config + signal handling bekerja.

Penutup

Pada episode 16 ini konfigurasi dan lifecycle aplikasi beres:

  • Env vars sebagai satu sumber kebenaran; .env untuk dev, .env.example sebagai template.
  • Struct Config dengan fail-fast validation, dimasukkan ke AppState.
  • JWT secret (dan nilai berubah lain) tidak lagi hardcoded.
  • with_graceful_shutdown menangani SIGINT & SIGTERM.
  • Pool database ditutup saat shutdown untuk restart yang bersih.

Di episode 17 selanjutnya kita bikin aplikasi bisa diintip: logging & observability — tracing structured logs, TraceLayer, metrics Prometheus, dan OpenTelemetry untuk aplikasi yang siap dipantau produksi. Sampai jumpa di episode 17!