AWS Lambda Go without Go

とある勉強会用にLTネタを作っていたのですが、発表できなくなったので腐る前にブログに書いておきます。

お前は何を(ry

記事のタイトルについて お前は何を言っているんだ なのですが、元ネタは以下のツイートです。

LTのネタになりそうだったのでやってみたわけです。

aws-lambda-goについて

じゃあ、まあaws-lambda-goは一体どういう仕組みで動いているんだろうと、ソースを読んでみました。

で、entry.gofunction.goあたりでだいたい分かりましたが、net/rpcパッケージをつかってFunction#InvokeFunction#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くらいは使えますね…と。