PRODसंप्रभु यूरोपीय BaaS मंचडैशबोर्ड खोलें →

इंजीनियरिंग · 10 मिनट पढ़ा

Rust में एक डुअल-प्लेन एपीआई गेटवे डिजाइन करना

Affane Daylami · Fondateur · 6 अगस्त 2026

ब्लॉग पर वापस जाएँ

एक गेटवे जो आपके उपयोगकर्ताओं के एसडीके ट्रैफ़िक और आपके बैक ऑफिस एडमिन ट्रैफ़िक दोनों को एक ही समय में खराब सुरक्षा प्रदान करता है। ऑराबेस गेटवे (ऑरा-गेटवे, रस्ट/एक्सम) इस तनाव को अपस्ट्रीम में हल करता है: दो अलग-अलग राउटर, दो पोर्ट, दो प्रमाणीकरण मॉडल, एक एकल साझा स्थिति। यह मार्गदर्शिका इस डेटा-प्लेन/प्रबंधन-प्लेन पैटर्न का वर्णन करती है क्योंकि यह वास्तव में कोड में मौजूद है - रूट, मिडलवेयर ऑर्डर, रेट लिमिटिंग, सर्किट ब्रेकर और प्रॉक्सी - वास्तुशिल्प आरेख का एक आदर्श संस्करण नहीं है।

यह अंग्रेजी पाठ फ़्रेंच मूल से स्वचालित रूप से उत्पन्न हुआ था और अभी तक इसकी समीक्षा नहीं की गई है।
यह पृष्ठ स्वचालित रूप से अनुवादित किया गया था. अंग्रेजी संस्करण प्रामाणिक है.

अनिवार्य है

दो बंदरगाहों पर दो Router अक्ष (8080 डेटा प्लेन, 8090 प्रबंधन विमान), एक ही साझा AppStateसे निर्मित। डेटा प्लेन को किसी भी रूट पर एपीआई कुंजी की आवश्यकता होती है; विमान प्रबंधन के लिए एक समर्पित JWT ऑडियंस कंसोल की आवश्यकता होती है - दोनों तंत्र कभी भी ओवरलैप नहीं होते हैं। दर सीमित करना दो बार लागू होता है: प्रमाणीकरण से पहले आईपी द्वारा, फिर उसके बाद प्रमाणित अभिनेता द्वारा। सर्किट ब्रेकर एक वैश्विक मिडलवेयर नहीं है: यह प्रति सेवा (और PostgREST के लिए समर्पित लक्ष्य) के लिए एक ऑब्जेक्ट है, जिसे सीधे प्रॉक्सी कोड में लागू किया जाता है। और अंतिम प्रॉक्सी मार्ग के आधार पर परिवहन को बदलता है - कभी-कभी HTTP विधि या डेटाबेस लुकअप के अनुसार: अधिकांश ट्रैफ़िक के लिए NATS अनुरोध/उत्तर, भंडारण के लिए प्रत्यक्ष HTTP प्रवाह, तीन रीयलटाइम वेरिएंट और समर्पित पोस्टग्रेज़ CRUD।

#
समस्या

एक एकल प्रवेश द्वार, दो बिल्कुल अलग दर्शक वर्ग

डेटा प्लेन ट्रैफ़िक एसडीके या क्लाइंट ऐप से आता है: एपीआई कुंजी द्वारा प्रमाणित गुमनाम अनुरोधों या अनुरोधों की मात्रा, किसी भी सार्वजनिक एपीआई के करीब दुरुपयोग प्रोफ़ाइल के साथ। ट्रैफ़िक प्रबंधन विमान स्टूडियो से आता है - एक परियोजना का प्रशासन इंटरफ़ेस - और संवेदनशील संचालन करता है: परियोजना निर्माण, कुंजी रोटेशन, किरायेदार के लॉग को पढ़ना। दोनों एक समान लक्ष्य साझा करते हैं (समान आंतरिक सेवाओं के समीप: ऑरा-ऑथ, ऑरा-डीबी, ऑरा-स्टोरेज, आदि) लेकिन जोखिम की सतह समान नहीं है।

दोनों को एक ही राउटर से गुजारने के लिए दो खराब विकल्पों के बीच चयन करने की आवश्यकता होती है: या तो स्टूडियो सीओआरएस को सार्वजनिक एसडीके (Access-Control-Allow-Origin: *) के लिए आवश्यक वाइल्डकार्ड विरासत में मिलता है, या एसडीके को आंतरिक डैशबोर्ड के लिए डिज़ाइन की गई मूल की एक प्रतिबंधित सूची विरासत में मिलती है। ऑरा-गेटवे कोड इस तनाव को main.rsसे हल करता है: दो अलग-अलग Router, प्रत्येक का अपना CorsLayer है - डेटा प्लेन पक्ष पर अधिकृत वाइल्डकार्ड, इनकार कर दिया गया और प्रबंधन विमान पक्ष पर एक त्रुटि के रूप में लॉग किया गया।

#
स्टेप 1

दो एक्सम राउटर, एक साझा ऐपस्टेट

पृथक्करण एक अलग परिनियोजन नहीं है: दो योजनाएँ एक ही प्रक्रिया में, एक ही AppState (पोस्टग्रेज पूल, NATS क्लाइंट, मोका कैश, सर्किट ब्रेकर) पर चलती हैं। केवल Router का निर्माण अलग-अलग है, प्रत्येक को स्टार्टअप पर एक बार बुलाए गए दो समर्पित कार्यों के माध्यम से और दो अलग-अलग TcpListenerद्वारा परोसा जाता है।

gateway/server.rsrust
// दो पोर्ट, दो राउटर, एक ऐपस्टेट

let data_addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?;
let mgmt_addr: SocketAddr = format!("{}:{}", config.host, config.management_port).parse()?;

let data_listener = TcpListener::bind(data_addr).await?;
let mgmt_listener = TcpListener::bind(mgmt_addr).await?;

// एक ही स्थिति, दो अलग-अलग रूट ग्राफ़
let data_app = build_data_plane_router(state.clone(), data_cors);
let mgmt_app = build_management_plane_router(state);

tokio::join!(
    axum::serve(data_listener, data_app),
    axum::serve(mgmt_listener, mgmt_app),
);

दो रूट ग्राफ़ एक ही आधार से शुरू होते हैं, service_routes(): समान प्रॉक्सी हैंडलर (db_proxy, storage_proxy, functions_proxy…) दोनों योजनाओं पर लगाए जाते हैं, प्रत्येक के लिए विशिष्ट अतिरिक्त रूट होते हैं। समान हैंडलर का पुन: उपयोग करने से प्रॉक्सी के दोहरे कार्यान्वयन से बचा जा सकता है; केवल मिडलवेयर पर विचलन करने से सुरक्षा सीमा हासिल करने के लिए व्यावसायिक तर्क की नकल करने से बचा जा सकता है। यदि आपका बैकएंड स्वयं एक बहु-सेवा कार्गो कार्यक्षेत्र के रूप में संरचित है, तो हमारा कार्गो कार्यक्षेत्र आर्किटेक्चर गाइड देखें - इस प्रभाग में प्रवेश द्वार दूसरों के बीच सिर्फ एक टोकरा है।

#
चरण दो

प्रवेश पर प्रमाणीकरण अलग हो जाता है

डेटा प्लेन पर, कुछ वास्तविक सार्वजनिक पथों (/health, JWKS, पंजीकरण समापन बिंदु) को छोड़कर, किसी भी मार्ग पर एपीआई कुंजी अनिवार्य है। यह apikey या X-API-Key हेडर के रूप में यात्रा करता है - या, केवल WebSocket और SSE स्ट्रीमिंग रूट के लिए, ?apikey=पैरामीटर के रूप में। कोड स्पष्ट रूप से service_role कुंजी के लिए इस अंतिम मोड को प्रतिबंधित करता है: एक यूआरएल कुंजी एक्सेस लॉग, ओटेल ट्रेस और रेफरर हेडर में लीक हो जाती है। डेटा प्लेन पक्ष पर एक JWT वैकल्पिक रहता है: इसके बिना, कॉलर anonरहता है; इसके साथ, यह authenticatedबन जाता है।

प्रबंधन स्तर पर, एपीआई कुंजी मौजूद नहीं है: केवल एक जेडब्ल्यूटी कंसोल स्वीकार किया जाता है, जिसका दर्शक बिल्कुल aurabase-controlहोना चाहिए। भूमिका टोकन द्वारा ही नहीं ली जाती है - यह उस संगठन में उपयोगकर्ता की सदस्यता से प्रत्येक अनुरोध पर पुनर्गणना की जाती है जो परियोजना का मालिक है, परियोजना के माध्यम से विरासत में मिला है → संगठन संबंध।

टोकन आवश्यक हैएपीआई कुंजी (एपीकी/एक्स-एपीआई-कुंजी), हमेशाJWT कंसोल (प्राधिकरण: बियरर), हमेशा
भूमिका उन्नयनवैकल्पिक JWT: anon → प्रमाणितआरबीएसी को संगठन से विरासत में मिला (मालिक/व्यवस्थापक/डेवलपर/दर्शक)
क्वेरी स्ट्रिंग में कुंजीकेवल WS/SSE पर सहन किया जाता है, service_role के लिए कभी नहींलागू नहीं
अपेक्षित दर्शकलक्षित परियोजना (पथ का UUID)निश्चित "ऑराबेस-नियंत्रण"
CORSवाइल्डकार्ड * की अनुमति हैवाइल्डकार्ड ने मना कर दिया, केवल स्टूडियो की उत्पत्ति
#
चरण 3

मिडलवेयर का वास्तविक क्रम (और यह क्यों मायने रखता है)

एक्सम क्रमिक .layer() कॉल के साथ मिडलवेयर को स्टैक करता है - और निष्पादन के क्रम को नियंत्रित करने वाला नियम व्यवहार में आश्चर्यजनक है: अंतिम .layer() रखा गया सबसे बाहरी परत बन जाता है, इसलिए आने वाले अनुरोध द्वारा सबसे पहले ट्रैवर्स किया जाता है, और प्रतिक्रिया को देखने के लिए अंतिम छोड़ दिया जाता है। इसलिए फ़ाइल की एक रैखिक रीडिंग वास्तविक निष्पादन आदेश का उलटा क्रम देती है।

gateway/router.rsrust
// इस प्रकार लिखा गया है (वास्तविक उद्धरण, फ़ाइल क्रम):

service_routes(...).merge(data_plane_extra)
    .layer(metrics_auth_middleware)      // (1) प्रथम स्थान पर → अंतरतम
    .layer(rate_limit_actor_middleware)  // (2)
    .layer(data_plane_auth_middleware)   // (3)
    .layer(rate_limit_middleware)        // (4)
    .layer(request_id_middleware)        // (5)
    .layer(AccessLogLayer)               // (6)
    .layer(prometheus_layer)             // (7)
    .layer(TraceLayer)                   // (8)
    .layer(RequestBodyLimitLayer)        // (9)
    .layer(security_headers_middleware)  // (10)
    .layer(cors)                         // (11) अंतिम स्थान पर → सबसे बाहरी

// इसलिए आने वाला अनुरोध (11) → (1) से होकर गुजरता है, कभी नहीं (1) → (11)।
इस आदेश का एक ठोस असर

request_id मिडलवेयर केवल X-Request-Id हेडर को RESPONSE पर सेट करता है, आने वाले अनुरोध पर कभी नहीं। चूंकि AccessLogLayer को फ़ाइल में इसके बाद रखा गया है - इसलिए अधिक बाहरी, इसलिए पहले ट्रैवर्स किया गया - request_id फ़ील्ड का इसका कैप्चर हेडर को पढ़ता है क्योंकि क्लाइंट ने इसे भेजा है, न कि श्रृंखला में आगे उत्पन्न पहचानकर्ता को। यदि कॉल करने वाले ने कोई X-Request-Idप्रदान नहीं किया है, तो एक्सेस-लॉग लाइन एक खाली फ़ील्ड के साथ निकल जाती है, जबकि लौटाई गई प्रतिक्रिया में ताज़ा जेनरेट किया गया यूयूआईडी होता है। कोई छिपा हुआ दोष नहीं - एक अनुस्मारक कि जिस क्रम में .layer() स्ट्रिंग लिखी जाती है वह उस तार्किक क्रम के बारे में कुछ भी गारंटी नहीं देता है जिसे हम विशेषता देते हैं।

#
चरण 4

दर सीमित करना: आईपी पहले, अभिनेता उसके बाद

दर सीमित करना श्रृंखला में दो अलग-अलग समयों पर दो अलग-अलग पासों में लागू किया जाता है। पहला आईपी पते द्वारा प्रमाणीकरण और सीमा से पहले चलता है - एक सामान्य बाढ़-रोधी फ़िल्टर, जो सार्वजनिक सड़कों पर भी सक्रिय है: इसके बिना, एक अप्रमाणित प्रवाह एक महंगे समापन बिंदु, जैसे कि लॉग एकत्रीकरण, को जेडब्ल्यूटी जांच को ट्रिगर किए बिना प्रभावित कर सकता है। दूसरा अभिनेता द्वारा प्रमाणीकरण और सीमा के बाद चलता है - एपीआई कुंजी या उपयोगकर्ता - उन दावों का उपयोग करते हुए कि प्रमाणीकरण अभी इंजेक्ट किया गया है: यह वास्तविक उत्पाद कोटा है, जो बिलिंग और योजनाओं के लिए मायने रखता है।

कार्यान्वयन स्थानीय गणना के लिए governor क्रेट (टोकन बकेट) पर निर्भर करता है, गेटवे इंस्टेंस के बीच वितरण के लिए स्लाइडिंग विंडो लुआ रेडिस स्क्रिप्ट और रेडिस अनुपलब्ध होने पर स्थानीय फ़ॉलबैक (मोका कैश) पर निर्भर करता है। रिपॉजिटरी डिफ़ॉल्ट: 100 अनुरोध/सेकंड, 1000 का विस्फोट।

#
चरण 5

सर्किट ब्रेकर एक परत नहीं है, यह प्रति लक्ष्य एक वस्तु है

शेष श्रृंखला के विपरीत, सर्किट ब्रेकर किसी भी .layer()में प्रकट नहीं होता है। AppState प्रति सेवा एक CircuitBreaker इंस्टेंस (auth, db, रीयलटाइम, स्टोरेज, फ़ंक्शंस, नोटिफिकेशन, एआई, प्रोविज़नर, कंट्रोल) प्रदान करता है, और यह प्रॉक्सी कोड ही है - राउटर नहीं - जो अनुरोध का प्रयास करने से पहले try_acquire_probe() को कॉल करता है, फिर परिणाम के आधार पर record_success() या record_failure() को कॉल करता है।

PostgREST मामला अलग है: समर्पित टोपोलॉजी (प्रोजेक्ट-विशिष्ट Postgres और PostgREST) ​​में परियोजनाओं में एक साझा गलती डोमेन नहीं है - प्रत्येक PostgREST प्रक्रिया का अपना लक्ष्य है। इसलिए गेटवे हल किए गए लक्ष्य द्वारा अनुक्रमित सर्किट ब्रेकरों की एक तालिका बनाए रखता है, जिसे तुरंत पॉप्युलेट किया जाता है और हर 60 सेकंड में एक स्वीप द्वारा शुद्ध किया जाता है जो निष्क्रिय प्रविष्टियों को हटा देता है: इस शुद्धिकरण के बिना, प्रत्येक नई समर्पित परियोजना एक प्रविष्टि जोड़ती है जो कभी गायब नहीं होती है।

gateway/circuit_breaker.rsrust
let Some(probe) = circuit_breaker.try_acquire_probe() else {
    return Err(ServiceUnavailable);
};

// ...NATS क्वेरी प्रयास, सीमित पुनर्प्रयास के साथ...

match resultat {
    Ok(Ok(_))     => match probe.take() { Some(p) => p.record_success(), _ => {} },
    Ok(Err(_))    => match probe.take() { Some(p) => p.record_failure(), _ => {} },
    Err(_timeout) => {} // जांच का उपभोग नहीं किया गया → ड्रॉप द्वारा लौटाया गया
}

जांच टोकन को Drop के रूप में लौटाया जाता है यदि इसे कभी भी स्पष्ट रूप से उपभोग नहीं किया जाता है - तब उपयोगी होता है जब अनुरोध पर सभी प्रयास उस शाखा तक पहुंचने के बिना समाप्त हो जाते हैं जो इसे मुक्त कर देता। और स्वचालित रीप्ले केवल NATS पक्ष (NoResponders) पर गैर-डिलीवरी के सख्त प्रमाण पर ट्रिगर किया जाता है: एक साधारण गेटवे टाइमआउट अनुरोध की वास्तविक डिलीवरी के बारे में कुछ भी साबित नहीं करता है, और इसे दोबारा चलाने से इसे दो बार निष्पादित किया जा सकता है।

#
चरण 6

अंतिम लिंक: NATS या प्रत्यक्ष HTTP, कभी भी यादृच्छिक रूप से नहीं

अंतिम प्रॉक्सी किसी भी प्रोटोकॉल को पीछे की ओर नहीं बोलता है, और विकल्प रूट द्वारा तय नहीं किया जाता है: यह HTTP विधि, या डेटाबेस लुकअप पर भी निर्भर हो सकता है। अधिकांश ट्रैफ़िक (ऑथ, फ़ंक्शंस, नोटिफिकेशन, नियंत्रण और अधिकांश डीबी) के लिए, गेटवे HTTP अनुरोध को NATS लिफाफे में क्रमबद्ध करता है और इसे सेवा के लिए समर्पित विषय के अनुरोध/उत्तर के रूप में भेजता है - टीसीपी हैंडशेक के बिना एक राउंड ट्रिप, इस आरपीसी प्रकार के ट्रैफ़िक के लिए क्लासिक HTTP प्रॉक्सी की तुलना में काफी तेज़ कोड में प्रलेखित।

भंडारण, रीयलटाइम के तीन प्रकार (वेबसॉकेट, एसएसई, और प्रसारण/चैनल/उपस्थिति के लिए आरईएसटी) और - सशर्त - पोस्टग्रेज सीआरयूडी अनुरोध इस पथ से बाहर निकलते हैं और एक लाइव, पूल किए गए HTTP क्लाइंट से गुजरते हैं। स्टोरेज ने स्पष्ट रूप से यह विकल्प चुना: NATS लिफाफे में एक बाइनरी बॉडी को एन्कोड करने के लिए इसे क्रमबद्ध करना, इसे पूरी तरह से दोनों सिरों पर मेमोरी में लोड करना और NATS संदेश आकार कैप के तहत रहना आवश्यक है - बड़ी वस्तुओं के लिए एक वास्तविक लागत। वेबसॉकेट और एसएसई केवल अनुरोध/उत्तर शब्दार्थ को बर्दाश्त नहीं करते हैं: एक प्रोटोकॉल अपग्रेड और एक प्रवाह जो खुला रहता है उसका कोई NATS समकक्ष नहीं होता है।

सबसे दिलचस्प मामला /v1/db/*है, जिसका हैंडलर प्रत्येक अनुरोध पर स्वयं निर्णय लेता है: प्रबंधन मार्ग (स्कीमा, नीतियां, कच्चा SQL) हमेशा NATS में aura-db पर जाते हैं, एक PUT हमेशा NATS में जाता है (PostgREST पूर्ण प्रतिस्थापन पर 405 लौटाता है), एक MongoDB प्रोजेक्ट हमेशा NATS में जाता है - और समर्पित PostgREST उदाहरण के साथ पोस्टग्रेज प्रोजेक्ट पर केवल एक CRUD डायरेक्ट HTTP में जाता है। यदि इस समर्पित उदाहरण का समाधान नहीं किया जाता है, तो गेटवे साझा पोस्टग्रेस्ट पर वापस जाने के बजाय 503 पर प्रतिक्रिया करता है: असफल-बंद मान लिया गया है, अपमानित मूक फ़ॉलबैक नहीं। सुरक्षा हेडर (सख्त CSP, कोई CORS क्रेडेंशियल) इन सभी पथों पर समान रूप से लागू होते हैं, जो प्रतिक्रिया गेटवे छोड़ने से पहले श्रृंखला के बिल्कुल अंत में रखे जाते हैं।

#
चरण 7

प्रति रूट टाइमआउट बजट, वैश्विक टाइमआउट नहीं

गेटवे वैश्विक टाइमआउट के बजाय मार्गों के प्रति समूह TimeoutLayer को लागू करता है - मिडलवेयर ऑर्डर के समान स्टैकिंग यांत्रिकी से जुड़ा एक विकल्प। एज फ़ंक्शंस रूट को बाकियों की तुलना में बहुत लंबे बजट की आवश्यकता होती है (एक फ़ंक्शन वैध रूप से कई मिनटों तक चल सकता है): अधिकांश रूटों के लिए रिपॉजिटरी डिफ़ॉल्ट 30 सेकंड है, जबकि /v1/functions/*के लिए 380 सेकंड है।

किसी एक वैश्विक TimeoutLayer को हर चीज़ के ऊपर रखने से दोनों समूह एक ही सीमा पर कट जाते: यह हमेशा सबसे बाहरी स्थिति में रखा गया सबसे छोटा टाइमआउट होता है जो जीतता है, भले ही आगे कितना भी लंबा टाइमआउट रखा गया हो। कार्यों को एक अलग बजट देने का एकमात्र तरीका यह है कि वे कभी भी एक सामान्य आवरण में प्रवेश न करें: मार्गों की प्रत्येक शाखा का अपना TimeoutLayerहोता है, जिसे दो राउटरों के विलय से पहले रखा जाता है - और बाद में कोई वैश्विक टाइमआउट लागू नहीं किया जाता है।

#
याद करने के लिए

इस पैटर्न को अन्यत्र पुन: प्रस्तुत करें: चेकलिस्ट

  1. PLAN (एक्सपोज़र सतह) से अलग, सेवा से नहीं: एक समझौता किए गए सार्वजनिक SDK को कभी भी आपके व्यवस्थापक डैशबोर्ड की CORS मूल सूची तक नहीं पहुंचना चाहिए।
  2. दो अलग-अलग तैनाती के बजाय एक ही साझा स्थिति रखें - व्यावसायिक तर्क की नकल करने में राउटर की नकल करने की तुलना में अधिक लागत आती है।
  3. वास्तविक मिडलवेयर ऑर्डर को अंतिम .layer()से ट्रेस करके जांचें, फ़ाइल की रैखिक रीडिंग से कभी नहीं।
  4. प्रति अभिनेता कोटा (बाद में) से प्रति आईपी (प्रमाणीकरण से पहले) अलग दर सीमा - अन्यथा एक अप्रमाणित प्रवाह बिना सीमा के महंगा सत्यापन करता है।
  5. ब्रेकर को यथासंभव वास्तविक नेटवर्क कॉल के करीब, प्रॉक्सी में रखें - और जब गलती डोमेन साझा नहीं किया जाता है तो इसे लक्ष्य के अनुसार आकार दें।
  6. किसी अनुरोध को केवल गैर-डिलीवरी के प्रमाण पर दोबारा चलाएं, साधारण टाइमआउट पर कभी नहीं।
  7. राउटर्स को मर्ज करने से पहले प्रत्येक रूट समूह को अपना स्वयं का टाइमआउट बजट सेट दें - कभी भी वैश्विक TimeoutLayer नहीं होगा जो सबसे लंबे बजट को अधिलेखित कर देगा।

तैनाती के लिए तैयार हैं?

पाँच मिनट में आपका बैकएंड।

किसी क्रेडिट कार्ड की आवश्यकता नहीं · 500 एमबी निःशुल्क · 50,000 एमएयू