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テンプレートにはHandler
やRuntime
などのプロパティが不足しています。
今回はそのあたりのプロパティは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ライフをお過ごしください
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"