Ubuntu 18.04 + nginx + lua拡張モジュールでRedis Serverへ接続

事の発端はLINE APIを触っていた時。
LINE API + Google App Engine + Pythonでボットを作成する

POSTされた値を処理した後にレスポンスを返すわけですが、処理に手間取るとタイムアウトするんじゃないか。
だったら、受け取ったPOSTデータを一旦退避してとりあえず200レスポンスを返す。
退避しておいたデータをじっくり眺めて処理すれば、タイムアウトを気にしなくてよいのでは。

というわけで、nginxをlua拡張モジュール付きでUbuntu Server 18.04にインストール。
Redisにpostされたデータを登録してみます。


nginx-extras



nginx-extrasをインストールすれば、lua拡張モジュール付きでビルドされた
nginxがインストールできます。
apt-getでDebian 8 + lua nginx moduleの環境構築(nginx-extras)


$ sudo apt install nginx-extras



動作確認のため、/etc/nginx/sites-available/defaultを編集。


$ sudo vi /etc/nginx/sites-available/default



locationの部分を編集します。


  1.         location / {
  2.                 default_type 'text/plain';
  3.                 content_by_lua "ngx.say('Hello,lua!')";
  4.         }



nginxの設定ファイルをリロード


$ sudo service nginx reload



ブラウザでアクセスしてみます。

877_01.png

luaが動いていますね。



スクリプトファイルの準備とデバッグ設定



毎回nginxの設定ファイルを変更するのは面倒なので、プログラム部分を別ファイルに記載できるようにします。
また、プログラムを書き換えるたびに設定ファイルをリロードするのも手間なので、
「lua_code_cache off」でスクリプトファイルをキャッシュしないよう指定しておきます。
※本番環境では使用しないほうが良いです。


  1.         location / {
  2.                 lua_code_cache off;
  3.                 default_type 'text/plain';
  4.                 content_by_lua_file /var/dev/lua/sample.lua;
  5.         }



編集がおわったらリロード。


$ sudo service nginx reload



/var/dev/lua/sample.luaにプログラムを記載してみます。

・sample.lua


  1. ngx.say('Hello, from script')



877_02.png

ちゃんと読み込まれていますね。



lua-nginx-redis



redisへの接続はlua-nginx-redisを使ってみます。
https://github.com/openresty/lua-resty-redis

aptでインストール。


$ sudo apt install lua-nginx-redis



サンプルを実行してみます。


  1. local redis = require "nginx.redis"
  2. local r = redis:new()
  3. -- データベース接続
  4. local ok, err = r:connect("127.0.0.1", 6379, 0)
  5. if not ok then
  6.     ngx.say("failed to connect: ", err)
  7.     return
  8. end
  9. -- データ登録
  10. ok, err = r:set("dog", "an animal")
  11. if not ok then
  12.     ngx.say("failed to set dog: ", err)
  13.     return
  14. end
  15. -- データ取得
  16. local res, err = r:get("dog")
  17. if not res then
  18.     ngx.say("failed to get dog: ", err)
  19.     return
  20. end
  21. if res == ngx.null then
  22.     ngx.say("dog not found.")
  23.     return
  24. end
  25. -- 取得したデータを表示
  26. ngx.say("dog: ", res)



877_03.png

いい感じです。



postデータの登録



postされたデータを登録してみます。
過去記事が参考になりました。
nginx + luaで簡易ファイルストレージ

・sample.lua


  1. local method = ngx.req.get_method()
  2. if method ~= 'POST' then
  3.     ngx.say('post only')
  4.     return
  5. end
  6. -- bodyの解析
  7. ngx.req.read_body()
  8. -- body_dataを取得してみる
  9. local req_body = ngx.req.get_body_data()
  10. -- 取得できなかったら、ファイルに保存されている
  11. if not req_body then
  12.     -- テンポラリファイル名を取得
  13.     local req_body_file_name = ngx.req.get_body_file()
  14.     -- 内容をすべて読み込み
  15.     local file = io.open(req_body_file_name, 'rb')
  16.     req_body = file:read('*a')
  17.     file.close()
  18. end
  19. local redis = require "nginx.redis"
  20. local r = redis:new()
  21. -- データベース接続
  22. local ok, err = r:connect("127.0.0.1", 6379, 0)
  23. if not ok then
  24.     ngx.say("failed to connect: ", err)
  25.     return
  26. end
  27. -- データ登録
  28. ok, err = r:set("body", req_body)
  29. if not ok then
  30.     ngx.say("failed to set body: ", err)
  31.     return
  32. end
  33. ngx.say('ok')



データ登録用のPythonスクリプトです。

・post.py


  1. # -*- coding:utf-8 -*-
  2. import urllib.request
  3. import json
  4. # 登録用のデータ
  5. data = {
  6.     'num' : 105,
  7.     'alphabet' : 'alphabet-value',
  8.     'str': '日本語登録テスト'
  9. }
  10. # JSON形式の文字列を取得
  11. jsonstr = json.dumps(data).encode("utf-8")
  12. url = 'http://192.168.1.102/'
  13. request = urllib.request.Request(url, data=jsonstr, method='POST')
  14. request.add_header('Content-Type', 'application/json')
  15. # 登録実行
  16. response = urllib.request.urlopen(request)
  17. ret = response.read()
  18. print('Response:', ret)



実行すると「ok」と応答が帰ってきました。


$ python3 post.py
Response: b'ok\n'



次はredisに登録されているはずのpostデータを取得してみます。


  1. # -*- coding:utf-8 -*-
  2. import json
  3. import redis
  4. r = redis.StrictRedis(host='192.168.1.102', port=6379, db=0)
  5. rawdata = r.get('body')
  6. data = json.loads(rawdata)
  7. print(data['num'])
  8. print(data['alphabet'])
  9. print(data['str'])



実行結果


$ python3 sample.py
105
alphabet-value
日本語登録テスト



ちゃんとpostされたデータをredis経由で取得できました。

Riot ajaxで取得したjsonデータをテーブルに表示する(axios使用)

lua nginx moduleでデータベースにアクセスし、json形式のデータを返却することができました。
lua nginx moduleからMariaDB(MySQL)に接続する(lua-sql-mysql)

Riotで取得したjsonを表示してみようと思います。


axios



Riotにajax通信を行う機能はないので、別途ライブラリを使用することにしました。
こちらを参考に「axios」を使ってみます。
superagentとaxiosの使い分け
https://github.com/mzabriskie/axios

CDNを使うことにしました。
https://cdnjs.com/libraries/axios



json形式のデータ



lua nginx moduleからMariaDB(MySQL)に接続する(lua-sql-mysql)
こちらで使ったluaをそのまま使用することにしました。


  1. local driver = require 'luasql.mysql'
  2. env = assert (driver.mysql())
  3. con = assert (env:connect('sample', 'root', 'P@ssw0rd', 'localhost', 3306))
  4. -- 接続文字コードを変更
  5. assert (con:execute('SET NAMES utf8mb4'))
  6. -- データをを取得
  7. result = {}
  8. cur = assert (con:execute("SELECT id, value from t"))
  9. row = cur:fetch ({}, "a")
  10. while row do
  11. local copy_row = {}
  12. for field in pairs(row) do
  13. copy_row[field] = row[field]
  14. end
  15. table.insert(result, copy_row)
  16. row = cur:fetch (row, "a")
  17. end
  18. cur:close()
  19. con:close()
  20. env:close()
  21. -- テーブルをjsonに変換
  22. local cjson = require "cjson"
  23. json_text = cjson.encode(result)
  24. ngx.say(json_text)




テーブルの内容をそのまま返します。


nginxの設定ファイルの抜粋はこのようになりました。


    root /var/www/html;

    location /api {
        lua_code_cache off;
        default_type 'application/json';
        content_by_lua_file /var/dev/lua/sample.lua;
    }
    
    location / {
        try_files $uri $uri/ =404;
    }



/apiでアクセスされた時はluaを実行。
それ以外は、/var/www/htmlにあるファイルを表示します。


Riot + axiosを記載するhtmlファイルは、/var/www/html/index.htmlに配置します。


  1. <!doctype html>
  2. <html lang="ja">
  3.     
  4. <html>
  5. <head>
  6.     <meta charset="UTF-8">
  7.     <title>Riot + axios</title>
  8.     <meta http-equiv="X-UA-Compatible" content="IE=edge">
  9. </head>
  10. <body>
  11.     <!-- ここにタグの内容を反映 -->
  12.     <my-tag></my-tag>
  13. </body>
  14. <!-- ここからRiotプログラム -->
  15. <!-- カスタムタグ -->
  16. <script type="riot/tag">
  17. <my-tag>
  18. <h3>ボタンクリックでデータ取得</h3>
  19. <div><input type="button" value="追加" onclick={ add }></div>
  20. <table border="1">
  21.     <thead>
  22.      <tr>
  23.         <td>id</td><td>value</td>
  24.      </tr>
  25.     </thead>
  26.     <tbody>
  27.     <tr each={ items }>
  28.         <td>{ id }</td>
  29.         <td>{ value }</td>
  30.     </tr>
  31.     </tbody>
  32. </table>
  33. var self = this;
  34. self.items = []
  35. add(event) {
  36.      axios.get('/api')
  37.         .then(function (response) {
  38.             self.items = response.data
  39.             self.update()
  40.         })
  41.         .catch(function (error) {
  42.             console.log(error)
  43.         });
  44. }
  45. </my-tag>
  46. </script>
  47. <!-- マウント -->
  48. <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.1/axios.min.js"></script>
  49. <script src="https://cdnjs.cloudflare.com/ajax/libs/riot/3.4.0/riot%2Bcompiler.min.js"></script>
  50. <script>
  51. riot.mount('my-tag')
  52. </script>
  53.     
  54. </html>



データが再描画されなくて悩みましたが、こちらが参考になりました。
Riot.jsでAjaxを使った非同期通信について


初期状態

742_01.png

ボタンクリック後

742_02.png


ちゃんとデーブルの内容が更新されました。

lua nginx moduleでMessagePackのデータを使用(lua-MessagePack)

lua nginx moduleでいろいろ試しています。
apt-getでDebian 8 + lua nginx moduleの環境構築(nginx-extras)
lua nginx moduleでjson形式のレスポンスを返す(cjson)
lua nginx moduleからMariaDB(MySQL)に接続する(lua-sql-mysql)

jsonの使い方に続き、MessagePackが使えるか調べてみます。


lua-MessagePackとluarocks



これでMessagePack形式のデータが扱えそうです。
lua-MessagePack

インストールにはluarocksを使うのが楽そう。
まず、aptでluarocksをインストールします。


# apt-get install luarocks



luarocksでlua-messagepackをインストール。


# luarocks install lua-messagepack
Installing https://rocks.moonscript.org/lua-messagepack-0.5.0-1.src.rock...
Using https://rocks.moonscript.org/lua-messagepack-0.5.0-1.src.rock... switching to 'build' mode
Updating manifest for /usr/local/lib/luarocks/rocks
lua-messagepack 0.5.0-1 is now built and installed in /usr/local (license: MIT/X11)



インストールできた模様。


nginxの設定ファイル(/etc/nginx/sites-available/default)の抜粋はこうなります。


    location / {
        lua_code_cache off;
        default_type 'application/octet-stream';
        content_by_lua_file /var/dev/lua/sample.lua;
    }




動作確認用に用意した/var/dev/lua/sample.lua


  1. local mp = require 'MessagePack'
  2. value = { true, { foo = "bar" } }
  3. mp_value = mp.pack(value)
  4. ngx.say(mp_value)




packでmessagepack形式に変換してくれます。
ブラウザで確認してみると、こんな表示になりました。

741_01.png



クライアントをPythonで記載してみます。
PythonでMessagePackを使用して、Mapのシリアライズと復元

pipでmsgpack-pythonをインストール。


$ sudo pip install msgpack-python




サンプルはこうなりました。


  1. # -*- coding:utf-8 -*-
  2. import urllib2
  3. import msgpack
  4. f = urllib2.urlopen('http://192.168.1.104/')
  5. packed = f.read().strip()
  6. ret = msgpack.unpackb(packed, encoding='utf-8')
  7. print ret




レスポンスデータの最後に改行が入ります。
stripで取り除いておかないと、こんなエラーになりました。


Traceback (most recent call last):
File "sample.py", line 8, in <module>
    ret = msgpack.unpackb(packed, encoding='utf-8')
File "msgpack/_unpacker.pyx", line 143, in msgpack._unpacker.unpackb (msgpack/_unpacker.cpp:2143)
msgpack.exceptions.ExtraData: unpack(b) received extra data.




実行してみると、ちゃんと復元できています。


$ python sample.py
[True, {u'foo': u'bar'}]








MariaDBからデータ取得



lua nginx moduleからMariaDB(MySQL)に接続する(lua-sql-mysql)
こちらのサンプルを流用し、jsonではなくmessagepack形式のデータに変更してみます。

lua側のプログラム


  1. local mp = require 'MessagePack'
  2. -- bodyの解析
  3. ngx.req.read_body()
  4. -- body_dataを取得してみる
  5. local req_body = ngx.req.get_body_data()
  6. -- もし取得できていなかったら、データはファイルに行っている
  7. if not req_body then
  8.     -- テンポラリのファイル名を取得。内容を全部読み込む
  9.     local req_body_file_name = ngx.req.get_body_file()
  10.     if req_body_file_name then
  11.         local file = io.open(req_body_file_name, 'rb')
  12.         req_body = file:read('*a')
  13.         file:close()
  14.     end
  15. end
  16. if not req_body then
  17.     ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
  18. end
  19. -- postされた値を解析
  20. -- unpackでバイナリからオブジェクトに変換
  21. local post = mp.unpack(req_body)
  22. local driver = require 'luasql.mysql'
  23. env = assert (driver.mysql())
  24. con = assert (env:connect('sample', 'root', 'P@ssw0rd', 'localhost', 3306))
  25. -- 接続文字コードを変更
  26. assert (con:execute('SET NAMES utf8mb4'))
  27. -- 結果を取得
  28. result = {}
  29. cur = assert (con:execute(post.query))
  30. row = cur:fetch ({}, "a")
  31. while row do
  32. local copy_row = {}
  33. for field in pairs(row) do
  34. copy_row[field] = row[field]
  35. end
  36. table.insert(result, copy_row)
  37. row = cur:fetch (row, "a")
  38. end
  39. -- close everything
  40. cur:close() -- already closed because all the result set was consumed
  41. con:close()
  42. env:close()
  43. -- 結果セットをpackでバイナリに変換
  44. mp_raw = mp.pack(result)
  45. ngx.say(mp_raw)




クライアントとなるPython側のプログラム


  1. # -*- coding:utf-8 -*-
  2. import urllib2
  3. import msgpack
  4. data = msgpack.packb({u'query':u'select * from t where id = 2'})
  5. f = urllib2.urlopen('http://192.168.1.104/', data)
  6. mp_raw = f.read().strip()
  7. mp_data = msgpack.unpackb(mp_raw, encoding='utf-8')
  8. for row in mp_data:
  9.     print row['id'], row['value']




思いの外あっさり動きました。


$ python sample.py
2 日本語テスト2




lua nginx moduleからMariaDB(MySQL)に接続する(lua-sql-mysql)

Debian 8にインストールしたlua nginx moduleでいろいろ試しています。
apt-getでDebian 8 + lua nginx moduleの環境構築(nginx-extras)
lua nginx moduleでjson形式のレスポンスを返す(cjson)

MariaDB(MySQL)への接続を試してみます。


lua-sql-mysql



luaからMariaDBに接続する方法を調べてみると、lua-sql-mysqlを使用するのが良さそうでした。
https://keplerproject.github.io/luasql/

apt-getでインストールします。


# apt-get install lua-sql-mysql




同じサーバーにMariaDBをインストール。
「sample」データベース、「t」テーブルを作成しておきました。


  1. create table t (
  2. id int not null,
  3. value varchar(100) not null
  4. );






サンプル



「t」テーブルにデータを登録。
結果をjson形式で表示してみます。


  1. local driver = require 'luasql.mysql'
  2. env = assert (driver.mysql())
  3. con = assert (env:connect('sample', 'root', 'P@ssw0rd', 'localhost', 3306))
  4. -- 接続文字コードを変更
  5. assert (con:execute('SET NAMES utf8mb4'))
  6. -- 一旦テーブルの内容を削除
  7. assert (con:execute('DELETE FROM t'))
  8. -- 登録するデータ
  9. list = {
  10. { id=1, value='日本語テスト1', },
  11. { id=2, value='日本語テスト2', },
  12. }
  13. for i, row in pairs (list) do
  14. res = assert (con:execute(string.format([[
  15.     INSERT INTO t
  16.     VALUES (%s, '%s')]], row.id, con:escape(row.value))
  17. ))
  18. end
  19. -- 結果を取得
  20. result = {}
  21. cur = assert (con:execute("SELECT id, value from t"))
  22. row = cur:fetch ({}, "a")
  23. while row do
  24. local copy_row = {}
  25. for field in pairs(row) do
  26. copy_row[field] = row[field]
  27. end
  28. table.insert(result, copy_row)
  29. row = cur:fetch (row, "a")
  30. end
  31. -- close everything
  32. cur:close() -- already closed because all the result set was consumed
  33. con:close()
  34. env:close()
  35. local cjson = require "cjson"
  36. json_text = cjson.encode(result)
  37. ngx.say(json_text)






コネクション作成時の「env:connect」の引数は
データベース名,ID,パスワード,ホスト,ポート
となっており文字コードの指定はありません。

「SET NAMES utf8mb4」を実行して変更してやればうまく日本語が登録できました。

ブラウザで結果を表示してみます。

740_01.png




もう少し汎用的に



postで実行するsql文を受け取るように変更してみます。


  1. local cjson = require "cjson"
  2. -- bodyの解析
  3. ngx.req.read_body()
  4. -- body_dataを取得してみる
  5. local req_body = ngx.req.get_body_data()
  6. -- もし取得できていなかったら、データはファイルに行っている
  7. if not req_body then
  8.     -- テンポラリのファイル名を取得。内容を全部読み込む
  9.     local req_body_file_name = ngx.req.get_body_file()
  10.     if req_body_file_name then
  11.         local file = io.open(req_body_file_name, 'rb')
  12.         req_body = file:read('*a')
  13.         file:close()
  14.     end
  15. end
  16. if not req_body then
  17.     ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
  18. end
  19. -- postされた値を解析
  20. local post = cjson.decode(req_body)
  21. local driver = require 'luasql.mysql'
  22. env = assert (driver.mysql())
  23. con = assert (env:connect('sample', 'root', 'P@ssw0rd', 'localhost', 3306))
  24. -- 接続文字コードを変更
  25. assert (con:execute('SET NAMES utf8mb4'))
  26. -- 結果を取得
  27. result = {}
  28. cur = assert (con:execute(post.query))
  29. row = cur:fetch ({}, "a")
  30. while row do
  31. local copy_row = {}
  32. for field in pairs(row) do
  33. copy_row[field] = row[field]
  34. end
  35. table.insert(result, copy_row)
  36. row = cur:fetch (row, "a")
  37. end
  38. -- close everything
  39. cur:close() -- already closed because all the result set was consumed
  40. con:close()
  41. env:close()
  42. json_text = cjson.encode(result)
  43. ngx.say(json_text)




結果を要求するクライアントはPythonで作成してみました。


  1. # -*- coding:utf-8 -*-
  2. import urllib2
  3. import json
  4. data = json.dumps({u'query':u'select * from t where id = 2'})
  5. f = urllib2.urlopen('http://192.168.1.104/', data)
  6. json_text = f.read()
  7. json_data = json.loads(json_text)
  8. for row in json_data:
  9.     print row['id'], row['value']




狙い通りの実行結果です。


$ python sample.py
2 日本語テスト2





【参考URL】

LuaスクリプトからMySQLを利用するサンプル
https://keplerproject.github.io/luasql/examples.html
Luaのお勉強 テーブルの操作

lua nginx moduleでjson形式のレスポンスを返す(cjson)

Debian 8でlua nginx moduleが動く環境を用意し、デバッグ方法を調べました。
apt-getでDebian 8 + lua nginx moduleの環境構築(nginx-extras)

簡単な文字列をngx.sayで返却しましたが、json形式の文字列を返してみます。



cjson



luaでjsonを扱うにはcjsonが便利そうです。
lua-cjson-manual

aptでインストールできるか調べたところ、


# apt-cache search cjson
lua-cjson - JSON parser/encoder for Lua
lua-cjson-dev - JSON parser/encoder for Lua, development files
python-cjson - Very fast JSON encoder/decoder for Python
python-cjson-dbg - Very fast JSON encoder/decoder for Python (debug extension)




lua-cjsonでインストールできそうです。


# apt-get install lua-cjson







サンプル



こちらで作成した環境を使います。
apt-getでDebian 8 + lua nginx moduleの環境構築(nginx-extras)

nginxの設定ファイル(/etc/nginx/sites-available/default)の抜粋。


    charset UTF-8;
    charset_types application/json;

    location / {
        lua_code_cache off;
        default_type 'application/json';
        content_by_lua_file /var/dev/lua/sample.lua;
    }




/var/dev/lua/sample.luaにプログラムを記載します。


  1. local cjson = require "cjson"
  2. value = { true, { foo = "bar" } }
  3. json_text = cjson.encode(value)
  4. ngx.say(json_text)




ブラウザで確認すると、ちゃんとjson形式の文字列になっているようです。

739_01.png


簡単なPythonのプログラムでも読み込みを試してみます。


  1. # -*- coding:utf-8 -*-
  2. import urllib2
  3. import json
  4. f = urllib2.urlopen('http://192.168.1.104/')
  5. json_text = f.read()
  6. print json_text
  7. print '-' * 10
  8. print json.loads(json_text)




ちゃんと復元できました。


$ python sample.py
[true,{"foo":"bar"}]

----------
[True, {u'foo': u'bar'}]






もう少し複雑な例



postでjson形式のデータを受け取り、内容に応じで値を返却してみます。

lua側のサンプル


  1. local cjson = require "cjson"
  2. -- bodyの解析
  3. ngx.req.read_body()
  4. -- body_dataを取得してみる
  5. local req_body = ngx.req.get_body_data()
  6. -- もし取得できていなかったら、データはファイルに行っている
  7. if not req_body then
  8.     -- テンポラリのファイル名を取得。内容を全部読み込む
  9.     local req_body_file_name = ngx.req.get_body_file()
  10.     if req_body_file_name then
  11.         local file = io.open(req_body_file_name, 'rb')
  12.         req_body = file:read('*a')
  13.         file:close()
  14.     end
  15. end
  16. if not req_body then
  17.     ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
  18. end
  19. -- postされた値を解析
  20. local post = cjson.decode(req_body)
  21. data = { status = 'ok', value = '取得したvalue:' .. post['value'] }
  22. json_text = cjson.encode(data)
  23. ngx.say(json_text)





リクエストの内容解析は過去の記事が役に立ちました。
nginx 1.6.2 + lua-nginx-moduleで簡易ファイルアップローダー


リクエストを送信するPython側のサンプル


  1. # -*- coding:utf-8 -*-
  2. import urllib2
  3. import json
  4. data = json.dumps({u'value':u'日本語'})
  5. f = urllib2.urlopen('http://192.168.1.104/', data)
  6. json_text = f.read()
  7. json_data = json.loads(json_text)
  8. print json_data['value']




実行結果


$ python sample.py
取得したvalue:日本語




日本語も問題なく処理できていますね。


【参考URL】

https://www.kyne.com.au/~mark/software/lua-cjson-manual.html
nginxで静的jsonファイル配信時に日本語が文字化けしないようにする

プロフィール

Author:symfo
blog形式だと探しにくいので、まとめサイト作成中です。
https://symfo.web.fc2.com/

PR

検索フォーム

月別アーカイブ