Skip to content

Instantly share code, notes, and snippets.

@catatsuy
Last active November 12, 2019 11:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save catatsuy/fac033905be76b2cd9e8727d51b58dfd to your computer and use it in GitHub Desktop.
Save catatsuy/fac033905be76b2cd9e8727d51b58dfd to your computer and use it in GitHub Desktop.

Google Cloud Functionsを使ってSlackのSlash Commandsを受け取る話とnotify_slackの話

See Tiny Spec Tokyo at Slack Platform Community Tokyo https://slackcommunity.com/events/details/slack-tokyo-presents-tiny-spec-tokyo/

2019/11/12 #TinySpec2019

自己紹介

  • @catatsuy
    • かたついって呼ばれています
  • メルカリ SRE
    • 主にGoを書いています
  • ISUCON9予選の運営で問題作成・ベンチマーカー実装などやりました
    • ISUCONはお題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトル
    • http://isucon.net/
  • 今回はSlackのAPIを利用して私が開発したものについて紹介します
    • 実装は全部Go

Google Cloud Functionsを使ってSlackのSlash Commandsを受け取る

Google Cloud Functionsを使ってSlackで簡単にCDN上のキャッシュを消せるようにする話 - Mercari Engineering Blog https://tech.mercari.com/entry/2019/09/20/110000

SlackでCDN上のキャッシュを削除している様子

詳しくはこちらのエントリーを読んでください。今回はSlackのAPIの使い方を中心に解説します。

SlackのSlash Commands

https://api.slack.com/interactivity/slash-commands

  • Slack上で/todoのようなコマンドを使えるようにできる
  • 登録したURLに決められた形式のPOSTのリクエストをSlack側が送ってくれる
  • 適切なレスポンスを返すとSlack上にメッセージを表示できる
  • コマンドが実行されたときだけリクエストを投げてくれるのでチャンネル上の会話が外部に漏れることがない
  • しかしリクエストを処理するアプリケーションを常に動かすのは面倒
    • そこでGoogle Cloud Functionsを使う方法がおすすめ

Google Cloud Functions

今回はGo言語を利用する前提で解説します。

  • http.HandlerFuncインターフェイスを満たす関数をそのままデプロイできる
    • デプロイはgcloudコマンドを実行するだけ
  • 専用のライブラリを読み込む必要は無く、一般的なGoのコードがそのまま動かせる
  • 秘匿情報などは環境変数で渡せる
  • デプロイに時間がかかる(数分程度)のとコンパイルできないコードはデプロイ中にエラーになる
    • デプロイ前にgo buildだけしてコンパイルができるかどうか確認してからデプロイするのがおすすめ
  • SlackのSlash Commandsに使うことはGoogle Cloud Functionsのドキュメント上の使用例でも紹介されている
package gcloudf

import (
	"net/http"

	"github.com/catatsuy/gcloudf_example/slackcmd"
)

// この関数がそのままデプロイできる
func ExampleEntryPoint(w http.ResponseWriter, r *http.Request) {
	slackcmd.ExampleCmd(w, r)
}
export GO111MODULE=on

.PHONY: check_for_deploy
check_for_deploy:
    go build gcloudf.go

.PHONY: deploy_slack
deploy_slack: check_for_deploy
    gcloud functions deploy <URLに含まれる名前> --project <プロジェクト名> --runtime go111 --entry-point ExampleCmd --trigger-http --region <リージョン名>
package slackcmd

import (
	"encoding/json"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
)

type Payload struct {
	ResponseType string `json:"response_type"`
	Text         string `json:"text"`
}

func ExampleCmd(w http.ResponseWriter, r *http.Request) {
	b, err := ioutil.ReadAll(r.Body)
	if err != nil {
		log.Fatal(err)
	}

	v, err := url.ParseQuery(string(b))
	if err != nil {
		log.Fatal(err)
	}

	channelID := v.Get("channel_id")

	// チャンネルIDで挙動を変えたい場合はここで色々する
	_ = channelID

	text := v.Get("text")

	// textを見てなにか処理をする
	_ = text

	payload := &Payload{
		// cf: https://api.slack.com/interactivity/slash-commands#responding_immediate_response
		// デフォルトの ephemeral は実行した本人にしか表示されない。 in_channel にすると全員に見える。
		ResponseType: "in_channel",
		Text:         "ここで色々メッセージを書く",
	}

	by, err := json.Marshal(payload)
	if err != nil {
		// Stackdriver Loggingでログを確認できる
		log.Fatal(err)
	}

	w.Header().Set("Content-Type", "application/json")
	w.Write(by)
}

notify_slack

https://github.com/catatsuy/notify_slack

Go言語製の自作ツール。現在GitHub上でStarが40。もし気に入ってもらえたらStarを押してもらえると嬉しいです!

demo

https://www.youtube.com/watch?v=wmKSr9Aoz-Y

設計思想

「とにかくシンプルに」「使うときに細かいことは意識させずに」「Slackに投稿するCLIを提供する」

機能

./deploy.sh | notify_slack
notify_slack README.md
  • 流れるログをいい感じにまとめて(デフォルトは1秒間分)Incoming WebHooksを使ってSlackに投稿
    • teeも内部で実装しているので画面への出力は止まらない
  • ファイル名を指定した場合はファイルの中身をsnippetとして投稿
  • トークンなどの秘匿情報は ~/.notify_slack.toml などに保存
    • (主にコンテナー向けに)環境変数で渡す機能もある

実装

  • slackパッケージにSlackのAPIを叩く実装がある
    • 使っているのはIncoming WebHooksとhttps://slack.com/api/files.upload
    • httptestを使ってSlackのAPIをMockしたサーバーを起動してテスト
    • slackパッケージ以外はslackに依存していないので他のチャットツールにも対応できる余地がある
  • throttleパッケージが一番複雑で、入力をバッファリングしてchannelが送られてきたらバッファをフラッシュしつつ指定された処理を実行する
    • 一応テストもあるので、そこを読めば分かるかもしれません

SlackのAPIクライアントのテスト例

Incoming WebHooksのクライアントのテストに説明用のコメントを追加

cf: https://github.com/catatsuy/notify_slack/blob/e07ed8c03219219e5c07ff372cf5b77240b08181/slack/client_test.go#L45-L93

func TestPostText_Success(t *testing.T) {
	// テスト用のHTTPサーバーを準備
	muxAPI := http.NewServeMux()
	testAPIServer := httptest.NewServer(muxAPI)
	defer testAPIServer.Close()

	// このparamを関数に渡すのでテスト用のサーバーに渡されているかテストする
	param := &PostTextParam{
		Channel:   "test-channel",
		Username:  "tester",
		Text:      "testtesttest",
		IconEmoji: ":rocket:",
	}

	// テスト用のHTTPサーバーにリクエストを送ると動く処理
	muxAPI.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		contentType := r.Header.Get("Content-Type")
		expectedType := "application/json"
		// 想定したContent-Typeでリクエストされているか
		if contentType != expectedType {
			t.Fatalf("Content-Type expected %s, but %s", expectedType, contentType)
		}

		// リクエストのbodyを読み込む
		bodyBytes, err := ioutil.ReadAll(r.Body)
		if err != nil {
			t.Fatal(err)
		}
		defer r.Body.Close()

		// リクエストのbodyは形式の決まったJSONなので構造体にUnmarshalする
		actualBody := &PostTextParam{}
		err = json.Unmarshal(bodyBytes, actualBody)
		if err != nil {
			t.Fatal(err)
		}

		// リクエストのbodyとparamが一致しているか
		if !reflect.DeepEqual(actualBody, param) {
			t.Fatalf("expected %q to equal %q", actualBody, param)
		}

		// SlackのAPIと同じレスポンスを返す
		http.ServeFile(w, r, "testdata/post_text_ok.html")
	})

	// テスト用のHTTPサーバーに向けてリクエストを飛ばすようにする
	c, err := NewClient(testAPIServer.URL, nil)
	if err != nil {
		t.Fatal(err)
	}

	// 実際にリクエストを飛ばしてエラーにならないか
	// 上のHTTPサーバーの処理は実際にはここで初めて実行される
	err = c.PostText(context.Background(), param)

	if err != nil {
		t.Fatal(err)
	}
}

実際の活用例

自分のチームのISUCONでの戦い方 - catatsuy - Medium https://medium.com/@catatsuy/自分のチームのisuconでの戦い方-c8fe121316aa

  • 元々ISUCONの時のチーム内の情報共有のために作った
  • デプロイスクリプトのログやアクセスログの集計結果やスロークエリの解析結果などを共有するために使用
  • もし使っている方がいればどう使っているのか是非教えてください

SlackのAPIについて困っていること

  • Incoming WebHooksの仕様変更で新しいIncoming WebHooksではチャンネルやアイコン変更ができなくなった
    • notify_slackは既に機能として変更する機能を提供しているが、READMEでどう表現するか、今後も機能として提供していくべきか悩み中
  • Slackのドキュメントにdeprecatedやlegacyという風に書かれている箇所が割とあり、結局どうするのが推奨なのか迷うことがある
  • notify_slackはsnippet投稿に必要なtokenとして以前はLegacy tokensを使っていたが、現在ではlegacy扱いに
    • Slack Appを自分で作らないといけないので少し煩雑になった
    • そもそもIncoming WebHooksみたいにお手軽にsnippetを投稿したい
    • アイコンなども簡単に変えたい
  • notify_slackはCLIとして機能を提供しているので、途中から非推奨になったり使えない機能ができたりすると対応がつらい
    • 使っている人が自分だけでは無いことに起因した悩みなので使ってくれている方には感謝しています
    • セキュリティ要件など色々あるのは理解しているので今後もよろしくお願いします
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment