0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

構成のイメージ

スクリーンショット 2024-11-23 10.09.09.png

環境構築

今回は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設定

(抜粋)

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をセット

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

リクエスト投げてみる

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

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

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

ログ

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に変更した場合、ソースに変更が必要なのか試して見たいと思います。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?