AWS
lambda
APIGateway
aws-sam-cli

SAM + SwaggerでAPIGateway + Lambdaを構築

aws-sam-cliとSwaggerを組み合わせてAPIGateway+Lambdaのサーバレス構成を構築する際にいろんな罠にハマり、CloudFormationに振り回されたので、この記事では最低限の構成を構築するために必要なテンプレートの記述を紹介たいと思います。


前提

当記事では以下のAPIGateway + Lambdaを構築していきます。


APIGateway


  • リソース名:FugaApi

  • エンドポイント



    • /hoge


      • GET

      • OPTIONS






Lambda


  • HogeFunction



    • /hogeで呼び出されるLambda



これからこの構成を一つのyamlファイルでテンプレートを用意していきます。


LamdbaRole

まずはLambdaにつけるロールを用意します。

ManagedPolicyArnsでAWSが用意しているポリシーをつけられるのは便利ですね。

  LambdaRole:

Type: AWS::IAM::Role
Properties:
RoleName: HogeLambdaRole
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
- arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess


Lambda

Lambda本体を定義します。

Roleには先程定義したLambdaRoleを指定しています。

EventsにはAPIGatewayを指定するようにしています。

RestApiIdに後述するAPIGatewayのリソースFugaApiを指定しています。

RestApiIdをつけ忘れるとFugaApiとは別のAPIGatewayが作られてしますので注意してください。

(というかSwaggerで定義した場合はEventブロックそのものが要らない可能性もあります。

確認出来次第修正します。)

  HogeFunction:

Type: AWS::Serverless::Function
Properties:
CodeUri: HogeFunction/
Role: !GetAtt LambdaRole.Arn
FunctionName: HogeFunction
Events:
Api:
Type: Api
Properties:
RestApiId: !Ref FugaApi
Path: /hoge
Method: GET


Globalセクション

実は上記のLambdaテンプレートにはHandlerRuntimeなどのプロパティが不足しています。

今回はそのあたりのプロパティはGlobalセクションに記述しています。

SAMテンプレートではGlobalセクションでテンプレート内のリソースの共有設定を記述できます。

今回の構成ではあまり意味がありませんが、Lambdaを複数定義したい場合などに共通する設定は

Globalセクションに記述できるのでテンプレートの記述が非常に楽になります。

また、各リソースのテンプレートで個別に定義したいプロパティを記述すれば上書きしてくれます。

SAMを利用する場合は是非活用しましょう。

Globals:

Function:
Timeout: 30
Handler: app.lambdaHandler
Runtime: nodejs8.10
Environment:
Variables:
TZ: Asia/Tokyo


Permission

APIGatewayからLambdaにアクセスできるようにPermissonを定義しましょう。

Permissonを定義し忘れても構築自体はできますがAPIGatewayからLambdaにアクセスできないので、

APIを叩いても必ず500エラーが返り、Lambdaも実行自体されないのでログもでないという状態になります。

私はそれで小一時間悩みました。。

  HogeFunctionPermission:

Type: "AWS::Lambda::Permission"
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref HogeFunction
Principal: apigateway.amazonaws.com


APIGateway

APIGatewayはSwagger形式でエンドポイントを定義できます。

DefinitionBodyにSwaggerを記述していきましょう。

今回はCORSを利用できるようにするためoptionsメソッドも記述しています。

注意点としてはoptions.responsesのステータスコードは数値でなく文字列で定義してください。

今回のようなSAMテンプレートに直接Swaggerを定義した構成の場合、数値のままだとCloudFormationで失敗します。

  FugaApi:

Type: AWS::Serverless::Api
Properties:
Name: FugaApi
StageName: api
DefinitionBody:
swagger: "2.0"
schemes:
- "https"
paths:
/hoge:
get:
responses: {}
x-amazon-apigateway-integration:
uri: !Join [ '' , [ 'arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/' , !GetAtt HogeFunction.Arn , '/invocations' ] ]
passthroughBehavior: "when_no_match"
httpMethod: "POST"
type: "aws_proxy"
options:
consumes:
- "application/json"
produces:
- "application/json"
responses:
"200": # ここは文字列で定義する
description: "200 response"
schema:
$ref: "#/definitions/Empty"
headers:
Access-Control-Allow-Origin:
type: "string"
Access-Control-Allow-Methods:
type: "string"
Access-Control-Allow-Headers:
type: "string"
x-amazon-apigateway-integration:
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Methods: "'*'"
method.response.header.Access-Control-Allow-Headers: "'*'"
method.response.header.Access-Control-Allow-Origin: "'*'"
requestTemplates:
application/json: "{\"statusCode\": 200}"
passthroughBehavior: "when_no_match"
type: "mock"


まとめ

最終的に以下のようなテンプレートファイルになります。

特に気をつけたい点、というか私がハマった点は以下になります。


  • LambdaにAPIGatewayのPermmison設定を忘れずに


  • RestApiIdの定義も忘れずに

  • Swaggerでresponseを定義する場合は文字列で定義すること

みなさんは是非CloudFormationに振り回されないようなAWSライフをお過ごしください


samtemplate.yaml

AWSTemplateFormatVersion: '2010-09-09'

Transform: AWS::Serverless-2016-10-31
Description: >
pokemon web api template

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 30
Handler: app.lambdaHandler
Runtime: nodejs8.10
Environment:
Variables:
TZ: Asia/Tokyo

Resources:
LambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: HogeLambdaRole
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
- arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
LambdaRoleProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Path: "/"
Roles:
- !Ref LambdaRole

HogeFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: HogeFunction/
Role: !GetAtt LambdaRole.Arn
FunctionName: HogeFunction
Events:
Api:
Type: Api
Properties:
RestApiId: !Ref FugaApi
Path: /hoge
Method: GET
HogeFunctionPermission:
Type: "AWS::Lambda::Permission"
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref HogeFunction
Principal: apigateway.amazonaws.com

FugaApi:
Type: AWS::Serverless::Api
Properties:
Name: FugaApi
StageName: api
DefinitionBody:
swagger: "2.0"
schemes:
- "https"
paths:
/hoge:
get:
responses: {}
x-amazon-apigateway-integration:
uri: !Join [ '' , [ 'arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/' , !GetAtt HogeFunction.Arn , '/invocations' ] ]
passthroughBehavior: "when_no_match"
httpMethod: "POST"
type: "aws_proxy"
options:
consumes:
- "application/json"
produces:
- "application/json"
responses:
"200":
description: "200 response"
schema:
$ref: "#/definitions/Empty"
headers:
Access-Control-Allow-Origin:
type: "string"
Access-Control-Allow-Methods:
type: "string"
Access-Control-Allow-Headers:
type: "string"
x-amazon-apigateway-integration:
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Methods: "'*'"
method.response.header.Access-Control-Allow-Headers: "'*'"
method.response.header.Access-Control-Allow-Origin: "'*'"
requestTemplates:
application/json: "{\"statusCode\": 200}"
passthroughBehavior: "when_no_match"
type: "mock"