PROD주권 유럽 BaaS 플랫폼대시보드 열기 →

공학 · 10분 읽음

Rust에서 이중 평면 API 게이트웨이 설계

Affane Daylami · Fondateur · 2026년 8월 6일

블로그로 돌아가기

사용자의 SDK 트래픽과 백오피스 관리 트래픽을 모두 제공하는 게이트웨이는 두 트래픽을 동시에 제대로 보호하지 못합니다. Aurabase 게이트웨이(aura-gateway, Rust/axum)는 두 개의 개별 라우터, 두 개의 포트, 두 개의 인증 모델, 단일 공유 상태 등 이러한 업스트림 긴장을 해결합니다. 이 가이드에서는 아키텍처 다이어그램의 이상적인 버전이 아닌 코드(경로, 미들웨어 순서, 속도 제한, 회로 차단기 및 프록시)에 실제로 존재하는 데이터 평면/관리 평면 패턴을 설명합니다.

이 영어 텍스트는 프랑스어 원본에서 자동으로 생성되었으며 아직 검토되지 않았습니다.
이 페이지는 자동으로 번역되었습니다. 영어 버전은 권위가 있습니다.

필수사항

동일한 공유 AppState에서 구축된 두 포트(8080 데이터 평면, 8090 관리 평면)에 있는 두 개의 Router 축. 데이터 플레인에는 모든 경로에 API 키가 필요합니다. 플레인 관리에는 전용 JWT 청중 콘솔이 필요합니다. 두 메커니즘은 결코 겹치지 않습니다. 속도 제한은 두 번 적용됩니다. 즉, 인증 전 IP로, 그 다음 인증된 행위자별로 적용됩니다. 회로 차단기는 전역 미들웨어가 아닙니다. 서비스별(및 PostgREST의 전용 대상별) 개체이며 프록시 코드에서 직접 호출됩니다. 그리고 최종 프록시는 경로에 따라 전송을 변경합니다. 때로는 HTTP 방법이나 데이터베이스 조회에 따라 전송을 변경합니다. 즉, 대부분의 트래픽에 대한 NATS 요청/응답, 스토리지에 대한 직접 HTTP 흐름, 세 가지 실시간 변형 및 전용 Postgres CRUD가 있습니다.

#
문제

단일 게이트웨이, 매우 다른 두 대상

데이터 영역 트래픽은 SDK 또는 클라이언트 앱에서 발생합니다. 즉, 공개 API에 가까운 남용 프로필을 사용하여 API 키로 인증된 대량의 익명 요청 또는 요청입니다. 트래픽 관리 플레인은 프로젝트의 관리 인터페이스인 Studio에서 제공되며 프로젝트 생성, 키 순환, 테넌트 로그 읽기 등 민감한 작업을 수행합니다. 두 가지 모두 공통 대상(동일한 내부 서비스(aura-auth, aura-db, aura-storage 등)에 대한 프록시)을 공유하지만 동일한 위험 표면은 아닙니다.

동일한 라우터를 통과하려면 두 가지 잘못된 옵션 중에서 선택해야 합니다. Studio CORS가 공개 SDK(Access-Control-Allow-Origin: *)에 필요한 와일드카드를 상속하거나 SDK가 내부 대시보드용으로 설계된 제한된 원본 목록을 상속합니다. aura-gateway 코드는 main.rs에서 이러한 장력을 분류합니다. 두 개의 별도 Router는 각각 자체 CorsLayer를 포함합니다. 즉, 데이터 플레인 측에서 승인된 와일드카드는 거부되고 관리 플레인 측에서는 오류로 기록됩니다.

#
1단계

두 개의 axum 라우터, 하나의 공유 AppState

분리는 별도의 배포가 아닙니다. 두 계획은 동일한 AppState(Postgres 풀, NATS 클라이언트, Moka 캐시, 회로 차단기)의 동일한 프로세스에서 실행됩니다. Router의 구성만 다릅니다. 각각 시작 시 한 번 호출되고 두 개의 별도 TcpListener에 의해 제공되는 두 개의 전용 함수를 통해 다릅니다.

gateway/server.rsrust
// 포트 2개, 라우터 2개, AppState 1개

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…)가 두 계획 모두에 탑재되며 각각에 특정한 추가 경로가 있습니다. 동일한 핸들러를 재사용하면 프록시의 이중 구현을 피할 수 있습니다. 미들웨어에서만 분기하면 보안 경계를 확보하기 위해 비즈니스 논리가 중복되는 것을 방지할 수 있습니다. 백엔드 자체가 다중 서비스 화물 작업 공간으로 구성된 경우 화물 작업 공간 아키텍처 가이드를 참조하세요. 게이트웨이는 이 부문의 다른 상자 중 하나일 뿐입니다.

#
2단계

진입 시 인증이 분기됨

데이터 영역에서 API 키는 소수의 실제 공개 경로(/health, JWKS, 등록 엔드포인트)를 제외한 모든 경로에서 필수입니다. apikey 또는 X-API-Key 헤더로 이동하거나 WebSocket 및 SSE 스트리밍 경로의 경우에만 ?apikey=매개변수로 이동합니다. 코드는 service_role 키에 대해 이 마지막 모드를 명시적으로 금지합니다. URL 키는 액세스 로그, OTel 추적 및 Referer 헤더에서 누출됩니다. JWT는 데이터 플레인 측에서 선택 사항으로 유지됩니다. JWT가 없으면 호출자는 anon로 유지됩니다. 이를 사용하면 authenticated이 됩니다.

관리 플레인에는 API 키가 존재하지 않습니다. JWT 콘솔만 허용되며 해당 콘솔의 대상은 정확히 aurabase-control이어야 합니다. 역할은 토큰 자체에 의해 전달되지 않습니다. 이는 프로젝트 → 조직 관계를 통해 상속된 프로젝트를 소유한 조직의 사용자 멤버십에서 요청할 때마다 다시 계산됩니다.

토큰이 필요합니다API 키(apikey/X-API-Key), 항상JWT 콘솔(권한: Bearer), 항상
역할 승격선택적 JWT: 익명 → 인증됨조직(소유자/관리자/개발자/뷰어)으로부터 상속받은 RBAC
쿼리 문자열의 키WS/SSE에서만 허용되며 service_role에서는 허용되지 않습니다.해당 없음
예상 청중대상 프로젝트(경로의 UUID)"aurabase-control" 수정
코르스와일드카드 * 허용됨와일드카드는 거부됨, Studio 오리진만 해당
#
3단계

미들웨어의 실제 순서(그리고 그것이 중요한 이유)

연속적인 .layer() 호출을 포함하는 axum 스택 미들웨어 — 실행 순서를 제어하는 규칙은 실제로 놀랍습니다. 배치된 마지막 .layer()는 가장 바깥쪽 레이어가 되므로 들어오는 요청이 가장 먼저 통과하고 응답이 떠나는 것을 마지막으로 확인합니다. 따라서 파일의 선형 읽기는 실제 실행 순서의 역순을 제공합니다.

gateway/router.rsrust
// 다음과 같이 작성되었습니다(실제 추출, 파일 순서):

service_routes(...).merge(data_plane_extra)
    .layer(metrics_auth_middleware)      // (1) 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 미들웨어는 RESPONSE에만 X-Request-Id 헤더를 설정하고 들어오는 요청에는 설정하지 않습니다. AccessLogLayer는 파일에서 그 뒤에 배치되므로 더 외부적이므로 이전에 통과하므로 request_id 필드 캡처는 체인에서 추가로 생성된 식별자가 아니라 클라이언트가 보낸 헤더를 읽습니다. 호출자가 X-Request-Id를 제공하지 않은 경우 액세스 로그 줄은 빈 필드로 남고 반환된 응답에는 새로 생성된 UUID가 전달됩니다. 숨겨진 결함이 아닙니다. .layer() 문자열이 기록되는 순서는 우리가 해당하는 논리적 순서에 대해 어떤 것도 보장하지 않는다는 점을 상기시켜줍니다.

#
4단계

속도 제한: IP 우선, 행위자 그 다음

속도 제한은 체인의 서로 다른 두 시간에 두 개의 개별 패스로 적용됩니다. 첫 번째는 인증 전에 실행되고 IP 주소로 제한됩니다. 이는 공공 도로에서도 활성화되는 일반 홍수 방지 필터입니다. 이 필터가 없으면 인증되지 않은 흐름으로 인해 JWT 검사를 트리거하지 않고도 로그 집계와 같은 비용이 많이 드는 엔드포인트를 망칠 수 있습니다. 두 번째는 AFTER 인증을 실행하고 인증에서 방금 삽입한 클레임을 사용하여 행위자(API 키 또는 사용자)별로 제한합니다. 이는 청구 및 계획에 포함되는 실제 제품 할당량입니다.

구현은 게이트웨이 인스턴스 간 배포를 위한 슬라이딩 창 Lua Redis 스크립트와 Redis를 사용할 수 없는 경우 로컬 폴백(Moka 캐시)을 사용하여 로컬 계산을 위해 governor 상자(토큰 버킷)를 사용합니다. 리포지토리 기본값: 초당 요청 100개, 버스트 1000개.

#
5단계

차단기는 레이어가 아닌 대상별 개체입니다.

체인의 나머지 부분과 달리 회로 차단기는 ANY .layer()에 나타나지 않습니다. AppState는 서비스(인증, db, 실시간, 스토리지, 기능, 알림, ai, 프로비저너, 제어)당 하나의 CircuitBreaker 인스턴스를 전달하며, 요청을 시도하기 전에 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에 의해 반환됨
}

프로브 토큰은 명시적으로 사용되지 않는 경우 Drop로 반환됩니다. 이는 요청을 해제한 분기에 도달하지 않고 요청 시간이 초과될 때 유용합니다. 그리고 자동 재생은 NATS 측(NoResponders)의 엄격한 비전달 증명에서만 트리거됩니다. 간단한 게이트웨이 시간 초과는 요청의 실제 전달에 대해 아무 것도 증명하지 않으며 재생하면 두 번 실행될 수 있습니다.

#
6단계

마지막 링크: NATS 또는 직접 HTTP, 절대로 무작위로 사용하지 않음

최종 프록시는 단일 프로토콜을 거꾸로 사용하지 않으며 경로에 따라 선택이 고정되지 않습니다. HTTP 메서드 또는 데이터베이스 조회에 따라 달라질 수 있습니다. 대부분의 트래픽(인증, 기능, 알림, 제어 및 대부분의 db)에 대해 게이트웨이는 NATS 봉투에서 HTTP 요청을 직렬화하고 이를 서비스 전용 주제에 요청/응답으로 보냅니다. 이는 TCP 핸드셰이크가 없는 왕복이며 이 RPC 유형 트래픽에 대한 기존 HTTP 프록시보다 훨씬 빠른 것으로 코드에 문서화되어 있습니다.

저장소, 실시간의 세 가지 변형(브로드캐스트/채널/존재를 위한 REST) 및 조건에 따라 Postgres CRUD 요청은 이 경로를 종료하고 실시간 풀링된 HTTP 클라이언트를 통과합니다. Storage는 이를 명시적으로 선택했습니다. NATS 봉투에서 이진 본문을 인코딩하려면 이를 직렬화하고 양쪽 끝에서 메모리에 완전히 로드해야 하며 NATS 메시지 크기 제한을 유지해야 합니다. 이는 대형 개체의 경우 실제 비용입니다. WebSocket과 SSE는 단순히 요청/응답 의미 체계를 허용하지 않습니다. 프로토콜 업그레이드와 열려 있는 흐름에는 이에 상응하는 NATS가 없습니다.

가장 흥미로운 사례는 /v1/db/*입니다. 핸들러는 각 요청에 대해 스스로 결정합니다. 관리 경로(스키마, 정책, 원시 SQL)는 항상 NATS에서 aura-db로 이동하고, PUT는 항상 NATS로 이동하고(PostgREST는 전체 교체 시 405를 반환함) MongoDB 프로젝트는 항상 NATS로 이동하며, 전용 PostgREST 인스턴스가 확인된 Postgres 프로젝트의 CRUD만 Direct HTTP로 이동합니다. 이 전용 인스턴스가 해결되지 않으면 게이트웨이는 공유 PostgREST로 폴백하는 대신 503에 응답합니다. 성능이 저하된 자동 폴백이 아닌 장애 폐쇄 가정입니다. 보안 헤더(엄격한 CSP, CORS 자격 증명 없음)는 응답이 게이트웨이를 떠나기 전에 체인의 맨 끝에 배치된 모든 경로에 균일하게 적용됩니다.

#
7단계

전역 시간 초과가 아닌 경로당 시간 초과 예산

게이트웨이는 전역 시간 초과가 아닌 TimeoutLayer PER GROUP 경로를 적용합니다. 이는 미들웨어 순서와 동일한 스태킹 메커니즘에 연결된 선택입니다. Edge 기능 경로에는 나머지 경로보다 훨씬 더 긴 예산이 필요합니다(기능은 몇 분 동안 합법적으로 실행될 수 있음). 저장소 기본값은 대부분의 경로에서 30초인 반면 /v1/functions/*의 경우 380초입니다.

모든 것 위에 단일 전역 TimeoutLayer을 쌓으면 두 그룹 모두 동일한 제한이 적용됩니다. 더 안쪽에 배치된 더 긴 시간 제한에 관계없이 항상 가장 바깥쪽 위치에 배치된 가장 짧은 제한 시간이 승리합니다. 따라서 기능에 별도의 예산을 부여하는 유일한 방법은 공통 랩에 들어가지 않는 것입니다. 경로의 각 분기는 두 라우터가 병합되기 전에 배치된 자체 TimeoutLayer을 전달하며 이후에는 전역 시간 제한이 적용되지 않습니다.

#
기억하다

이 패턴을 다른 곳에서 재현해 보세요: 체크리스트

  1. 서비스가 아닌 PLAN(노출 표면)으로 분리: 손상된 공개 SDK는 관리 대시보드의 CORS 원본 목록에 절대 도달해서는 안 됩니다.
  2. 두 개의 별도 배포가 아닌 단일 공유 상태를 유지하십시오. 비즈니스 로직을 복제하는 것은 라우터를 복제하는 것보다 비용이 더 많이 듭니다.
  3. 파일의 선형 읽기가 아닌 마지막 .layer()에서 추적하여 실제 미들웨어 순서를 확인하세요.
  4. IP당(인증 전) 속도 제한과 행위자당 할당량(후)을 분리합니다. 그렇지 않으면 인증되지 않은 흐름으로 인해 제한 없이 비용이 많이 드는 검증이 필요합니다.
  5. 차단기를 프록시의 실제 네트워크 호출에 최대한 가깝게 배치하고 장애 도메인이 공유되지 않는 경우 대상별로 크기를 조정합니다.
  6. 단순한 시간 초과가 아닌 배달 실패 증명에 대해서만 요청을 재생합니다.
  7. 라우터를 병합하기 전에 각 경로 그룹에 자체 시간 초과 예산 세트를 제공하십시오. 가장 긴 예산을 덮어쓰는 글로벌 TimeoutLayer는 절대 제공하지 마십시오.

배포할 준비가 되셨나요?

5분 만에 백엔드를 완성하세요.

신용카드 불필요 · 500MB 무료 · 50,000 MAU