CloudFormationでAPI Gateway＋LambdaなAPIを作成する

CloudFormationを利用してlambdaとAPI Gatewayの連携をさらっとまとめてみました
今回のハマりポイントはlambdaPermissionの設定ぐらいでしょうか。
さくっと出来ちゃうのでCloudFormationはホント便利ですね！

・deploy用にS3バケットを用意
・swaggerファイルとlambdaパッケージをアップロード
・CFn実行
という手順で進めていきます

## 対象者
・環境構築を楽にしたい人
・サーバーレスなAPIに興味がある人

## deploy用のS3バケットを作成
swaggerファイルとlambdaファイルをアップロードする先として
S3バケットとバケットポリシーを定義します

```yml:deployXXXBucket.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: This CloudFormation template to create S3 Bucket

Parameters:
  DeployBucket:
    Description: deploy workspace
    Type: String

Resources:

  #==============================
  # lambda,swaggerファイルをdeploy時に参照するバケット
  #==============================

  DeployS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref DeployBucket
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  DeployS3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref DeployS3Bucket
      PolicyDocument:
        Id: DeployS3BucketPolicy
        Version: '2012-10-17'
        Statement:
        - Sid: XXXS3Acl
          Effect: Allow
          Principal: {
            Service: "cloudformation.amazonaws.com"
          }
          Action:
          - "s3:GetObject"
          Resource:
          - !Sub "arn:aws:s3:::${DeployBucket}/*"
```

定義したymlファイルを使ってstackを作成します

```bash:deployXXXBucket.sh
#!/bin/bash
set -Ceu

BASE_DIR=$(cd $(dirname $0); pwd)
REGION="ap-northeast-1"
CFN_INIT_TEMPLATE_FILE="deployXXXBucket.yml"
STACK_NAME_DEPLOY_BUCKET="apiXXXDeployS3Bucket"
DEPLOY_BUCKET="api-xxx-deploy-bucket"

/usr/bin/aws cloudformation create-stack \
--template-body file://${BASE_DIR}/${CFN_INIT_TEMPLATE_FILE} \
--stack-name ${STACK_NAME_DEPLOY_BUCKET} \
--region ${REGION} \
--parameters ParameterKey=DeployBucket,ParameterValue=${DEPLOY_BUCKET}
```

## lambdaを作成

```javascript:index.js
exports.handler = async (event, context) => {
  context.done(null, new Response(200, JSON.stringify({data: "hello world"})));
};

class Response{
  constructor(statusCode, body){
    this.statusCode = statusCode;
    this.body = body;
    this.headers = {"Access-Control-Allow-Origin" : "*"};
  }
}
```

## swagger.ymlを作成
```yml:swagger.yml
swagger: "2.0"
info:
  version: "2019-03-26T11:14:17Z"
  title:
    Fn::Sub: ${ApiGwName}
host: "api-xxx.hoge.me"
schemes:
- "https"
paths:
  /:
    get:
      x-amazon-apigateway-integration:
        uri:
          Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiXXXFunction.Arn}/invocations
        responses:
          default:
            statusCode: "200"
        passthroughBehavior: "when_no_match"
        httpMethod: "POST"
        contentHandling: "CONVERT_TO_TEXT"
        type: "aws_proxy"
      produces:
      - "application/json"
      responses:
        200:
          description: "200 response"
          schema:
            $ref: "#/definitions/Empty"
          headers:
            Access-Control-Allow-Origin:
              type: "string"

definitions:
  Empty:
    type: "object"
    title: "Empty Schema"

x-amazon-apigateway-policy:
  Version: "2012-10-17"
  Statement:
  - Effect: "Allow"
    Principal: "*"
    Action: "execute-api:Invoke"
    Resource:
      Fn::Sub: "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}*"
```

## CFnテンプレートを作成します
```yaml:template.cfn.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: This CloudFormation template to create ApiXXX

Parameters:
  DeployBucket:
    Description: to use swagger file, lambda package at build
    Type: String
  GwStage:
    Description: API Gateway deploy stage
    Type: String
    AllowedValues:
    - v1
  ApiXXXGwRole:
    Description: API Gateway role name
    Type: String
  LambdaName:
    Description: lambda function name
    Type: String
  LambdaRoleName:
    Description: lambda function role
    Type: String

Resources:
  #==============================
  # apigateway
  #==============================
  ApiXXX:
    Properties:
      DefinitionBody:
        Fn::Transform:
          Name: AWS::Include
          Parameters:
            Location: !Sub "s3://${DeployBucket}/swagger.yml"
      StageName:
        Fn::Sub: ${GwStage}
    Type: AWS::Serverless::Api
    Role:
      Fn::GetAtt:
        - ApiXXXApiGwRole
        - Arn
    DependsOn:
      - ApiXXXApiGwRole
      - ApiXXXFunction

  #==============================
  # lambda(SAM管理)
  #==============================
  ApiXXXFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref LambdaName
      Handler: index.handler
      Runtime: nodejs10.x
      CodeUri: src/function
      MemorySize: 128
      Timeout: 60
      Role:
        Fn::GetAtt:
          - LambdaExecutionRole
          - Arn

  ApiXXXApiGwRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref ApiXXXGwRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - apigateway.amazonaws.com
          Action:
          - sts:AssumeRole
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref LambdaRoleName
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

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

定義したymlファイルを使って変更セットを作成します

```bash:deployXXX.sh
#!/bin/bash
set -Ceu

BASE_DIR=$(cd $(dirname $0); pwd)
REGION="ap-northeast-1"
SWAGGER_FILE="swagger.yml"
DEPLOY_BUCKET="api-xxx-deploy-bucket"
CFN_TEMPLATE_FILE="template.cfn.yml"
CFN_TEMPLATE_OUTPUT_FILE="output-template.cfn.yml"
STACK_NAME="apiXXXDeploy"

/usr/bin/aws s3 cp ./${SWAGGER_FILE} s3://${DEPLOY_BUCKET}/

/usr/bin/aws cloudformation package \
--template-file ${CFN_TEMPLATE_FILE} \
--output-template-file ${CFN_TEMPLATE_OUTPUT_FILE} \
--s3-bucket ${DEPLOY_BUCKET}

GwStage="v1"
DeployBucket=$DEPLOY_BUCKET
ApiGwName="apiXXX"
ApiXXXGwRole="apiXXXGwRole"
LambdaName="apiXXXLambda"
LambdaRoleName="apiXXXLambdaRole"
/usr/bin/aws cloudformation deploy \
--template-file ${BASE_DIR}/${CFN_TEMPLATE_OUTPUT_FILE} \
--stack-name ${STACK_NAME} \
--region ${REGION} \
--parameter-overrides GwStage=${GwStage} DeployBucket=${DeployBucket} ApiGwName=${ApiGwName} ApiXXXGwRole=${ApiXXXGwRole} LambdaName=${LambdaName} LambdaRoleName=${LambdaRoleName}  \
--capabilities CAPABILITY_NAMED_IAM
```

## CFn変更セットを実行

ビルドを実行します
変更セットにステータスがCREAE_IN_PROGRESSで作成されている様子が確認できますね
![スクリーンショット 2019-06-10 10.53.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10683/44ef1e61-1ac1-6bdd-18c0-c367f74896d4.png)

ステータスがCREATE_COMPLETEになると、
変更セット詳細ページから作成リソースを確認できます
実行すると用意したリソースが作成されています。簡単ですね
![スクリーンショット 2019-06-10 10.50.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10683/2371bc65-e7bc-b819-1205-3fbc7ea7279c.png)