AWS Lambda+Goで、fujiwara/ridgeからapex/gatewayへ移行する

AWS Lambdaでは以前から、非公式にNode.jsをランタイムとして、Go実装のバイナリを実行させるツールがある。例えばApexとか。さらに、Apex、API Gatewayとの組み合わせを前提としたfujiwara/ridgeというライブラリがあり、net/httpのインターフェースにそった実装ができるようになっていた。

実際に会社でも個人的にも、Apex+Go+fujiwara/ridgeという組み合わせで稼働させているものがある。現時点ではランタイムはNode.jsで動いているが、公式にGoがサポートされるようになったので、移行をしたくてしばらく検証していた。

公式のライブラリ、aws/aws-lambda-goを使用した実装に直接入れ替えるのはしんどそうな感じだったが、apex/gatewayを見つけたのでそれを試してみた。

結果、問題なさそうだったので、とりあえず個人的なやつはそちらに移行した。

移行に伴う修正点

どちらもnet/httpのインターフェースを前提としているので、スムーズに移行できる。

ridgeのサンプルを変更すると以下になる。importするパッケージは当然変わるが、Multiplexerはそのまま使用できる。直接aws-lambda-goを使う必要がないので、大きな変更がなくて助かる。

--- main_old.go  2018-04-07 00:06:44.000000000 +0900
+++ main.go   2018-04-07 00:09:00.000000000 +0900
@@ -4,7 +4,7 @@
    "fmt"
    "net/http"

-  "github.com/fujiwara/ridge"
+   "github.com/apex/gateway"
 )

 var mux = http.NewServeMux()
@@ -15,7 +15,7 @@
 }

 func main() {
-  ridge.Run(":8080", "/api", mux)
+   gateway.ListenAndServe(":8080", mux)
 }

ちょっとapex/gatewayのコードを見てみる

https://github.com/apex/gateway/blob/master/gateway.go#L16

// ListenAndServe is a drop-in replacement for
// http.ListenAndServe for use within AWS Lambda.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, h http.Handler) error {
    if h == nil {
        h = http.DefaultServeMux
    }

    lambda.Start(func(ctx context.Context, e events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
        r, err := NewRequest(ctx, e)
        if err != nil {
            return events.APIGatewayProxyResponse{}, err
        }

        w := NewResponse()
        h.ServeHTTP(w, r)
        return w.End(), nil
    })

    return nil
}

ListenAndServeの中では、lambda.Startを呼び出しつつnet/httpのインターフェースに沿うように対応されている。w.Endevents.APIGatewayProxyResponseが返却されるはず。もう少し読み込んでみる。

https://github.com/apex/gateway/blob/master/response.go#L13

// ResponseWriter implements the http.ResponseWriter interface
// in order to support the API Gateway Lambda HTTP "protocol".
type ResponseWriter struct {
    out           events.APIGatewayProxyResponse
    buf           bytes.Buffer
    header        http.Header
    wroteHeader   bool
    closeNotifyCh chan bool
}

ResponseWriterはこんな構造になっている。

https://github.com/apex/gateway/blob/master/response.go#L39

// Write implementation.
func (w *ResponseWriter) Write(b []byte) (int, error) {
    if !w.wroteHeader {
        w.WriteHeader(http.StatusOK)
    }

    return w.buf.Write(b)
}

WriteのなかではResponseWriter.bufに対しての書き込みが行われるようになっている。

https://github.com/apex/gateway/blob/master/response.go#L77

// End the request.
func (w *ResponseWriter) End() events.APIGatewayProxyResponse {
    w.out.IsBase64Encoded = isBinary(w.header)

    if w.out.IsBase64Encoded {
        w.out.Body = base64.StdEncoding.EncodeToString(w.buf.Bytes())
    } else {
        w.out.Body = w.buf.String()
    }

    // notify end
    w.closeNotifyCh <- true

    return w.out
}

ヘッダーを見て、バイナリのデータかどうか確認しているっぽい。AWS API Gatewayではバイナリデータを返す際にはBase64エンコードされている必要があるので、それにも対応している。

out.Bodyが実際のレスポンスになるわけで、そこにResponseWriter.bufに書き込まれたデータが代入される。

ResponseWriter.closeNofityChtrueが送信されるようになっているが、これはapex/gateway内部では利用していないっぽい。利用側に通知するためのチャネルのようだ。(経緯としてはfeat(Response) Add CloseNotify support. by wolfeidau · Pull Request #7 · apex/gatewayに書いてある)

最終的には、ResponseWriteroutevents.APIGatewayProxyResponseになっているので、それを返すようにしている。

気になるところ

ちゃんとコード全体を見ていないのだけど。

fujiwara/ridge ではできていた以下の点が apex/gatewayでできるのかきちんと確認してない(必要性もないので)

  • Proxy Integrationに対応していて、リクエストがあったパスの情報をきちんとMultiplexerで処理できるようになっていた
    • 自分の要求としては/を前提にしているので、そこまで確認してない
  • ローカルでの起動と実行
    • 自分ではそこまで必要性がなく、実際今まで使ったことがないんだけど

参考