自動返信LINE BotをAWS SAM+DynamoDBで作ってみた

こんにちは、関口です。
最近はUber EatsとAmazonフレッシュを多用して以前にもまして引きこもり気味です。

今回は現状所属しているプロジェクトのキャッチアップ的意味合いを込めて、
ポジティブなワードを返してくれる自動LineBotを
AWS SAMとDynamoDBで試してみました。

雛形作成

まずは雛形のプロジェクトを作成します。

$ sam init --runtime go1.x --name positive-line-bot

$ cd positive-line-bot

//依存モジュール管理
$ go mod init line-positive-bot

DynamoDB

Lineと連携する前に、
DynamoDBから自動返信されるワードを取得する部分を実装します。

一覧をDynamoDBのテーブルから取得してきて、
そのうち1つをランダムに抽出しています。

type Positive struct {
    ID   int    `json:"ID"`
    Name string `json:"Name"`
}

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {

    endpoint := os.Getenv("DYNAMODB_ENDPOINT")
    tableName := os.Getenv("DYNAMODB_TABLE_NAME")

    sess := session.Must(session.NewSession())
    config := aws.NewConfig().WithRegion("ap-northeast-1")
    if len(endpoint) > 0 {
        config = config.WithEndpoint(endpoint)
    }

    db := dynamodb.New(sess, config)

    result, err := db.Scan(&dynamodb.ScanInput{
        TableName:              aws.String(tableName),
        ConsistentRead:         aws.Bool(true),
        ReturnConsumedCapacity: aws.String("NONE"),
    })
    if err != nil {
        return events.APIGatewayProxyResponse{}, err
    }

    var positives []Positive

    err = dynamodbattribute.UnmarshalListOfMaps(result.Items, &positives)
    if err != nil {
        fmt.Println(err)
        return events.APIGatewayProxyResponse{}, err
    }

    var words []string
    for _, positive := range positives {
        words = append(words, positive.Name)
    }

    rand.Seed(time.Now().UnixNano())
    i := rand.Intn(len(words))
    word := words[i]

    return events.APIGatewayProxyResponse{
        Body:       word,
        StatusCode: 200,
    }, nil
}

func main() {
    lambda.Start(handler)
}

template.yml

Resources:
  PositiveLineBotFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: positive-line-bot/
      Handler: positive-line-bot
      Runtime: go1.x
      Tracing: Active
      Policies:
        - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
      Events:
        CatchAll:
          Type: Api
          Properties:
            Path: /positive
            Method: GET
      Environment:
        Variables:
          DYNAMODB_ENDPOINT: ''
          DYNAMODB_TABLE_NAME: 'PositiveLineBotTable'

  PositiveLineBotTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: 'PositiveLineBotTable'
      AttributeDefinitions:
        - AttributeName: 'ID'
          AttributeType: 'N'
      KeySchema:
        - AttributeName: 'ID'
          KeyType: 'HASH'
      ProvisionedThroughput:
        ReadCapacityUnits: '2'
        WriteCapacityUnits: '2'

DynamoDB Local

うまくできているかどうかローカルで確認します。

docker-compose.yml

version: '3'

services:
  dynamodb:
    image: amazon/dynamodb-local
    container_name: dynamodb
    ports:
      - 8000:8000

ローカルテストで用いるテーブル定義および、テストデータを作成します。
test/positive-line-bot_table.json

{
  "AttributeDefinitions": [
    {
      "AttributeName": "Id",
      "AttributeType": "N"
    }
  ],
  "TableName": "PositiveLineBotTable",
  "KeySchema": [
    {
      "AttributeName": "Id",
      "KeyType": "HASH"
    }
  ],
  "ProvisionedThroughput": {
    "ReadCapacityUnits": 2,
    "WriteCapacityUnits": 2
  }
}

test/positive-line-bot_table_data.json

{
  "PositiveLineBotTable": [
    {
      "PutRequest": {
        "Item": {
          "ID": {"N": "1"},
          "Name": {"S": "test"}
        }
      }
    },
    {
      "PutRequest": {
        "Item": {
          "ID": {"N": "2"},
          "Name": {"S": "testtest"}
        }
      }
    }
  ]
}

ローカルテスト用の環境変数を設定します

{
  "PositiveLineBotFunction": {
    "DYNAMODB_ENDPOINT": "http://{ポート番号}:8000",
    "DYNAMODB_TABLE_NAME": "PositiveLineBotTable"
  }
}

AWS CLI のコマンドを実行してデータを反映した上で
実行すると、テストデータの値が1つランダムで返されます

$ docker-compose up -d

$ aws dynamodb create-table --cli-input-json file://test/positive-line-bot_table.json --endpoint-url http://127.0.0.1:8000

$ aws dynamodb batch-write-item --request-items file://test/positive-line-bot_table_data.json --endpoint-url http://127.0.0.1:8000

$ aws dynamodb scan --table-name PositiveLineBotTable --endpoint-url http://127.0.0.1:8000

$ sam local start-api --env-vars test/env.json --profile dummy

$ curl http://localhost:3000/positive
{"id":1,"name":"testtest"}

Lineと連携する

Messaging APIを利用するにはなどを参考にLine側の設定行います。

設定時、ChannelSecretとアクセストークン(ロングターム)をメモします。

Lineに入力された値が渡ってくるように、コードとtemplate.ymlを修正します。

func UnmarshalLineRequest(data []byte) (LineRequest, error) {
    var r LineRequest
    err := json.Unmarshal(data, &r)
    return r, err
}

type LineRequest struct {
    Events      []Event `json:"events"`
    Destination string  `json:"destination"`
}

type Event struct {
    Type       string  `json:"type"`
    ReplyToken string  `json:"replyToken"`
    Source     Source  `json:"source"`
    Timestamp  int64   `json:"timestamp"`
    Message    Message `json:"message"`
}

type Message struct {
    Type string `json:"type"`
    ID   string `json:"id"`
    Text string `json:"text"`
}

type Source struct {
    UserID string `json:"userId"`
    Type   string `json:"type"`
}

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {

    myLineRequest, err := UnmarshalLineRequest([]byte(request.Body))
    if err != nil {
        log.Fatal(err)
    }

    bot, err := linebot.New(
        "ChannelSecret",
        "アクセストークン(ロングターム)")
    if err != nil {
        log.Fatal(err)
    }

    // 中略

    var tmpReplyMessage string
    tmpReplyMessage = word
    if _, err = bot.ReplyMessage(myLineRequest.Events[0].ReplyToken, linebot.NewTextMessage(tmpReplyMessage)).Do(); err != nil {
        log.Fatal(err)
    }

    return events.APIGatewayProxyResponse{
        Body:       word,
        StatusCode: 200,
    }, nil

}

template.yml

Method: POST

デプロイ

最後にデプロイをして実際に確認してみます。

$ sam validate --profile my_profile --debug

$ sam build --template template.yaml --profile my_profile

$ sam deploy --guided --profile my_profile

作成されたdynamoDBテーブルにデータを入れた状態で
試しに自身の携帯から確認したところ無事自動返答がされました。

参考サイト