Search
Duplicate

API Gateway Part 3 - Kong 도입(2)

Tags
Engineering

이전편 링크

Plugin 개발

Lua 개발환경 설정

Kong Plugin 개발의 시작점은 아래 공식문서이다. Nginx 는 lua-nginx-module 을 통해 Lua 스크립트를 이용해 확장 기능을 추가할 수 있다. Nginx 에 해당 모듈을 포함하여 컴파일 하는 대신에, Kong 은 OpenResty 라는, 이미 lua-nginx-module 이 포함된 버전과 함께 배포된다. Kong 의 Plugin 을 개발하는 것은 OpenResty 확장을 개발하는 것과 매우 비슷한 작업이다. 이 작업은 Kong 의 Plugin Developement Kit: PDK 를 이용하는 개발 작업이다.
아래의 명령으로 homebrew 를 이용해 OSX 환경에 Lua 컴파일러를 설치하고, REPL을 실행해볼 수 있다.
$ brew install lua Lua 5.3.5 Copyright (C) 1994-2018 Lua.org, PUC-Rio > print "hello, world" hello, world
Bash
하지만, Kong Plugin 을 개발하기 위해서는 Nginx 의 lua-nginx-module 을 이용하여 Nginx 의 다양한 기능 (특히 logging과 request/response)을 사용하는 것이 필수적이다. 그러므로 아래 명령을 이용해 OpenResty 를 설치라면 수월하게 개발을 진행할 수 있다.
$ brew install openresty/brew/openresty
Bash
Nginx 를 이용해 웹사이트의 데이터를 가져오는 간단한 코드를 작성하고, resty를 이용해 실행해보자.

Plugin 구조

가장 간단한 방식의 Kong Plugin 은 아래 두 개의 모듈로 구성된다.
simple-plugin ├── handler.lua └── schema.lua
Bash
handler.lua 실제 실행되는 비즈니스 로직이 담긴 파일. Request / Connection Lifecycle 을 여기서 처리한다.
schema.lua Plugin 에 대한 설정에 대한 shema 을 구성하는 파일.

Plugin 등록 및 적용

Plugin 을 등록하기 위해서는, 시스템에 Lua 모듈을 등록하고, 등록한 Plugin 을 Kong 에서 사용할 수 있도록 활성화해주는 두가지 과정이 필요하다. Kong 이 시스템에서 Lua 모듈을 찾아올 때, lua_package_path 를 참조하므로, 설정 파일(kong.conf)에 명시하거나, 환경변수를 사용해 등록할 수 있다. 설정파일에 등록하는 모든 값은 환경변수를 통해 읽어올 수 있고, 앞에 KONG_ 이라는 prefix 를 붙인 대문자 이름으로 작성해주면 된다. 따라서 Kong 에 Lua 모듈을 등록하기 위해 환경변수 KONG_LUA_PACKAGE_PATH 값을 등록하면 된다.
예를 들어, kong.conf 파일에 아래와 같이 등록되거나,
lua_package_path=/plugins/?.lua
Bash
환경변수에 아래와 같이 등록된다면,
KONG_LUA_PACKAGE_PATH=/plugins/?.lua
Bash
/plugins/ 하위의 모든 *.lua 파일을 찾아 module 형식으로 등록되고, 이 중 kong.plugins.<plugin_name>.<module_name> 에 해당하는 module 들이 Plugin 으로 등록된다. 디렉토리 구조가 다음과 같은 경우, kong.plugins.hello-world.handlerkong.plugins.hello-world.schema 두 모듈이 kong.plugins.hello-world라는 하나의 Plugin 으로 등록된다.
$ tree ./plugins ./plugins └── kong └── plugins └── hello-world ├── handler.lua └── schema.lua
Bash
그리고, 설정 파일 또는 환경변수를 통해
plugins = bundled,hello-world
Bash
또는
KONG_PLUGINS=bundled,hello-world
Bash
와 같이 설정하면, 새로 작성한 hello-world 라는 Plugin 을 사용할 수 있다.

Hello-World Plugin

아래는, Kong 으로 받은 요청을 처리하면서, Nginx 의 ERR 레벨로 Hello World 를 출력하고, 응답 헤더에도 X-Hello-World 라는 항목에 설정에 따른 값을 추가하는 hello-worldPlugin 의 코드이다.
-- file: plugins/kong/plugins/hello-world/schema.lua -- Plugin 설정에서 say_hello = true | false 를 설정할 수 있도록 한다. return { no_consumer = true, fields = { say_hello = { type = "boolean", default = true } } } `plugins/kong/plugins/hello-world/handler.lua` local BasePlugin = require "kong.plugins.base_plugin" local HelloWorldHandler = BasePlugin:extend() -- 기본 Plugin 을 확장한다. 서브클래스를 만드는 것과 유사하다. HelloWorldHandler.PRIORITY = 2000 -- Plugin 실행 우선순위를 지정한다. function HelloWorldHandler:new() HelloWorldHandler.super.new(self, "hello-world") end function HelloWorldHandler:access(conf) HelloWorldHandler.super.access(self) -- 설정에서 say_hello boolean 값을 가져와서, 값에 따라 다른 응답을 추가한다. -- ngx 요청/응답을 읽고 쓰는 기능은 OpenResty 의 문서를 살펴보면 된다. if conf.say_hello then ngx.log(ngx.ERR, "============ Hello World! ============") ngx.header["X-Hello-World"] = "Hello World!!!" else ngx.log(ngx.ERR, "============ Bye World! ============") ngx.header["X-Hello-World"] = "Bye World!!!" end end return HelloWorldHandler
Lua

Plugin 등록 및 테스트

_format_version: "1.1" services: - name: json-placeholder url: https://jsonplaceholder.typicode.com plugins: - name: hello-world config: say_hello: true routes: - name: json-placeholder-route paths: - /json - name: static-response url: https://google.com plugins: - name: request-termination config: status_code: 200 message: "hello, world!" routes: - name: hello-world-route strip_path: false paths: - /hello
YAML
kong.yml 파일을 위처럼 수정하고, Kong 을 재시작 (kong stop && kong start -c kong.conf) 하고 다시 요청하면, 요청 응답에 Kong-Hello-World 헤더가 포함된 것을 확인할 수 있을 것이다.
$ http :8000/json/todos/1 HTTP/1.1 200 OK Access-Control-Allow-Credentials: true CF-Cache-Status: MISS CF-RAY: 51b16c612df39845-LAX Cache-Control: public, max-age=14400 Connection: keep-alive Content-Encoding: gzip Content-Type: application/json; charset=utf-8 Date: Tue, 24 Sep 2019 02:45:38 GMT Etag: W/"53-hfEnumeNh6YirfjyjaujcOPPT+s" Expect-CT: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" Expires: Tue, 24 Sep 2019 06:45:38 GMT Pragma: no-cache Server: cloudflare Set-Cookie: __cfduid=d316de6bcb239812cd0a96e2adaa6b4a31569293138; expires=Wed, 23-Sep-20 02:45:38 GMT; path=/; domain=.typicode.com; HttpOnly Transfer-Encoding: chunked Vary: Origin, Accept-Encoding Via: kong/1.2.1 X-Content-Type-Options: nosniff X-Hello-World: Hello World!!! X-Kong-Proxy-Latency: 80 X-Kong-Upstream-Latency: 697 X-Powered-By: Express { "completed": false, "id": 1, "title": "delectus aut autem", "userId": 1 }
Bash

인증 문제 해결을 위한 Plugin 개발

플랫폼 서비스는 authorization 헤더에 담긴 Bearer Token 을 통한 인증 및 인가를 직접 처리한다. 하지만, 하위서비스는 Token 인증에 대한 기능을 가지고 있지 않다. 하위서비스에 인증 기능을 추가하는 대신, 해당 기능을 Kong 에 통합하기로 했다. 하지만 jwt 관련 플러그인은 Kong CE 버전에 제공되지 않고 내부 비즈니스 로직에 의존성이 있는 관계로 직접 개발하는 것으로 진행되었다.
인증 문제 해결을 위한 Plugin 의 전체코드를 소개하기는 어렵지만, 대강의 구조와 코드는 다음과 같다.
파일 구조:
$ tree ./plugins ./plugins └── kong └── plugins └── glam-token-validator ├── access.lua ├── handler.lua └── schema.lua
Bash
-- filename: schema.lua -- 환경설정을 통해 받을 수 있는 값을 이곳에서 정의한다. local typedefs = require "kong.db.schema.typedefs" return { name = "glam-token-validator", fields = { { config = { type = "record", fields = { { validate_token_url = typedefs.url({ required = true }) }, -- query_string 에서 access_token 을 분리할 수 있도록 설정. -- 기본적으로는 HTTP Header 에서 Bearer access_token 을 추출해 사용. { uri_param_names = { type = "set", elements = { type = "string" }, default = { "access_token" }, }, }, { keepalive = { type = "number", default = 60000 }, }, }, }, } }, }
Lua
- filename: handler.lua - 비즈니스 로직의 흐름을 이곳에서 정의한다. - Token 검증을 수행하고, 응답 값을 받아서 응답값을 요청 헤더에 추가한 후, - 변경된 요청을 이용하여 하위 서비스를 호출한다. local BasePlugin = require "kong.plugins.base_plugin" local access = require "kong.plugins.glam-token-validator.access" local fmt = string.format local req_set_header = ngx.req.set_header local GlamTokenValidator = BasePlugin:extend() local kong = kong function GlamTokenValidator:new() GlamTokenValidator.super.new(self, "glam-token-validator") end function GlamTokenValidator:access(conf) GlamTokenValidator.super.access(self) local err, user_data = access.do_authenticate(conf) if err then return kong.response.exit(err.status, { error = "error occcurred", }) end req_set_header("extra-header-for-request", user_data.extra_data) end return GlamTokenValidator
Lua
access.lua : 비즈니스 로직의 세부 구현을 정의.
-- filename: access.lua -- 비즈니스 로직의 세부 구현을 이곳에 작성한다. -- (유틸리티 함수와 세부 구현이 생략되어 있다) local _M = {} local function retrieve_token(conf) -- 설정값에서 `access_token` 을 검증할 주소(`validate_token_url`)를 획득하고, -- Kong 요청에서 `access_token` 을 분리하여 리턴 local authorization_header = kong.request.get_header("authorization") if authorization_header then local iterator, iter_err = re_gmatch( authorization_header, "\\s*[Bb]earer\\s+(.+)") -- 에러 처리 또는 access_token 리턴 return access_token end end function _M.do_authenticate(conf, token) local timeout = conf.timeout or 3000 local http_endpoint = conf.validate_token_url token = token or retrieve_token(conf) -- token 획득에 실패했다면 HTTP 400 응답 생성 및 종료 if token == nil then return { status = 400, message = "invalid_request" } end -- validate_token_url 에 access_token 을 검증하는 HTTP 요청을 실행하고 -- 결과를 리턴한다. (코드 생략됨) return false, response_json end return _M
Lua
Plugin 등록을 위해 kong.conf 파일 또는 환경변수를 아래와 같이 수정하고, kong.yml 파일에 Plugin 설정 값을 등록하면 모든 준비가 끝난다.
# filename: kong.conf lua_package_path = /plugins/?.lua plugins = bundled,hello-world,glam-token-validator
Lua
환경변수를 사용할 경우
KONG_LUA_PACKAGE_PATH=/plugins/?.lua KONG_PLUGINS=bundled,hello-world,glam-token-validator
Bash
kong.yml - glam-token-validator 적용
_format_version: '1.1' services: - name: glam-svc1-public url: 'http://svc1.com' routes: - name: glam-svc1-public-route strip_path: false paths: - /public/svc1 - name: glam-svc1-protected url: 'http://svc1.com' plugins: - name: glam-token-validator config: validate_token_url: 'http://auth-svc.com/oauth/validate_token' routes: - name: glam-svc1-protected-route strip_path: false paths: - /public/svc1 - name: glam-svc2-public url: 'http://svc2.com' routes: - name: glam-svc2-public-route strip_path: false paths: - /public/svc2 - name: glam-svc2-protected url: 'http://svc2.com' plugins: - name: glam-token-validator config: validate_token_url: 'http://auth-svc.com/oauth/validate_token' routes: - name: glam-svc2-protected-route strip_path: false paths: - /api/svc2 - name: health-check url: 'https://example.com' plugins: - name: request-termination config: status_code: 200 message: 'health check response' routes: - name: health-check-route strip_path: false paths: - /health-check
YAML
이제 access_token 을 이용한 보안 처리를 http://auth-svc.com/oauth/validate_token 이라는 내부 엔드포인트를 이용해 단일화하고, 보안에 대한 비즈니스 로직 처리를 Kong 으로 통합하게 되었다.

개발 및 실서비스용 Docker Container 생성

Kong 설정 중 대부분은 환경변수를 이용하여 설정할 수 있다. Declarative Config 를 개발용과 실서비스용을 분리하는 부분이 애매한 부분이 있었는데, 두가지 설정 파일을 모두 주입하고, 환경변수를 통해 필요한 설정파일은 선택해서 사용하는 방식으로 이미지를 빌드했다.
추가로 개발한 커스텀 플러그인 역시, 환경변수 설정과 파일 복사로 적용이 되었다.
FROM kong:1.2.1-alpine ENV KONG_DATABASE off ENV KONG_LUA_PACKAGE_PATH /plugins/?.lua ENV KONG_PLUGINS bundled,hello-world,glam-token-validator ENV KONG_DECLARATIVE_CONFIG /usr/local/kong/declarative/kong.yml ENV KONG_PROXY_ACCESS_LOG /dev/stdout ENV KONG_ADMIN_ACCESS_LOG /dev/stdout ENV KONG_PROXY_ERROR_LOG /dev/stderr ENV KONG_ADMIN_ERROR_LOG /dev/stderr ENV KONG_ADMIN_LISTEN 0.0.0.0:8001, 0.0.0.0:8444 ssl RUN mkdir -p /plugins RUN mkdir -p /usr/local/kong/declarative COPY ./config/kong.yml /usr/local/kong/declarative/kong.yml COPY ./config/kong-prod.yml /usr/local/kong/declarative/kong-prod.yml COPY ./plugins/ /plugins/
Plain Text

배포

현재 GLAM 백엔드 서비스 중 Kong API Gateway 를 포함한 일부 서비스는 ECS 클러스터 내에서 운영되고 있다. ECS 배포에 대한 모든 내용을 기술하기는 어렵지만, 컨테이너를 생성하는 방식을 docker-compose.yml 파일의 형식으로 설명하자면 다음과 같다.
개발 환경:
version: "3" services: glam-api-gateway-test: image: glam/api-gateway-test:1.0.1 environment: - KONG_DECLARATIVE_CONFIG=/usr/local/kong/declarative/kong.yml ports: - "8000:8000" - "8001:8001"
YAML
서비스 환경:
version: "3" services: glam-api-gateway-prod: image: glam/api-gateway-prod:1.0.1 environment: - KONG_DECLARATIVE_CONFIG=/usr/local/kong/declarative/kong-prod.yml ports: - "8000:8000" - "8001:8001"
YAML
KONG_DECLARATIVE_CONFIG 환경변수를 조정함으로서 세부 설정(Route 및 Plugin 세팅)을 다르게 하여 컨테이너를 실행할 수 있다.

도입 회고

서비스의 도입으로 현재의 서비스 구성은 아래와 같이 변경되었다.
서비스의 입구가 API Gateway 로 일원화 되었으며, 하위서비스의 인증도 별도의 인증 서비스 응답으로 통합되었다. 실제 구성은 AWS Application Load Balancer, 일부 레거시 서비스 등의 존재로 그림과 다른 면이 있으나, 하위서비스의 인프라 구성이 일관성있게 정리되었고 배포 과정 역시 독립되어 빠른 배포가 가능하게 되었다.

앞으로의 진행 방향

서비스의 QoS 를 관리하기 위해 rate-limit 플러그인을 사용하게 되거나, 서비스 개발시 Kong 서비스 자체의 배포 의존성을 제거할 수 있도록 DB-less Config 대신 DB 기반의 설정으로 변경할 수 있을 것으로 예상한다.
API endpoint 가 추가될 때 점진적인 배포를 진행할 수 있도록 Canary Release 를 사용할 계획이 있으나, 해당 플러그인은 Kong Enterprise 버전에만 제공된다. 글램의 요구사항에 맞는 최소버전의 Canary Release Plugin 을 개발하여 이 문제를 해결할 수 있을 것이다.
Canary ReleaseCanary Release 는 새로운 버전의 소프트웨어를 운영 환경에 배포할 때, 전체 사용자들이 사용하도록 모든 인프라에 배포하기 전에 소규모의 사용자들에게만 먼저 배포함으로서 리스크를 줄이는 기법이다.API Gateway 에 Canary Release 를 적용하여 배포하면, 기존 svc1/v1/some-api 로 Route 되던  /api/some-api 가 신규 하위 서비스 svc1/v2/some-api 로 매핑될 때, 10% 요청은 신규 API 로 Route, 90% 요청은 기존 API 로 Route 하며 에러발생이나 서비스효과 등에 대한 검증을 수행한 후 %를 조정해 점진적으로 전체 사용자들이 사용하도록 조정할 수 있다.
아직 Plugin 을 테스트할 수 있는 환경이 구축되어 있지 않다. Kong Plugin 의 테스트는 spec.helpers 를 이용한 Test Suite 를 제공한다. 테스트 환경을 구축하면 더욱 견고한 코드를 작성할 수 있을 것으로 기대한다.
Kong API Gateway 는 다양한 기능을 가지고 있다. gRPC 요청을 HTTP 로 변환하여 응답할 수도 있고, AWS Lambda 나 Azure Function 을 실행하여 결과를 응답할 수 있는 Plugin 도 제공된다. 이를 통해 서비스의 요구사항에 맞는 다양한 확장을 이룰 수 있는 가능성이 추가되었다. 이는 결국 더 좋은 서비스를 제공하는 한가지 요소가 추가되었다는 의미가 있겠다.