My Note

自己理解のためのブログ

APIGateway(REST API) + Lambda環境に echo(Goのweb Framework) を ServerlessFramework でデプロイする

はじめに

今回は goのwebフレームワークであるEchoをLambdaにデプロイして簡単なAPIを実行する環境を構築します。 環境構築はServerlessFrameworkで実施しました。

echoをLambda環境で動かすためには以下のライブラリが必要になります。 このライブラリを使えばAPIGatewayを組み合わせるとGinやEchoやその他のGoフレームワークの実行ができます。

github.com github.com

コード

今回作成したコードはこのリポジトリです。

github.com

今回は試しに、Basic認証の機能とヘルスチェックのAPIを用意しました。

echoのコード

❯ tree go-echo-lambda
go-echo-lambda
├── api
│   └── healthcheck
│       └── healthcheck.go
├── conf
│   └── config.go
├── go.mod
├── go.sum
├── handler
│   └── auth
│       └── auth.go
├── main.go
└── serverless.yml
  • main.go
package main

import (
    "context"
    "log"
    "yhidetoshi/go-echo-lambda/api/healthcheck"
    "yhidetoshi/go-echo-lambda/handler/auth"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    echoadapter "github.com/awslabs/aws-lambda-go-api-proxy/echo"

    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

var echoLambda *echoadapter.EchoLambda

func init() {
    log.Printf("echo cold start")

    e := echo.New()
    e.Use(middleware.Recover())
    e.Use(auth.BasicAuth())
    e.GET("/api/healthcheck", healthcheck.Healthcheck)

    echoLambda = echoadapter.New(e)
}

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

func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    return echoLambda.ProxyWithContext(ctx, req)
}
  • api/healthcheck.go
package healthcheck

import (
    "encoding/json"
    "log"
    "net/http"

    "github.com/labstack/echo/v4"
)

type HealthcheckMessage struct {
    Status  int    `json:"status"`
    Message string `json:"message"`
}

func Healthcheck(c echo.Context) error {
    msg := &HealthcheckMessage{
        Status:  http.StatusOK,
        Message: "Success to connect echo",
    }
    res, err := json.Marshal(msg)
    if err != nil {
        log.Println(err)
    }
    return c.String(http.StatusOK, string(res))
}
  • handler/auth.go
package auth

import (
    "yhidetoshi/go-echo-lambda/conf"

    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

var (
    id = conf.BASIC_AUTH_ID
    pw = conf.BASIC_AUTH_PASS
)

func BasicAuth() echo.MiddlewareFunc {
    return middleware.BasicAuth(func(username string, password string, context echo.Context) (bool, error) {
        if username == id && password == pw {
            return true, nil
        }
        return false, nil
    })
}
  • conf/config.go
package conf

const (
    BASIC_AUTH_ID   = "test"
    BASIC_AUTH_PASS = "pass"
)

ライブラリをざっくり確認

  • それぞれのライブラリの役割

    • github.com/aws/aws-lambda-go/events(events.APIGatewayProxyRequest)
      • APIGatewayのリクエストイベントを受け取る
    • github.com/awslabs/aws-lambda-go-api-proxy/echo
      • コンテキストとAPIGatewayのイベントをhttp.Requestオブジェクトへ変換してechoのルーティングに渡す
  • github.com/aws/aws-lambda-go/events にて Handler(ctx context.Context, req events.APIGatewayProxyRequest) でcontextとAPIGatewayのリクエストをlambdaで受けます。

  • contextの役割

処理の締め切りを伝達
キャンセル信号の伝播
リクエストスコープ値の伝達

github.com

→ lambdaでAPIGatewayのイベントを受け取ってハンドリングするためのライブラリ

  • main.go
func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    return echoLambda.ProxyWithContext(ctx, req)
}

echoLambda.ProxyWithContext (echo/adapter.go)に渡せば以下の処理をしてくれる。

ProxyWithContextはcontextとAPI Gatewayのイベントをhttp.Request オブジェクトに変換し、echo.Echo に送信してルーティングを行います

// ProxyWithContext receives context and an API Gateway proxy event,
// transforms them into an http.Request object, and sends it to the echo.Echo for routing.
// It returns a proxy response object generated from the http.ResponseWriter.

func (e *EchoLambda) ProxyWithContext(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    echoRequest, err := e.EventToRequestWithContext(ctx, req)
    return e.proxyInternal(echoRequest, err)
}

これで、この部分だけ用意しておけばlambdaを使ったサーバレスを意識せずに開発できそうですね!

環境構築 ( ServerlessFramework )

ServerlessFrameworkで Lambda関数とAPI Gatewayを作成します。

GoをコンパイルしてLambda関数にデプロイするには以下のコマンドを実行します。なお、 AWSアカウントIDと許可するFromIPを --param に付与します。 また、リソースポリシーで接続元のIP制限をかけています。

  • デプロイコマンド
$ GOARCH=amd64 GOOS=linux go build "-ldflags=-s -w" ./main.go
$ sls deploy --param="account_id=${AWS_ACCOUNT_ID}" --param="allow_ip=X.X.X.X/32"
  • serverless.yml
service: go-echo-lambda
frameworkVersion: "3"

provider:
  name: aws
  stage: dev
  runtime: go1.x
  region: ap-northeast-1
  apiName: ${self:service}-${self:provider.stage}
  endpointType: REGIONAL
    
  apiGateway:
    resourcePolicy:
      - Effect: Allow
        Principal: '*'
        Action: execute-api:Invoke
        Resource:
          - arn:aws:execute-api:ap-northeast-1:${param:account_id}:*/*
        Condition:
          IpAddress:
            aws:SourceIp:
              - ${param:allow_ip}

functions:
  GoEchoLambda:
    handler: main
    role: GoEchoLambda
    timeout: 10
    description: go echo lambda test 
    memorySize: 128
    events:
      - http:
          path: /api/{proxy+}
          method: any
          #integration: lambda

resources:
  Resources:
    GoEchoLambda:
      Type: AWS::IAM::Role
      Properties:
        RoleName: GoEchoLambda
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action: sts:AssumeRole
        Policies:
          - PolicyName: GoEchoLambda
            PolicyDocument:
              Version: '2012-10-17'
              Statement:
                - Effect: Allow
                  Action:
                    - "logs:CreateLogGroup"
                    - "logs:CreateLogStream"
                    - "logs:PutLogEvents"
                  Resource: "*"

作成されるリソース

Lambda

APIGateway

  • リソース

  • ステージ

  • リソースポリシー

APIGWのリソース

  • 用意したAPIリソースは Catch-allパス変数"を利用しました。
    • /api/{proxy+} : /api/ 配下をすべてキャッチしてくれます
    events:
      - http:
          path: /api/{proxy+}

動作確認

$ curl -u test:pass https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/api/healthcheck
{"status":200,"message":"Success to connect echo"}

Basic認証が通りヘルスチェック成功のメッセージを受け取り動作確認ができました。

備考

  • ServerlessFrameworkで API Gatewayにlambda integrationするために以下のように設定をしましたがダメでした。lambda関数のeventとして integration: lambda せずともlambda integrationされました。
    events:
      - http:
          path: /api/{proxy+}
          method: any
          #integration: lambda <-- ここで integrationを指定すると以下のエラーになりました
{"errorMessage":"json: cannot unmarshal object into Go struct field APIGatewayProxyRequest.body of type string","errorType":"UnmarshalTypeError"}

さいごに

今回、echoをlambdaで動かすために以下のライブラリを利用しました。 https://github.com/awslabs/aws-lambda-go-api-proxy

個人開発でEchoを利用するときはGoogle App Engineにデプロイしていましたが、今回のこのライブラリを知ったのでAWS(APIGW + Lambda)も利用でき選択肢が増えてよかったです!個人開発ではなるべくクラウド利用料を減らしたいので、Fargateのように常時起動させて利用外でも課金されるよりもリクエストをベースに実行できる環境はとても良いですね!