ついにGOがAWSにきた!!
🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉
ついにGolangがAws lambdaに対応しました!!!!
🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉
今まで、様々なライブラリを使ってなんとか無理くり、やっていましたが、これでできるようになりました!
それに伴い、serverlessも対応しましたので、早速バイト先のserverlessのボットのプロジェクトをしれっと全部Goにしたので、
その時のことを書いていきます。
serverlessがどうなったのか
serverlessフレームワークは今は、使われているところが徐々に増えてきていますが、serverlessはこれまで、Goは対応しておらず、バイナリをaws cliであげるとか、pythonのCのライブラリとしてあげるなど、様々な方法でアプローチしていました。しかし、今回serverlessの公式がサポートした!とういう流れですね
とりあえずserverless + go
まぁともかくやってみましょう
$ sls -v
1.26.0
$ sls create -t aws-go -p myservice
$ cd myservice
$ tree
.
├── Makefile
├── hello
│ └── main.go
├── serverless.yml
└── world
└── main.go
こんな感じですね
とりあえずこいつらをデプロイしてみましょう!!!
makeファイルもgoであるがゆえに用意させたので、これを使いましょう!
(僕が最初にやったときは、なかった。。)
$ make build
$ sls deploy
$ sls invoke -f hello
{
"message": "Go Serverless v1.0! Your function executed successfully!"
}
$ sls invoke -f world
{
"message": "Okay so your other function also executed successfully!"
}
とりあえずこれでlambda関数はawsにあがりました!!バンザーーーイ!!!!
おうむ返し
さて、ボットといえばおうむ返しです。
なので、今回はおうむ返しをやっていきます!!
$ tree
.
├── Makefile
├── bin
│ └── bot
├── bot
│ ├── line.go
│ ├── main.go
│ └── parse.go
├── conf
│ └── dev
│ └── dev.yml
└── serverless.yml
構成としてはこんな感じです
ではまず一つ一つ見ていきましょう
設定
serverless.yml
service: myservice
provider:
name: aws
runtime: go1.x
region: ap-northeast-1
stage: ${opt:stage, self:custom.defaultStage}
custom:
defaultStage: dev
profiles:
dev: sls
otherfile:
environment:
dev: ${file(./conf/dev/dev.yml)}
package:
exclude:
- ./**
include:
- ./bin/**
functions:
bot:
handler: bin/bot
environment:
CHANNEL_SECRET: ${self:custom.otherfile.environment.${self:provider.stage}.CHANNEL_SECRET}
CHANNEL_TOKEN: ${self:custom.otherfile.environment.${self:provider.stage}.CHANNEL_TOKEN}
events:
- http:
path: callback
method: post
cors: true
./conf/dev/dev.yml
CHANNEL_SECRET: "<your CHANNEL_SECRET>"
CHANNEL_TOKEN: "<your CHANNEL_TOKEN>"
基本的にここに、AWSのlambda以外のサービスをいくつ立てて接続してとかはここに記述します。
IAMもここで定義しますね。ですが今回はおうむ返しなので、特に設定することはないですが、lineのCHANNEL_SECRETとCHANNEL_TOKENを環境変数として設定したいので、./conf/dev/dev.ymlに置いています。
次にコードを見ていきましょう。
ソースコード
main.go
package main
import (
"errors"
"fmt"
"os"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/line/line-bot-sdk-go/linebot"
)
func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
line := Line{}
err := line.New(
os.Getenv("CHANNEL_SECRET"),
os.Getenv("CHANNEL_TOKEN"))
if err != nil {
fmt.Println(err)
}
eve, err := ParseRequest(line.ChannelSecret, request)
if err != nil {
status := 200
if err == linebot.ErrInvalidSignature {
status = 400
} else {
status = 500
}
return events.APIGatewayProxyResponse{StatusCode: status}, errors.New("Bat Request")
}
line.EventRouter(eve)
return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: 200}, nil
}
func main() {
lambda.Start(Handler)
}
ここでは先ほどのCHANNEL_SECRET, CHANNEL_TOKEN設定して、botのクライアントを作成しますが、一つ問題で、lineの公式のsdkの中にある、サンプルは、goのnet/httpを使っていますが、残念なことに、互換性がないので、自分で、構造体を定義してやるしかなかったので、その辺りはSDKの中で必要な部分をパクってきて、パーサーを書いています。
parse.go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"github.com/aws/aws-lambda-go/events"
"github.com/line/line-bot-sdk-go/linebot"
)
func ParseRequest(channelSecret string, r events.APIGatewayProxyRequest) ([]*linebot.Event, error) {
if !validateSignature(channelSecret, r.Headers["X-Line-Signature"], []byte(r.Body)) {
return nil, linebot.ErrInvalidSignature
}
request := &struct {
Events []*linebot.Event `json:"events"`
}{}
if err := json.Unmarshal([]byte(r.Body), request); err != nil {
return nil, err
}
return request.Events, nil
}
func validateSignature(channelSecret, signature string, body []byte) bool {
decoded, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return false
}
hash := hmac.New(sha256.New, []byte(channelSecret))
hash.Write(body)
return hmac.Equal(decoded, hash.Sum(nil))
}
ここでパースしています。詳しくはSDKの中を見るとわかります。
line.go
package main
import (
"fmt"
"github.com/line/line-bot-sdk-go/linebot"
)
type Line struct {
ChannelSecret string
ChannelToken string
Bot *linebot.Client
}
func (r *Line) SendTextMessage(message string, replyToken string) error {
return r.Reply(replyToken, linebot.NewTextMessage(message))
}
func (r *Line) SendTemplateMessage(replyToken, altText string, template linebot.Template) error {
return r.Reply(replyToken, linebot.NewTemplateMessage(altText, template))
}
func (r *Line) Reply(replyToken string, message linebot.Message) error {
if _, err := r.Bot.ReplyMessage(replyToken, message).Do(); err != nil {
fmt.Printf("Reply Error: %v", err)
return err
}
return nil
}
func (r *Line) NewCarouselColumn(thumbnailImageURL, title, text string, actions ...linebot.TemplateAction) *linebot.CarouselColumn {
return &linebot.CarouselColumn{
ThumbnailImageURL: thumbnailImageURL,
Title: title,
Text: text,
Actions: actions,
}
}
func (r *Line) NewCarouselTemplate(columns ...*linebot.CarouselColumn) *linebot.CarouselTemplate {
return &linebot.CarouselTemplate{
Columns: columns,
}
}
func (l *Line) New(secret, token string) error {
l.ChannelSecret = secret
l.ChannelToken = token
bot, err := linebot.New(
l.ChannelSecret,
l.ChannelToken,
)
if err != nil {
return err
}
l.Bot = bot
return nil
}
func (r *Line) EventRouter(eve []*linebot.Event) {
for _, event := range eve {
switch event.Type {
case linebot.EventTypeMessage:
switch message := event.Message.(type) {
case *linebot.TextMessage:
r.handleText(message, event.ReplyToken, event.Source.UserID)
}
}
}
}
func (r *Line) handleText(message *linebot.TextMessage, replyToken, userID string) {
r.SendTextMessage(message.Text, replyToken)
}
ここがイベントハンドリングや先ほどmainで出てきた自作の構造体が定義されているところです。
EventRouterでイベントの種類を分けて、handleText内でどうゆう風なものがきたらこうするとかの条件分岐を書くようにしています。
が今回はおうむ返しなので、そのまま返すようにしています。
その他
はじめにslsでプロジェクトを作った時と、構造を変えたので、そのあた理を考慮してmakeファイルも変更しておきます。
build:
go get github.com/aws/aws-lambda-go/lambda
env GOOS=linux go build -ldflags="-s -w" -o bin/bot bot/*.go
##デプロイ
デプロイは最初のものと同じ方法でできます。
$ make build
$ sls deploy
これであとは、出力されたurlをWebhook URLに設定してやればOK
てな感じでseverlessを使ってline bot開発でした!!
参考
単純にapiを作る場合は以下のリポジトリがいい感じですね