API Gateway->Lambdaを経てSlackのWebhookURLへ送信する-その2-

投稿日:

こんにちは。
…さて、日が経ってしまいましたが、前回 の続きでAPI GatewayからSlack通知を行った作業ログを残していきたいと思います。先週のうちに書いておく予定だった!!

前回の記事

API Gateway->Lambdaを経てSlackのWebhookURLへ送信する-その1- – Shimabox Blog

やったこと

  • API Gatewayでエンドポイントを作成 ← 今回の対象
  • API GatewayのトリガーとしてLambdaを作成 ← 今回の対象
  • このLambdaからSlackのWebhookURLに送信 ← 今回の対象
    • Slack AppのIncoming Webhooksを利用 ← 前回の記事参照

直接、WebhookURLにcurlで送ればいいじゃん!!と思うかもしれませんが、まぁ、あれです、素振りってやつです。大事なことなので前回に引き続き2回。

じゃあやってみましょう!今回はプログラミングしていきますよ!

免責

今回はAWSでの作業もありますが、お試し実装ということもありPowerUserAccess,IAMFullAccessで作っているので、そこらへんは察してください。
本来であれば権限を絞って設定するのがいいです。
APIGatewayとLambdaの使用なので、そんなにお金はかからないと思いますが、無茶はやめておきましょう。

AWSでの作業

ユーザーを用意

とりあえず作業用ユーザー、ユーザーグループを作成します。
上でも述べましたが、ユーザーグループにはPowerUserAccess, IAMFullAccessを付与しています。

作業用ユーザー(api-gateway-userとしました)は、このユーザーグループに追加しておきます。

以降、このユーザーで作業をしていきます。

ロールを作成

同じく、IAMからロールの作成より以下のロールを作成します。
Lambdaを作るときに、お任せするのもありですが名前をつけておきます。

  • ロール名
    • 任意
    • ここでは、api-gateway-to-lambda-roleとしています
  • 許可ポリシー
    • AWSLambdaBasicExecutionRole

Lambdaの作成

APIGatewayに紐付けるLambdaを作成します。

関数作成

  1. Lambda -> 関数の作成 を開きます
  2. 以下を入力し、関数の作成 をクリックします
    1. 関数の作成は、一から作成を選択
    2. 関数名
      1. 任意の関数名を入力
      2. ここでは、notification-to-slackとしました
    3. ランタイム
      1. Go 1.xを選択
    4. アーキテクチャ
      1. x86_64を選択
    5. デフォルトの実行ロールの変更
      1. 既存のロールを使用するを選択し、ロールを作成で作成したロール(api-gateway-to-lambda-role)を選択

ハンドラ名修正

※ ビルド時に、バイナリ名をhelloにして利用するのならこの作業は要りません

ランタイム設定の編集から、

ハンドラ名を main に変えて保存します。

環境変数の設定

環境変数の編集より、

以下の環境変数を設定して、保存します。

  • キー
    • SLACK_WEBHOOK_URL
    • Incoming Webhooksで発行したURL
    • 前回の記事 を参考にしてみてください

いったんここで、Lambdaの作成は止めておきます。あとでソースを書きます。

APIGatewayの作成

構築

API Gateway -> APIを作成 を開き、APIタイプを選択より、HTTP APIを選択し、構築をクリックします。

REST API使ったほうがいいのでは?と思うかもしれませんが、パッと試すためHTTP APIを利用します。

APIの作成

以下を入力し、次へをクリックします。

  • 統合を追加
    • Lambda を選択
  • Lambda関数
    • Lambda関数の作成で作成したLambda関数(notification-to-slack)を選択
  • API名
    • 任意の名前を入力
    • ここでは、notification-to-slack-apiとしました

ルートを設定

メソッドをPOSTにして、次へをクリックします。

ステージを定義

お試しなのでステージ名そのまま、自動デプロイを有効にしたまま、次へをクリックします。
開発用、本番用など分けたい場合は、自動デプロイは外して名前をつけておくといいと思います。

確認して作成

確認をして、作成をクリックします。

これで、APIGatewayの作成は完了です。

Lambdaの確認

作成したLambdaに戻り、設定 -> トリガー を確認してみると、上記で作成したAPIGatewayと紐づいていることがわかると思います。エンドポイントの確認も出来ますね。

Lambda関数の作成

いよいよ、Lambda関数の作成を行います。Goで書きます。

Goのバージョンは、1.20.4です。

go version
go version go1.20.4 darwin/amd64

前準備

適当にディレクトリを作って、

mkdir notification-to-slack
cd notification-to-slack

go mod init しておきます。

go mod init notification-to-slack
go: creating new go.mod: module notification-to-slack

Go の AWS Lambda 関数ハンドラー – AWS Lambda に則り、github.com/aws/aws-lambda-go/lambdaをインストールしておきます。

go get github.com/aws/aws-lambda-go/lambda
go: downloading github.com/aws/aws-lambda-go v1.41.0
go: added github.com/aws/aws-lambda-go v1.41.0

events package – github.com/aws/aws-lambda-go/events – Go Packages

コード

以下のjsonがPOSTされることを想定したコードを書きます。

{
  "name": "shimabox",
  "favorite_foods": [
    "ゴーヤーチャンプル",
    "マンゴー"
  ]
}

main.go

main.go を作成します。

package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

type myEvent struct {
	Name          string `json:"name"`
	FavoriteFoods list   `json:"favorite_foods"`
}

type list []string

type requestBody struct {
	Blocks []block `json:"blocks"`
}

type block struct {
	Type string   `json:"type"`
	Text textType `json:"text"`
}

type textType struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

func handler(r events.APIGatewayProxyResponse) error {
	// BodyにPOSTデータが入っているので構造体に変換
	var event myEvent
	err := json.Unmarshal([]byte(r.Body), &event)
	if err != nil {
		return err
	}

	// Slackへ送信するリクエストボディを作成
	body := &requestBody{
		Blocks: []block{
			{
				Type: "section",
				Text: textType{
					Type: "mrkdwn",
					Text: fmt.Sprintf("こんにちは `%v` さん:smile:", event.Name),
				},
			},
			{
				Type: "section",
				Text: textType{
					Type: "plain_text",
					Text: "そうですか、",
				},
			},
			{
				Type: "section",
				Text: textType{
					Type: "plain_text",
					Text: toListNotation(event.FavoriteFoods),
				},
			},
			{
				Type: "section",
				Text: textType{
					Type: "plain_text",
					Text: "が、好きなんですね!",
				},
			},
		},
	}
	bodyBytes, err := json.Marshal(body)
	if err != nil {
		return err
	}

	// 環境変数からWebhook用エンドポイントを取得
	endpoint := os.Getenv("SLACK_WEBHOOK_URL")

	// リクエストを生成
	req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(bodyBytes))
	if err != nil {
		return err
	}

	// Headerつけてリクエストを投げる
	req.Header.Add("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	// Slackからのレスポンス判定
	if res.StatusCode != http.StatusOK {
		b, err := io.ReadAll(res.Body)
		if err != nil {
			return err
		}
		return errors.New(fmt.Sprintf("error!! code: %d, body: %v", res.StatusCode, string(b)))
	}

	return nil
}

func toListNotation(list list) string {
	l := len(list)
	values := make([]string, 0, l)
	for i := 0; i < l; i++ {
		values = append(
			values,
			fmt.Sprintf(`• %v`, list[i]),
		)
	}
	return strings.Join(values, "\n")
}

func main() {
	lambda.Start(handler)
}

返り値で、(events.APIGatewayProxyResponse error)とすることもできますが、今回はSlackに送信するだけなので単純にerrorを返すだけにしています。

余談

リストのところは、マークダウンの-でリスト表記出来るかと思いきやまさかのを書くという力技…
Block Kit Builder – Slack

ガチでやりたい場合は、以下の形式でSlackに投げるとインデントを指定してリスト表記できるようです。

{
  "blocks": [
    {
      "type": "rich_text",
      "elements": [
        {
          "type": "rich_text_list",
          "elements": [
            {
              "type": "rich_text_section",
              "elements": [
                {
                  "type": "text",
                  "text": "test"
                }
              ]
            }
          ],
          "style": "bullet",
          "indent": 0
        },
        {
          "type": "rich_text_list",
          "elements": [
            {
              "type": "rich_text_section",
              "elements": [
                {
                  "type": "text",
                  "text": "test2"
                }
              ]
            }
          ],
          "style": "bullet",
          "indent": 1
        }
      ]
    }
  ]
}

参考: kotlin – How do I post a bulleted list using the slack api – Stack Overflow

Makefile

Makefileを用意してビルドをちょっと楽にしておきます。

.PHONY: build

build:
    CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main main.go
    zip main.zip main

構成

構成はこんな感じです。

tree
.
├── Makefile
├── go.mod
├── go.sum
└── main.go

これでLambda関数の作成は完了です。

デプロイ

ビルド

goをビルドしてzipを作ります。

make build

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main main.go
zip main.zip main
  adding: main (deflated 45%)

デプロイ

zipが出来たので、Lambdaを開いてコードアップロード元から.zipファイルを選択します。

アップロードから、zipファイルを選択し保存をクリックします。

関数 notification-to-slack が正常に更新されました。と表示されればOKです。

動作確認

curlでAPIGatewayを叩く

準備が出来たのでコマンドを叩きます。

curl -X POST -H 'Content-type: application/json' \
--data '
{
  "name": "shimabox",
  "favorite_foods": [
    "ゴーヤーチャンプル",
    "マンゴー"
  ]
}' \
https://1234xxxxxx.execute-api.ap-northeast-1.amazonaws.com/notification-to-slack // APIGatewayのエンドポイント

null が返却されれば成功です。
{"message":"Internal Server Error"}みたいなのが返ってきたらログ(CloudWatch)を確認してみてください。

CloudWatch

CloudWatchは以下で確認できると思います。

CloudWatch > ロググループ > /aws/lambda/notification-to-slack(Lambda名)

結果

slackに以下のように投稿されればOKです!

おわりに

というわけで、APIGatewayからLambdaを経由してSlackに投稿するまでを試してみました。
手順はちょっと多くて面倒かもです(AWSのところはCDKとかSAMとか使えばマシになるとは思います)が、1回用意してしまえば色々とお試し出来るのかなぁと思います!
(ちゃんと組むなら、VPCで制限とかIP制限とかしたほうがいいかなぁと思います。そのへん今度やってみよ〜。)

作成者: shimabox

Web系のプログラマをやっています。 なるべく楽しく生きていきたい。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください