CloudFormationでサーバーレスのLINEBOTをデプロイする。

#概要
AWSの学習の中で，CloudFormationを使ってみたくなったので使ってみました。

目標：
Lambda関数をスケジュール起動させて，LINEで通知を送るサーバーレスのBOTをCloudFormationでデプロイします。

今回は1分おきに「slimeはkawaii。」「dragonはkakkoii。」「ghostはkowai。」のメッセージいずれかをランダムでLINEにpush通知するアプリケーションを作ります。

※通知する内容はDynamoDBから取得します。

下のようなネットワーク図になります。
図中のAWSサービスをCloudFormationテンプレートにまとめます。
![lambdaCFndraw.png](https://qiita-image-store.s3.amazonaws.com/0/368169/3c953e3c-7cf6-6680-a7e6-4f35e7cd239b.png)


#CloudFormationのテンプレート
CloudFormationのテンプレートです。
リソースはAWS Lambda，Amazon DynamoDB，Amazon CloudWatchです。
また今回はテンプレートの中でLambdaとCloudWatchそれぞれのIAMロールも作るようにしました。

```yaml:CFnTemplate.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: Line_Push_bot
Parameters:
  pushFunctionName:
    Type: String
    Default: PushFunction
    Description: pushFunction
  dynamoDBTableName:
    Type: String
    Default: sampleContents
  pushFuncStoredS3BucketName:
    Type: String
    Description: "pushFunction Stored S3 Bucket" 
  S3KeyName:
    Type: String
    Default: "lambda_function.zip"
  pushFuncHandlerName:
    Type: String
    Default: lambda_function.lambda_handler
  pushFuncIAMPolicyName:
    Type: String
    Default: PushFuncIAM
  lineUserID:
    Type: String
  lineChannelAccessToken:
    Type: String   
Resources:
  pushFunc:
    Type: 'AWS::Lambda::Function'
    Properties:
      FunctionName: !Ref 'pushFunctionName'
      Code:
        S3Bucket: !Ref 'pushFuncStoredS3BucketName'
        S3Key: !Ref 'S3KeyName'
      Description: "push lambda template"
      Handler: !Ref 'pushFuncHandlerName'
      Role: !GetAtt 
        - LambdaExecutionRole
        - Arn
      Runtime: python3.6
      Environment:
        Variables:
          'uid': !Ref 'lineUserID'
          'LINE_CHANNEL_ACCESS_TOKEN': !Ref 'lineChannelAccessToken'
  LambdaExecutionRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Policies:
        - PolicyName: !Ref 'pushFuncIAMPolicyName'
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'dynamodb:GetItem'
                Resource: !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${dynamoDBTableName}'
              - Effect: Allow
                Action:
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                  - 'logs:CreateLogGroup'
                Resource: '*'
  PushLambdaScheduleEvent:
    Type: AWS::Events::Rule
    Properties:
        Description: 'schedule event for pushFunction'
        ScheduleExpression: 'rate(1 minute)'
        State: ENABLED
        Targets:
          - Arn: !GetAtt pushFunc.Arn
            Id: TargetFunctionV1 
  PushLambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties: 
      Action: lambda:InvokeFunction
      FunctionName: !Ref pushFunctionName
      Principal: events.amazonaws.com
      SourceArn: !GetAtt PushLambdaScheduleEvent.Arn
  sampleTable:
    Type: 'AWS::DynamoDB::Table'
    Properties:
      TableName: !Ref dynamoDBTableName
      AttributeDefinitions:
        - AttributeName: type
          AttributeType: S
      KeySchema:
        - AttributeName: type
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5
```

Lambdaのコードは下のようになります。

```python:lambda_function.py
import urllib.request, urllib.parse
import boto3
import json
import os
import random

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table('sampleContents')

def lambda_handler(event, context):
    types = ["dragon", "slime", "ghost"]
    type = random.choice(types)
    try:
        response = table.get_item(
            Key = {
                    'type': type
            }
        )
        msg = response['Item']['characteristic']
    except:
        msg = "まだいません"
    pushmsg = "{0}は{1}。".format(type, msg)
    url = 'https://api.line.me/v2/bot/message/push'
    body = {
        "to":  os.environ['uid'],
        "messages": [
            {
                "type": "text",
                "text": pushmsg
            }
        ]
    }
    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + os.environ['LINE_CHANNEL_ACCESS_TOKEN']
    }
    req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), method='POST', headers=headers)
    with urllib.request.urlopen(req) as res:
        pass
    return {'statusCode': 200, 'body': '{}'}
```

#手順
上に記述したコードを使ってデプロイする手順を追って説明します。

1. LINE MessagingAPIを利用できるようにします。
未登録の場合はLINE Developersに登録しましょう。
以下記載のURLにしたがって，プロバイダーとチャネルを作成します。
プロバイダーのプランはフリーではなくDveloper Trialです（Push Messageを利用するため）。
↓詳しくはLINE Developersのドキュメントを参照してください。↓  
[Messaging APIを利用するには]https://developers.line.biz/ja/docs/messaging-api/getting-started/  
このとき，**アクセストークン（ロングターム）**と**Your user ID**を控えておきます。

1. 1で作成したチャネルの基本設定の中にあるQRコードを読み取り，お友達追加をしてください。

2.  `lambda_function.py`を`lambda_function.zip`という名前でzip圧縮し，S3バケットにアップロードします。
この時の**S3バケット名**を控えておきます。
S3バケットを作成するリージョンとCloudFormationでスタックを作成するリージョンは一致させてください。
例えばap-northeast-1にスタックを作成する場合は，ap-northeast-1にS3バケットを作成してください。

1. `CFnTemplate.yaml`を基にしてスタックを作成します。
下図を参考にして**アクセストークン（ロングターム）**，**Your user ID**，**S3バケット名**を各パラメーターの値へ入れていきます。
手順通りに進めた場合，デフォルトで入力されている項目は特に変更する必要はありません。　　
※スタックの作成完了後は1分おきにLINEが届きますのでご注意ください。
![stackscene.png](https://qiita-image-store.s3.amazonaws.com/0/368169/0025a05b-902e-2c5e-82e3-34c51978d2bd.png)


#動作確認

1. LINEを確認します。
「・・・はまだいません。」とメッセージが届くはずです。
・・・の部分は slime, dragon, ghostのいずれかです。
例えば「slimeはkawaii。」のような「・・・は＊＊＊。」というメッセージがpush通知されることが今回の目標ですが，まだDynamoDBのテーブルに該当の要素がないため，pushFuncがこのようなメッセージを生成して通知しています。


1. DynamoDBへ要素を追加します。
「・・・は＊＊＊。」という形のメッセージをLINEにpush通知するために，DynamoDBのテーブルへ要素を追加します。
sampleContentsという名前のテーブルができているので，以下のようにします。

|type |characteristic|
|:-:|:-:|
|slime  |kawaii   |
|dragon   |kakkoii   |
|ghost   |kowai   |

上のように要素をDBに追加することで，LINEに「・・・は＊＊＊。」とpushされます。
・・・は**type**，＊＊＊は**characteristic**が対応します。

#削除方法

1. CloudFormationのスタックを削除します。
 
2. 次に`lambda_function.py` がアップロードされているS3バケットを削除します。

3. 最後にLINE Developersにログインし，チャネル，プロバイダーの順に削除します。