社内でgoの勉強会みたいなのをやっておりまして、それの事前調査的な意味でのやってみた系blog記事となります。
やりたいこと
こんな感じです。
- goで何かつくる
- 何かフレームワーク使う(gin)
- Lambdaとapi gateway使う
- deployはsam使う
- slack botとして動かす
環境
- 普通のmac(m1じゃない)
まずはhello world
なんか公式っぽいライブラリがあります。
https://github.com/awslabs/aws-lambda-go-api-proxy
これ使えばgoとlambdaとginとsamが一気にやれる見込みです。
ひとまずhelloworldしてみましょう。
とは言ってもREADME通りにやるだけ。
まずは当然これ。
git clone https://github.com/awslabs/aws-lambda-go-api-proxy
あとはこんな感じで一気に。
ただ、makeの時にbuildのオプションがないとLambdaでexec format error: PathError null
みたいなエラーになったので下記にならってMakefileを修正しています。(GOBUILD=GOOS=linux GOARCH=amd64 $(GOCMD) build
みたいに)
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/golang-package.html
$ cd aws-lambda-go-api-proxy
$ go get github.com/aws/aws-lambda-go/events
$ go get github.com/aws/aws-lambda-go/lambda
$ go get github.com/awslabs/aws-lambda-go-api-proxy/...
$ make
$ export YOUR_DEPLOYMENT_BUCKET=hoge
$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket YOUR_DEPLOYMENT_BUCKET
export YOUR_STACK_NAME=hoge
$ aws cloudformation deploy --template-file output-sam.yaml --stack-name $YOUR_STACK_NAME --capabilities "CAPABILITY_IAM"
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - hoge
これであっさりとdeployが始まったっぽいです。
cloud formationのstackが作られて、deployが進むわけですが、進捗は下記のコマンドで確認できます。
$ aws cloudformation describe-stacks --stack-name $YOUR_STACK_NAME --query 'Stacks[]'
"StackStatus": "CREATE_COMPLETE",
みたいに出力されていれば正常終了しています。
で、"OutputValue"
という所にapi gatewayのendpointが出力されています。
これをcurlで叩いてみますね。
$ curl -XGET https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/pets -H 'application/json' | jq '.[0,1,2]'
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1225 100 1225 0 0 2097 0 --:--:-- --:--:-- --:--:-- 2097
{
"id": "b4a89cf0-6136-4507-af13-023df7cb5e39",
"breed": "Dalmatian",
"name": "Cody",
"dateOfBirth": "2008-07-02T11:20:25Z"
}
{
"id": "f7a7a1d4-e499-4e16-9130-27de6176587f",
"breed": "Norwegian Elkhound",
"name": "Buster",
"dateOfBirth": "2012-03-27T15:53:16Z"
}
{
"id": "84852a28-b48d-49b1-9465-075ab8dd7305",
"breed": "Jack Russell Terrier",
"name": "Charlie",
"dateOfBirth": "2012-07-30T20:47:20Z"
}
あっさり成功。
GET /pets/:id でペットを取得できたり、POST /petsでペットを登録できたり、
get /petsでペットのリストを取得できたりと、ペットを大量に飼育しているご家庭におすすめのサービスです。
なお、GET /pets 内ではgetRamdomPet
っていうメソッドが呼ばれていて、登録してなくてもランダムでペットが生まれてきます。
いや、/pet/:idでもランダムでした。現状だと createPet
は意味ないのかな。
curl -XGET https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/pets/1 -H 'application/json'
{"id":"1","breed":"Beagle","name":"Buster","dateOfBirth":"2021-01-09T03:33:19Z"}%
slack bot
めんどくさいのでこのexampleをそのまま使ったれ。
前提として、botを動かすためのslackのワークスペースおよび、botを登録する権限は必要です。
とりあえずbotを作る
まずは深く考えずにbotを作ります。
- まだ作成していない場合には、Slack アプリを作成します。作成してある場合には、既存のアプリを選択してください。
From app an manifest
って何だろう?なんかmanifest使えるっぽいですね。
とりあえず、これで適当なやつを作っておいてみよう。
_metadata:
major_version: 1
minor_version: 1
display_information:
name: "俺の亀"
description: An example app
background_color: "#da3a79"
features:
bot_user:
display_name: "orekame"
settings:
interactivity:
is_enabled: true
request_url: https://example.com/slack/message_action
oauth_config:
scopes:
bot:
- channels:read
- chat:write
- chat:write.public
redirect_urls:
- https://example.com/slack/auth
OK。
とりあえずさっくりとはできた。細かい設定はまた後で。
※なおredirect_urlsはいらなかった
incoming webhookの利用
まずは下記をやります。
- 「機能」ページで「Incoming Webhook をアクティブにする」をオンにします。
- アプリの投稿先となるチャンネルを選択して「許可する」をクリックします。
で、
3. Incoming Webhook URL を使用して Slack に投稿します。
$ curl -X POST -H 'Content-type: application/json' --data '{"text":"Hello, World!"}' https://hooks.slack.com/services/xxx/xxx/xxx
みたいにやると外部からslackへの投稿は確認できました。
Lambdaからのレスポンスはこれを使おう。
Event Subscriptions
以前はOutgoings Webhooksってのがあったけど廃止されたみたいです。けっこうslack周りはupdate早いんすよね。。
ご参考:
https://qiita.com/risto24/items/342256f6ed6cb504059a
先程のapiを再利用しつつ、 https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/
みたいに登録しておきます。
ただ、challengeってレスポンスを返す仕様である必要があるみたいなので、main.go
に追記をしておきます。(雑ですまんせん。)
type Challenge struct {
Challenge string `json:"challenge"`
}
...
r.POST("/", postChallenge)
...
func postChallenge(c *gin.Context) {
json := Challenge{}
err := c.BindJSON(&json)
if err != nil {
return
}
c.JSON(http.StatusAccepted, json)
}
あと、sam.yamlのEndpointも追加が必要でした。
Events:
GetResource:
Type: Api
Properties:
Path: /{proxy+}
Method: any
PostResource:
Type: Api
Properties:
Path: /
Method: post
そんなわけでエンドポイントは登録できました。
channelに投稿されたメッセージを拾いたいので message:channels
をsubscribeしています。
メッセージを拾う
messageはこんな感じみたいです。
typeに event_callback
って入ってきて、 event.typeが message
って入ってるのを拾えばいいっぽい?
if payload.Type == "url_verification" {
c.JSON(http.StatusOK, gin.H{"challenge": payload.Challenge, "message": "ok"})
} else if payload.Type == "event_callback" {
fmt.Println(payload.Event.Text)
fmt.Println(payload.Event.Type)
こんな感じでメッセージは拾えました。
あとはincoming webhookに投げるだけかな。
slackにレスポンスを返す。
最終的にこんな感じになりました。とりあえず動くだけのレベルのやつ。
/pets
は消しました。
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"strings"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin"
"github.com/gin-gonic/gin"
)
var ginLambda *ginadapter.GinLambda
type Event struct {
Type string `json:"type"`
Text string `json:"text"`
}
type Payload struct {
Challenge string `json:"challenge"`
Type string `json:"type"`
Event Event `json:"event"`
}
type ResponseBody struct {
Text string `json:"text"`
}
// Handler is the main entry point for Lambda. Receives a proxy request and
// returns a proxy response
func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
if ginLambda == nil {
// stdout and stderr are sent to AWS CloudWatch Logs
log.Printf("Gin cold start")
r := gin.Default()
r.POST("/", postChallenge)
ginLambda = ginadapter.New(r)
}
return ginLambda.ProxyWithContext(ctx, req)
}
func main() {
lambda.Start(Handler)
}
func postChallenge(c *gin.Context) {
payload := Payload{}
err := c.ShouldBindJSON(&payload)
if err != nil {
return
}
fmt.Println(payload.Type)
if payload.Type == "url_verification" {
c.JSON(http.StatusOK, gin.H{"challenge": payload.Challenge, "message": "ok"})
} else if payload.Type == "event_callback" && strings.Contains(payload.Event.Text, "Hello") {
fmt.Println(payload.Event.Text)
fmt.Println(payload.Event.Type)
webhookURL := "https://hooks.slack.com/services/xxxx/xxxx/xxxx"
p, err := json.Marshal(ResponseBody{Text: "ha?"})
if err != nil {
fmt.Println(err)
}
resp, err := http.PostForm(webhookURL, url.Values{"payload": {string(p)}})
if err != nil {
fmt.Println(err)
}
defer resp.Body.Close()
c.JSON(http.StatusOK, gin.H{"message": "ok"})
} else {
fmt.Println(payload.Type)
c.JSON(http.StatusOK, gin.H{"message": "ok"})
}
}
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Example Lambda Gin
Resources:
SampleFunction:
Type: AWS::Serverless::Function
Properties:
Handler: main
CodeUri: main.zip
Runtime: go1.x
MemorySize: 128
Policies: AWSLambdaBasicExecutionRole
Timeout: 3
Events:
PostResource:
Type: Api
Properties:
Path: /
Method: post
Outputs:
SampleGinApi:
Description: URL for application
Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/'
Export:
Name: SampleGinApi
一度、 strings.Contains(payload.Event.Text, "Hello")
とか絞り込みをやらないで無限ループとかやらかしたりはしましたが・・とりあえずの所まではやれました。
あとは自分なりにカスタマイズしていきましょう。
あとlocalでのテスト
% sam local start-api -t sam.yaml
Mounting SampleFunction at http://127.0.0.1:3000/ [POST]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2021-06-20 01:02:47 * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
Invoking main (go1.x)
Decompressing /xxx/sample/main.zip
Failed to download a new amazon/aws-sam-cli-emulation-image-go1.x:rapid-1.0.0 image. Invoking with the already downloaded image.
Mounting /private/var/folders/_f/nyppwytj6cb3_8ggkpwpqj1m0000gp/T/tmpcyochpbp as /var/task:ro,delegated inside runtime container
START RequestId: 270fc1a5-c05f-1d91-d42e-ff60ef331920 Version: $LATEST
Gin cold start
[GIN-debug] [WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST / --> main.postChallenge (3 handlers)
test
test
[GIN] 2021/06/19 - 16:02:55 | 200 | 129µs | | POST /
END RequestId: 270fc1a5-c05f-1d91-d42e-ff60ef331920
REPORT RequestId: 270fc1a5-c05f-1d91-d42e-ff60ef331920 Init Duration: 190.28 ms Duration: 7.55 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 24 MB
2021-06-20 01:02:55 127.0.0.1 - - [20/Jun/2021 01:02:55] "POST / HTTP/1.1" 200 -
% curl -XPOST localhost:3000 -H 'application/json' -d '{"type":"test"}'
{"message":"ok"}%