見出し画像

GopherLuaを使ってデプロイせずに機能を追加してみた REALITY Advent Calendar 2023

REALITYのサーバエンジニアの落合と申します。
やっと冬らしく寒くなってきましたがみなさんいかがお過ごしでしょうか。自分は寒いので引きこもってゲームする毎日で、今年の年末はバルダーズゲート3に捧げようと思います。この令和の世に、D&DベースのゲームがGOTY取るなんてオールドゲーマーとしては感慨深いです。
さて今回は開発合宿でGopherLuaを使って遊んでみたので、それについての紹介をしてみたいと思います!
この記事は REALITY Advent Calendar 2023 の16日目となります。

作ったもの

今回作ったのは、LuaのコードをAPI経由でサーバに登録して、それを実行させる簡易的な仕組みです。
…と、具体的に何をできるのかがわからないと思いますので、この機能を使って、オリジナルの配信コマンドを実装し、かつ、それをサーバのデプロイといったサーバの修正なしに機能を変更してみせます。

まず、前提として、REALITYの配信にはコメント機能が存在しています。
このコメントは、コメントをユーザーのアプリへ送るためにコメントサーバと呼ばれるwebsocketのサーバが存在しています。
コメント機能は、単に各ユーザーのコメントの投稿を表示するだけでなく、特定のワードで呼び出せるコマンドが存在していて、このコマンドの機能もコメントサーバに実装されています。
このコマンドはコメントサーバのコードで実装されているので、新たに機能を追加したり、修正したりするにはコメントサーバのコードのデプロイが必要になるのですが、このコマンドをもっと手軽に登録できたら良いなと思ったので、そのために先ほどの"LuaのコードをAPI経由でサーバに登録して、それを実行させる簡易的な仕組み"を利用します。

では実際に動作させてみます。
まず、社内の開発サーバの以下のAPIに、request bodyにLuaのコードを設定してリクエストします。このAPIリクエストによって、Luaのコードをサーバに登録します。

# API
POST /lua/code/talk

# request bodyで送るLuaのコード
return "話しかけないでください"

次に、REALITYの配信のコメントで「talk ohayou」と入力すると、以下のように「話しかけないでください」と応答します。

次に、先ほどと同じAPIに新たなLuaのコードを送信します。

# API
POST /lua/code/talk

# request bodyで送るLuaのコード
responses = {
    ["ohayou"] = "おはようの挨拶なんてするんですね",
    ["oyasumi"] = "勝手に寝てください",
    ["genki?"] = "応える必要あります?",
}
if responses[val1] then
    return responses[val1]
end
return "話しかけないでください"

その後、同じようにコメントで「talk ohayo」と送ると、コメントの返信が変わります。

このように、APIでLuaのコードをサーバへ送ることで動作を変更します。
(これだけ書くと結構危険な実装ですが、これは社内の開発サーバだけで動作しています)
この実装は、APIサーバ(golangで実装されたサーバ)で、先ほど登録したLuaコードを実行し、その結果をコメントで応答しています。
GolangのAPIサーバでLuaを実行するには、GopherLuaというGoのオープンソースパッケージを利用しています。

GopherLuaとは

GopherLuaとは、Goで書かれたLuaの実装で、Lua5.1相当のLuaコードをGoから呼び出して実行することが可能です。

なぜLuaを採用したのかというと、もともとlua-nginx-moduleなどでLuaを使ったことがあった、ということもあるのですが、以下のいくつかのLuaの長所が今回は最適と感じたからです。

  • Luaはメモリフットプリントがとても小さいので、サーバのメモリへの負担がとても小さくて済む。

  • 基本的なプログラミング言語の機能は抑えつつ、学習コストが低いので、プログラミング言語に詳しくない人でも比較的覚えやすい。

  • 組み込みスクリプトとしていろんなところで使われている実績があるので安心感がある。

今回実装した機能は、超簡易的なfunctions(AWSだとLambda、GCPだとCloudFunctionsと呼ばれる、コードを登録することでサーバレスで使えるサービス)のようなものです。
実際、functionsを使ってもよかったのですが、そこまで汎用性は必要なかったのと、functionsが対応しているランタイムがNode.jsやPythonといった立派な言語(という言い方もないですが、Luaよりは学習コストのある言語)であったため、もっと簡易的なスクリプトの実行に適した言語を使いたかったというのがあります。
今回は社内の開発環境で、かつ、合宿で一時的に使うコードであるためあまりセキュリティについてなどは考えず、とても簡易的な実装にしました(以下は重要な部分を省いているのでちゃんと動作はしませんので雰囲気だけ掴んでください)。

さて、先ほどのデモで、内部的にどのような動作をしていたのかと言いますと、

  1. コメントに「talk ohayo」と入力すると、コメントサーバからGolangのAPIサーバにLuaコードの実行を要求するAPIのリクエストが行われる。

  2. APIでLuaコードの実行を要求されたGolangのサーバは、登録されたLuaコードを実行する。

  3. Luaコードの実行結果を、コメントサーバへレスポンスする。

  4. コメントサーバはレスポンスの内容を、コメントへ表示する。

という動作を行います。
以下は、GolangのAPIサーバがLuaコードの実行を行う実装になります。

func runLua(codeKey string, val1 string) (string, string, error) {
	L := lua.NewState()
	defer L.Close()

	L.SetGlobal("val1", lua.LString(val1))

	var code string

	//cacheから取得
	cacheKey := fmt.Sprintf(cacheKeyLuaCodeFormat, codeKey)
	err := cache.Get(cacheKey, &code)
	if err != nil {
		return "", "", err
	}

	//cacheから取得したLuaコードを実行
	if err := L.DoString(code); err != nil {
		return "", "", err
	}

	result1, result2 := lua.LNil, lua.LNil

	result1 = L.Get(-2)
	if result1 == lua.LNil {
		//returnが1つしかない場合
		result1 = L.Get(-1)
	} else {
		//returnが2つの場合
		result2 = L.Get(-1)
	}

	//LuaValueはStringerを実装してるのでStringで返す
	return result1.String(), result2.String(), nil
}

単純に引数を1つだけ渡して結果を取っています。これをAPIでリクエストして実行し、APIのレスポンスで結果を受け取れるようにしています。
上記以外に、コードを登録できるツールも作成しましたが、そちらは単にキャッシュにコードを登録しています。

先ほど登録したLuaのコードをAPI経由で実行した結果が以下のような感じです。

POST /lua/code/talk/run?val1=ohayou

{
  "resp": "話しかけないでください"
}

この結果をコメントに表示したのが「作ったもの」に記載したものになります。

おわりに

開発合宿でやることとしてはちょっと小粒な開発になってしまいましたが、Luaは昔使っていたこともあり結構好きだったので、弊社サービスでも何かに使えたら…と思って実験的なことをしてみました。
LuaについてはRedis上で起動できたり、nginx-luaのようにnginxの動作を変更できたり、またゲーム開発ではよくスクリプトとして使われたりと、Luaのメモリフットプリントの小ささや、学習コストの低さからいろんな用途に使われていますので、読者のみなさんもぜひ使ってみてください。

明日の17日目はぴかさんによる「お試しVC機能」です!