GS리테일 DX 블로그

Digital Transformation으로 고객 생활 가치의 이노베이션을 꿈꾸는 IT 사람들의 이야기

Cloud&Security

Nginx 기반의 API Gateway 구현(with Python)

bongdang 2022. 10. 7. 14:24

시작하며

IT이노랩의 김광섭입니다.

 

GS리테일에서 업무를 한지 벌써 1년이 지났습니다. 제가 속한 이노랩에서는 여러가지 일을 하고 있으며 최근에는 GSRetail API HUB프로젝트를 통해 1 결과물을 냈습니다. 이미 다른 회사에서 API Gateway 관련 개발을 진행 했었고 현재도 사용 되고 있지만, 프로젝트를 진행하면서 시도한 새로운 경험을 공유하기 위해 글을 쓰기 시작했습니다. 기존에 이미 널리 퍼진 기술들이 있기 때문에 새로운 방식이 크게 의미가 없을 있습니다. 하지만, 기술이 쉽게 설명되고 쉽게 사용될 있으면 기술을 이해하고 사용하는 개발자들은 다른 시각을 가지고 문제를 해결 있기 때문에 개발자들에게는 작게나마 도움이 것이라고 생각합니다.

nginx ?

nginx는 대부분 알고 계시겠지만 강력한 웹서버입니다. 2022년 기준 전체 웹서버의 34% 차지하고 있는데, 2019 이후 부터 유명한 아파치보다 많이 사용 되고 있으며 지금도 점유율이 늘어나고 있습니다. 강력한 이유는 C 짜여진 탄탄한 소스와 이벤트 드리븐의 간결한 구조 등이 언급됩니다.

그런데 nginx 언급할까요? 그냥 많이 사용 되기 때문이 아닙니다.

Reverse Proxy 대해

Reverse proxy에 대해 알아 봅시다. Reverse proxy 생각보다 많이 사용 되고 있지만 느끼지 못하는 용어입니다. Proxy라는 용어가 익숙하지 않을 있지만 뭔가를 대행해 준다는 느낌은 입니다. Reverse proxy 처음 접하는 경우 설명하기 쉽지않지만, 예를 들자면 이렇습니다. 사용자는 대형가전업체의 애프터서비스를 선택하지만, 마지막에 일을 하는 것은 고용된 개인인 경우를 생각하면 좋겠습니다. 사용자의 입장에서 인지하는 것과 실제 동작하는 것이 다른 경우인데요, 일반적인 Proxy라는 것과 유사하지만 접근하는 방향이 다르다고 생각하면 쉬울 같습니다.

nginx Reverse Proxy

nginx는 Reverse proxy 강력합니다. 지금은 다른 것으로 대체가 가능하지만 쿠버네티스의 잉그레스 컨트롤러에 기본적으로 nginx 사용 되었습니다. 외부에서 들어오는 여러가지 요청을 nginx 적절한 내부 서비스로 연결해 주는 역할인데 기본적인 동작 방법은 위에 설명한 Reverse proxy 기능입니다.

API Gateway 동작

API Gateway 동작 방법을 알아보겠습니다. 사용자는 특정한 API 호출하기 위해 필요한 정보가 있습니다. 대표 URL, Access KEY, API 사용법등이 기본이라고 있습니다. 예를 들어 "https://api.gsretail.com"이라고 공개될 대표 URL, 그리고 보통은 의미가 없는 문자열을 가지는 Access KEY, 그리고 원하는 서비스 API , products/items 같은 하위 서비스 URL 그것에 해당하는 사용법에 대한 설명이 있습니다. 사용자가 https get 방식으로 "https://api.gsretail.com/products/items/12345" 같은 API 호출하면 DNS 통해 변환된 IP https 요청을 보냅니다. IP에서 요청을 받고 필요한 동작을 하는 것이 API Gateway 입니다. 물론 로드밸런서가 존재하는 것이 일반적이지만 구조적인 설명에서는 API Gateway 맨 앞에 있다고 가정하고 도식화하면 아래와 같습니다.

API Gateway

 

API Gateway 실제 원하는 호출을 하기 전에 적절한 호출인지를 확인하기 위해 Access KEY 검사합니다. , 일반적으로 URL 호출하기 전에 auth 과정을 거칩니다. auth 따로 두느냐에 대한 이견들은 있지만 일부 보안과 성능문제 때문에 인증과정을 분리하는 것이 유리합니다.

auth 여러가지 방법이 가능하지만 여기서는 단순하게 토큰을 발행하는 것을 가정해 보겠습니다. 대표URL reserveded 인증 API(Auth) 규칙에 따라 호출하면(일반적으로 커스텀 헤더 방식을 사용합니다.) 이후의 API호출에 필요한 토큰 값을 돌려 받습니다. 이후에 사용자가 토큰을 가지고 원하는 API 호출하면 API Gateway Access KEY는 검사하지 않고 토큰이 맞는지만 체크한 해당하는 서비스를 호출합니다.

원하는 서비스를 호출하는 방식은 사용자가 호출할 서비스와 내부에서 개발된 엔드포인트를 테이블 형태로 가지고 있다가 사용자가 호출한 서비스를 비교해서 내부의 엔드포인트로 바꿔서 호출하고(. 호출이 Reverse proxy 동작입니다!) 리턴 값을 사용자에게 돌려주는 방식을 사용합니다.

이런 서비스의 장점은 외부로 알려지는 서비스 URL 내부 URL 테이블로 관리하므로 필요에 따라 내부 URL 바꿔서 응답을 수도 있으며, 내부 URL https 지원하지 않더라고 외부에는 https형태로 노출이 가능하게 되는 것입니다. MSA 구조에서 API Gateway 언급이 많은 이유가 다양한 내부의 서비스에 대한 관리 편리성과 공통으로 개발해야할 부분을 API Gateway 해결해 주기 때문입니다.

nginx 활용한 API Gateway

이러한 API Gateway 구현 방법에는 여러가지가 있으며 실제로 다양한 형태의 구현체가 실제 서비스에서 사용 되고 있습니다. 중에서 nginx방식을 사용하는 대표적인 경우가 kong이며 kong nginx addon으로 openresty 채택하여 lua라는 랭귀지를 사용해 필요한 기능을 구현한 경우입니다. 하지만 openresty 사용해야만 API Gateway 구현할 있는 것은 아니며 글에서는 방법에 대해 설명하고자 합니다. openresty 사용하는 방법은 nginx 해당 addon enable해야 하기 때문에 OS 기본 설치된 nginx로는 구현 하기가 어렵지만 여기서 소개하는 방법은 기본 nginx 구현이 가능하므로 응용할 있는 여지가 많다고 생각됩니다.

간략한 구현

API Gateway 기본적으로 Auth 토큰 발행, 요청한 서비스를 Reverse Proxy 호출하는 기본 기능과 서비스를 위한 토큰 관리, KEY관리 핵심입니다. 물론 IP 블랙리스트,화이트리스트 관리 호출 횟수 관리, 내부 서비스 상태 관리, 모니터링 등이 있지만 기본 동작이 되지 않는다면 의미가 없는 기능 들입니다. 이번 섹션에서는 기본 기능들의 구현에 대해 방법과 방향성에 대해 정리해 보겠습니다.

Auth 토큰 발행

초창기의 API Gateway 호출할 마다 Access KEY를 같이 보내는 방법을 쓰기도 했었습니다. 따지고 보면 크게 문제가 없어 보입니다만, Access KEY를 모든 요청에 보내는 것은 기본적인 보안 규칙에도 맞지 않았고 같은 KEY를 사용하는 다양한 클라이언트를 구분하기가 어렵다는 문제 때문에, 번째 호출에서 인증 과정을 거치고 인증에서 발행된 토큰으로 추가 호출을 하는 방식으로 굳어졌습니다. 이렇게 토큰을 발행하는 장점은 클라이언트별 동작을 구분하기가 쉬워졌고, 토큰에 유효기간을 두어 만료된 토큰은 인증을 요청하도록 구성해서 Access KEY와 토큰을 통한 호출 제어도 어느 정도 가능하게 것입니다. , 이상 동작하는 클라이언트만 골라서 억세스가 불가능하게 하거나, 유출된 Access KEY를 폐기하여 추가 접근이 불가능하게 있는 장점이 생겼습니다.

 

먼저 구현한 소스의 일부를 보겠습니다.

 

nginx 설정 파일(부분)

# parts of nginx configuration file

# gunicorn으로 동작하는 fastAPI upstream 정의. Unix Domain Socket 사용.
upstream auth_server {
    server unix:/tmp/gunicorn1.sock fail_timeout=0;
}
upstream token_server {
    server unix:/tmp/gunicorn2.sock fail_timeout=0;
}

...

# Auth를 담당하는 reserved 서비스. python 모듈의 keyauth를 호출함.
location /auth {
    proxy_pass_request_body off;            # 인증할 때 body는 필요 없음. (속도를 위한 통신데이터 절약)
    proxy_pass http://auth_server/keyauth;
}

# 토큰을 체크하기 위해 만든 내부 서비스. python 모듈의 checktoken을 호출함.
location /check_token {
    proxy_pass_request_body off;            # 토큰 검사할때에도 body는 필요 없음.
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;
    proxy_set_header X-Original-URI $request_uri;
    proxy_set_header X-Api-Subqry $subqry;
    proxy_set_header X-Api-SubArgs $subargs;
    proxy_set_header X-Client-IP $clientip;
    proxy_pass http://token_server/checktoken;
}

# 실제 API Gateway 서비스. 모든 호출에 대해 처리
location ^~ / {
    set $subqry $uri;
    set $subargs $args;
    set $clientip $remote_addr;
    proxy_pass_request_headers on;
    auth_request /check_token;             # 먼저 토큰 검사를 위해 내부 서비스 /check_token 호출
    proxy_intercept_errors on;
    error_page 401 = @myerror401;
    auth_request_set $new_api_url $upstream_http_x_new_api_url;             # 정상 인증시 세팅될 변수
    proxy_pass $new_api_url;                 # reverse proxy
}

...
fastAPI로 구현된 python 소스(부분)
@app.post("/keyauth")
async def keyauth(response: Response, x_api_key: Union[str, None] = Header(default=None)):
    status_code, ret_data = await handle_request(x_api_key, None, None)
    response.status_code = status_code
    return ret_data

...

@app.get("/checktoken")
async def checktoken(
    response: Response,
    x_api_access_token: Union[str, None] = Header(default=None),
    x_api_subqry: Union[str, None] = Header(default=None),
    x_api_subargs: Union[str, None] = Header(default=None)
):
    status_code = 401          # assume not autorized.
    if x_api_access_token is not None:
        status_code = await tokens_class.check_access_token(x_api_access_token)
        if status_code == 200 and x_api_subqry is not None:
            status_code, new_url = await abumap_class.get_route_path(x_api_subqry, x_api_subargs)
            response.headers["x-new-api-url"] = new_url
        else:
             ...(생략)...
    response.status_code = status_code
    return ""
 

이런 Auth 구현하기 위해 여기서는 핸들러 루틴을 Python FastAPI 사용해서 간단하게 구현했습니다. 주의할 점은 http connection less , 새로운 호출이 일어날 마다 새로운 세션이 시작된다는 것입니다. nginx 새로운 접속 마다 주요 내부 변수들이 리셋 됩니다. 따라서 인증되고 발급된 토큰을 다른 프로세스에서 가지고 있어야 하는데, 문제를 해결하기 위해 Python으로 구현한 내부 서비스를 따로 실행하는 방법을 선택했습니다. 다시 설명하자면, Auth 담당하는 서비스는 인증후에 발행된 토큰을 가지고 있어야 이후에 호출되는 서비스에서 토큰을 비교해서 맞는 요청인지 확인 있다는 것입니다. 물론, 데이터베이스 등을 사용해서 발행된 토큰을 따로 관리하고 이후의 요청에서는 데이터베이스를 조회하는 방법을 사용할 수도 있습니다. 하지만, 짧은 기간동안 대용량의 API호출이 되는 상황을 가정했을 때는 I/O 많아서 성능에 영향을 주게 되므로 되도록 I /O 적은 구조를 선택하거나 메모리 DB등을 사용하는 방법을 사용하는 경우가 많습니다.

구현에서는 I/O 최소화 하기 위해 DB 사용하지 않으며, PUB/SUB 구조를 사용해 토큰 핸들러에 데이터를 전달했습니다. 최소의 구현에서는 이런 구조도 필요하지 않지만 최대의 성능을 위해 nginx worker갯수 만큼은 토큰핸들러가 동작하는 것이 좋다고 판단했으며 전체의 흐름를 볼때 API호출에 비해 인증이 훨씬 적은 것이 일반적인 구조(최악의 경우는 반대이기도 합니다.)라고 판단하여 인증은 한개의 구조로 구현 했습니다. 특히, 인증 서비스(작지만 내부에서 호출되는 API 형태입니다.), PUB/SUB Unix Domain Socket 사용하여 일반 소켓의 접속과 해제에 사용 되는 리소스와 Latency 줄일 있도록 하였습니다.

, 사용자가 등록된 Access KEY로 인증 API 호출하면 임의의 nginx 설정된 directive 통해 Python으로 구현된 핸들러(Unix Domain Socket으로 설정된 Upstream) 호출하고, 핸들러는 억세스 토큰과 리프레시 토큰을 발행하고, 토큰 핸들러들에게 발행된 억세스토큰을 퍼블리싱 nginx 리턴합니다. 핸들러는 추가 동작으로 억세스토큰의 유효시간 관리를 위해 타이머를 동작 시킵니다.

요청한 서비스의 Reverse Proxy

동작은 nginx 지원하는 Auth directive 통해 구현이 가능한데, nginx 여러가지 인증을 자체적으로 지원하는 이외에 auth_request directive 사용해서 특정 upstream 인증을 요청할 있습니다. 만약에 해당 upstream에서 200 정상 응답을 하지 않는 경우에는 nginx에서 401 Unauthorized error return 합니다. 정상응답의 경우에는 auth_request directive 아래에 있는 proxy_pass 호출하게 되는데 방식을 통해 원하는 서비스로 reverse proxy 가능하게 되는 것입니다.

사용자가 위에서 발급된 억세스 토큰을 헤더에 넣고(헤더를 사용한다고 가정) API 호출하면 nginx 먼저 auth_request directive 있는 토큰 핸들러(토큰 핸들러도 Unix Domain Socket으로 설정된 Upstream입니다.) 호출하고 토큰 핸들러는 핸들러가 퍼블리싱한 토큰을 서브스크라이브 해서 가지고 있다가 요청한 호출이 가지고 있는 토큰중에 있는지 확인하고 있다면 200 리턴 합니다.

nginx 토큰 핸들러의 리턴 값이 정상일 경우에 proxy_pass 통해 호출(Reverse proxy입니다!)하는데 여기서 가장 문제가 있습니다. 문제 때문에 기존에 openresty같은 방법을 사용했던 것이라고 추측이 됩니다. 최근의 nginx auth_request_set 이라는 directive 가지고 있는데 nginx directive auth_request에서 Variables 설정할 있게 줍니다. 방식이 글에서 설명하는 기본으로 설치된 nginx를 이용한 API Gateway 핵심 부분입니다.

, 토큰 핸들러에서 토큰 비교만 하는 것이 아니라, 등록된 서브URL 내부 엔드포인트의 매핑데이터를 가지고 있어서 토큰 비교에 성공하면 특정 값을 nginx 돌려 주는 것입니다. 여기서는 등록된 엔드포인트를 돌려주는데, 사실 방법은 다양하게 응용이 가능 하다고 보입니다. 기본으로 설치된 nginx 내가 만든 특정 서비스가 원하는 값을 전달해 있으므로 필요한 경우 Chain Reaction 가능해져 여러가지 변형된 기능 구현을 생각해 있을 같습니다.

추가 기능들

실제 서비스를 위해서는 앞서 설명한 두 가지 기능 이외에도 여러 가지가 필요합니다. 등록/삭제. 서비스 엔드포인트 설정 관리 동작에 필요한 기능 이외에도 관리를 위한 모니터링, 제한사항 관리, 비정상 호출을 막기위한 여러가지 기능등이 있어야 것입니다. 글에서는 이러한 기능에 대한 설명은 생략하겠습니다. 동작하는 전체 소스는 공유할 예정입니다.

성능에 관한 고찰

이렇게 비교적 쉽게(?) 기본 nginx Python 가지고 API Gateway 구현해 봤는데 성능은 어떨까 궁금하실 것입니다. 많은 클라이언트가 사용하지 않는 경우에는 간단하게 쓰기에 무리가 없을 정도인데, 오래된 i7, 16GB 메모리의 리눅스머신에서 API Gateway 동작시키고 다른 PC에서 locust 사용해서 진행한 테스트에서 4,000 TPS정도는 무리없이 처리할 있는 수준입니다. AWS t2.micro 인스턴스에서도 1000TPS이상 성능을 확인했습니다. 멀티 노드는 다른 이슈가 생깁니다만, 특히 인증은 적고 서비스 호출이 많은 내부 서비스 같은 경우에는 구조적으로 구현 자체가 복잡하지 않아서 간단하게 사용하는 검토는 가치가 있을 것입니다. 참고로 아래 그림에서 노란색이 튀는 부분은 토큰 유효기간 체크 기간을 짧게 두어 자주 체크하는 경우를 만들어 테스트한 상황에서 캡쳐한 것입니다.

첨언

이노랩에서 개발한 GSRetail API HUB 사용된 API Gateway 글에서 설명한 방식을 사용하지는 않았습니다. 글은 API Gateway 동작 방식에 대한 글이라고 이해해 주면 좋겠습니다. 시작에서 언급했지만, 동작을 이해하고 있으면 문제를 해결하기가 쉬워진다고 생각합니다. 글이 다양한 문제 해결에 도움이 되기를 기대해 봅니다.

 



김광섭 | 뉴테크본부 IT Inno Lab.

그냥 프로그래머 입니다.