AWS CloudFormationでLambdaを使用するAPI Gatewayを構築しよう

# はじめに
AWS CloudFormationを利用してLambdaを使用するAPI Gateway構築のテンプレートのサンプルです。
Lambdaは、S3アーティファクトを使用します。

テンプレートの概要が分からない場合は、[はじめてのAWS CloudFormationテンプレートを理解する](https://qiita.com/miyabiz/items/eb7b0fa1675b5199b9b7)を参考にしてください。

コードは[GitHub](https://github.com/miyabiz-room/CloudFormation-Template/tree/apigw-01)にもあります。

今回は、akane というシステムの dev 環境を想定しています。
同じ構成で違う環境を作成する場合は、{環境名}-parameters.jsonを別途作成します。

```text:ディレクトリ構成
akane (システム)
  ├── apigw (スタック)
  │   ├── apigw.yml (CFnテンプレート)
  │   └── dev-parameters.json (dev 環境のパラメータ)
  ├── lambda (スタック)
  │   ├── code
  │   │   └── getTiAmo.py (S3アーティファクトソース)
  │   ├── code-lambda-getTiAmo.zip (S3アーティファクト)
  │   ├── delete_artifact.dev.sh (S3アーティファクト削除シェル)
  │   ├── dev-parameters.json (dev 環境のパラメータ)
  │   ├── lambda.yml (CFnテンプレート)
  │   ├── mkzip.sh (S3アーティファクト作成シェル)
  │   └── upload_artifact.dev.sh (S3アーティファクトアップロードシェル)
  └─ s3 (スタック)
      ├─ s3.yml (CFnテンプレート)
      └─ all-parameters.json (all 環境のパラメータ)
```

# AWS リソース構築内容
  1. apigwスタック
      - CloudWatch ログのロール
      - API Gateway
      - 使用量プラン
      - API キー
  2. lambdaスタック
      - Lambdaロール
      - Lambda
  3. s3スタック
      - s3バケット (akane-all-s3-artifacts)
      - バケットポリシー (s3:GetObject)


# 実行環境の準備
[AWS CloudFormationを動かすためのAWS CLIの設定](https://qiita.com/miyabiz/items/fed11796f0ea2b7608f4)を参考にしてください。

# AWS リソース構築手順
1. 下記を実行してスタックを作成

    ```
    ./create_stacks.sh
    ```

2. 下記を実行してLambdaの動作を確認

    ```
    ./test_lambda.sh
    ```

3. 下記を実行してAPI Gatewayの動作を確認

    ```
    ./test_apigw.sh <URL> <API_KEY>
    ```

4. 下記を実行してスタックを削除

    ```
    ./delete_stacks.sh
    ```

# 構築テンプレート
**1. s3スタック**
 
```yaml:s3.yml
AWSTemplateFormatVersion: 2010-09-09
Description: S3 For Akane

# Metadata:

Parameters:
  SystemName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'
  EnvType:
    Description: Environment type.
    Type: String
    AllowedValues: [all, dev, stg, prod]
    ConstraintDescription: must specify all, dev, stg, or prod.

# Mappings

# Conditions

# Transform

Resources:
  # S3 Bucket作成
  akaneS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      AccessControl: Private
      BucketName: !Sub
        - ${SystemName}-${EnvType}-s3-artifacts
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-s3-artifacts
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # S3 BucketPolicy作成
  akaneS3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    DependsOn: akaneS3Bucket
    Properties:
      Bucket: !Ref akaneS3Bucket
      PolicyDocument:
        Statement:
          - Action:
              - s3:GetObject
            Effect: Allow
            Resource: !Join
              - ''
              - - 'arn:aws:s3:::'
                - !Ref akaneS3Bucket
                - /*
            Principal:
              AWS: '*'

Outputs:
  akaneS3Bucket:
    Value: !Ref akaneS3Bucket
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-s3-artifacts
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
```

```json:all-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "all"
        }
    ]
}
```

**２. lambdaスタック**

```yaml:lambda.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Lambda For Akane

# Metadata:

Parameters:
  SystemName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'
  EnvType:
    Description: Environment type.
    Type: String
    AllowedValues: [all, dev, stg, prod]
    ConstraintDescription: must specify all, dev, stg, or prod.
  ArtifactS3Bucket:
    Type: String
  LambdaName:
    Type: String
  LambdaMemorySize:
    Type: Number
  LambdaRuntime:
    Type: String
  GirlName:
    Type: String

# Mappings

# Conditions

# Transform

Resources:
  # ロール作成
  akaneRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Description: !Sub
        - ${SystemName}-${EnvType}-role-lambda-${AWS::Region}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Path: /
      RoleName: !Sub
        - ${SystemName}-${EnvType}-role-lambda-${AWS::Region}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-role-lambda-${AWS::Region}
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # Lambda作成
  akaneLambdaGetTiAmo:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref ArtifactS3Bucket
        S3Key: !Sub
          - code-lambda-${LambdaName}.zip
          - {LambdaName: !Ref LambdaName}
      Description: !Sub
        - ${SystemName}-${EnvType}-lambda-${LambdaName}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType, LambdaName: !Ref LambdaName}
      Environment:
        Variables:
          GIRL_NAME: !Ref GirlName
      FunctionName: !Sub
        - ${SystemName}-${EnvType}-lambda-${LambdaName}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType, LambdaName: !Ref LambdaName}
      Handler: !Sub
        - ${LambdaName}.handler
        - {LambdaName: !Ref LambdaName}
      MemorySize: !Ref LambdaMemorySize
      Role: !GetAtt akaneRole.Arn
      Runtime: !Ref LambdaRuntime
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-lambda-${LambdaName}
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
      Timeout: 900
      TracingConfig:
        Mode: PassThrough

Outputs:
  akaneLambdaGetTiAmoArn:
    Value: !GetAtt akaneLambdaGetTiAmo.Arn
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-lambda-arn
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
```

```json:dev-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "dev"
        },
        {
            "ParameterKey": "ArtifactS3Bucket",
            "ParameterValue": "akane-all-s3-artifacts"
        },
        {
            "ParameterKey": "LambdaName",
            "ParameterValue": "getTiAmo"
        },
        {
            "ParameterKey": "LambdaMemorySize",
            "ParameterValue": "128"
        },
        {
            "ParameterKey": "LambdaRuntime",
            "ParameterValue": "python3.8"
        },
        {
            "ParameterKey": "GirlName",
            "ParameterValue": "akane"
        }
    ],
    "Capabilities": [
        "CAPABILITY_NAMED_IAM"
    ]
}
```

**3. apigwスタック**

```yaml:apigw.yml
AWSTemplateFormatVersion: 2010-09-09
Description: API Gateway For Akane

# Metadata:

Parameters:
  SystemName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'
  EnvType:
    Description: Environment type.
    Type: String
    AllowedValues: [all, dev, stg, prod]
    ConstraintDescription: must specify all, dev, stg, or prod.
  EnvTypeCommon:
    Description: Environment type.
    Type: String
    AllowedValues: [all, dev, stg, prod]
    ConstraintDescription: must specify all, dev, stg, or prod.
  APIPathPart:
    Type: String
  APIGatewayStage:
    Type: String

# Mappings

# Conditions

# Transform

Resources:
  # ApiGateway CloudWatchRole 作成
  akaneCloudWatchRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - apigateway.amazonaws.com
            Action: sts:AssumeRole
      Description: !Sub
        - ${SystemName}-${EnvTypeCommon}-role-apigw-cloudwatch-${AWS::Region}
        - {SystemName: !Ref SystemName, EnvTypeCommon: !Ref EnvTypeCommon}
      ManagedPolicyArns:
        -  arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs
      Path: /
      RoleName: !Sub
        - ${SystemName}-${EnvTypeCommon}-role-apigw-cloudwatch-${AWS::Region}
        - {SystemName: !Ref SystemName, EnvTypeCommon: !Ref EnvTypeCommon}
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvTypeCommon}-role-apigw-cloudwatch-${AWS::Region}
          - {SystemName: !Ref SystemName, EnvTypeCommon: !Ref EnvTypeCommon}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvTypeCommon
          Value: !Ref EnvTypeCommon
  akaneApiGatewayAccount:
    Type: AWS::ApiGateway::Account
    Properties:
      CloudWatchRoleArn: !GetAtt akaneCloudWatchRole.Arn

  # ApiGateway 作成
  akaneApiGatewayRestApi:
    Type: AWS::ApiGateway::RestApi
    DependsOn: akaneApiGatewayAccount
    Properties:
      Name: !Sub
        - ${SystemName}-${EnvType}-apigw
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Description: !Sub
        - ${SystemName}-${EnvType}-apigw
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      EndpointConfiguration:
        Types:
          - REGIONAL
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-apigw
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  akaneApiGatewayResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref akaneApiGatewayRestApi
      ParentId: !GetAtt akaneApiGatewayRestApi.RootResourceId
      PathPart: !Ref APIPathPart
  akaneApiGatewayResourceMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref akaneApiGatewayRestApi
      ResourceId: !Ref akaneApiGatewayResource
      ApiKeyRequired: true
      AuthorizationType: NONE
      HttpMethod: GET
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Join
          - ''
          - - 'arn:aws:apigateway:'
            - !Ref AWS::Region
            - ':lambda:path/2015-03-31/functions/'
            - Fn::ImportValue: !Sub
              - ${SystemName}-${EnvType}-lambda-arn
              - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
            - '/invocations'

  # ApiGateway LambdaPermission 作成
  akaneApiGatewayLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName:
        Fn::ImportValue: !Sub
          - ${SystemName}-${EnvType}-lambda
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Action: lambda:InvokeFunction
      Principal: apigateway.amazonaws.com

  # ApiGateway Deployment 作成
  akaneApiGatewayDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn: akaneApiGatewayResourceMethod
    Properties:
      Description: !Sub
        - ${SystemName}-${EnvType}-apigw-deployment
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      RestApiId: !Ref akaneApiGatewayRestApi
  akaneApiGatewayStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      DeploymentId: !Ref akaneApiGatewayDeployment
      Description: !Sub
        - ${SystemName}-${EnvType}-apigw-stage
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      MethodSettings:
        - CacheDataEncrypted: false
          CachingEnabled: false
          DataTraceEnabled: true # CloudWatch ログを有効化
          HttpMethod: '*'
          LoggingLevel: ERROR
          MetricsEnabled: false # 詳細 CloudWatch メトリクスを有効化
          ResourcePath: '/*'
      RestApiId: !Ref akaneApiGatewayRestApi
      StageName: !Ref APIGatewayStage
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-apigw-stage
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
      TracingEnabled: false # X-Ray トレースの有効化

  # ApiGateway APIキー 作成
  akaneApiGatewayKey:
    Type: AWS::ApiGateway::ApiKey
    Properties:
      Name: !Sub
        - ${SystemName}-${EnvType}-apigw-key
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Description: !Sub
        - ${SystemName}-${EnvType}-apigw-key
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Enabled: true
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-apigw-key
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  akaneApiGatewayUsagePlan:
    Type: AWS::ApiGateway::UsagePlan
    DependsOn: akaneApiGatewayStage
    Properties:
      ApiStages:
        - ApiId: !Ref akaneApiGatewayRestApi
          Stage: !Ref APIGatewayStage
      Description: !Sub
        - ${SystemName}-${EnvType}-apigw-usage-plan
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      UsagePlanName: !Sub
        - ${SystemName}-${EnvType}-apigw-usage-plan
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-apigw-usage-plan
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  akaneApiGatewayUsagePlanKey:
    Type: AWS::ApiGateway::UsagePlanKey
    Properties:
      KeyId: !Ref akaneApiGatewayKey
      KeyType: API_KEY
      UsagePlanId: !Ref akaneApiGatewayUsagePlan

```

```json:dev-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "dev"
        },
        {
            "ParameterKey": "EnvTypeCommon",
            "ParameterValue": "all"
        },
        {
            "ParameterKey": "APIPathPart",
            "ParameterValue": "whisper"
        },
        {
            "ParameterKey": "APIGatewayStage",
            "ParameterValue": "v1"
        }
    ],
    "Capabilities": [
        "CAPABILITY_NAMED_IAM"
    ]
}
```

**4. 実行ファイル**

```sh:create_stacks.sh
#!/bin/sh

cd `dirname $0`

SYSTEM_NAME=akane

create_stack () {
    ENV_TYPE=$1
    STACK_NAME=$2
    aws cloudformation create-stack \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME} \
    --template-body file://./${SYSTEM_NAME}/${STACK_NAME}/${STACK_NAME}.yml \
    --cli-input-json file://./${SYSTEM_NAME}/${STACK_NAME}/${ENV_TYPE}-parameters.json

    aws cloudformation wait stack-create-complete \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}
}

create_stack all s3
./akane/lambda/mkzip.sh
./akane/lambda/upload_artifact.dev.sh
create_stack dev lambda
create_stack dev apigw

exit 0
```

```sh:test_lambda.sh
#!/bin/sh

cd `dirname $0`

echo " --- \033[0;33m TEST LAMBDA \033[0;39m --- "

LAMBDA_NAME=akane-dev-lambda-getTiAmo
OUTPUT_FILE=response.json

aws lambda invoke --function-name ${LAMBDA_NAME} --log-type Tail ${OUTPUT_FILE} --query 'LogResult'  --output text |  base64 -D

cat ${OUTPUT_FILE}

exit 0
```

```sh:test_apigw.sh
#!/bin/sh

set -e

if [ $# -ne 2 ]; then
    echo " --- \033[0;31m [ERROR]: The following arguments are required \033[0;39m --- "
    echo "      \033[0;33m$0 <URL> <API_KEY>\033[0;39m"
    echo "      \033[0;33m<URL> Example: https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/v1/whisper\033[0;39m"
    exit 1
fi

cd `dirname $0`

echo " --- \033[0;33m TEST APIGW \033[0;39m --- "

URL=$1
API_KEY=$2

curl -X GET -H 'Content-Type: application/json' \
-H "x-api-key: ${API_KEY}" \
"${URL}"

exit 0
```

```sh:delete_stacks.sh
#!/bin/sh

cd `dirname $0`

SYSTEM_NAME=akane

delete_stack () {
    ENV_TYPE=$1
    STACK_NAME=$2
    aws cloudformation delete-stack \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}

    aws cloudformation wait stack-delete-complete \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}
}

delete_stack dev apigw
delete_stack dev lambda
./akane/lambda/delete_artifact.dev.sh
delete_stack all s3

exit 0
```

**4. アーティファクト関連ファイル**

```sh:mkzip.sh
#!/bin/sh

cd `dirname $0`

ENV_TYPE=dev
LAMBDA_NAME=$(cat ${ENV_TYPE}-parameters.json | jq -r '.Parameters[] | select(.ParameterKey == "LambdaName").ParameterValue')

FILE=../code-lambda-${LAMBDA_NAME}.zip

cd ./code

ls . |  grep -v -E ".gitignore|${LAMBDA_NAME}.py" | xargs rm -rf

rm -f ${FILE}
zip -r ${FILE} ./*

exit 0
```

```sh:upload_artifact.dev.sh
#!/bin/sh

cd `dirname $0`

BASENAME=$(basename $0)
FILENAME=${BASENAME%.*}
ENV_TYPE=${FILENAME##*.}

BUCKET_NAME=$(cat ${ENV_TYPE}-parameters.json | jq -r '.Parameters[] | select(.ParameterKey == "ArtifactS3Bucket").ParameterValue')

ARTIFACT_NAME=code-lambda-$(cat ${ENV_TYPE}-parameters.json | jq -r '.Parameters[] | select(.ParameterKey == "LambdaName").ParameterValue')

aws s3 cp ${ARTIFACT_NAME}.zip s3://${BUCKET_NAME}/${ARTIFACT_NAME}.zip

exit 0
```

```sh:delete_artifact.dev.sh
#!/bin/sh

cd `dirname $0`

BASENAME=$(basename $0)
FILENAME=${BASENAME%.*}
ENV_TYPE=${FILENAME##*.}

BUCKET_NAME=$(cat ${ENV_TYPE}-parameters.json | jq -r '.Parameters[] | select(.ParameterKey == "ArtifactS3Bucket").ParameterValue')

ARTIFACT_NAME=code-lambda-$(cat ${ENV_TYPE}-parameters.json | jq -r '.Parameters[] | select(.ParameterKey == "LambdaName").ParameterValue')

aws s3 rm s3://${BUCKET_NAME}/${ARTIFACT_NAME}.zip

exit 0
```