本記事は ぷりぷりあぷりけーしょんず 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 は環境構築が完了していることを前提とします。
アーキテクチャ
特定の路線の遅延情報を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はプッシュメッセージのみの利用なので、応答設定はこのようにしています。
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を定義する
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にプッシュメッセージを送信
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の時間通りに通知がくるのを待つのみです。
わたしの場合は毎朝起床時間に通知がくるようにしています。
botのアイコンは実家で飼っているトイプードルです。かわいい。
最後に
Goでシステムを作ったのは今回が初めてだったのですが、難しいですね!
Jsonを構造体にマッピングするところで若干ハマりました。エラーハンドリングとか、その他もろもろ設計とか、もっとソースコードを読んで勉強していこうと思います。
GoでLINE BOTを作ってみたい、CDKでインフラコード化したい、といった方の参考に少しでもなれば幸いです。ありがとうございました!