AWS Lambda Function と Go でシュッとやってバーンってしたい

前提

  • AWS Lambda Function でWEBリクエストをうけとってなんかしたい
  • Go 使う。ふつーのWEBアプリケーションはかけるものとする
  • 設定少なくさっさと立ち上げたい

最終形

Lambda Function + Function URL w/CORS を立てる。バイナリは↓で作っておく。

なお以下のソースコードは既存のものから不要なものを載せないよう切り貼りしたものであり、これ単体での動作確認は行っていないので雰囲気で読み取ってください。

// cmd/lambda/main.go
package main

import (
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter"

    "github.com/hkdnet/tekitou/handler"
)

func main() {
    mux := handler.NewHandler()
    lambda.Start(httpadapter.NewV2(mux).ProxyWithContext)
}
// handler/handler.go
package handler

import "github.com/gorilla/mux"

func NewHandler() http.Handler {
    // なんでもいいけどとりあえず gorilla にしておく。
    r := mux.NewRouter()
    // てけとーにルーティング足す
    return r
}

ローカル確認はこんな感じにしておくとよい。

// cmd/local/main.go
package main

import (
    "fmt"
    "net/http"
    "os"
    "strings"

    "log"

    "github.com/hkdnet/tekitou/handler"
)

var corsHeaders map[string]string

func init() {
    corsHeaders = make(map[string]string)
    corsHeaders["Access-Control-Allow-Origin"] = "*"
    corsHeaders["Access-Control-Allow-Methods"] = "GET,PUT"
    corsHeaders["Access-Control-Allow-Headers"] = "content-type"
}

// CORS header. This is set by Lambda Function URL in production. Our lambda handler doesn't need to care CORS.
// But in local we do need it.
func setCorsHeaders(h *http.Header) {
    for k, v := range corsHeaders {
        h.Set(k, v)
    }
}

type respWriterWithCors struct {
    w http.ResponseWriter

    wroteHeader bool
}

func (w *respWriterWithCors) Header() http.Header {
    return w.w.Header()
}
func (w *respWriterWithCors) Write(b []byte) (int, error) {
    if !w.wroteHeader {
        w.WriteHeader(http.StatusOK)
    }
    return w.w.Write(b)
}
func (w *respWriterWithCors) WriteHeader(statusCode int) {
    h := w.Header()
    setCorsHeaders(&h)
    w.w.WriteHeader(statusCode)
    w.wroteHeader = true
}

func main() {
    h := handler.NewHander()

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if strings.EqualFold(r.Method, "options") {
            h := w.Header()
            setCorsHeaders(&h)
            fmt.Fprintf(w, "")
            return
        }
        w = &respWriterWithCors{w: w, wroteHeader: false}
        h.ServeHTTP(w, r)
    })

    log.Println("start listening")

    err := http.ListenAndServe(":3001", nil)
    if err != nil {
        fmt.Fprintf(os.Stderr, "%s", err)
        os.Exit(1)
    }
}

紆余曲折部分

普通にやってると aws-lambda-go にいきつくと思うんだけど、これはそもそもリフレクションバリバリつかっていてちょっと追いにくい。

github.com

そして Function URL を使えば API Gateway なくてもいいらしいという風のうわさを聞いたものとしては API Gateway をはさみたくない。ついでにいうとルーティングも自前で解析しないで Go の既存のWEBフレームワークとかにお願いしたい。

そのへんを解決してくれるのがこれ。

github.com

Function URL 対応とは一言も書いてないが API Gateway V2にすれば普通に動く(ということはおそらく Function URL + aws-lambda-go の APIGWv2でも動くはずだが未検証)。でもとりあえず http.Handler なものを返せば動いてくれるので考えることが減って楽

次の問題はどうやってローカル確認するか。これも http.Handler があるので上述のように cmd を2つに分けてローカル確認用と実際にLambdaに置く用でそれぞれエントリポイントを用意すればよい。

ここまできて最後の問題がCORS設定。Function URL は CORS の設定があるのでこれをそのまま使えたほうが楽。だけどローカルでは当然そんな便利なやつはないので書く必要がある。それが respWriterWithCors 部分。ローカルでサーバを立てる前に ResponseWriter をラップしてあげればよい。

これでだいたい動くものができるので、あとは中身をどうにかするだけ。

謝辞

本記事の構成にいたるまで、takeshinoda さんにアドバイスをたくさんいただきました。ありがとうございました!