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で簡単にCDN上のキャッシュを消せるようにする話 - Mercari Engineering Blog https://tech.mercari.com/entry/2019/09/20/110000
詳しくはこちらのエントリーを読んでください。今回はSlackのAPIの使い方を中心に解説します。
https://api.slack.com/interactivity/slash-commands
- Slack上で
/todo
のようなコマンドを使えるようにできる - 登録したURLに決められた形式のPOSTのリクエストをSlack側が送ってくれる
- 適切なレスポンスを返すとSlack上にメッセージを表示できる
- コマンドが実行されたときだけリクエストを投げてくれるのでチャンネル上の会話が外部に漏れることがない
- しかしリクエストを処理するアプリケーションを常に動かすのは面倒
- そこでGoogle Cloud Functionsを使う方法がおすすめ
- クイックスタート: gcloud コマンドライン ツールの使用 | Cloud Functions のドキュメント | Google Cloud https://cloud.google.com/functions/docs/quickstart
- Go ランタイム | Cloud Functions のドキュメント | Google Cloud https://cloud.google.com/functions/docs/concepts/go-runtime
今回はGo言語を利用する前提で解説します。
http.HandlerFunc
インターフェイスを満たす関数をそのままデプロイできる- デプロイはgcloudコマンドを実行するだけ
- 専用のライブラリを読み込む必要は無く、一般的なGoのコードがそのまま動かせる
- 秘匿情報などは環境変数で渡せる
- Using Environment Variables | Cloud Functions Documentation | Google Cloud https://cloud.google.com/functions/docs/env-var
- デプロイに時間がかかる(数分程度)のとコンパイルできないコードはデプロイ中にエラーになる
- デプロイ前にgo buildだけしてコンパイルができるかどうか確認してからデプロイするのがおすすめ
- SlackのSlash Commandsに使うことはGoogle Cloud Functionsのドキュメント上の使用例でも紹介されている
- https://cloud.google.com/functions/docs/concepts/overview#use_cases
- Node.jsでの開発例は公式チュートリアルにある
- Slack のチュートリアル - Slash コマンド | Cloud Functions | Google Cloud https://cloud.google.com/functions/docs/tutorials/slack
- 実際めっちゃ簡単でメンテナンス不要なのでおすすめです
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)
}
https://github.com/catatsuy/notify_slack
Go言語製の自作ツール。現在GitHub上でStarが40。もし気に入ってもらえたらStarを押してもらえると嬉しいです!
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に依存していないので他のチャットツールにも対応できる余地がある
- 使っているのはIncoming WebHooksと
- throttleパッケージが一番複雑で、入力をバッファリングしてchannelが送られてきたらバッファをフラッシュしつつ指定された処理を実行する
- 一応テストもあるので、そこを読めば分かるかもしれません
Incoming WebHooksのクライアントのテストに説明用のコメントを追加
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の時のチーム内の情報共有のために作った
- デプロイスクリプトのログやアクセスログの集計結果やスロークエリの解析結果などを共有するために使用
- もし使っている方がいればどう使っているのか是非教えてください
- 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として機能を提供しているので、途中から非推奨になったり使えない機能ができたりすると対応がつらい
- 使っている人が自分だけでは無いことに起因した悩みなので使ってくれている方には感謝しています
- セキュリティ要件など色々あるのは理解しているので今後もよろしくお願いします