Lua dan OpenResty #
Nginx terkenal sebagai server web yang sangat cepat karena arsitekturnya yang digerakkan oleh event (event-driven). Namun, secara tradisional, menambahkan logika bisnis atau pemrograman dinamis yang kompleks ke dalam Nginx mengharuskan kita menulis modul kustom dalam bahasa C. Menulis modul C sangat rentan terhadap kebocoran memori (memory leaks) dan bisa membuat seluruh server crash jika terdapat kesalahan sekecil apa pun.
Integrasi Lua melalui modul ngx_http_lua_module mengubah paradigma ini secara total. Dengan memadukan efisiensi Nginx, penerjemah LuaJIT (Just-In-Time compiler) yang sangat cepat, serta pustaka non-blocking, kita dapat menulis logika aplikasi dinamis berkinerja tinggi langsung di tingkat web server. OpenResty hadir sebagai platform web berbasis Nginx yang mengemas seluruh pustaka tersebut ke dalam satu ekosistem terpadu yang siap digunakan untuk kebutuhan produksi skala besar.
OpenResty vs Nginx + ngx_lua Manual #
Ada dua jalur utama yang bisa kita pilih untuk menjalankan kode Lua di dalam lingkungan Nginx kita:
- OpenResty (Sangat Direkomendasikan):
Ini adalah distribusi Nginx siap pakai yang dikelola secara profesional oleh komunitas OpenResty. Di dalamnya sudah terpasang Nginx versi stabil, interpreter LuaJIT berkinerja tinggi, modul inti
ngx_lua, serta puluhan pustaka non-blocking bawaan untuk mengakses Redis, MySQL, Postgres, JSON parser, dan DNS resolver. Jalur ini paling stabil dan mudah dikelola di server produksi. - Kompilasi Modul
ngx_luaManual: Jalur ini dipilih jika kita memiliki instalasi Nginx kustom yang sudah berjalan and tidak ingin mengganti binary utamanya dengan OpenResty. Kita harus menginstal dependensi LuaJIT secara mandiri di OS, mengunduh modul sourcengx_devel_kitdanlua-nginx-module, lalu mengompilasinya bersama Nginx. Jalur ini membutuhkan pemeliharaan manual yang lebih rumit saat terjadi pembaruan versi.
Panduan Instalasi OpenResty di Server Produksi #
Berikut adalah langkah-langkah praktis untuk menginstal OpenResty pada distribusi Linux standar:
1. Instalasi pada Ubuntu atau Debian #
Kita menggunakan repositori resmi OpenResty agar selalu mendapatkan pembaruan versi keamanan terbaru:
# Instal dependensi awal
sudo apt-get update
sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates lsb-release
# Impor GPG Key resmi OpenResty
wget -O - https://openresty.org/package/pubkey.gpg | sudo gpg --dearmor -o /usr/share/keyrings/openresty.gpg
# Tambahkan repositori APT
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/openresty.gpg] http://openresty.org/package/ubuntu $(lsb_release -sc) main" \
| sudo tee /etc/apt/sources.list.d/openresty.list
# Update indeks dan pasang paket OpenResty
sudo apt-get update
sudo apt-get install openresty -y
# Jalankan service OpenResty
sudo systemctl start openresty
sudo systemctl enable openresty
Setelah instalasi selesai, direktori kerja OpenResty akan berada di /usr/local/openresty/. File konfigurasi utamanya tersimpan di /usr/local/openresty/nginx/conf/nginx.conf, dan berkas binary Nginx yang berjalan berada di /usr/local/openresty/nginx/sbin/nginx.
Fase Pemrosesan Permintaan (Request Handling) Nginx #
Untuk menulis kode Lua yang efektif, kita harus memahami di mana kode kita dieksekusi di dalam siklus hidup pemrosesan permintaan Nginx. Nginx membagi pemrosesan HTTP menjadi 11 fase berurutan. Modul ngx_lua menyediakan direktif (hooks) khusus untuk menyisipkan kode Lua kita pada fase-fase penting tersebut.
Berikut adalah visualisasi alur fase pemrosesan permintaan Nginx beserta titik masuk direktif Lua:
flowchart TD
Request["Permintaan Klien Masuk"] --> PostRead["1. post-read"]
PostRead --> Rewrite["2. rewrite"]
NoteRewrite["rewrite_by_lua_block<br/>(Manipulasi URI / Redirect)"] -.-> Rewrite
Rewrite --> FindConfig["3. find-config"]
FindConfig --> Preaccess["4. preaccess"]
Preaccess --> Access["5. access"]
NoteAccess["access_by_lua_block<br/>(Autentikasi & Otorisasi)"] -.-> Access
Access --> PostAccess["6. post-access"]
PostAccess --> Precontent["7. precontent"]
Precontent --> Content["8. content"]
NoteContent["content_by_lua_block<br/>(Generate respons konten)"] -.-> Content
Content --> Filter["9. filter (Header & Body)"]
NoteFilter["header_filter_by_lua_block<br/>body_filter_by_lua_block"] -.-> Filter
Filter --> Log["10. log"]
NoteLog["log_by_lua_block<br/>(Catat log / kirim metrik)"] -.-> Log
Log --> Response["Respons Terkirim ke Klien"]
classDef default fill:#f9f9f9,stroke:#d1d5db,stroke-width:1px,color:#111827;
classDef phaseStyle fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a;
classDef luaStyle fill:#fff7ed,stroke:#ea580c,stroke-width:1px,stroke-dasharray: 5 5,color:#c2410c;
class PostRead,Rewrite,FindConfig,Preaccess,Access,PostAccess,Precontent,Content,Filter,Log phaseStyle;
class NoteRewrite,NoteAccess,NoteContent,NoteFilter,NoteLog luaStyle;Penjelasan Peran Direktif Fase Lua: #
init_by_lua_block: Berjalan saat Nginx melakukan start-up awal atau reload konfigurasi di tingkat proses master. Sangat cocok untuk inisialisasi variabel global atau pre-loading pustaka Lua.init_worker_by_lua_block: Berjalan sesaat setelah proses worker dibuat. Berguna untuk membuat background timer (cron-like job) menggunakanngx.timer.atuntuk sinkronisasi data periodik.rewrite_by_lua_block: Mengeksekusi kode Lua sebelum pencarian lokasi dilakukan. Sering digunakan untuk menulis ulang URL (URL rewriting) secara dinamis.access_by_lua_block: Fase terbaik untuk menangani aturan keamanan, firewall tingkat aplikasi (WAF), verifikasi token API, dan rate limiting. Jika otorisasi gagal, kita bisa memotong request sebelum menyentuh backend.content_by_lua_block: Tempat untuk menghasilkan respons HTTP secara langsung. Ini adalah tempat kode aplikasi dinamis kita berjalan (mirip seperti handler di Node.js atau Go).header_filter_by_lua_block: Memungkinkan kita memanipulasi header respons secara dinamis setelah konten selesai dibuat.body_filter_by_lua_block: Berfungsi sebagai filter stream respons body. Kita bisa menyensor teks, mengubah format, atau menyisipkan skrip tambahan secara streaming.log_by_lua_block: Berjalan secara asinkron setelah koneksi klien ditutup. Sempurna untuk mencatat log khusus ke database, mengirim metrik performa ke Prometheus, atau mengumpulkan statistik tanpa mengganggu waktu respons klien.
Contoh Kasus Produksi 1: Hello World Dinamis dengan JSON #
Mari kita buat contoh paling dasar untuk memverifikasi bahwa OpenResty dapat menyajikan konten JSON dinamis tanpa menyentuh disk:
server {
listen 8080;
server_name localhost;
location /api/status {
content_by_lua_block {
# 1. Atur header respons
ngx.header.content_type = "application/json; charset=utf-8"
ngx.header.x_custom_server = "OpenResty-Lua"
# 2. Ambil parameter query string dari URL
local args = ngx.req.get_uri_args()
local user = args["user"] or "guest"
# 3. Kirim data JSON ke klien
ngx.say(string.format('{"status": "running", "authorized_as": "%s", "timestamp": %d}', user, ngx.time()))
}
}
}
Contoh Kasus Produksi 2: Autentikasi Bearer Token Dinamis #
Kita bisa menggunakan access_by_lua_block untuk memotong akses ilegal di tingkat gateway Nginx. Berikut adalah implementasi validasi token otorisasi sederhana:
server {
listen 443 ssl;
server_name secure-gateway.unisbadri.com;
location /api/v1/secure-data {
access_by_lua_block {
# 1. Ambil header Authorization dari klien
local auth_header = ngx.req.get_headers()["Authorization"]
if not auth_header then
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.header.content_type = "application/json"
ngx.say('{"error": "Unauthorized", "message": "Missing Authorization Header"}')
return ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
# 2. Ekstrak format token Bearer
local token = auth_header:match("^Bearer%s+(.+)$")
if not token then
ngx.status = ngx.HTTP_BAD_REQUEST
ngx.header.content_type = "application/json"
ngx.say('{"error": "Bad Request", "message": "Invalid Authorization format"}')
return ngx.exit(ngx.HTTP_BAD_REQUEST)
end
# 3. Verifikasi token (Contoh statis, bisa diganti pencarian DB/Redis)
local secret_key = "token-rahasia-produksi-kita"
if token ~= secret_key then
ngx.status = ngx.HTTP_FORBIDDEN
ngx.header.content_type = "application/json"
ngx.say('{"error": "Forbidden", "message": "Access Denied: Invalid Token"}')
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
# Token valid, biarkan Nginx melanjutkan request ke proxy_pass
}
# Teruskan ke backend jika lolos pemeriksaan Lua
proxy_pass http://api_backend;
}
}
Contoh Kasus Produksi 3: Rate Limiting Dinamis per User ID via Redis #
Secara bawaan, modul rate limit Nginx hanya mendukung limitasi berbasis IP klien. Dengan Lua, kita bisa membuat rate limiter dinamis yang membatasi akses berdasarkan ID pengguna yang masuk, meskipun mereka menggunakan IP yang sering berubah-ubah.
Menyiapkan Shared Dictionary Memory di nginx.conf #
Kita perlu mendefinisikan zona memori bersama (shared dictionary) di dalam blok http untuk menyimpan data counter rate limit secara instan:
http {
# Alokasikan 10MB RAM untuk dictionary bernama 'rate_limit_store'
lua_shared_dict rate_limit_store 10m;
server {
listen 80;
server_name api.unisbadri.com;
location /api/resource {
# Jalankan pemeriksaan rate limit dari file eksternal Lua
access_by_lua_file /etc/nginx/lua/rate_limiter.lua;
proxy_pass http://api_backend;
}
}
}
Logika Script /etc/nginx/lua/rate_limiter.lua
#
-- Muat modul limit request bawaan OpenResty
local limit_req = require "resty.limit.req"
-- Buat objek limiter: Batasan 50 request per detik, burst 20 request
local lim, err = limit_req.new("rate_limit_store", 50, 20)
if not lim then
ngx.log(ngx.ERR, "Gagal menginisialisasi rate limiter: ", err)
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
-- Identifikasi pengguna berdasarkan custom header (X-User-ID)
-- Jika tidak ada, gunakan alamat IP biner klien sebagai fallback
local user_id = ngx.req.get_headers()["X-User-ID"] or ngx.var.binary_remote_addr
-- Evaluasi request masuk
local delay, err = lim:incoming(user_id, true)
if not delay then
if err == "rejected" then
# Batas burst terlampaui (Too Many Requests)
ngx.status = 429
ngx.header.content_type = "application/json"
ngx.header["Retry-After"] = "1"
ngx.say('{"error": "Too Many Requests", "retry_after_seconds": 1}')
return ngx.exit(429)
end
ngx.log(ngx.ERR, "Error saat memproses rate limit: ", err)
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
# Jika delay bernilai positif kecil, kita tahan request sesaat (shaping traffic)
if delay >= 0.001 then
ngx.sleep(delay)
end
Contoh Kasus Produksi 4: Caching Cepat Menggunakan Redis #
Salah satu pola arsitektur paling populer menggunakan OpenResty adalah melakukan caching data respons dinamis langsung di tingkat gateway menggunakan Redis. Nginx langsung mencari data ke Redis RAM cache, bypass backend aplikasi sama sekali jika cache ditemukan (HIT).
server {
listen 8080;
location /get-product {
content_by_lua_block {
# 1. Muat library redis bawaan
local redis = require "resty.redis"
local red = redis:new()
# Setel timeout: connect, send, read (dalam milidetik)
red:set_timeouts(1000, 1000, 1000)
# 2. Hubungkan ke server Redis lokal kita
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Koneksi Redis gagal: ", err)
ngx.exit(500)
return
end
# 3. Cari cache berdasarkan parameter ID produk di URL
local args = ngx.req.get_uri_args()
local product_id = args["id"]
if not product_id then
ngx.status = ngx.HTTP_BAD_REQUEST
ngx.say('{"error": "Missing product ID"}')
return
end
local cache_key = "product_cache:" .. product_id
local cached_data, err = red:get(cache_key)
if cached_data and cached_data ~= ngx.null then
# CACHE HIT: Langsung kembalikan respons dari Redis RAM
ngx.header.content_type = "application/json"
ngx.header.x_cache_status = "HIT_REDIS"
ngx.say(cached_data)
else
# CACHE MISS: Panggil backend menggunakan internal sub-request
ngx.header.x_cache_status = "MISS"
# Mengirim request ke upstream backend internal
local res = ngx.location.capture("/fallback-backend" .. ngx.var.request_uri)
if res.status == ngx.HTTP_OK then
# Simpan data respons baru ke Redis dengan TTL 10 Menit (600 detik)
red:setex(cache_key, 600, res.body)
ngx.header.content_type = "application/json"
ngx.say(res.body)
else
ngx.status = res.status
ngx.say(res.body)
end
end
# 4. Kembalikan koneksi ke connection pool (sangat penting!)
# Maksimum 10 detik idle time, dengan pool size 100 koneksi
red:set_keepalive(10000, 100)
}
}
# Upstream backend internal yang tidak bisa diakses langsung klien
location /fallback-backend {
internal;
proxy_pass http://backend_app_upstream;
}
}
Menulis Modul Lua Kustom Kita Sendiri #
Saat basis kode Lua kita semakin membesar, menaruh kode di dalam file konfigurasi Nginx akan merusak kerapian arsitektur berkas kita. Kita harus memisahkan logika ke dalam modul Lua kustom eksternal.
1. Buat Berkas Modul /usr/local/openresty/lualib/mycompany/utils.lua
#
local _M = {}
# Fungsi pembersih string HTML kustom
function _M.clean_string(str)
if not str then return nil end
# Hapus karakter HTML mencurigakan (XSS prevention)
return str:gsub("<", "<"):gsub(">", ">")
end
# Fungsi enkripsi sederhana
function _M.obfuscate_email(email)
if not email then return nil end
local name, domain = email:match("([^@]+)@([^@]+)")
if not name or not domain then return email end
return name:sub(1, 2) .. "****@" .. domain
end
return _M
2. Memanggil Modul di Nginx #
Kita memanggil modul di atas di dalam location block menggunakan direktif require:
http {
# Tambahkan direktori kustom kita ke dalam library search path Nginx
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
server {
listen 80;
location /process {
content_by_lua_block {
# Panggil modul kustom kita
local utils = require "mycompany.utils"
local args = ngx.req.get_uri_args()
local email = args["email"]
local safe_email = utils.obfuscate_email(email)
ngx.header.content_type = "application/json"
ngx.say(string.format('{"original": "%s", "masked": "%s"}', email, safe_email))
}
}
}
}
Memantau Kinerja Shared Dictionary (lua_shared_dict)
#
Ketika kita memanfaatkan memori RAM bersama (lua_shared_dict) untuk pencatatan rate limit, session data, atau blacklist IP, kita wajib memantau kapasitas memori yang tersisa agar tidak memicu kegagalan alokasi data.
Berikut adalah endpoint kustom Lua untuk mengawasi status RAM shared dictionary kita secara real-time:
location /dict-stats {
allow 127.0.0.1;
deny all;
content_by_lua_block {
local shared_dict = ngx.shared.rate_limit_store
# Ambil daftar seluruh keys aktif (membutuhkan library resty)
local capacity = 10 * 1024 * 1024 # 10MB dalam bytes
local free_page_bytes = shared_dict:free_space()
local used_bytes = capacity - free_page_bytes
ngx.header.content_type = "application/json"
ngx.say(string.format(
'{"dict_name": "rate_limit_store", "capacity_bytes": %d, "used_bytes": %d, "free_bytes": %d, "utilization_percentage": %.2f}',
capacity, used_bytes, free_page_bytes, (used_bytes / capacity) * 100
))
}
}
Kapan Harus Menggunakan Lua di Nginx? #
Meskipun modul Lua Nginx sangat kuat, ia bukan alat sapu jagat untuk memecahkan segala masalah aplikasi kita. Kita harus memisahkan fungsionalitas secara bijaksana:
Skenario yang Tepat untuk Lua di Nginx: #
- Autentikasi & Otorisasi Terpusat: Memeriksa tanda tangan JWT, mencocokkan cookie session ke database Redis, atau verifikasi OAuth di tingkat gerbang masuk (Gateway).
- Tindakan Pengamanan Dinamis: Blokir IP penyerang secara otomatis berdasarkan kriteria perilaku tertentu di shared dict RAM.
- A/B Testing Kompleks: Penentuan upstream server klien berdasarkan kombinasi multi-variabel (IP, negara, jenis OS browser, data cookie) secara real-time.
- Transformasi Header/Response Streaming: Menyisipkan metadata, memodifikasi respons JSON, atau menyensor data sensitif sebelum dikirim ke klien.
Skenario yang Kurang Tepat untuk Lua di Nginx: #
- Logika Bisnis yang Kompleks: Mengolah transaksi pembayaran, memproses data relasional berat, atau menghasilkan PDF. Pindahkan pekerjaan ini ke backend aplikasi kita (Go, Node.js, Python) agar arsitektur web server tetap bersih.
- Pekerjaan CPU-Bound yang Berat: Melakukan kompresi gambar kustom, enkripsi video, atau komputasi matematika rumit. Loop CPU-bound akan menahan (block) worker thread Nginx, menghancurkan throughput concurrency web server kita secara keseluruhan.
Ringkasan dan Praktik Terbaik #
- Selalu Kembalikan Koneksi ke Pool: Setelah memanggil Redis (
red:connect) atau MySQL, jangan lupa menjalankanred:set_keepalive(). Tanpa ini, Nginx akan terus membuat koneksi TCP baru per request, memicu kelangkaan socket (socket exhaustion) di server database kita.- Gunakan open_by_lua_file di Produksi: Untuk penulisan kode panjang, pisahkan kode Lua ke berkas luar dan gunakan direktif
_fileuntuk mengaktifkan fitur caching kode Lua di RAM (lua_code_cache on).- Hindari Pemanggilan Sistem Operasi Pemblokir (Blocking OS Calls): Jangan gunakan fungsi bawaan standard library Lua seperti
io.open()atauos.execute()di dalam location block Nginx. Gunakan pustaka openresty padanannya (sepertingx.threadatau resty-socket) agar proses server kita tetap non-blocking.