とある勉強会用にLTネタを作っていたのですが、発表できなくなったので腐る前にブログに書いておきます。
お前は何を(ry
記事のタイトルについて お前は何を言っているんだ なのですが、元ネタは以下のツイートです。
ここからTCPでやり取りするプロトコルが読み取れるし、mainって名前で同じプロトコル喋るバイナリならstaticにビルドすれば別の言語でもネイティブに動かせるんじゃね?https://t.co/dWjs8YiuFe
— Masashi Terui (@marcy_terui) 2018年1月17日
LTのネタになりそうだったのでやってみたわけです。
aws-lambda-goについて
じゃあ、まあaws-lambda-goは一体どういう仕組みで動いているんだろうと、ソースを読んでみました。
で、entry.goとfunction.goあたりでだいたい分かりましたが、net/rpcパッケージをつかってFunction#Invoke
とFunction#Ping
を呼んでいる感じでした。
ミニマムなハンドラ
必要なrpcのメソッドさえ実装すればaws-lambda-goがなくても動きます。 いろいろと削ってみて、ほぼ最小のコードは以下のようになりました。
package main import ( "encoding/json" "fmt" "log" "net" "net/rpc" "os" ) type PingRequest struct { } type PingResponse struct { } type Function struct { // handler lambdaHandler } type InvokeRequest struct { Payload []byte //RequestId string //XAmznTraceId string //Deadline InvokeRequest_Timestamp //InvokedFunctionArn string //CognitoIdentityId string //CognitoIdentityPoolId string //ClientContext []byte } type InvokeResponse struct { Payload []byte //Error *InvokeResponse_Error } func (fn *Function) Ping(req *PingRequest, res *PingResponse) (err error) { *res = PingResponse{} return } func (fn *Function) Invoke(req *InvokeRequest, response *InvokeResponse) error { response.Payload, _ = json.Marshal(100) return nil } func main() { port := os.Getenv("_LAMBDA_SERVER_PORT") l, err := net.Listen("tcp", fmt.Sprintf("localhost:%s", port)) if err != nil { log.Fatal(err) } f := new(Function) rpc.Register(f) rpc.Accept(l) log.Fatal("accept should not have returned") }
構造体のメンバを結構削っても、一応動くんですよね…
ローカルでハンドラを動かす
ハンドラはrpcのメソッドを呼ばれてるだけなので、ローカルから実行することもできます。
- ハンドラ
package main import ( "github.com/aws/aws-lambda-go/lambda" ) func hello(event interface{}) (int, error) { return 1, nil } func main() { lambda.Start(hello) }
- ローカル実行用のクライアント
package main import ( "fmt" "github.com/aws/aws-lambda-go/lambda/messages" "log" "net/rpc" ) func ping(client *rpc.Client) { req := &messages.PingRequest{} var res *messages.PingResponse err := client.Call("Function.Ping", req, &res) if err != nil { log.Fatal(err) } fmt.Printf("Ping: %v\n", *res) } func invoke(client *rpc.Client) { req := &messages.InvokeRequest{Payload: []byte("{\"foo\":100}")} res := messages.InvokeResponse{} err := client.Call("Function.Invoke", req, &res) if err != nil { log.Fatal(err) } fmt.Printf("Invoke: %v\n", string(res.Payload)) } func main() { client, err := rpc.Dial("tcp", "localhost:1234") if err != nil { log.Fatal("dialing:", err) } ping(client) invoke(client) }
実行はこんな感じで
_LAMBDA_SERVER_PORT=1234 ./hello
./client Ping: {} Invoke: 1
RustのハンドラをAWS Lambda Goで動かす
それで本題なのですが、net/rpcパッケージはGoに特化しているとはいえ、シリアライズされたデータをネットワーク経由でやりとりしているので、やろうと思えばほかの言語とも通信ができるはずです。 ただシリアライズにつかっているエンコーディングがgobで、さすがにこのエンコーダの他言語実装を見つけることはできませんでした。
仕方ないので、net/rpcのサーバ・クライアント間にプロキシ立ててパケットをキャプチャして流れているデータを調べた上で、そのデータをそのまま返すようなサーバを作ってみました。
use std::net::{TcpListener, TcpStream}; use std::thread; use std::io::Read; use std::io::Write; use std::env; fn handle_client(mut stream: TcpStream) { let mut buf; loop { buf = [0; 2048]; let _ = match stream.read(&mut buf) { Err(e) => panic!("Got an error: {}", e), Ok(m) => { if m == 0 { break; } m } }; let s = String::from_utf8_lossy(&buf); let ret: &[u8]; if s.contains("Ping") { ret = b":\xFF\x81\x03\x01\x01\x08Response\x01\xFF\x82\x00\x01\x03\x01\rServiceMethod\x01\x0c\x00\x01\x03Seq\x01\x06\x00\x01\x05Error\x01\x0c\x00\x00\x00\x12\xFF\x82\x01\rFunction.Ping\x00\x18\xFF\x83\x03\x01\x01\x0cPingResponse\x01\xFF\x84\x00\x00\x00\x03\xFF\x84\x00"; } else { ret = b"\x16\xFF\x82\x01\x0FFunction.Invoke\x01\x01\x00(\xFF\x85\x03\x01\x01\x0EInvokeResponse\x01\xFF\x86\x00\x01\x01\x01\x07Payload\x01\n\x00\x00\x00\x08\xFF\x86\x01\x03111\x00"; } match stream.write(ret) { Err(_) => break, Ok(_) => continue, } } } fn main() { let port = env::var("_LAMBDA_SERVER_PORT").unwrap(); let listener = TcpListener::bind(format!("localhost:{}", port)).unwrap(); for stream in listener.incoming() { match stream { Err(e) => println!("failed: {}", e), Ok(stream) => { thread::spawn(move || handle_client(stream)); } } } }
コードはRust Echo Server Example | Andrei Vacariu, Software Developerをほぼそのままコピーしています。 飛んできたメッセージを無理矢理Stringにして「Ping」という文字が入っていたらPing用のデータ、それ以外のデータはInveke用のデータを返すようにしています。
これ、linux-amd64でビルドしてLambdaにGolangとして登録すると、普通に動きます(「111」という値が返ってきます)
ということで、AWS Lambda Goはほかの言語でも動きます! やったぜ!
その他
Goに特化したrpcに対応するなら、先にgRPCに対応してもよかったのでは…と思わなくもない。
追記
そういえばRustはstatic linkにしてません。 ぱっとリンクしているライブラリを調べてみると
[ec2-user@ip-10-0-1-204 release]$ ldd hello linux-vdso.so.1 => (0x00007fff055a1000) libdl.so.2 => /lib64/libdl.so.2 (0x00007fee696d1000) librt.so.1 => /lib64/librt.so.1 (0x00007fee694c9000) libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fee692ad000) libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fee69097000) libc.so.6 => /lib64/libc.so.6 (0x00007fee68cd3000) /lib64/ld-linux-x86-64.so.2 (0x00007fee69b4f000)
こんな感じでした。 まあGoでもオプション指定しないとdynamic linkになるので、libcくらいは使えますね…と。