Goとlambda-http-adaptorでAPIGW・ALBで動くLambdaを作る

2021/09/29に公開

概要

ご存じの通り、Goはクロスコンパイルが容易な上、成果物がシングルバイナリになります。
その為、AWS Lambdaに容易にデプロイできる上、コールドスタート直後からパフォーマンスも良く[1]とてもおすすめです。

この特徴を気軽に活用できるように、net/httpベースのアプリケーションをそのまま利用する事が出来るlambda-http-adaptorを作りましたので紹介します。

これを使うと、net/http向けに書いたhttp.Handlerが、1つのLambdaで同時に以下の環境で使う事が出来ます。

  • APIGateway Lambda統合(REST、HTTP、WebSocket[2])
  • Application Load Balancer(以下ALB)のLambda Target
package main

import (
	"github.com/yacchi/lambda-http-adaptor"
	_ "github.com/yacchi/lambda-http-adaptor/all"
	"log"
	"net/http"
)

func main() {
	log.Fatalln(adaptor.ListenAndServe("", http.HandlerFunc(echoReplyHandler)))
}

func echoReplyHandler(w http.ResponseWriter, r *http.Request) {
	m := r.URL.Query().Get("message")
	w.Header().Set("Content-Type", "text/plain")
	w.Write([]byte(m))
}

特徴

  • APIGatewayのRESTAPI・HTTPAPI・WebSocketのいずれのモードでも、同じコードで動きます
  • ALBのTargetGroupに指定するコードも、同じコードで動きます
  • RequestのContextから生Lambda EventやLambda Contextを取得することが出来ます
  • MultiValueHeaderやMultiValueQueryの設定に依らず動作します

使い方

例の通り、いつものhttp.ListenAndServeに任意のハンドラを渡すだけで利用できます。
実際にサーバーを起動するわけでは無い為、第一引数のアドレス・ポートの指定は不要です(現状、指定しても何も起きません)

ステージ変数の扱い

API Gatewayを利用した場合、URL中にステージ変数が入る場合があります。APIGateway専用のコードであれば問題ありませんが、net/http向けに普通に書かれたコードの場合はパスが変わってしまう為、都合が悪い場合があります。

この場合、付属のミドルウェア[APIGatewayStripStageVar]を使うことで、ステージ変数を削除すると良いかもしれません。

ミドルウェアの使用例
package main

import (
	adaptor "github.com/yacchi/lambda-http-adaptor"
	_ "github.com/yacchi/lambda-http-adaptor/all"
	api2 "github.com/yacchi/lambda-http-adaptor/example/simple/api"
	"github.com/yacchi/lambda-http-adaptor/middlewares"
	"log"
)

func main() {
	api := api2.ProvideAPI()
	log.Fatalln(adaptor.ListenAndServe("0.0.0.0:8888", middlewares.APIGatewayStripStageVar(api)))
}

WebSocketの扱い

デフォルトでは、WebSocketのリクエストは次のパスへのリクエストとして動作します。

  • 接続イベント($connect) => /websocket/$connect
  • 切断イベント($disconnect) => /websocket/$disconnect
  • デフォルトルート($default) => /websocket/$default

接続・切断のタイミングではレスポンスを返す事はできません。デフォルトルートではResponseWriterに書き込むことで、APIGatewayのPostToConnectionAPIにより、リクエスト元へレスポンスが返ります。

サンプルコード

背景

AWSのAPIGateway+Lambda統合やALBのTargetGroupとして指定するLambdaをGoで作成する場合、APIGatewayの種別やALBなどに合わせてコードを書く必要があります。

このコードはnet/httpモジュールを利用したごく一般的なアプリケーションとは全く異なるため、既存のnet/httpで動作するアプリケーションをそのまま利用する事はできません。

APIGatewayのRESTAPIを使う場合のGoコード例
package main

import (
	"encoding/json"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

func handler(request *events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) {
	// requestを解釈し、APIGatewayProxyResponseとして返す
	res, err := json.Marshal(request)
	if err != nil {
		return nil, err
	}
	return events.APIGatewayProxyResponse{
		Body:       string(res),
		StatusCode: 200,
	}, nil
}

func main() {
	lambda.Start(handler)
}

一応公式に aws-lambda-go-api-proxyと言う物があります。
一応こちらでもAPI GatewayのRESTAPIもしくはHTTPAPIには対応できます。しかし、

  • 双方向で通信できるWebSocketを使いたい
  • ALBのTargetに指定して、IPアドレス制限などを実施したい

などWebSocketを使いたい、そもそもAPI GatewayではなくApplication Load Balancer以外を使いたいと言う場合には利用できません。
また、予めRESTAPIもしくはHTTPAPI向けに作る必要があります。

脚注
  1. Rustに並び非常に優れたパフォーマンスという比較もあります。
    AWS Lambda battle 2021: performance comparison for all languages (cold and warm start) ↩︎

  2. APIGatewayの動作の都合上、メッセージが送られる度にLambdaが起動されます。 ↩︎

Discussion