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が用意しているポリシーをつけられるのは便利ですね。

```yaml
  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`ブロックそのものが要らない可能性もあります。
確認出来次第修正します。）


```yaml
  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を利用する場合は是非活用しましょう。

```yaml
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も実行自体されないのでログもでないという状態になります。
私はそれで小一時間悩みました。。

```yaml
  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で失敗します。

```yaml
  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"

```