APISIX와 Lua를 활용한 MongoDB API 동적 라우팅 구현 (feat. RESTHeart)

1 min read

최근 마이크로서비스 아키텍처(MSA)에서 API 게이트웨이는 인증, 로깅, 속도 제한 등 공통 기능을 처리하는 핵심 컴포넌트로 자리 잡았습니다. 특히 Apache APISIX는 Lua 스크립트를 통해 동적이고 유연한 라우팅 및 플러그인 개발이 가능하여 높은 인기를 얻고 있습니다.

이 글에서는 APISIX의 강력한 Lua 실행 환경을 활용하여, MongoDB를 위한 REST API 서버인 RESTHeart와 연동하는 실전 예제를 다룹니다. 이 구조를 통해 API 게어트웨이 단계에서 직접 MongoDB 데이터를 조회, 등록, 수정, 삭제하는 동적 CRUD(Create, Read, Update, Delete) 엔드포인트를 구현하는 방법을 상세히 소개합니다.

아키텍처 개요

우리가 구현할 시스템의 구조는 매우 간단하면서도 강력합니다.

[Client][APISIX (Lua Plugin)][RESTHeart (MongoDB REST API)][MongoDB]

  1. Client: API 게이트웨이에 표준 HTTP 요청을 보냅니다.
  2. APISIX (Lua Plugin):
    • 클라이언트의 요청(HTTP Method, Query Parameter, Body)을 분석합니다.
    • 요청에 맞춰 내부적으로 RESTHeart API를 호출할 URL과 데이터를 구성합니다.
    • RESTHeart에 HTTP 요청을 보내고 응답을 받습니다.
    • 받은 응답을 가공하여 최종적으로 클라이언트에게 전달합니다.
  3. RESTHeart: APISIX로부터 받은 요청을 해석하여 실제 MongoDB 쿼리를 실행하고, 결과를 JSON 형태로 반환합니다.

이 구조의 핵심은 클라이언트가 RESTHeart의 존재를 전혀 알 필요가 없다는 점입니다. 모든 복잡성은 APISIX의 Lua 플러그인 레이어에서 추상화됩니다.

1. APISIX Lua 플러그인 개발: 동적 CRUD 처리

먼저, 클라이언트의 HTTP 요청 메서드(GET, POST, PUT, DELETE)에 따라 RESTHeart에 적절한 API를 호출하는 Lua 플러그인을 작성합니다.

1-1. 플러그인 기본 구조 (rest-crud.lua)

아래 코드는 resty.http 라이브러리를 사용하여 RESTHeart와 통신하는 커스텀 플러그인의 전체 구조입니다.

-- resty.http: non-blocking HTTP client
-- cjson.safe: safe and fast JSON encoding/decoding
local http = require("resty.http")
local cjson = require("cjson.safe")

local _M = {}

-- access 단계에서 실행될 함수
function _M.access(conf, ctx)
    -- 1. 클라이언트 요청 분석
    local method = ctx.var.request_method
    local uri_args = ngx.req.get_uri_args()

    -- 쿼리 파라미터에서 MongoDB의 collection과 document ID 추출
    local path = uri_args["path"] or "/users" -- 기본 collection은 'users'
    local id = uri_args["id"]

    -- 2. RESTHeart API URL 구성
    local restheart_url = "http://restheart.default.svc" .. path -- Kubernetes 내부 서비스 주소 예시

    if id then
        restheart_url = restheart_url .. "/" .. id
    end

    -- 3. HTTP 클라이언트 생성 및 요청 분기
    local httpc = http.new()
    local res, err

    if method == "GET" then
        res, err = httpc:request_uri(restheart_url, { method = "GET" })

    elseif method == "POST" then
        ngx.req.read_body()
        local body_data = ngx.req.get_body_data()
        res, err = httpc:request_uri(restheart_url, {
            method = "POST",
            body = body_data,
            headers = { ["Content-Type"] = "application/json" }
        })

    elseif method == "PUT" then
        ngx.req.read_body()
        local body_data = ngx.req.get_body_data()
        res, err = httpc:request_uri(restheart_url, {
            method = "PUT",
            body = body_data,
            headers = { ["Content-Type"] = "application/json" }
        })

    elseif method == "DELETE" then
        res, err = httpc:request_uri(restheart_url, { method = "DELETE" })
    end

    -- 4. 예외 처리 및 응답 반환
    if not res then
        ngx.log(ngx.ERR, "request to RESTHeart failed: ", err)
        return ngx.exit(502, cjson.encode({ message = "Bad Gateway: Failed to connect to backend service." }))
    end

    -- RESTHeart의 응답을 클라이언트에게 그대로 전달
    ngx.status = res.status
    ngx.header["Content-Type"] = res.headers["Content-Type"] or "application/json"
    ngx.say(res.body)

    -- 요청 처리를 여기서 종료
    return ngx.exit(res.status)
end

return _M

2. APISIX에 플러그인 등록 및 적용

개발한 Lua 플러그인을 APISIX가 인식하도록 설정해야 합니다.

  1. 파일 저장: 작성한 rest-crud.lua 파일을 APISIX의 플러그인 디렉토리(예: /usr/local/apisix/plugins/)에 저장합니다.
  2. config.yaml 설정: APISIX의 메인 설정 파일인 config.yaml에 커스텀 플러그인을 추가합니다. plugins: - ... # 기존 플러그인들 - rest-crud # 새로 추가한 플러그인 이름
  3. APISIX 재시작: apisix restart 명령으로 설정을 다시 로드합니다.
  4. Route에 플러그인 적용: Admin API를 통해 특정 경로(/crud)에 rest-crud 플러그인을 활성화하는 라우팅 규칙을 생성합니다. curl -i -X PUT http://127.0.0.1:9180/apisix/admin/routes/1 \ -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \ -H 'Content-Type: application/json' -d ' { "uri": "/crud", "plugins": { "rest-crud": {} }, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:80": 1 -- upstream은 형식적으로만 필요, 실제 요청은 플러그인이 처리 } } }'

3. 클라이언트 API 사용 예시

이제 클라이언트는 APISIX의 /crud 엔드포인트를 통해 MongoDB 데이터를 간편하게 조작할 수 있습니다.

데이터 조회 (GET)

# 'users' collection에서 '_id'가 'user123'인 문서 조회
curl "http://127.0.0.1:9080/crud?path=/users&id=user123"

데이터 등록 (POST)

curl -X POST "http://127.0.0.1:9080/crud?path=/users" \
  -H "Content-Type: application/json" \
  -d '{"_id": "user123", "name": "홍길동", "email": "hong@test.com"}'

데이터 수정 (PUT)

curl -X PUT "http://127.0.0.1:9080/crud?path=/users&id=user123" \
  -H "Content-Type: application/json" \
  -d '{"name": "김철수"}'

데이터 삭제 (DELETE)

curl -X DELETE "http://127.0.0.1:9080/crud?path=/users&id=user123"

4. 실전 적용을 위한 보완 팁

  • 인증 정보 추가: 만약 RESTHeart가 인증(Basic Auth 등)을 요구한다면, Lua 코드에서 headers 테이블에 Authorization 헤더를 추가해야 합니다.
    lua headers = { ["Content-Type"] = "application/json", ["Authorization"] = "Basic " .. ngx.encode_base64("user:password") }
  • 설정 값 외부화: RESTHeart의 URL과 같은 설정 값을 코드에 하드코딩하는 대신, 플러그인 설정(conf 파라미터)을 통해 주입받도록 수정하면 재사용성이 높아집니다.
  • 에러 핸들링 강화: res.status 코드를 검사하여 4xx, 5xx 에러에 대해 일관된 형식의 JSON 에러 메시지를 생성하면 클라이언트가 오류를 처리하기 용이합니다.
  • 성능 최적화: httpc:set_keepalive()를 사용하여 RESTHeart와의 연결을 재사용하고, httpc:set_timeout()으로 타임아웃을 설정하여 안정성을 높일 수 있습니다.

이처럼 APISIX와 Lua를 조합하면, 기존 백엔드 서비스의 변경 없이도 API 게이트웨이 레벨에서 강력하고 동적인 데이터 처리 로직을 구현할 수 있습니다. 이는 MSA 환경에서 서비스 간 의존성을 줄이고, 공통 로직을 중앙에서 관리하는 효율적인 아키텍처를 구축하는 데 큰 도움이 됩니다.

쿠버네티스 시크릿 관리, 어떤 방법이 최선일까? 4가지 방식 장단점…

쿠버네티스에서 애플리케이션을 운영할 때, DB 접속 정보나 API 키 같은 민감한 정보, 즉 ‘시크릿(Secret)’을 어떻게 관리해야 할지는 모두의 공통된 고민입니다. 관리 방식은 보안, 운영...
eve
13 sec read

루아 Lua 프로그래밍 : 모듈과 패키지 가이드

지금까지 우리는 함수로 코드를 묶고, 테이블로 데이터를 구조화하는 방법을 익혔습니다. 하지만 프로젝트의 규모가 커지기 시작하면, 모든 코드를 단 하나의 파일에 담는 것은 금세 한계에...
eve
53 sec read

루아 (Lua) 프로그래밍: 테이블과 메타테이블의 모든 것

Lua 프로그래밍의 여정에서 가장 중요하고 흥미로운 지점에 도달했습니다. 바로 Lua 언어의 심장이자 가장 중심적인 기능인 테이블(Table)입니다. Lua에는 배열, 딕셔너리, 리스트, 객체 등을 위한 별도의...
eve
1 min read