Go + AWS Lambda + LINE Messaging API で電車遅延通知bot

本記事は [ぷりぷりあぷりけーしょんず Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ppap) の12日目の記事です。

# はじめに
Qiita初投稿です。
現在業務では Vue.js + TypeScript でフロントエンドの開発を主に担当しています。
そんな筆者が今回はn番煎じですが、電車遅延通知botを作ったので実装方法を紹介していきます。

# 使用技術
LINE Messaging API
AWS Lambda
CloudWatch Events
AWS CDK 1.18.0
Go 1.13.3
[鉄道遅延情報のjson](https://rti-giken.jp/fhc/api/train_tetsudo/)

※ CDK, Go は環境構築が完了していることを前提とします。

# アーキテクチャ
アーキテクチャはこんな感じです。
![train_delay_notice.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/356412/69de0a1f-1af1-d75c-169c-e868b46d3027.png)

特定の路線の遅延情報をLINEにpush通知するGoプログラムをLambdaにデプロイしています。

LambdaとCloudWatch EventsはCDKで定義し、`cdk deploy`コマンドを叩くだけで上記アーキテクチャがデプロイできるようにしています。

# LINEチャネルの作成
まず、Messaging APIを使用するためにチャネルを作成します。

下記の公式手順に沿ってチャネルを作成し、**チャネルシークレット**/**チャネルアクセストークン**/**通知したいアカウントのユーザーID**を取得してください。
https://developers.line.biz/ja/docs/messaging-api/getting-started/

今回作ったbotはプッシュメッセージのみの利用なので、応答設定はこのようにしています。
<img width="672" alt="スクリーンショット 2019-12-07 13.46.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/356412/9b96a971-a1c7-e48b-0094-fcf8dc2b8f4d.png">

# CDKでAWSリソースを定義
続いて、CDKを使ってLambdaとCloudWatch Eventsのコードを書いていきます。

CDKでGAとなっている言語はTypeScriptとPythonがありますが、型の恩恵(コード補完・コンパイルエラーなど)が受けられるTypeScriptを使うことを強くオススメします！

## CDKのプロジェクトの作成

```
cdk init app --language=typescript
```

これでTypeScriptのCDKプロジェクトが作成されます。

## CloudFormationテンプレートを保存するS3バケットを用意


```
cdk bootstrap
```

CDKは裏側ではCloudFormationテンプレートが生成されているため、テンプレートを保存するためのS3バケットを用意する必要があります。初回に実行するだけでOKです。

## CloudFormationテンプレートをデプロイしておく

```
cdk deploy
```

あとで差分を確認できるようにデプロイしておきます。
CloudFormationのコンソールにアクセスし、作成したプロジェクト名のスタックが作成されていればOKです。

## 必要なライブラリをインストール


```
npm install @aws-cdk/aws-lambda @aws-cdk/core @aws-cdk/aws-events @aws-cdk/aws-events-targets
```
AWSサービスごとに`AWS Construct Library`というライブラリが用意されているので、使用するパッケージをnpmインストールしておきます。

ライブラリの公式リファレンスは[こちら](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-construct-library.html)です。

## LambdaとCloudWatch Eventsを定義する

```/lib/〇〇-stack.ts
import cdk = require('@aws-cdk/core');
import lambda = require('@aws-cdk/aws-lambda');
import events = require('@aws-cdk/aws-events');
import eventsTargets = require("@aws-cdk/aws-events-targets");
import { Duration } from '@aws-cdk/core';

export class TrainDelayNoticeCdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const postLineMessage = new lambda.Function(this, 'PostLineMessageHandler', {
      runtime: lambda.Runtime.GO_1_X,
      code: lambda.Code.fromAsset('lambda'),
      handler: 'postLineMessage',
      timeout: Duration.seconds(30),
      environment: {
        "CHANNEL_SECRET": "<your channel secret>",
        "CHANNEL_TOKEN": "<your channel access token>",
        "USER_ID": "<your user id>"
      }
    });

    new events.Rule(this, "TrainDeleyNoticeEvent", {
      schedule: events.Schedule.cron({
        minute: "30",
        hour: "22",
        day: "*",
        month: "*",
        year: "*"
      }),
      targets: [new eventsTargets.LambdaFunction(postLineMessage)]
    });
  }
}
```

### Lambda

- `runtime`
    - 名前の通り、ランタイムを指定
- `code`
    - デプロイ対象のネイティブバイナリを置いているディレクトリを指定
- `handler`
    - Goプログラム内のハンドラ名を指定
- `timeout`
    - デフォルトは3秒なので少し余裕を持たせておく
- `environment`
    - LINEのチャネルシークレット, チャネルアクセストークン, ユーザーIDを環境変数にセット
    - **認証系の情報は絶対にpublicリポジトリで公開しないようにしてください。**本当は設定ファイルに外出ししてコミットしない、が理想だと思います。privateリポジトリの場合でも間違って公開しちゃった〜なんてことの無いように気をつけましょう。

### CloudWatch Events

- `schedule`
    - 通知したい時間のcronを指定。UTCなので9時間前にする。
- `targets`
    - 対象のLambda関数を指定

# Goプログラムを作る

プロジェクトルート配下に`/lambda`ディレクトリを作成し、`main.go`ファイルを作成します。

## LambdaとLinebotのライブラリをインストール

公式で配布されているSDKをインストールしておきます。

```
go get github.com/aws/aws-lambda-go/lambda
go get github.com/line/line-bot-sdk-go/linebot
```

## main.goの実装

- `func main()`がLambda関数のエントリーポイントで、`lambda.Start()`によって関数が実行される
- `http.NewRequest()`で遅延情報を取得し、レスポンスのJsonをGoの構造体(`struct`)にマッピング
    - 構造体は必要なプロパティが定義できていればマッピング可能
- 必要なデータからメッセージの文字列に整形し、`bot.PushMessage()`でLINEにプッシュメッセージを送信

```/lambda/main.go
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"strings"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/line/line-bot-sdk-go/linebot"
)

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

func postLineMessage() {
	bot, err := linebot.New(os.Getenv("CHANNEL_SECRET"), os.Getenv("CHANNEL_TOKEN"))
	if err != nil {
		fmt.Println(err)
	}
	if _, err := bot.PushMessage(os.Getenv("USER_ID"), linebot.NewTextMessage(createTrainDelayInfo())).Do(); err != nil {
		fmt.Println(err)
	}
}

func createTrainDelayInfo() string {
	targetCompany := "JR東日本"
	targetNames := []string{"京浜東北線", "高崎線", "宇都宮線"}
	var notifyNames []string

	for _, train := range getTrainDelayInfo() {
		company := train.Company
		name := train.Name

		for _, targetName := range targetNames {
			if company == targetCompany && name == targetName {
				notifyNames = append(notifyNames, name)
			}
		}
	}

	if len(notifyNames) == 0 {
		return "遅延情報はありませんでした！\n良い一日を〜♪"
	}
	return strings.Join(notifyNames, ", ") + "が遅延しています。詳細をご確認ください。\nhttps://transit.yahoo.co.jp/traininfo/area/4/"
}

type TrainDelayInfo struct {
	Name    string `json:"name"`
	Company string `json:"company"`
}

func getTrainDelayInfo() []TrainDelayInfo {
	url := "https://tetsudo.rti-giken.jp/free/delay.json"

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		fmt.Println(err)
	}

	client := new(http.Client)
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println(err)
	}
	defer resp.Body.Close()

	byteArray, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println(err)
	}

	var trainDelayInfos []TrainDelayInfo
	if err := json.Unmarshal([]byte(byteArray), &trainDelayInfos); err != nil {
		fmt.Println(err)
	}
	return trainDelayInfos
}
```

このようにGoでは`if err != nil {}`が頻出します。
今回は細かいエラーハンドリングは必要ないのでログ吐いて終わりです。（というかGoでエラーハンドリングどうするべきなのか理解していない。。今後の課題。。）

## main.goをコンパイル

`main.go`ファイルをコンパイルし、ネイティブバイナリを生成します。
バイナリ名と`lambda.Start()`で呼び出しているハンドラ名は同じにします。

```
GOOS=linux GOARCH=amd64 go build -o postLineMessage
```

# デプロイ

差分を確認し、デプロイします。

```
cdk diff
cdk deploy
```

デプロイが完了したらLambdaのコンソールからテスト実行をして、指定したLINEアカウントに通知がくるか確認します。
動作確認ができれば、あとはcronの時間通りに通知がくるのを待つのみです。

わたしの場合は毎朝起床時間に通知がくるようにしています。

<img width="433" alt="スクリーンショット 2019-12-08 16.03.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/356412/0b6db3c2-2ad9-4d39-eb78-a8a8d2897fe0.png">

botのアイコンは実家で飼っているトイプードルです。かわいい。

# 最後に

Goでシステムを作ったのは今回が初めてだったのですが、難しいですね！
Jsonを構造体にマッピングするところで若干ハマりました。エラーハンドリングとか、その他もろもろ設計とか、もっとソースコードを読んで勉強していこうと思います。

GoでLINE BOTを作ってみたい、CDKでインフラコード化したい、といった方の参考に少しでもなれば幸いです。ありがとうございました！