みなさん、こんにちは。サービス開発課でCloud Automatorの開発・運用を担当している尾崎です。
今日はCloud Automatorの内部ツールの開発で採用した、API Gateway(HTTP API)とGoを利用したLambda Functionの実装方法を紹介したいと思います。
この内容は2021/12/01現時点の内容となっており、AWSのアップデートや紹介するライブラリの更新で変わる可能性があります。
=> aws-lambda-go-api-proxyのv0.12.0リリースでServeHTTPを使わなくとも書けるようになりました!
- 背景
- GoでAPI Gateway(REST API)を利用したLambda Functionの実装方法
- GoでAPI Gateway(HTTP API)を利用したLambda Functionの実装方法
- aws-lambda-go-api-proxy v0.12.0以降でAPI Gateway(HTTP API)を利用したLambda Functionの実装方法
- おまけ
- まとめ
背景
Cloud Automatorは主にRuby on Railsを利用して開発されており、開発チームでの主なプログラミング言語はRubyとなっています。
しかし、今回開発した内部ツールはそれほどアクセスが発生しない、ということが事前にわかっていたので、ランニングコストを最小化するため、API GatewayとLambda Functionを利用したサーバレス構成を採用しました。
また、プログラミング言語も動的言語のRubyではなく、型がネイティブにサポートされた言語で、さらに後方互換が強い言語を採用することになりました。
これは、内部ツールは作ったあとのメンテナンスに時間を割きづらいという特性があるため、それなら言語仕様が変わりづらく、型を利用した静的解析がある言語の方がメンテナンスコストを下げられると考えたためです。
以上の理由でAPI GatewayとLambda Function(Go)を選択しました。
GoでAPI Gateway(REST API)を利用したLambda Functionの実装方法
API GatewayとGoを利用したLambda Functionでは awslabs/aws-lambda-go-api-proxy を利用した開発が便利です。
GinやEchoといったWebフレームワークを利用して実装することが出来ます。
公式のREADME.mdにはGinの例が載っていますが、Echoだと次のようになります。
package main import ( "context" "log" "net/http" "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" ) var echoLambda *echoadapter.EchoLambda func init() { // stdout and stderr are sent to AWS CloudWatch Logs log.Printf("echo cold start") e := echo.New() e.GET("/", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"message": "pong"}) }) echoLambda = echoadapter.New(e) } func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { // If no name is provided in the HTTP request body, throw an error return echoLambda.ProxyWithContext(ctx, req) } func main() { lambda.Start(Handler) }
注意すべき点として2021/12/01現在、このEchoを利用したサンプルコードでHTTPリクエストをルーティングさせるにはAPI GatewayのREST APIを選択する必要があります。
API Gatewayの料金ページではREST APIに比べてHTTP APIの方がリクエスト当たりのコストが小さいので、要件が満たせばHTTP APIを利用したいところです。
実際、今回作成した内部ツールでもHTTP APIで要件をカバーできることがわかったので、REST APIではなく、HTTP APIで運用できるようにaws-lambda-go-api-proxyのコードを読んで対応方法を探しました。調べてわかった動くコードは次のセクションです。
GoでAPI Gateway(HTTP API)を利用したLambda Functionの実装方法
Ginを利用する場合はaws-lambda-go-api-proxyがすでにサポートしているので、READMEのコードを元にしてHTTP API用のLambda Functionコードを書くことが出来ます。
他のフレームワークを利用する場合にはaws-lambda-go-api-proxyのhandlerfunc
パッケージを利用して書くことが出来ます。
例えば、先ほどのEchoだと次のように書けます。
package main import ( "context" "log" "net/http" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/awslabs/aws-lambda-go-api-proxy/handlerfunc" "github.com/labstack/echo/v4" ) var ( httpLambda *handlerfunc.HandlerFuncAdapterV2 ) func init() { // stdout and stderr are sent to AWS CloudWatch Logs log.Printf("echo cold start") e := echo.New() e.GET("/", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"message": "pong"}) }) httpLambda = handlerfunc.NewV2(e.ServeHTTP) } // APIGatewayV2HTTPRequest、APIGatewayV2HTTPResponseと引数の型も変わっている func Handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { return httpLambda.ProxyWithContext(ctx, req) } func main() { lambda.Start(Handler) }
Echoや各種フレームワークが実装しているhttp.Handler
インターフェースをhandlerfunc.NewV2
に渡すのがポイントです。
aws-lambda-go-api-proxy v0.12.0以降でAPI Gateway(HTTP API)を利用したLambda Functionの実装方法
package main import ( "context" "log" "net/http" "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" ) var ( echoLambda *echoadapter.EchoLambdaV2 ) func init() { // stdout and stderr are sent to AWS CloudWatch Logs log.Printf("echo cold start") e := echo.New() e.GET("/", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"message": "pong"}) }) echoLambda = echoadapter.NewV2(e) } // APIGatewayV2HTTPRequest、APIGatewayV2HTTPResponseと引数の型も変わっている func Handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { return echoLambda.ProxyWithContext(ctx, req) } func main() { lambda.Start(Handler) }
おまけ
HTTP APIとLambda Function用のCloudFormationを貼っておきます。
AWSTemplateFormatVersion: 2010-09-09 Parameters: LambdaCodeS3BucketName: Type: String LambdaFunctionName: Type: String LambdaIAMRoleName: Type: String Outputs: ApiGatewayInvokeURL: Description: HTTP API Endpoint Value: !GetAtt ApiGatewayV2.ApiEndpoint Resources: ApiGatewayV2: Type: AWS::ApiGatewayV2::Api Properties: Name: HTTPAPI ProtocolType: HTTP Target: !Sub >- arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaApiGatewayHandler.Arn}/invocations ApiGatewayV2LambdaIntegration: Type: AWS::ApiGatewayV2::Integration Properties: ApiId: !Ref ApiGatewayV2 Description: Lambda Integration IntegrationType: AWS_PROXY IntegrationUri: !Sub >- arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaApiGatewayHandler.Arn}/invocations IntegrationMethod: POST PayloadFormatVersion: '2.0' ApiGatewayV2ProxyRoute: Type: AWS::ApiGatewayV2::Route Properties: ApiId: !Ref ApiGatewayV2 RouteKey: ANY /{proxy+} Target: !Sub >- integrations/${ApiGatewayV2LambdaIntegration} LambdaPermissionProxy: Type: 'AWS::Lambda::Permission' Properties: FunctionName: !Ref LambdaApiGatewayHandler Action: 'lambda:InvokeFunction' Principal: 'apigateway.amazonaws.com' SourceArn: !Sub >- arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGatewayV2}/*/*/{proxy+} LambdaRole: Type: AWS::IAM::Role Properties: RoleName: !Ref LambdaIAMRoleName AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole LambdaApiGatewayHandler: Type: AWS::Lambda::Function Properties: Architectures: - x86_64 Code: S3Bucket: !Ref LambdaCodeS3BucketName S3Key: main.zip FunctionName: !Ref LambdaFunctionName Handler: handler MemorySize: 128 PackageType: Zip Role: !GetAtt LambdaRole.Arn Runtime: go1.x Timeout: 30
まとめ
これで最低限の運用コストで内部ツールをホストすることが出来るようになりました。
API GatewayへのリクエストをLambda Functionにプロキシする構成は、ローカル環境ではサーバーを起動して開発できるため、いちいちLambdaにデプロイする必要も無く、実装サイクルを高速に回せるというメリットがあります。
今後もこの記事の背景にも書いたとおり、開発時だけではなく、運用もしやすい開発を心がけていきたいと考えています。