AWS_SAMを使ってLNEbotを爆速で開発する

# AWS_SAMを使ってオウム返しLINEbotを作る

こんにちは!
突然ですが、サーバレスは好きですか？僕は大好きです。
ただ、クラウドのリソース管理とかよくわからず、なかなか最初は手が出しにくい気がします。

今回はAWSのSAM(Serverless Application Model)を使ってサーバレスAPIを構築してLINEbotを作っていきます。
構築したリソースは一括削除ができるため、サーバレスアプリを手軽に体験することができます。

---

- aws_samとは
    - サーバレスアプリケーションモデル
    - サーバレスアプリケーション構築用のオープンソースフレームワーク
    - yamlファイルをもちいてモデリング
    - デプロイ中sam構文をCloudFormation構文に変換および拡張することで、サーバレスアプリケーションの構築を高速化することができる



★サーバレスアプリ構築用のフレームワークのようなもの、AWSに自動でサーバレスAPIの構築を行ってくれます。
★YAMLに記述したSAM構文をCloudFundationに適用していくことでLambdaやAPIGatewayの設定を自動で行ってくれます。
★不要になればスタックを削除して無駄な料金を省くことができます。(後述)

（※以下MACにて実施しています。手順はおおむね変わりありませんがWindowsの方はコマンドを読み替えていただければと思います。）

### AWS_SAMをインストール

---

※下記サイトの手順参照
[AWS サーバーレスアプリケーションモデル - アマゾン ウェブ サービス](https://aws.amazon.com/jp/serverless/sam/)

# Hello Worldテンプレートでアプリの雛形作成



既存のtemplateを編集して実装を進めていきます。

その前に使用するテンプレートの動作確認を行っていきます。

今回runtime(言語)はpython3.8を選択しました。

```bash
sam init

Which template source would you like to use?
	1 - AWS Quick Start Templates
	2 - Custom Template Location
Choice: 1
What package type would you like to use?
	1 - Zip (artifact is a zip uploaded to S3)
	2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 1

Which runtime would you like to use?
	1 - nodejs14.x
	2 - python3.8
	3 - ruby2.7
	4 - go1.x
	5 - java11
	6 - dotnetcore3.1
	7 - nodejs12.x
	8 - nodejs10.x
	9 - python3.7
	10 - python3.6
	11 - python2.7
	12 - ruby2.5
	13 - java8.al2
	14 - java8
	15 - dotnetcore2.1
Runtime: 2

Project name [sam-app]: LINEbot_Fitness

Cloning app templates from https://github.com/aws/aws-sam-cli-app-templates

AWS quick start application templates:
	1 - Hello World Example
	2 - EventBridge Hello World
	3 - EventBridge App from scratch (100+ Event Schemas)
	4 - Step Functions Sample App (Stock Trader)
	5 - Elastic File System Sample App
Template selection: 1

    -----------------------
    Generating application:
    -----------------------
    Name: LINEbot_Fitness
    Runtime: python3.8
    Dependency Manager: pip
    Application Template: hello-world
    Output Directory: .

    Next steps can be found in the README file at ./LINEbot_Fitness/README.md
```

```
LINEbot_Fitness/
   ├── README.md
   ├── events/
   │   └── event.json
   ├── hello_world/
   │   ├── __init__.py
   │   ├── app.py            #Contains your AWS Lambda handler logic.
   │   └── requirements.txt  #Contains any Python dependencies the application requires, used for sam build
   ├── template.yaml         #Contains the AWS SAM template defining your application's AWS resources.
   └── tests/
       └── unit/
           ├── __init__.py
           └── test_handler.py
```

- チュートリアルに沿ってデプロイまで実行

    ---

    ### ビルド

    ---

    ```bash
    sam build

    Building codeuri: hello_world/ runtime: python3.8 metadata: {} functions: ['HelloWorldFunction']
    Running PythonPipBuilder:ResolveDependencies
    Running PythonPipBuilder:CopySource

    Build Succeeded

    Built Artifacts  : .aws-sam/build
    Built Template   : .aws-sam/build/template.yaml

    Commands you can use next
    =========================
    [*] Invoke Function: sam local invoke
    [*] Deploy: sam deploy --guided
    ```

    ### デプロイ

    ---

    ```bash
    sam deploy --guided

    Configuring SAM deploy
    ======================

    	Looking for config file [samconfig.toml] :  Found
    	Reading default arguments  :  Success

    	Setting default arguments for 'sam deploy'
    	=========================================
    	Stack Name [LINEbot_Fitness]: LINEbotFitness
    	AWS Region [ap-northeast-1]:
    	#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
    	Confirm changes before deploy [Y/n]: y
    	#SAM needs permission to be able to create roles to connect to the resources in your template
    	Allow SAM CLI IAM role creation [Y/n]: y
    	HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y
    	Save arguments to configuration file [Y/n]: y
    	SAM configuration file [samconfig.toml]:
    	SAM configuration environment [default]:

    	Looking for resources needed for deployment: Found!

    		Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-1lsd1f1o23h47
    		A different default S3 bucket can be set in samconfig.toml

    	Saved arguments to config file
    	Running 'sam deploy' for future deployments will use the parameters saved above.
    	The above parameters can be changed by modifying samconfig.toml
    	Learn more about samconfig.toml syntax at
    	https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html

    Uploading to LINEbotFitness/7e23deb2b18497df06aec41e21909939  584056 / 584056.0  (100.00%)

    	Deploying with following values
    	===============================
    	Stack name                   : LINEbotFitness
    	Region                       : ap-northeast-1
    	Confirm changeset            : True
    	Deployment s3 bucket         : aws-sam-cli-managed-default-samclisourcebucket-1lsd1f1o23h47
    	Capabilities                 : ["CAPABILITY_IAM"]
    	Parameter overrides          : {}
    	Signing Profiles             : {}

    Initiating deployment
    =====================
    HelloWorldFunction may not have authorization defined.
    Uploading to LINEbotFitness/3a56556d2a056416039674cf9f7ee7f1.template  1113 / 1113.0  (100.00%)

    Waiting for changeset to be created..

    CloudFormation stack changeset
    ---------------------------------------------------------------------------------------------------------------------
    Operation                     LogicalResourceId             ResourceType                  Replacement
    ---------------------------------------------------------------------------------------------------------------------
    + Add                         HelloWorldFunctionHelloWorl   AWS::Lambda::Permission       N/A
                                  dPermissionProd
    + Add                         HelloWorldFunctionRole        AWS::IAM::Role                N/A
    + Add                         HelloWorldFunction            AWS::Lambda::Function         N/A
    + Add                         ServerlessRestApiDeployment   AWS::ApiGateway::Deployment   N/A
                                  47fc2d5f9d
    + Add                         ServerlessRestApiProdStage    AWS::ApiGateway::Stage        N/A
    + Add                         ServerlessRestApi             AWS::ApiGateway::RestApi      N/A
    ---------------------------------------------------------------------------------------------------------------------

    Changeset created successfully. arn:aws:cloudformation:ap-northeast-1:056822081946:changeSet/samcli-deploy1613024810/08b59885-e90b-4d4b-953b-91ae3ecab8bd

    Previewing CloudFormation changeset before deployment
    ======================================================
    Deploy this changeset? [y/N]: y

    2021-02-11 15:27:05 - Waiting for stack create/update to complete

    CloudFormation events from changeset
    ---------------------------------------------------------------------------------------------------------------------
    ResourceStatus                ResourceType                  LogicalResourceId             ResourceStatusReason
    ---------------------------------------------------------------------------------------------------------------------
    CREATE_IN_PROGRESS            AWS::IAM::Role                HelloWorldFunctionRole        -
    CREATE_IN_PROGRESS            AWS::IAM::Role                HelloWorldFunctionRole        Resource creation Initiated
    CREATE_COMPLETE               AWS::IAM::Role                HelloWorldFunctionRole        -
    CREATE_IN_PROGRESS            AWS::Lambda::Function         HelloWorldFunction            -
    CREATE_COMPLETE               AWS::Lambda::Function         HelloWorldFunction            -
    CREATE_IN_PROGRESS            AWS::Lambda::Function         HelloWorldFunction            Resource creation Initiated
    CREATE_IN_PROGRESS            AWS::ApiGateway::RestApi      ServerlessRestApi             -
    CREATE_IN_PROGRESS            AWS::ApiGateway::RestApi      ServerlessRestApi             Resource creation Initiated
    CREATE_COMPLETE               AWS::ApiGateway::RestApi      ServerlessRestApi             -
    CREATE_IN_PROGRESS            AWS::ApiGateway::Deployment   ServerlessRestApiDeployment   -
                                                                47fc2d5f9d
    CREATE_IN_PROGRESS            AWS::Lambda::Permission       HelloWorldFunctionHelloWorl   Resource creation Initiated
                                                                dPermissionProd
    CREATE_IN_PROGRESS            AWS::ApiGateway::Deployment   ServerlessRestApiDeployment   Resource creation Initiated
                                                                47fc2d5f9d
    CREATE_IN_PROGRESS            AWS::Lambda::Permission       HelloWorldFunctionHelloWorl   -
                                                                dPermissionProd
    CREATE_COMPLETE               AWS::ApiGateway::Deployment   ServerlessRestApiDeployment   -
                                                                47fc2d5f9d
    CREATE_IN_PROGRESS            AWS::ApiGateway::Stage        ServerlessRestApiProdStage    -
    CREATE_IN_PROGRESS            AWS::ApiGateway::Stage        ServerlessRestApiProdStage    Resource creation Initiated
    CREATE_COMPLETE               AWS::ApiGateway::Stage        ServerlessRestApiProdStage    -
    CREATE_COMPLETE               AWS::Lambda::Permission       HelloWorldFunctionHelloWorl   -
                                                                dPermissionProd
    CREATE_COMPLETE               AWS::CloudFormation::Stack    LINEbotFitness                -
    ---------------------------------------------------------------------------------------------------------------------

    CloudFormation outputs from deployed stack
    -----------------------------------------------------------------------------------------------------------------------
    Outputs
    -----------------------------------------------------------------------------------------------------------------------
    Key                 HelloWorldFunctionIamRole
    Description         Implicit IAM Role created for Hello World function
    Value               arn:aws:iam::056822081946:role/LINEbotFitness-HelloWorldFunctionRole-JP6UNGHDH2RA

    Key                 HelloWorldApi
    Description         API Gateway endpoint URL for Prod stage for Hello World function
    Value               https://ppp8jf5dic.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/

    Key                 HelloWorldFunction
    Description         Hello World Lambda Function ARN
    Value               arn:aws:lambda:ap-northeast-1:056822081946:function:LINEbotFitness-
    HelloWorldFunction-5KDQ6WQ06FEZ
    -----------------------------------------------------------------------------------------------------------------------

    Successfully created/updated stack - LINEbotFitness in ap-northeast-1
    ```

    ### 動作確認
    ---

    ```bash
    curl https://ppp8jf5dic.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/

    {"message": "hello world"}%
    ```

Hello World Templateの動作確認ができました。

### ローカルで動作確認

---

```bash
sam local start-api

curl http://127.0.0.1:3000/hello

{"message": "hello world"}
```

※変更後は再度buildしstart-api

### LINEbotのwebhook用にYAML修正します。

---

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  LINEbot_Fitness

  Sample SAM Template for LINEbot_Fitness

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  LINEbotFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: fitness/
      Handler: app.lambda_handler
      Runtime: python3.8
      Events:
        LINEbot:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /fitness
            Method: post

Outputs:
  LINEbotApi:
    Description: "API Gateway endpoint URL for Prod stage for LINEbot fitness function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/fitness/"
  LINEbotFunction:
    Description: "LINEbot fitness Lambda Function ARN"
    Value: !GetAtt LINEbotFunction.Arn
  LINEbotFunctionIamRole:
    Description: "Implicit IAM Role created for LINEbot fitness function"
    Value: !GetAtt LINEbotFunctionRole.Arn
```

実行するLambda関数

```python
import json

# import requests

def lambda_handler(event, context):

    print(event)

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": event,
            # "location": ip.text.replace("\n", "")
        }),
    }
```
build後、Localで起動します。

```bash
sam build 
sam local satart-api 
```
動作確認をします。

```bash
curl -X POST http://127.0.0.1:3000/fitness

{
   "message":{
      "body":null,
      "headers":{
         "Accept":"*/*",
         "Host":"127.0.0.1:3000",
         "User-Agent":"curl/7.71.1",
         "X-Forwarded-Port":"3000",
         "X-Forwarded-Proto":"http"
      },
      "httpMethod":"POST",
      "isBase64Encoded":false,
      "multiValueHeaders":{
         "Accept":[
            "*/*"
         ],
         "Host":[
            "127.0.0.1:3000"
         ],
         "User-Agent":[
            "curl/7.71.1"
         ],
         "X-Forwarded-Port":[
            "3000"
         ],
         "X-Forwarded-Proto":[
            "http"
         ]
      },
      "multiValueQueryStringParameters":null,
      "path":"/fitness",
      "pathParameters":null,
      "queryStringParameters":null,
      "requestContext":{
         "accountId":"123456789012",
         "apiId":"1234567890",
         "domainName":"127.0.0.1:3000",
         "extendedRequestId":null,
         "httpMethod":"POST",
         "identity":{
            "accountId":null,
            "apiKey":null,
            "caller":null,
            "cognitoAuthenticationProvider":null,
            "cognitoAuthenticationType":null,
            "cognitoIdentityPoolId":null,
            "sourceIp":"127.0.0.1",
            "user":null,
            "userAgent":"Custom User Agent String",
            "userArn":null
         },
         "path":"/fitness",
         "protocol":"HTTP/1.1",
         "requestId":"f7af1c8c-2897-4ce8-b88a-b4a34d805ca0",
         "requestTime":"11/Feb/2021:07:21:09 +0000",
         "requestTimeEpoch":1613028069,
         "resourceId":"123456",
         "resourcePath":"/fitness",
         "stage":"Prod"
      },
      "resource":"/fitness",
      "stageVariables":null,
      "version":"1.0"
   }
}"%"
```
AWSにデプロイします。

```bash
sam deploy
```
成功するとTerminalにリクエスト用のURLが表示されるため、LINEのwebhookに紐づけしていきます。

### LINEbotのwebhookにひもづけ、上記で

---

![Screenshot_2021-02-11_16.30.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/637111/96055a46-c25a-26d9-1240-0f9be007cc40.png)

Verifyを押下することで接続確認ができます。
LINEBotを友達追加し、適当なメッセージを送るとLamdaのログにwebhookで受け付けたリクエストが表示されます。

![Screenshot_2021-02-11_16.33.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/637111/9049e286-ca5a-7954-3904-d8a7bbe8b342.png)


```json
{
   "resource":"/fitness",
   "path":"/fitness/",
   "httpMethod":"POST",
   "headers":{
      "CloudFront-Forwarded-Proto":"https",
      "CloudFront-Is-Desktop-Viewer":"true",
      "CloudFront-Is-Mobile-Viewer":"false",
      "CloudFront-Is-SmartTV-Viewer":"false",
      "CloudFront-Is-Tablet-Viewer":"false",
      "CloudFront-Viewer-Country":"JP",
      "content-type":"application/json; charset=utf-8",
      "Host":"ppp8jf5dic.execute-api.ap-northeast-1.amazonaws.com",
      "User-Agent":"LineBotWebhook/2.0",
      "Via":"2.0 f90df03a8129371b68786cdf0a407d89.cloudfront.net (CloudFront)",
      "X-Amz-Cf-Id":"sRjdd5w93zl6dpKhLVBj_ySYPMeVM6AAZByzctHcWUeNV2iJrFtb1g==",
      "X-Amzn-Trace-Id":"Root=1-6024dd24-7dbc07d771a0ba0708f04f31",
      "X-Forwarded-For":"147.92.150.198, 64.252.167.145",
      "X-Forwarded-Port":"443",
      "X-Forwarded-Proto":"https",
      "x-line-signature":"uiS1/an5ll7ZyWHkqwIeNl/YZ3V71l4yzM+GE3LucYA="
   },
   "multiValueHeaders":{
      "CloudFront-Forwarded-Proto":[
         "https"
      ],
      "CloudFront-Is-Desktop-Viewer":[
         "true"
      ],
      "CloudFront-Is-Mobile-Viewer":[
         "false"
      ],
      "CloudFront-Is-SmartTV-Viewer":[
         "false"
      ],
      "CloudFront-Is-Tablet-Viewer":[
         "false"
      ],
      "CloudFront-Viewer-Country":[
         "JP"
      ],
      "content-type":[
         "application/json; charset=utf-8"
      ],
      "Host":[
         "ppp8jf5dic.execute-api.ap-northeast-1.amazonaws.com"
      ],
      "User-Agent":[
         "LineBotWebhook/2.0"
      ],
      "Via":[
         "2.0 f90df03a8129371b68786cdf0a407d89.cloudfront.net (CloudFront)"
      ],
      "X-Amz-Cf-Id":[
         "sRjdd5w93zl6dpKhLVBj_ySYPMeVM6AAZByzctHcWUeNV2iJrFtb1g=="
      ],
      "X-Amzn-Trace-Id":[
         "Root=1-6024dd24-7dbc07d771a0ba0708f04f31"
      ],
      "X-Forwarded-For":[
         "147.92.150.198, 64.252.167.145"
      ],
      "X-Forwarded-Port":[
         "443"
      ],
      "X-Forwarded-Proto":[
         "https"
      ],
      "x-line-signature":[
         "uiS1/an5ll7ZyWHkqwIeNl/YZ3V71l4yzM+GE3LucYA="
      ]
   },
   "queryStringParameters":"None",
   "multiValueQueryStringParameters":"None",
   "pathParameters":"None",
   "stageVariables":"None",
   "requestContext":{
      "resourceId":"9ndb9o",
      "resourcePath":"/fitness",
      "httpMethod":"POST",
      "extendedRequestId":"akd9vGFENjMFXWQ=",
      "requestTime":"11/Feb/2021:07:30:44 +0000",
      "path":"/Prod/fitness/",
      "accountId":"056822081946",
      "protocol":"HTTP/1.1",
      "stage":"Prod",
      "domainPrefix":"ppp8jf5dic",
      "requestTimeEpoch":1613028644617,
      "requestId":"1022d09d-c140-4b1c-9dc8-61eebc302e80",
      "identity":{
         "cognitoIdentityPoolId":"None",
         "accountId":"None",
         "cognitoIdentityId":"None",
         "caller":"None",
         "sourceIp":"147.92.150.198",
         "principalOrgId":"None",
         "accessKey":"None",
         "cognitoAuthenticationType":"None",
         "cognitoAuthenticationProvider":"None",
         "userArn":"None",
         "userAgent":"LineBotWebhook/2.0",
         "user":"None"
      },
      "domainName":"ppp8jf5dic.execute-api.ap-northeast-1.amazonaws.com",
      "apiId":"ppp8jf5dic"
   },
   "body":"{\"events\":[{\"type\":\"message\",\"replyToken\":\"8447990a2e764180be549ac585187480\",\"source\":{\"userId\":\"Ub3460cc5512a5791102446b8d075b066\",\"type\":\"user\"},\"timestamp\":1613028644559,\"mode\":\"active\",\"message\":{\"type\":\"text\",\"id\":\"13538493890029\",\"text\":\"fsd\"}}],\"destination\":\"Ued4095ce4cf48cdb76b561d079297db7\"}",
   "isBase64Encoded":false
}
```

# LINEbotにオウム返し実装

templateの```app.py```にオウム返しのロジックを入力していきます。

```python
import json
import os
# import requests
from linebot import (LineBotApi, WebhookHandler)
from linebot.exceptions import (LineBotApiError, InvalidSignatureError)
from linebot.models import (MessageEvent, TextMessage, TextSendMessage,)

def lambda_handler(event, context):

    # 環境変数取得
    YOUR_CHANNEL_ACCESS_TOKEN = os.environ["YOUR_CHANNEL_ACCESS_TOKEN"]
    YOUR_CHANNEL_SECRET = os.environ["YOUR_CHANNEL_SECRET"]

    line_bot_api = LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN)
    handler = WebhookHandler(YOUR_CHANNEL_SECRET)

    # get X-Line-Signature header value
    signature = event["headers"]['x-line-signature']
    # get request body as text
    body = event["body"]
    print("Request body: " + body)

    @handler.add(MessageEvent, message=TextMessage)
    def message(line_event):
        text = line_event.message.text
        line_bot_api.reply_message(
            line_event.reply_token, TextSendMessage(text=text))

    try:
        handler.handle(body, signature)
    except LineBotApiError as e:
        logger.error("Got exception from LINE Messaging API: %s\n" % e.message)
        for m in e.error.details:
            logger.error("  %s: %s" % (m.property, m.message))
        
    except InvalidSignatureError:
        logger.error("sending message happen error")
 
```
Lambdaに環境変数でLINEBotのチャンネル情報を登録します。

![Screenshot_2021-02-11_17.54.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/637111/67cfaa43-0459-6f07-a3f8-cdb920cfa776.png)


再度ビルド後デプロイすることでLINEbotがオウム返しをします。

![Videotogif (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/637111/4046c426-255d-1be4-520f-df4f13541f8d.gif)




## クリーンアップ_公式より引用

1. AWS マネジメントコンソール にサインインして、[https://console.aws.amazon.com/cloudformation](https://console.aws.amazon.com/cloudformation/) で AWS CloudFormation コンソールを開きます。
2. 左のナビゲーションペインで [**スタック**] を選択します。
3. スタックのリストで、[または作成したスタックの名前]  を選択します。
4. [**削除**] を選択します。

完了すると、スタックのステータスが **DELETE_COMPLETE** に変わります。

または、次の AWS CloudFormation コマンドを実行して AWS CLI スタックを削除することもできます。

```bash
aws cloudformation delete-stack --stack-name sam-app --region region
```

### **削除されたスタックの確認**

どちらの方法でも AWS CloudFormation スタックを削除する場合は、[AWS CloudFormation コンソール](https://console.aws.amazon.com/cloudformation/)に移動して削除済みであることを確認できます。左のナビゲーションペインで、[**スタック**] を選択し、検索ボックスの横にあるドロップダウンリストから [**削除済み**] を選択します。削除されたスタックのリストにスタックの名前が表示されます。

### 参考

---

[AWS サーバーレスアプリケーションモデル - アマゾン ウェブ サービス](https://aws.amazon.com/jp/serverless/sam/)

[チュートリアル: Hello World アプリケーションのデプロイ](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-getting-started-hello-world.html)

[Python + HerokuでLINE BOTを作ってみた - Qiita](https://qiita.com/shimajiri/items/cf7ccf69d184fdb2fb26)

[[新ツール] AWS Serverless Application Model (AWS SAM) を使ってサーバーレスアプリケーションを構築する | DevelopersIO](https://dev.classmethod.jp/articles/aws-serverless-application-model/)