17
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ぷりぷりあぷりけーしょんずAdvent Calendar 2019

Day 12

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

Last updated at Posted at 2019-12-11

本記事は ぷりぷりあぷりけーしょんず Advent Calendar 2019 の12日目の記事です。

はじめに

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

使用技術

LINE Messaging API
AWS Lambda
CloudWatch Events
AWS CDK 1.18.0
Go 1.13.3
鉄道遅延情報のjson

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

アーキテクチャ

アーキテクチャはこんな感じです。
train_delay_notice.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はプッシュメッセージのみの利用なので、応答設定はこのようにしています。
スクリーンショット 2019-12-07 13.46.35.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インストールしておきます。

ライブラリの公式リファレンスはこちらです。

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の時間通りに通知がくるのを待つのみです。

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

スクリーンショット 2019-12-08 16.03.06.png

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

最後に

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

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

17
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?