magcho's blog

MySQL, Nginx(Openresty)で動的リバースプロキシ

開発検証のために本番環境ドメイン(example.com)のサブドメインに PR 名を付与した(PR-1.example.com)を staging 環境として公開した手順メモ

今回は 1 台の VPS 内に Jenkins を用い PR 数分の docker-compose コンテナ群を作り、空いている任意のポートに公開したのち Nginx を用いて{PR 名}.example.com へのアクセスを 127.0.0.1:{公開中のポート}にリバースプロキシしていきます。

概要

vps 内で稼働している DB に以下のようにサブドメインと port 番号のペアを登録しておき、これらを Nginx から読み込んでPR-16.example.comへのアクセスを127.0.0.1:49161へ転送する。

mysql> select * from rpx;
+-----------+-------+
| subdomain | port  |
+-----------+-------+
| PR-16     | 49161 |
| PR-17     | 49162 |
| PR-19     | 49164 |
+-----------+-------+
3 rows in set (0.00 sec)

構成

  • ubuntu18.04.5
  • Nginx(Openresty)
  • lua-nginx-module
  • mysql-nginx-module
  • MySQL

Nginx 動的リバースプロキシで検索すると Redis を用いた例が多く出てくる。今回は検証環境なのでアクセス数や処理性能は考えなくていいので手軽に MySQL にした。

手順

Openresty のセットアップ

Nginx の config ファイル内に Lua を用いて MySQL へのアクセス処理を記述する。通常のapt install nginxでインストールできる Nignx には lua-module が含まれていないので自分で Nginx をビルドし直すか諸モジュールが含まれる Openresty ビルド版をインストールする必要がある。今回は既存の Nginx を止め Openresty をインストールした。手順はOpenresty document

この VPS では別の web アプリケーションを動かしていたので、その設定を Nginx から引き継ぐために Openresty の設定を変更する。/usr/local/openresty/nginx/conf/nginx.confが最初に読み込まれる設定ファイル。デフォルトの設定のうち必要ない server 部をコメントアウトし、既存の Nignx の設定を読みにいくように末尾にinclude /usr/local/openresty/nginx/conf/conf.d/*.conf;を書き加えシンボリックリンクを貼る

/usr/local/openresty/nginx/conf/nginx.conf
~~~~
    # HTTPS server
+   #
+   #server {
+   #    listen       443 ssl;
+   #    server_name  localhost;
+
+   #    ssl_certificate      cert.pem;
+   #    ssl_certificate_key  cert.key;
+
+   #    ssl_session_cache    shared:SSL:1m;
+   #    ssl_session_timeout  5m;
+
+   #    ssl_ciphers  HIGH:!aNULL:!MD5;
+   #    ssl_prefer_server_ciphers  on;
+
+   #    location / {
+   #        root   html;
+   #        index  index.html index.htm;
+   #    }
+   #}
+   include /usr/local/openresty/nginx/conf/conf.d/*.conf;
 }
$ sudo ln -s /etc/nginx/conf.d /usr/local/openresty/nginx/conf/conf.d/

設定変更後 Openresty を有効化して動作を確認する。

MySQL の準備

subdomain と port の対応を示すための DB を作りアクセスする用のユーザーも作る

CREATE database databasename;
CREATE USER 'username'@'localhost' IDENTIFIED by 'password';
GRANT SELECT ON databasename.* TO 'username'@'localhost' IDENTIFIED BY 'password';
CREATE TABLE tablename(subdomain VARCHAR(255) PRIMARY KEY, port INT);

Nginx からは DB の読みとり権限だけ必要なので当該ユーザーに SELECT 権限を与えておきます。別途転送したいサブドメインと port 番号を insert する処理を作っておきましょう。

Nginx の設定ファイルへの Lua スクリプトの記述

/etc/nginx/conf.d/hoge.conf
server{
listen 443 ssl;
	server_name ~^(?<subdomain>[^\.]+).example.com$;
	location / {
		proxy_set_header X-Forwarded-Host $host:$server_port;
		proxy_set_header X-Forwarded-Proto $scheme;
		set $localport "";
		rewrite_by_lua '
			local mysql = require "resty.mysql"

			local db, err = mysql:new()
			if not db then
				ngx.say("failed to instantiate mysql: ", err)
				return
			end
			local ok, err, errcode, sqlstate = db:connect{
				host = "127.0.0.1",
				port = 3306,
				database = "databasename",
				user = "username",
				password = "password",
				charset = "utf8",
				max_packet_size = 1024 * 1024,
			}
			if not ok then
				ngx.log(ngx.ERR, "failed to connect: ", err, ": ", errcode, " ", sqlstate)
				return ngx.exit(500)
			end

			res, err, errcode, sqlstate = db:query("select port from tablename where subdomain = \'" ..ngx.var.subdomain.. "\' limit 1;")
			if not res then
				ngx.log(ngx.ERR, "bad result #1: ", err, ": ", errcode, ": ", sqlstate, ".")
				return ngx.exit(404)
			else
				ngx.var.localport = res[1]["port"]
			end
		';
		proxy_pass http://127.0.0.1:$localport;
		break;
	}
}

server 部の server_name で~^(?<subdomain>[^\.]+).example.com$;としてサブドメインに subdomain という名前をつけてマッチしておくと後続の処理で変数として利用することができる。

lua から Nginx conf の変数を参照・代入することができる。しかし lua で新しい変数を宣言することはできないので先に変数localportを宣言しておくと lua からngx.env.localportでアクセスできる。

db:query()に SQL クエリを入れて実行する。Lua の結合演算子..を利用できます。クエリの実行結果はresに2次元の配列(table)で返却されます。Lua は 1 based index な言語なので最初の要素の index は 1 です、クエリにlimit 1をかけているのでres[1]['port']で転送すべき port 番号が分かります。なのでこの番号にproxy_passすれば OK です。

おまけ

サブドメインが可変なので https にするには毎回発行・無効化せずワイルドカード証明書を当てておきます。ワイルドカード証明書は DNS 認証しかできないので

sudo certbot certonly \
  --manual \
  --domain *.example.com \
  --email mail@example.com \
  --agree-tos \
  --manual-public-ip-logging-ok \
  --preferred-challenges dns

表示された文字列をネームサーバの TXT レコードに登録し認証すれば OK。

自動更新するにはネームサーバーのレコード登録を自動化する必要があり外部 API に対応したネームサーバにすると良さそうです。

参考

google analyticsを導入しています