LoginSignup
2
1

More than 1 year has passed since last update.

GoとGinとLambdaでSlack botを作成する

Last updated at Posted at 2021-06-19

社内で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を作ります。

  1. まだ作成していない場合には、Slack アプリを作成します。作成してある場合には、既存のアプリを選択してください。

スクリーンショット 2021-06-19 21.04.37.png
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はいらなかった

スクリーンショット 2021-06-19 21.28.46.png

incoming webhookの利用

まずは下記をやります。

  1. 「機能」ページで「Incoming Webhook をアクティブにする」をオンにします。
  2. アプリの投稿先となるチャンネルを選択して「許可する」をクリックします。

スクリーンショット 2021-06-19 21.28.46.png

スクリーンショット 2021-06-19 21.36.35.png

で、
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への投稿は確認できました。

スクリーンショット 2021-06-19 21.40.51.png

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 に追記をしておきます。(雑ですまんせん。)

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も追加が必要でした。

sam.yaml
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /{proxy+}
            Method: any
        PostResource:
          Type: Api
          Properties:
            Path: /
            Method: post

そんなわけでエンドポイントは登録できました。
channelに投稿されたメッセージを拾いたいので message:channels をsubscribeしています。

スクリーンショット 2021-06-19 22.49.36.png

メッセージを拾う

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)

こんな感じでメッセージは拾えました。

スクリーンショット 2021-06-20 0.45.10.png

あとはincoming webhookに投げるだけかな。

slackにレスポンスを返す。

最終的にこんな感じになりました。とりあえず動くだけのレベルのやつ。
/pets は消しました。

main.go
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"})
    }
}
sam.yaml
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

スクリーンショット 2021-06-20 0.41.58.png

一度、 strings.Contains(payload.Event.Text, "Hello") とか絞り込みをやらないで無限ループとかやらかしたりはしましたが・・とりあえずの所まではやれました。
あとは自分なりにカスタマイズしていきましょう。

あとlocalでのテスト

term1
% 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 -
term2
% curl -XPOST localhost:3000 -H 'application/json' -d '{"type":"test"}'
{"message":"ok"}%           
2
1
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
2
1