nginx+lua+redisで動的プロキシを作る

## はじめに

nginxでRedisとやり取りする方法が気になったので、動的プロキシを作成しながら試してみる。

## 構成のイメージ

![スクリーンショット 2024-11-23 10.09.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3900459/277ebe8a-d5ba-2603-066f-61d776f0fd44.png)


## 環境構築
今回はDockerで環境を構築していきます。

[サンプルソース](https://github.com/kehuka11/nginx_redis/tree/main)

~~~docker
version: "3.9"
services:
  redis:
    image: "redis:7.4.1-alpine3.20"
    expose:
      - 6379
    healthcheck:
      test: ["CMD", "redis-cli", "--raw", "incr", "ping", "|", "grep", "PONG"]
      timeout: 5s
      retries: 5
      start_period: 5s

  app1:
    image: "sample-app1:latest"
    ports:
      - 8080:8080
  app2:
    image: "sample-app2:latest"
    ports:
      - 8081:8081

  nginx:
    image: "openresty/openresty:alpine"
    ports: 
      - 80:80
    volumes:
      - ./nginx/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      redis:
        condition: service_healthy
~~~

nginxはデフォルトではRedisとやり取りできないため、Luaモジュールをダウンロードする必要があります。
今回はLuaと統合しているOpenRestyを使用してます。

## nginx設定

今回は最低限の設定のみ入れてます。

[nginx設定](https://github.com/kehuka11/nginx_redis/blob/main/nginx/nginx.conf)

(抜粋)
~~~shell
location / {
            # 事前に$target変数を定義
            set $target "";

            # Luaスクリプトで振り分け先を取得してプロキシ設定
            access_by_lua_block {
                local redis = require "resty.redis"
                local cache = ngx.shared.redis_cache

                -- ヘッダからIDを取得
                local id = ngx.req.get_headers()["ID"]
                if not id then
                    ngx.log(ngx.ERR, "id header is missing")
                    return ngx.exit(ngx.HTTP_BAD_REQUEST)
                end

                -- キャッシュからデータを取得
                local cached_value = cache:get(id)
                if cached_value then
                    ngx.log(ngx.INFO, "Cache hit: ", cached_value)
                    ngx.var.target = cached_value
                    return
                end

                local red = redis:new()

                -- Redis接続設定
                red:set_timeout(1000)  -- タイムアウト設定 (ms)
                local ok, err = red:connect("redis", 6379)
                if not ok then
                    ngx.log(ngx.ERR, "failed to connect to Redis: ", err)
                    return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
                end

                ngx.log(ngx.INFO, "Redis access ok")

                -- Redisでidに対応する振り分け先を取得
                local target, err = red:get(id)
                if not target or target == ngx.null then
                    ngx.log(ngx.ERR, "Target not found for id: ", id)
                    return ngx.exit(ngx.HTTP_NOT_FOUND)
                end

                cache:set(id, target)

                -- 振り分け先の設定
                ngx.var.target = target

                -- Redis接続を閉じる
                local ok, err = red:set_keepalive(10000, 100)
                if not ok then
                    ngx.log(ngx.ERR, "failed to set keepalive: ", err)
                end
            }

            # 振り分け先を動的に設定するプロキシ
            proxy_pass http://$target;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
~~~

`access_by_lua_block`はリクエストがきたタイミングでLuaスクリプトを実行させることができます。
今回は全てのリクエストに対して、毎回Luaスクリプトが実行されます。

Luaスクリプトで`ngx.exit`をするとnginx 自体の処理が終了します。
単純にreturnだけしている箇所は、Luaスクリプトの処理を終了し、nginxの処理が続行されます。

なので、流れとしては、
①リクエストが来るとLuaスクリプトが実行
②ヘッダからIDを取得
③キャッシュに存在すればキャッシュからデータを取得し、Luaスクリプトを終了し、プロキシする
④キャッシュにデータが存在しなければRedisに接続し、値を取得し、キャッシュに保存し、プロキシする

という設定にしてます。

## 動作確認

今回APP1にプロキシされた場合、「アプリ1です」というレスポンスが帰ってきて、App2にプロキシされた場合、「アプリ2です」というレスポンスが返ってくるようにしてます。

### RedisにKey、Valueをセット

~~~shell
/data # redis-cli set 001 app1:8080
OK
/data # redis-cli set 002 app2:8081
OK
~~~

### リクエスト投げてみる

~~~shell
curl --location 'localhost:80/sample' \
--header 'Content-Type: application/json' \
--header 'ID: 001'
アプリ1です
~~~

~~~shell
curl --location 'localhost:80/sample' \
--header 'Content-Type: application/json' \
--header 'ID: 001'
アプリ1です
~~~

キャッシュされているかどうかみるために2回リクエストしてます。

~~~shell
curl --location 'localhost:80/sample' \
--header 'Content-Type: application/json' \
--header 'ID: 002'
アプリ2です
~~~

### ログ

~~~shell
2024/11/23 02:18:05 [info] 7#7: *1 [lua] access_by_lua(nginx.conf:22):30: Redis access ok, client: xxx.xx.x.x, server: , request: "GET /sample HTTP/1.1", host: "localhost"
xxx.xx.x.x - - [23/Nov/2024:02:18:05 +0000] "GET /sample HTTP/1.1" 200 16 "-" "curl/8.4.0"
2024/11/23 02:18:05 [info] 7#7: *1 client xxx.xx.x.x closed keepalive connection
2024/11/23 02:18:21 [info] 7#7: *5 [lua] access_by_lua(nginx.conf:22):15: Cache hit: app1:8080, client: xxx.xx.x.x, server: , request: "GET /sample HTTP/1.1", host: "localhost"
xxx.xx.x.x - - [23/Nov/2024:02:18:21 +0000] "GET /sample HTTP/1.1" 200 16 "-" "curl/8.4.0"
2024/11/23 02:18:21 [info] 7#7: *5 client xxx.xx.x.x closed keepalive connection
2024/11/23 02:18:40 [info] 7#7: *7 [lua] access_by_lua(nginx.conf:22):30: Redis access ok, client: xxx.xx.x.x, server: , request: "GET /sample HTTP/1.1", host: "localhost"
xxx.xx.x.x - - [23/Nov/2024:02:18:40 +0000] "GET /sample HTTP/1.1" 200 16 "-" "curl/8.4.0"
2024/11/23 02:18:40 [info] 7#7: *7 client xxx.xx.x.x closed keepalive connection
~~~

1回目のApp1へのアクセスはキャッシュがないので、Redisへ接続してデータを取得していますね。
2回目はキャッシュにヒットしているので、Redisへ接続してないです。
3回目はApp2へのアクセスなので、キャッシュがなく、Redisへ接続してデータを取得しています。

想定通りの結果になってますね。

## さいごに

nginxで使用するレベルのLuaならさほど難しくなかったので、割と簡単に実装できますね。
Redisのライセンス変更もあって、これからはValkeyに変更されていくのではないかと思うので、時間ある時に、Valkeyに変更した場合、ソースに変更が必要なのか試して見たいと思います。

## 参考

https://openresty.org/en/download.html

https://github.com/openresty/lua-resty-redis