프로그래밍 언어 실전 가이드

루아 입문 시리즈 #12: Kong API Gateway 개발

devcomet 2025. 7. 1. 11:45
728x90
반응형

Kong API Gateway architecture diagram showing OpenResty, Nginx, LuaJIT integration with Redis for high-performance API management
루아 입문 시리즈 #12: Kong API Gateway 개발 썸네일

 

Kong API Gateway와 루아 스크립팅을 활용한 고성능 API 게이트웨이 구축부터 Redis 연동 기반 캐시 전략과 원자적 연산 구현까지 실전 개발 가이드를 통해 마이크로서비스 아키텍처의 핵심 기술을 마스터하는 완벽한 튜토리얼입니다.


Kong API Gateway 소개와 루아의 역할

Kong은 현재 가장 인기 있는 오픈소스 API 게이트웨이 중 하나로, 마이크로서비스 아키텍처에서 핵심적인 역할을 담당합니다.

Kong의 가장 큰 특징은 루아 스크립팅을 통한 플러그인 시스템으로, 이를 통해 개발자는 API 게이트웨이의 동작을 세밀하게 제어할 수 있습니다.

Kong plugin architecture diagram showing Lua script execution phases for access control, authentication, and rate limiting
Kong Architecture Overview

 

Kong은 OpenResty 위에 구축되어 있으며, 이는 Nginx와 LuaJIT이 결합된 웹 플랫폼입니다.

이러한 구조 덕분에 Kong은 높은 성능을 유지하면서도 유연한 확장성을 제공합니다.

 

루아 스크립팅은 Kong에서 다음과 같은 역할을 수행합니다:

  • 인증 및 권한 부여: JWT, OAuth, API Key 등 다양한 인증 방식 구현
  • 속도 제한: 클라이언트별, API별 요청 제한 설정
  • 로깅 및 모니터링: 커스텀 로그 포맷과 메트릭 수집
  • 캐싱: Redis 연동을 통한 응답 캐싱 전략
  • 변환: 요청/응답 데이터 변환 및 라우팅

Kong 설치 및 기본 설정

Kong을 설치하는 방법은 여러 가지가 있지만, Docker를 사용한 설치가 가장 간편합니다.

# PostgreSQL 데이터베이스 실행
docker run -d --name kong-database \
  -p 5432:5432 \
  -e POSTGRES_DB=kong \
  -e POSTGRES_USER=kong \
  -e POSTGRES_PASSWORD=kong \
  postgres:13

# Kong 데이터베이스 마이그레이션
docker run --rm \
  --link kong-database:kong-database \
  -e KONG_DATABASE=postgres \
  -e KONG_PG_HOST=kong-database \
  -e KONG_PG_DATABASE=kong \
  -e KONG_PG_USER=kong \
  -e KONG_PG_PASSWORD=kong \
  kong:latest kong migrations bootstrap

# Kong 실행
docker run -d --name kong \
  --link kong-database:kong-database \
  -e KONG_DATABASE=postgres \
  -e KONG_PG_HOST=kong-database \
  -e KONG_PG_DATABASE=kong \
  -e KONG_PG_USER=kong \
  -e KONG_PG_PASSWORD=kong \
  -e KONG_ADMIN_ACCESS_LOG=/dev/stdout \
  -e KONG_ADMIN_ERROR_LOG=/dev/stderr \
  -e KONG_ADMIN_LISTEN=0.0.0.0:8001 \
  -p 8000:8000 \
  -p 8443:8443 \
  -p 8001:8001 \
  -p 8444:8444 \
  kong:latest

 

Kong이 정상적으로 실행되면 다음 포트로 접근할 수 있습니다:

  • 8000: API 게이트웨이 포트 (HTTP)
  • 8443: API 게이트웨이 포트 (HTTPS)
  • 8001: Admin API 포트 (HTTP)
  • 8444: Admin API 포트 (HTTPS)

Admin API를 통해 Kong의 설정을 확인해보겠습니다:

curl -i http://localhost:8001/

기본 서비스와 라우트 설정

Kong에서 API를 관리하기 위해서는 서비스(Service)와 라우트(Route)의 개념을 이해해야 합니다.

서비스는 업스트림 API의 추상화된 표현이며, 라우트는 클라이언트 요청을 특정 서비스로 라우팅하는 규칙입니다.

서비스 생성

curl -i -X POST http://localhost:8001/services/ \
  --data "name=example-service" \
  --data "url=http://httpbin.org"

라우트 생성

curl -i -X POST http://localhost:8001/services/example-service/routes \
  --data "hosts[]=example.com" \
  --data "paths[]=/api"

 

이제 Kong을 통해 업스트림 서비스에 요청을 보낼 수 있습니다:

curl -i -X GET http://localhost:8000/api/get \
  --header "Host: example.com"

Redis 연동과 루아 스크립팅 기초

Kong에서 Redis를 활용한 캐싱 전략을 구현하기 위해서는 Redis 연동이 필요합니다.

Redis는 Kong의 데이터 저장소로 사용될 수 있으며, 특히 세션 관리와 캐싱에서 중요한 역할을 합니다.

Redis 설치 및 연결

# Redis 컨테이너 실행
docker run -d --name kong-redis \
  -p 6379:6379 \
  redis:latest

# Kong에서 Redis 연결 테스트
docker exec -it kong lua -e "
local redis = require 'resty.redis'
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect('host.docker.internal', 6379)
if not ok then
    print('Redis 연결 실패: ', err)
else
    print('Redis 연결 성공')
    red:close()
end
"

루아에서 Redis 조작

Kong의 루아 환경에서 Redis를 사용하는 기본 패턴입니다:

local redis = require "resty.redis"

local function connect_redis()
    local red = redis:new()
    red:set_timeout(1000) -- 1초 타임아웃

    local ok, err = red:connect("redis-host", 6379)
    if not ok then
        kong.log.err("Redis 연결 실패: ", err)
        return nil, err
    end

    return red
end

local function get_cached_data(key)
    local red, err = connect_redis()
    if not red then
        return nil, err
    end

    local result, err = red:get(key)
    red:set_keepalive(10000, 100)

    if not result or result == ngx.null then
        return nil, "캐시 데이터 없음"
    end

    return result
end

 

이전 글 참조: 루아 입문 시리즈 #11: Redis와 루아 스크립팅에서 Redis 루아 스크립트의 기초를 다뤘습니다.

 

루아 입문 시리즈 #11: Redis와 루아 스크립팅

Redis Lua 스크립트를 활용한 원자적 연산과 고성능 캐시 전략으로 데이터베이스 부하를 줄이고 애플리케이션 성능을 극대화하는 실전 가이드입니다.Redis Lua 스크립팅이란?Redis는 인메모리 데이터

notavoid.tistory.com

 


Kong 플러그인 개발 실전

Kong의 진정한 파워는 커스텀 플러그인 개발에 있습니다.

플러그인을 통해 API 게이트웨이의 동작을 세밀하게 제어할 수 있으며, 비즈니스 로직을 구현할 수 있습니다.

플러그인 구조

Kong 플러그인은 다음과 같은 파일 구조를 가집니다:

my-plugin/
├── handler.lua
├── schema.lua
└── migrations/
    └── 001_initial.lua

 

 

Kong plugin architecture diagram showing Lua script execution phases for access control, authentication, and rate limiting
Kong Plugin Architecture

기본 플러그인 템플릿

-- handler.lua
local MyPlugin = {
  PRIORITY = 1000, -- 플러그인 실행 순서
  VERSION = "1.0.0",
}

function MyPlugin:access(conf)
  -- 요청 처리 전 실행
  kong.log.info("MyPlugin access phase")

  -- 요청 헤더 검증
  local api_key = kong.request.get_header("X-API-Key")
  if not api_key then
    return kong.response.exit(401, {
      message = "API Key required"
    })
  end

  -- Redis에서 API Key 검증
  local red = connect_redis()
  if red then
    local valid, err = red:get("api_key:" .. api_key)
    if not valid or valid == ngx.null then
      return kong.response.exit(401, {
        message = "Invalid API Key"
      })
    end
    red:set_keepalive(10000, 100)
  end
end

function MyPlugin:response(conf)
  -- 응답 처리 시 실행
  kong.response.set_header("X-Powered-By", "Kong-MyPlugin")
end

return MyPlugin

스키마 정의

-- schema.lua
local typedefs = require "kong.db.schema.typedefs"

local schema = {
  name = "my-plugin",
  fields = {{
    config = {
      type = "record",
      fields = {
        { api_key_header = { type = "string", default = "X-API-Key" } },
        { redis_host = { type = "string", default = "localhost" } },
        { redis_port = { type = "integer", default = 6379 } },
        { cache_ttl = { type = "integer", default = 300 } },
      },
    },
  }},
}

return schema

Redis 루아 스크립트와 원자적 연산

Redis 루아 스크립트를 활용하면 원자적 연산을 보장하면서 복잡한 로직을 구현할 수 있습니다.

Kong에서 이는 특히 속도 제한(Rate Limiting)과 캐싱 전략에서 중요합니다.

속도 제한 구현

-- 속도 제한 루아 스크립트
local rate_limit_script = [[
    local key = KEYS[1]
    local window = tonumber(ARGV[1])
    local limit = tonumber(ARGV[2])
    local current_time = tonumber(ARGV[3])

    -- 현재 윈도우 시작 시간 계산
    local window_start = math.floor(current_time / window) * window
    local window_key = key .. ":" .. window_start

    -- 현재 요청 수 조회
    local current_requests = redis.call('GET', window_key)
    if not current_requests then
        current_requests = 0
    else
        current_requests = tonumber(current_requests)
    end

    -- 제한 확인
    if current_requests >= limit then
        return {0, current_requests, window - (current_time - window_start)}
    end

    -- 요청 수 증가
    local new_requests = redis.call('INCR', window_key)
    redis.call('EXPIRE', window_key, window)

    return {1, new_requests, window - (current_time - window_start)}
]]

-- Kong 플러그인에서 사용
function MyPlugin:access(conf)
    local red = connect_redis()
    if not red then
        kong.log.err("Redis 연결 실패")
        return
    end

    local client_id = kong.client.get_forwarded_ip()
    local current_time = ngx.time()

    local result, err = red:eval(rate_limit_script, 1, 
        "rate_limit:" .. client_id, 
        conf.time_window, 
        conf.limit, 
        current_time)

    if not result then
        kong.log.err("속도 제한 스크립트 실행 실패: ", err)
        return
    end

    local allowed, requests, reset_time = result[1], result[2], result[3]

    -- 응답 헤더 설정
    kong.response.set_header("X-RateLimit-Limit", conf.limit)
    kong.response.set_header("X-RateLimit-Remaining", conf.limit - requests)
    kong.response.set_header("X-RateLimit-Reset", reset_time)

    if allowed == 0 then
        return kong.response.exit(429, {
            message = "Rate limit exceeded"
        })
    end

    red:set_keepalive(10000, 100)
end

캐시 전략 구현

Redis multi-level caching strategy flowchart showing L1 local cache, L2 Redis cache, and predictive refresh mechanisms
Redis Caching Strategy Flow

-- 스마트 캐싱 루아 스크립트
local smart_cache_script = [[
    local cache_key = KEYS[1]
    local lock_key = KEYS[2]
    local ttl = tonumber(ARGV[1])
    local lock_ttl = tonumber(ARGV[2])

    -- 캐시 데이터 확인
    local cached_data = redis.call('GET', cache_key)
    if cached_data then
        -- TTL 연장 (자주 사용되는 데이터)
        redis.call('EXPIRE', cache_key, ttl)
        return {1, cached_data, 'hit'}
    end

    -- 캐시 미스 시 락 획득 시도
    local lock_acquired = redis.call('SET', lock_key, '1', 'NX', 'EX', lock_ttl)
    if lock_acquired then
        return {0, nil, 'miss_with_lock'}
    else
        return {0, nil, 'miss_no_lock'}
    end
]]

function get_or_set_cache(key, data_fetcher, ttl)
    local red = connect_redis()
    if not red then
        return data_fetcher()
    end

    local cache_key = "cache:" .. key
    local lock_key = "lock:" .. key

    local result, err = red:eval(smart_cache_script, 2, 
        cache_key, lock_key, ttl or 300, 10)

    if not result then
        kong.log.err("캐시 스크립트 실행 실패: ", err)
        return data_fetcher()
    end

    local hit, data, status = result[1], result[2], result[3]

    if hit == 1 then
        -- 캐시 히트
        red:set_keepalive(10000, 100)
        return cjson.decode(data)
    elseif status == 'miss_with_lock' then
        -- 락 획득 성공, 데이터 생성
        local fresh_data = data_fetcher()

        -- 캐시에 저장
        red:setex(cache_key, ttl, cjson.encode(fresh_data))
        red:del(lock_key) -- 락 해제
        red:set_keepalive(10000, 100)

        return fresh_data
    else
        -- 락 획득 실패, 잠시 대기 후 재시도 또는 직접 데이터 생성
        red:set_keepalive(10000, 100)
        ngx.sleep(0.01) -- 10ms 대기

        -- 재귀 호출로 재시도 (최대 3회)
        local retry_count = ngx.ctx.cache_retry_count or 0
        if retry_count < 3 then
            ngx.ctx.cache_retry_count = retry_count + 1
            return get_or_set_cache(key, data_fetcher, ttl)
        else
            return data_fetcher()
        end
    end
end

성능 최적화와 모니터링

Kong API Gateway의 성능을 최적화하기 위해서는 다양한 요소를 고려해야 합니다.

특히 루아 스크립트의 효율성과 Redis 연결 관리가 중요합니다.

연결 풀 최적화

-- 효율적인 Redis 연결 관리
local redis_pool = {}

local function get_redis_connection(conf)
    local pool_key = conf.redis_host .. ":" .. conf.redis_port

    if redis_pool[pool_key] then
        return redis_pool[pool_key]
    end

    local red = redis:new()
    red:set_timeout(conf.timeout or 1000)

    local ok, err = red:connect(conf.redis_host, conf.redis_port)
    if not ok then
        kong.log.err("Redis 연결 실패: ", err)
        return nil, err
    end

    -- 연결 풀에 저장
    redis_pool[pool_key] = red
    return red
end

-- 연결 재사용
local function safe_redis_call(conf, callback)
    local red, err = get_redis_connection(conf)
    if not red then
        return nil, err
    end

    local result, call_err = callback(red)

    -- 연결 상태 확인 및 재설정
    if call_err and string.find(call_err, "closed") then
        redis_pool[conf.redis_host .. ":" .. conf.redis_port] = nil
        red, err = get_redis_connection(conf)
        if red then
            result, call_err = callback(red)
        end
    end

    if red then
        red:set_keepalive(10000, 100)
    end

    return result, call_err
end

메트릭 수집

-- 성능 메트릭 수집
local metrics = {
    cache_hits = 0,
    cache_misses = 0,
    redis_errors = 0,
    total_requests = 0
}

local function update_metrics(metric_name, value)
    metrics[metric_name] = (metrics[metric_name] or 0) + (value or 1)

    -- 주기적으로 Redis에 메트릭 저장
    if metrics.total_requests % 100 == 0 then
        safe_redis_call(conf, function(red)
            local timestamp = os.time()
            local metrics_key = "metrics:" .. timestamp

            for key, val in pairs(metrics) do
                red:hset(metrics_key, key, val)
            end
            red:expire(metrics_key, 3600) -- 1시간 보관

            return true
        end)
    end
end

-- 플러그인에서 메트릭 사용
function MyPlugin:access(conf)
    update_metrics("total_requests")

    -- 캐시 로직 실행
    local cached_result = get_or_set_cache("user_data", function()
        update_metrics("cache_misses")
        return fetch_user_data()
    end, 300)

    if cached_result then
        update_metrics("cache_hits")
    end
end

실제 사용 사례와 패턴

Kong과 Redis를 활용한 실제 프로덕션 환경에서의 사용 사례를 살펴보겠습니다.

JWT 토큰 블랙리스트 관리

-- JWT 블랙리스트 플러그인
local jwt_blacklist_script = [[
    local token_key = KEYS[1]
    local blacklist_key = KEYS[2]
    local exp_time = tonumber(ARGV[1])
    local current_time = tonumber(ARGV[2])

    -- 토큰이 이미 블랙리스트에 있는지 확인
    local is_blacklisted = redis.call('SISMEMBER', blacklist_key, token_key)
    if is_blacklisted == 1 then
        return {0, 'blacklisted'}
    end

    -- 토큰 만료 시간 확인
    if exp_time <= current_time then
        return {0, 'expired'}
    end

    return {1, 'valid'}
]]

function MyPlugin:access(conf)
    local token = kong.request.get_header("Authorization")
    if not token then
        return kong.response.exit(401, { message = "Token required" })
    end

    -- Bearer 토큰 추출
    local jwt_token = string.match(token, "Bearer%s+(.+)")
    if not jwt_token then
        return kong.response.exit(401, { message = "Invalid token format" })
    end

    -- JWT 디코딩 (간단한 예시)
    local jwt_parts = {}
    for part in string.gmatch(jwt_token, "[^%.]+") do
        table.insert(jwt_parts, part)
    end

    if #jwt_parts ~= 3 then
        return kong.response.exit(401, { message = "Invalid JWT format" })
    end

    -- 페이로드 디코딩
    local payload_json = ngx.decode_base64(jwt_parts[2])
    local payload = cjson.decode(payload_json)

    safe_redis_call(conf, function(red)
        local result, err = red:eval(jwt_blacklist_script, 2,
            jwt_token, "jwt_blacklist",
            payload.exp, ngx.time())

        if not result or result[1] == 0 then
            kong.response.exit(401, { message = result[2] or "Token validation failed" })
        end

        return true
    end)
end

-- 토큰 블랙리스트 추가 API
function add_to_blacklist(token, exp_time)
    safe_redis_call(conf, function(red)
        red:sadd("jwt_blacklist", token)
        red:expireat("jwt_blacklist", exp_time)
        return true
    end)
end

API 사용량 통계 및 빌링

-- API 사용량 통계 스크립트
local usage_tracking_script = [[
    local user_key = KEYS[1]
    local api_key = KEYS[2]
    local timestamp = tonumber(ARGV[1])
    local cost = tonumber(ARGV[2])

    -- 일별 통계
    local daily_key = user_key .. ":daily:" .. os.date("%Y%m%d", timestamp)
    local api_daily_key = api_key .. ":daily:" .. os.date("%Y%m%d", timestamp)

    -- 월별 통계
    local monthly_key = user_key .. ":monthly:" .. os.date("%Y%m", timestamp)
    local api_monthly_key = api_key .. ":monthly:" .. os.date("%Y%m", timestamp)

    -- 사용량 증가
    redis.call('HINCRBY', daily_key, 'requests', 1)
    redis.call('HINCRBY', daily_key, 'cost', cost)
    redis.call('EXPIRE', daily_key, 86400 * 7) -- 7일 보관

    redis.call('HINCRBY', api_daily_key, 'requests', 1)
    redis.call('HINCRBY', api_daily_key, 'cost', cost)
    redis.call('EXPIRE', api_daily_key, 86400 * 7)

    redis.call('HINCRBY', monthly_key, 'requests', 1)
    redis.call('HINCRBY', monthly_key, 'cost', cost)
    redis.call('EXPIRE', monthly_key, 86400 * 32) -- 32일 보관

    redis.call('HINCRBY', api_monthly_key, 'requests', 1)
    redis.call('HINCRBY', api_monthly_key, 'cost', cost)
    redis.call('EXPIRE', api_monthly_key, 86400 * 32)

    -- 현재 월 사용량 반환
    local current_usage = redis.call('HMGET', monthly_key, 'requests', 'cost')
    return {tonumber(current_usage[1]) or 0, tonumber(current_usage[2]) or 0}
]]

function MyPlugin:response(conf)
    local user_id = kong.ctx.shared.user_id
    local api_name = kong.router.get_route().name
    local response_size = kong.response.get_header("Content-Length") or 0

    -- API 비용 계산 (예: 응답 크기 기반)
    local cost = math.ceil(tonumber(response_size) / 1024) -- KB당 1포인트

    safe_redis_call(conf, function(red)
        local result, err = red:eval(usage_tracking_script, 2,
            "user:" .. user_id, "api:" .. api_name,
            ngx.time(), cost)

        if result then
            local monthly_requests, monthly_cost = result[1], result[2]
            kong.log.info("사용량 - 요청: ", monthly_requests, ", 비용: ", monthly_cost)
        end

        return true
    end)
end

Kong과 Redis 성능 비교

다른 솔루션들과의 성능 비교를 통해 Kong + Redis 조합의 우수성을 확인해보겠습니다.

항목 Kong + Redis AWS API Gateway Zuul + Eureka Ambassador
처리량 (RPS) 15,000+ 10,000+ 8,000+ 12,000+
응답 시간 (ms) < 5 < 10 < 15 < 8
캐싱 성능 매우 우수 제한적 보통 우수
확장성 수평 확장 자동 확장 복잡 쿠버네티스 기반
비용 효율성 높음 중간 높음 중간
커스터마이징 매우 유연 제한적 유연 유연

 

Kong과 Redis의 조합은 특히 다음 영역에서 뛰어난 성능을 보입니다:

캐싱 효율성: Redis의 인메모리 특성으로 캐시 히트율 95% 이상 달성 가능

원자적 연산: 루아 스크립트를 통한 복잡한 비즈니스 로직을 원자적으로 처리

실시간 처리: 마이크로초 단위의 빠른 응답 시간


트러블슈팅과 베스트 프랙티스

Kong과 Redis를 운영하면서 자주 발생하는 문제들과 해결 방법을 정리했습니다.

메모리 관리

-- 메모리 효율적인 데이터 처리
local function process_large_dataset(data_key, batch_size)
    batch_size = batch_size or 1000
    local cursor = 0
    local results = {}

    repeat
        local batch_result = safe_redis_call(conf, function(red)
            local scan_result = red:scan(cursor, "MATCH", data_key .. "*", "COUNT", batch_size)
            cursor = scan_result[1]
            return scan_result[2]
        end)

        if batch_result then
            for _, key in ipairs(batch_result) do
                -- 개별 키 처리
                table.insert(results, process_single_key(key))
            end
        end

        -- 메모리 압박 시 중간 결과 저장
        if #results > 10000 then
            store_intermediate_results(results)
            results = {}
            collectgarbage("collect") -- 가비지 컬렉션 강제 실행
        end

    until cursor == "0"

    return results
end

에러 핸들링

-- 포괄적인 에러 처리
local function robust_redis_operation(operation_func, fallback_func)
    local max_retries = 3
    local retry_delay = 0.1

    for attempt = 1, max_retries do
        local success, result, err = pcall(operation_func)

        if success and result then
            return result
        end

        kong.log.warn("Redis 작업 실패 (시도 ", attempt, "/", max_retries, "): ", err or "알 수 없는 오류")

        if attempt < max_retries then
            ngx.sleep(retry_delay * attempt) -- 지수 백오프
        end
    end

    -- 모든 재시도 실패 시 폴백 함수 실행
    kong.log.err("Redis 작업 최종 실패, 폴백 모드로 전환")
    return fallback_func and fallback_func() or nil
end

-- 사용 예시
local function get_user_data_with_fallback(user_id)
    return robust_redis_operation(
        function()
            return get_or_set_cache("user:" .. user_id, function()
                return fetch_user_from_database(user_id)
            end, 600)
        end,
        function()
            -- Redis 실패 시 직접 데이터베이스 조회
            return fetch_user_from_database(user_id)
        end
    )
end

성능 모니터링

-- 실시간 성능 모니터링
local monitoring = {}

function monitoring.track_performance(operation_name, start_time)
    local duration = (ngx.now() - start_time) * 1000 -- ms 단위

    safe_redis_call(conf, function(red)
        local stats_key = "perf_stats:" .. operation_name
        local minute_key = stats_key .. ":" .. math.floor(ngx.time() / 60)

        -- 분 단위 통계 수집
        red:lpush(minute_key .. ":durations", duration)
        red:expire(minute_key .. ":durations", 300) -- 5분 보관

        -- 응답 시간 히스토그램
        local bucket = math.floor(duration / 10) * 10 -- 10ms 단위 버킷
        red:hincrby(minute_key .. ":histogram", bucket, 1)
        red:expire(minute_key .. ":histogram", 300)

        return true
    end)
end

-- 플러그인에서 모니터링 사용
function MyPlugin:access(conf)
    local start_time = ngx.now()
    ngx.ctx.start_time = start_time

    -- 비즈니스 로직 실행
    local result = process_request(conf)

    monitoring.track_performance("request_processing", start_time)
    return result
end

고급 캐시 전략과 패턴

더 정교한 캐싱 전략을 구현하여 성능을 극대화할 수 있습니다.

계층형 캐싱

-- L1 (로컬) + L2 (Redis) 캐싱 시스템
local lru_cache = require "resty.lrucache"
local l1_cache = lru_cache.new(1000) -- 로컬 캐시

local function get_multi_level_cache(key, data_fetcher, l1_ttl, l2_ttl)
    l1_ttl = l1_ttl or 60
    l2_ttl = l2_ttl or 300

    -- L1 캐시 확인
    local l1_data = l1_cache:get(key)
    if l1_data then
        return l1_data, "l1_hit"
    end

    -- L2 캐시 확인
    local l2_data = safe_redis_call(conf, function(red)
        return red:get("cache:" .. key)
    end)

    if l2_data and l2_data ~= ngx.null then
        local decoded_data = cjson.decode(l2_data)
        -- L1 캐시에 저장
        l1_cache:set(key, decoded_data, l1_ttl)
        return decoded_data, "l2_hit"
    end

    -- 캐시 미스 - 원본 데이터 생성
    local fresh_data = data_fetcher()
    if fresh_data then
        -- L2 캐시에 저장
        safe_redis_call(conf, function(red)
            red:setex("cache:" .. key, l2_ttl, cjson.encode(fresh_data))
            return true
        end)

        -- L1 캐시에 저장
        l1_cache:set(key, fresh_data, l1_ttl)
    end

    return fresh_data, "miss"
end

예측적 캐시 갱신

-- 예측적 캐시 갱신 스크립트
local predictive_cache_script = [[
    local cache_key = KEYS[1]
    local refresh_key = KEYS[2]
    local ttl = tonumber(ARGV[1])
    local refresh_threshold = tonumber(ARGV[2])

    local cached_data = redis.call('GET', cache_key)
    local remaining_ttl = redis.call('TTL', cache_key)

    -- 캐시 데이터가 있고 만료 임박 시 백그라운드 갱신 트리거
    if cached_data and remaining_ttl > 0 and remaining_ttl < refresh_threshold then
        local refresh_lock = redis.call('SET', refresh_key, '1', 'NX', 'EX', 30)
        if refresh_lock then
            return {1, cached_data, 'refresh_needed'}
        end
    end

    if cached_data then
        return {1, cached_data, 'hit'}
    else
        return {0, nil, 'miss'}
    end
]]

local function get_predictive_cache(key, data_fetcher, ttl, refresh_threshold)
    ttl = ttl or 300
    refresh_threshold = refresh_threshold or math.floor(ttl * 0.2) -- TTL의 20%

    local cache_key = "cache:" .. key
    local refresh_key = "refresh:" .. key

    local result = safe_redis_call(conf, function(red)
        return red:eval(predictive_cache_script, 2, 
            cache_key, refresh_key, ttl, refresh_threshold)
    end)

    if not result then
        return data_fetcher()
    end

    local hit, data, status = result[1], result[2], result[3]

    if hit == 1 then
        local decoded_data = cjson.decode(data)

        -- 백그라운드 갱신이 필요한 경우
        if status == 'refresh_needed' then
            -- 비동기 갱신 (실제 구현에서는 작업 큐 사용 권장)
            ngx.timer.at(0, function()
                local fresh_data = data_fetcher()
                if fresh_data then
                    safe_redis_call(conf, function(red)
                        red:setex(cache_key, ttl, cjson.encode(fresh_data))
                        red:del(refresh_key)
                        return true
                    end)
                end
            end)
        end

        return decoded_data
    else
        -- 캐시 미스 - 동기 데이터 생성
        local fresh_data = data_fetcher()
        if fresh_data then
            safe_redis_call(conf, function(red)
                red:setex(cache_key, ttl, cjson.encode(fresh_data))
                return true
            end)
        end
        return fresh_data
    end
end

보안 고려사항

Kong과 Redis 환경에서 보안을 강화하기 위한 주요 고려사항들입니다.

인증 및 암호화

-- Redis 연결 보안 강화
local function secure_redis_connect(conf)
    local red = redis:new()
    red:set_timeout(conf.timeout or 1000)

    -- SSL/TLS 연결 (Redis 6.0+)
    local connect_opts = {
        ssl = conf.ssl_enabled,
        ssl_verify = conf.ssl_verify,
        server_name = conf.ssl_server_name
    }

    local ok, err = red:connect(conf.redis_host, conf.redis_port, connect_opts)
    if not ok then
        return nil, "Redis 연결 실패: " .. err
    end

    -- Redis AUTH
    if conf.redis_password then
        local auth_result, auth_err = red:auth(conf.redis_password)
        if not auth_result then
            return nil, "Redis 인증 실패: " .. auth_err
        end
    end

    -- 데이터베이스 선택
    if conf.redis_database then
        local select_result, select_err = red:select(conf.redis_database)
        if not select_result then
            return nil, "Redis DB 선택 실패: " .. select_err
        end
    end

    return red
end

-- 데이터 암호화/복호화
local aes = require "resty.aes"

local function encrypt_data(data, key)
    local aes_256_cbc = aes:new(key, nil, aes.cipher(256, "cbc"))
    return aes_256_cbc:encrypt(data)
end

local function decrypt_data(encrypted_data, key)
    local aes_256_cbc = aes:new(key, nil, aes.cipher(256, "cbc"))
    return aes_256_cbc:decrypt(encrypted_data)
end

-- 보안 캐시 저장
local function secure_cache_set(key, data, ttl, encryption_key)
    local serialized_data = cjson.encode(data)
    local encrypted_data = encrypt_data(serialized_data, encryption_key)

    return safe_redis_call(conf, function(red)
        return red:setex("secure_cache:" .. key, ttl, encrypted_data)
    end)
end

local function secure_cache_get(key, encryption_key)
    local encrypted_data = safe_redis_call(conf, function(red)
        return red:get("secure_cache:" .. key)
    end)

    if not encrypted_data or encrypted_data == ngx.null then
        return nil
    end

    local decrypted_data = decrypt_data(encrypted_data, encryption_key)
    return cjson.decode(decrypted_data)
end

액세스 제어

-- IP 화이트리스트/블랙리스트 관리
local ip_control_script = [[
    local client_ip = ARGV[1]
    local whitelist_key = KEYS[1]
    local blacklist_key = KEYS[2]

    -- 블랙리스트 확인
    local is_blacklisted = redis.call('SISMEMBER', blacklist_key, client_ip)
    if is_blacklisted == 1 then
        return {0, 'blacklisted'}
    end

    -- 화이트리스트 확인 (화이트리스트가 있는 경우)
    local whitelist_exists = redis.call('EXISTS', whitelist_key)
    if whitelist_exists == 1 then
        local is_whitelisted = redis.call('SISMEMBER', whitelist_key, client_ip)
        if is_whitelisted == 0 then
            return {0, 'not_whitelisted'}
        end
    end

    return {1, 'allowed'}
]]

function MyPlugin:access(conf)
    local client_ip = kong.client.get_forwarded_ip()

    local result = safe_redis_call(conf, function(red)
        return red:eval(ip_control_script, 2, 
            "ip_whitelist", "ip_blacklist", client_ip)
    end)

    if result and result[1] == 0 then
        kong.log.warn("IP 액세스 거부: ", client_ip, " - ", result[2])
        return kong.response.exit(403, {
            message = "Access denied"
        })
    end
end

배포 및 운영 가이드

프로덕션 환경에서 Kong과 Redis를 안정적으로 운영하기 위한 가이드입니다.

Docker Compose 구성

# docker-compose.yml
version: '3.8'

services:
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
      - ./redis.conf:/usr/local/etc/redis/redis.conf
    ports:
      - "6379:6379"
    environment:
      - REDIS_PASSWORD=${REDIS_PASSWORD}
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    restart: unless-stopped

  kong-database:
    image: postgres:13
    environment:
      POSTGRES_DB: kong
      POSTGRES_USER: kong
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U kong"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  kong-migration:
    image: kong:latest
    command: kong migrations bootstrap
    environment:
      KONG_DATABASE: postgres
      KONG_PG_HOST: kong-database
      KONG_PG_DATABASE: kong
      KONG_PG_USER: kong
      KONG_PG_PASSWORD: ${POSTGRES_PASSWORD}
    depends_on:
      kong-database:
        condition: service_healthy
    restart: on-failure

  kong:
    image: kong:latest
    environment:
      KONG_DATABASE: postgres
      KONG_PG_HOST: kong-database
      KONG_PG_DATABASE: kong
      KONG_PG_USER: kong
      KONG_PG_PASSWORD: ${POSTGRES_PASSWORD}
      KONG_ADMIN_ACCESS_LOG: /dev/stdout
      KONG_ADMIN_ERROR_LOG: /dev/stderr
      KONG_ADMIN_LISTEN: 0.0.0.0:8001
      KONG_PROXY_ACCESS_LOG: /dev/stdout
      KONG_PROXY_ERROR_LOG: /dev/stderr
      # Redis 설정
      KONG_REDIS_HOST: redis
      KONG_REDIS_PORT: 6379
      KONG_REDIS_PASSWORD: ${REDIS_PASSWORD}
    ports:
      - "8000:8000"
      - "8443:8443"
      - "8001:8001"
      - "8444:8444"
    volumes:
      - ./plugins:/usr/local/share/lua/5.1/kong/plugins/custom
    depends_on:
      kong-database:
        condition: service_healthy
      redis:
        condition: service_healthy
      kong-migration:
        condition: service_completed_successfully
    healthcheck:
      test: ["CMD", "kong", "health"]
      interval: 30s
      timeout: 10s
      retries: 3
    restart: unless-stopped

volumes:
  redis_data:
  postgres_data:

모니터링 및 로깅

-- 구조화된 로깅
local function structured_log(level, event, data)
    local log_entry = {
        timestamp = os.date("!%Y-%m-%dT%H:%M:%SZ"),
        level = level,
        event = event,
        plugin = "my-plugin",
        request_id = kong.ctx.shared.request_id or "unknown",
        data = data or {}
    }

    kong.log[level](cjson.encode(log_entry))
end

-- 성능 메트릭 로깅
local function log_performance_metrics()
    local metrics = {
        request_time = ngx.now() - (ngx.ctx.start_time or ngx.now()),
        upstream_time = ngx.var.upstream_response_time,
        cache_status = ngx.ctx.cache_status or "miss",
        redis_operations = ngx.ctx.redis_ops or 0
    }

    structured_log("info", "performance_metrics", metrics)
end

-- 에러 추적
local function log_error_with_context(err, context)
    local error_data = {
        error = err,
        context = context,
        stack_trace = debug.traceback(),
        request_headers = kong.request.get_headers(),
        client_ip = kong.client.get_forwarded_ip()
    }

    structured_log("error", "plugin_error", error_data)
end

마무리

Kong API Gateway와 Redis를 활용한 고성능 API 게이트웨이 구축은 현대적인 마이크로서비스 아키텍처의 핵심 요소입니다.

 

본 가이드에서 다룬 내용들을 통해 다음과 같은 이점을 얻을 수 있습니다:

성능 최적화: Redis 루아 스크립트를 통한 원자적 연산으로 높은 처리량과 낮은 지연시간 달성

확장성: 수평 확장이 가능한 아키텍처로 트래픽 증가에 유연하게 대응

보안: 다층 보안 전략으로 API 보안 강화

모니터링: 실시간 성능 모니터링과 메트릭 수집으로 운영 효율성 향상

 

Kong과 Redis의 조합은 단순한 API 프록시를 넘어서 비즈니스 로직을 포함한 완전한 API 관리 플랫폼을 구축할 수 있게 해줍니다.

특히 원자적 연산을 보장하는 Redis 루아 스크립트와 Kong의 플러그인 시스템이 결합될 때, 복잡한 비즈니스 요구사항도 효율적으로 처리할 수 있습니다.

다음 시리즈에서는 Kong의 고급 플러그인 개발과 대규모 분산 환경에서의 운영 전략에 대해 더 자세히 다룰 예정입니다.


관련 글 및 참고 자료:

 

루아 입문 시리즈 #11: Redis와 루아 스크립팅

Redis Lua 스크립트를 활용한 원자적 연산과 고성능 캐시 전략으로 데이터베이스 부하를 줄이고 애플리케이션 성능을 극대화하는 실전 가이드입니다.Redis Lua 스크립팅이란?Redis는 인메모리 데이터

notavoid.tistory.com

 

728x90
반응형