SREã®@deeeet ã§ãã
Mercariã§ã¯Slack Botã使ãæ§ã ãªæ¥åã®èªååãè¡ã£ã¦ãã¾ããä¾ãã°ã¡ã¤ã³ã®APIã®Releaseã¯Botã«ããèªååãããã¦ãããJPã¨USã¨UKã®3æ ç¹ã§1æ¥ã«10å以ä¸ã®ReleaseãSlackä¸ã§å®ç¾ãã¦ãã¾ãï¼ãã以å¤ã«ãå¤ãã®äºä¾ãããã¾ãï¼ã
ããã¾ã§ã®Slack Botã¯åºæ¬çã«ã¯æåãã¼ã¹ã§ã®ããåããæ®éã§ããï¼ã°ã©ããªã©ã®ç»åãè¿çã¨ãã¦å©ç¨ãããã¨ã¯ããã¾ãï¼ããSlackã¯ããInteractiveãªããã¨ããå®ç¾ã§ããInteractive Messageã¨ããä»çµã¿ãæä¾ãã¦ãã¾ããããã«ããButtonã«ãã決å®ãMenuã«ããé¸æã¨ãã£ãã¢ã¯ã·ã§ã³ãã¦ã¼ã¶ã«ã¨ããããã¨ãã§ããããã«ãªãã¾ãã
Buttonã®ä»çµã¿èªä½ã¯å¤ãããæä¾ããã¦ãã¾ãããä»ã®Teamã¸ã®é å¸ãåæã§ããOAuthã®ä»çµã¿ãæºåããå¿ è¦ããããªã©å©ç¨ãããã¼ãã«ã¯ä½ãã¯ããã¾ããã§ããï¼Third-party Serviceå°ç¨ã¨ããè²åããå¼·ãã£ãããã«æãã¾ãï¼ããããæè¿Internal Integrationã¨ããä»çµã¿ãæä¾ããTeamå ã«éå®ãããå ´åã«Interactive Messageã使ã£ãBotã®éçºãããç°¡åã«è¡ããããã«ãªãã¾ããã
Mercariã§ã¯æ©éãã®ä»çµãå©ç¨ããBotãéçºã使ãå§ãã¦ãã¾ããæ¬è¨äºã§ã¯Golangã使ãInteractive Messageã使ã£ãBotã®éçºãæ¸ãæ¹æ³ãç´¹ä»ãã¾ãããªããµã³ãã«ã³ã¼ãã¯å ¨ã¦ tcnksm/go-slack-interactive ã«å ¬éãã¦ããã®ã§èªç±ã«forkãã¦èªåãªãã®Botãéçºãã¦ã¿ã¦ä¸ããï¼Nodeã使ãããå ´å㯠slackapi/sample-message-menus-nodeãåèã«ãªãã§ãããï¼ã
Interactive Messageã®å©ç¹
ããããInteractive MessageãBotã§ä½¿ãå©ç¹ã¯ãªãã§ãããã? Buttonãæ¼ãã®ãæ°æã¡ã
Buttonã使ãã°ããå¦çã®å®è¡ã®ç¢ºèªã1ã¤ã®Buttonã«éç´ãããã¨ãã§ãã¾ããéè¦ãªå¦çã®å ´åã¯ãã®Buttonãæ¼ãã人ãéå®ãããã¨ã§ç°¡åãªæ¿èªããã¼ã®å®ç¾ãã§ãã¾ããMenuã使ãã°Botå´ããé¸æè¢ãæ示ãããã¨ãã§ããã¦ã¼ã¶ã«èªç±ã«å ¥åãããããããããå®å ¨ã«æå¾ ããå ¥åãåãä»ãããã¨ãã§ãã¾ãï¼éçºå´ããããã°å ¥åã®Validationã楽ã«ãªãã¾ãï¼ãã¾ãï¼ç¹ã«Buttonã¯ï¼Mobile Appããã®æä½ãé常ã«æ¥½ã§ãã
Botãä½ãã¢ããã¼ã·ã§ã³ã¨ãã¦Channelã«åå ãã¦ããã¡ã³ãã¼ã¨ã®ã³ã©ãã¬ã¼ã·ã§ã³ãæãããã¾ãããã¨ã³ã¸ãã¢ä»¥å¤ã®ä»²éã¸ãã®ãã¼ã«ã®å©ç¨ã®çªå£ãåºãããã¨ãéè¦ãªè¦ç´ ã ã¨æãã¾ãï¼ã¨ã³ã¸ãã¢ã ããªãCLIãã¼ã«ã ãã§ååãªãã¨ãå¤ãï¼ãInteractive Messageã使ããã¨ã§ã¨ã³ã¸ãã¢ä»¥å¤ã®ã¡ã³ãã¼ã«ããç´æçãªä½¿ããããã¤ã³ã¿ã¼ãã§ã¼ã¹ãæä¾ã§ãã¾ãã
Mercariã§ã®äºä¾
Mercariã§ã¯ä»¥ä¸ã®ãããªBotã§Interactive Messageãå©ç¨ãã¦ãã¾ãã
Kubernetes Deploy Bot
Mercari USã§ã¯ä¸é¨ã®ãµã¼ãã¹ãKubernetesï¼GKEï¼ã§åããã¦ãã¾ãï¼æ°ããä½ããã®ã¯åºæ¬çã«Kubernetesåæã§ãï¼ãKubernetes Deploy Botã¯æ°ããä½æãããDocker ImageãClusterã«Deployãã¾ããã¦ã¼ã¶ãBotã«Deployãä¾é ¼ããã¨Deployå¯è½ãªã¤ã¡ã¼ã¸ä¸è¦§ï¼å ·ä½çã«ã¯Tagï¼ãMenuã§æ示ããé¸æããããã®ãClusterä¸ã«å±éãã¾ãã
ã¡ãªã¿ã«ãã®Botã¯Kubernetesã®Manifestã®Docker Imageã®Versionãæ¸ãæãã¦PRãä½ãã¨ããã¾ã§ããããã¬ãã¸ããªã®è¨å®ãã¡ã¤ã«ã¨æ¬çªã§åãã¦ããImageã®Versionã«ä¹é¢ãçºçããªãããã«ãªã£ã¦ãã¾ãã
Account Bot
ç¹å®ã®ãµã¼ãã¼ã¸ã¢ã¯ã»ã¹ããããã®ã¢ã«ã¦ã³ããVPNã®ã¢ã«ã¦ã³ãä½æã¯SREãæ ã£ã¦ãã¾ããæã¯SREããµã¼ãã¼ã«ãã°ã¤ã³ãæåã§è¡ã£ã¦ãã¾ããããç¾å¨ã¯Botã§èªååããã¦ãã¾ããã¦ã¼ã¶ãBotã«å¯¾ãã¦ã¢ã«ã¦ã³ãä½æã®ä¾é ¼ãè¡ãã¨BotãSREã«å¯¾ãã¦Buttonã§æ¿èªãæ±ãã¾ããSREã®æ¿èªãåãã¦Botã¯ã¢ã«ã¦ã³ãã®ä½æãéå§ãã¾ããããã«ããèªååã¯ããã¦ãã¾ãã誰ã§ã好ãåæã«ã¢ã«ã¦ã³ããçºè¡ãããã¨ãé²ãã§ãã¾ãã
ã¡ãªã¿ã«ãã®Botã¯GAEä¸ã§åãã¦ããã¢ã«ã¦ã³ãä½æã®ä¾é ¼ãåããã¨JobãGCP Cloud PubSubã«æãã¾ããã¢ã«ã¦ã³ããå®éã«ä½æããããã®Workerã¯JPã¨USã¨UKã®3Regionã§åãã¦ããããããé©åãªTopicãSubscribeãã¦å®éã®å¦çãè¡ãã¾ãã
ä½æããBotã®ä¾
以ä¸ã§ã¯Interactive Messageã使ã£ãBotã®ä½ãæ¹ã¨ãµã³ãã«ã³ã¼ãã®è§£èª¬ãè¡ãã¾ãã
ä»åã¯ä¾ã¨ãã¦ãã¼ã«ã注æãã@beerbot
ãä½æãã¾ãï¼å®éã«ã¯æ³¨æã¯ãã¾ãããã¤ã³ã¿ã¼ãã§ã¼ã¹ã®ã¿ãä½æãã¾ãï¼ãBotã«è©±ããããã¨Botã¯Menuã使ã£ã¦æ³¨æå¯è½ãªãã¼ã«ã®éæä¸è¦§ãæ示ãã¦ã¦ã¼ã¶ã«é¸æããã¾ããããã¦Buttonã使ã£ã¦æ³¨æã確å®ããããã¯ãã£ã³ã»ã«ããããã¨ãã§ããããã«ãã¾ãã以ä¸ãä»åä½æããBotã®åä½ä¾ã§ãã
Slack Appã®æºå
ã§ã¯å®éã«Botãä½æãã¦ããã¾ããInteractive Messageã使ã£ãBotã¯Slack Appã¨ãã¦ä½æããå¿ è¦ãããã¾ããã¾ãã¯ããããæ°ããAppãä½æãã¾ãã
次ã«Featuresã®é ç®ããBot userã追å ãInteractive Messageãæå¹ã«ãã¾ããããã§å°ç¨ã®Request URLãå¿ è¦ã«ãªãã¾ããã¦ã¼ã¶ã®Interactive Messageã«å¯¾ããã¢ã¯ã·ã§ã³ã®çµæã¯ãã®URLã«POSTããããã¨ã«ãªãã¾ãã
æå¾ã«ä½æããSlack AppãTeamã«ã¤ã³ã¹ãã¼ã«ãã¾ããããã«ããBotãSlack APIã«ã¢ã¯ã»ã¹ããããã®Bot User OAuth Access Tokenã¨Interactive Messageã®ãªã¯ã¨ã¹ãããµã¼ãã¼å´ã§Verifyããããã®Verification Tokenãå¾ããã¾ãããããã®å¤ã¯Botãåããã®ã«å¿ è¦ã«ãªãã¾ããããã§æºåã¯å®äºã§ãã
Golangã«ããBotã®éçº
ã§ã¯å®éã«Botãæ¸ãã¦ããã¾ããBotã«å¿ è¦ãªã®ã¯ä»¥ä¸ã®2ã¤ã§ãã
- Slackã®EventãWatchãã¦é©åãªEventã«åå¿ãããã¨ï¼Slack Event Listenerï¼
- ã¦ã¼ã¶ã®Interactive Messageã¸ã®ã¢ã¯ã·ã§ã³ã®çµæãåãä»ãããã¨ï¼Interactive Handlerï¼
Slack Event Listener
ã¾ãã¯Slackã®Event Listenerãæ¸ãã¾ããä»åã®ä¾ã§ã¯ã@beerbot heyãã¨ããçºè¨ï¼Message Eventï¼ã«åå¿ãã¦Menuãã¦ã¼ã¶ã«æ示ããããã«ãã¾ããSlackã®API Clientã«ã¯nlopes/slack
packageãå©ç¨ãã¾ãã以ä¸ã®ãããªListenerãæ¸ãã¦EventãWatchãä»åå¿
è¦ãªMessageEvent
ãå¾
ã¡åãã¾ãã
func (s *SlackListener) ListenAndResponse() { // Start listening slack events rtm := s.client.NewRTM() go rtm.ManageConnection() // Handle slack events for msg := range rtm.IncomingEvents { switch ev := msg.Data.(type) { case *slack.MessageEvent: if err := s.handleMessageEvent(ev); err != nil { log.Printf("[ERROR] Failed to handle message: %s", err) } } } }
次ã«MessageEvent
ãå¦çãã以ä¸ã®ãããªé¢æ°ãæ¸ãã¾ãã
func (s *SlackListener) handleMessageEvent(ev *slack.MessageEvent) error
é¢æ°å
ã§ã¯ã¾ãMessageEvent
ã®Validateãè¡ãã¾ããæä½é以ä¸ãå¿
è¦ã§ãããã
- æå¾ ããChannelããã®Messageã§ãããï¼botã«ãã£ã¦ã¯åå¿ããã¹ãChannelãéå®ããã¹ãã§ããä¾ãã°Releaseãè¡ãBotã¯Release Channelã®ã¿ã§åä½ããã¹ãã§ãï¼
- Botã«å¯¾ããã¡ã³ã·ã§ã³ã§ããã
次ã«å®éã®çºè¨å
容ï¼MessageEvent.Msg.Text
ï¼ãParseãã¾ãããã®è¾ºãã¯CLIãã¼ã«ãæ¸ãã¨ãã¨åãããã«èãããã¨ãã§ãã¾ããããã§ã®ã³ãã³ãã®Parseçµæã«ãã以ä¸ã®Menuã®è¡¨ç¤ºãªã©ãå¤æ´ãã¾ãï¼ä»åã®ä¾ã¯ç°¡åã®ããã«åã«ãheyãã¨ããçºè¨ã«åå¿ããã®ã¿ã§ãï¼ã
次ã«å®éã«Menuã®è¡¨ç¤ºãè¡ãã¾ããMenuã®è¡¨ç¤ºã«ã¯Slack Post Message APIã®Attachment
ãã£ã¼ã«ããå©ç¨ãã¾ããGolangã§æ¸ããå ´åã¯ä»¥ä¸ã®ããã«ãªãã¾ãã
var attachment = slack.Attachment{ Text: "Which beer do you want? :beer:", Color: "#f9a41b", CallbackID: "beer", Actions: []slack.AttachmentAction{ { Name: actionSelect, Type: "select", Options: []slack.AttachmentActionOption{ { Text: "Asahi Super Dry", Value: "Asahi Super Dry", }, { Text: "Kirin Lager Beer", Value: "Kirin Lager Beer", }, { Text: "Sapporo Black Label", Value: "Sapporo Black Label", }, { Text: "Suntory Malt's", Value: "Suntory Malts", }, { Text: "Yona Yona Ale", Value: "Yona Yona Ale", }, }, }, { Name: actionCancel, Text: "Cancel", Type: "button", Style: "danger", }, }, }
ã¾ãAttachmentAction.Type
ã«ããselect
ï¼Menuï¼ãbutton
ãæå®ãã¾ããAttachmentActionOption
ã®Sliceããã®ã¾ã¾Menuã®é¸æè¢ã¨ãã¦è¡¨ç¤ºããã¾ãï¼ä»åã¯ãã¼ã«ã®éæã表示ãã¾ãï¼ããã¨ã¯ãããSlackã®Post APIã§EventãåããChannelã«æ稿ããã ãã§ããããã¨ä»¥ä¸ã®ãããªMenuã表示ãããã¨ãã§ãã¾ãã
Interactive Handler
次ã«ã¦ã¼ã¶ã®Interactive Messageã«å¯¾ããã¢ã¯ã·ã§ã³ã®çµæãåãåãHandlerãæ¸ãã¾ããä¾ãã°ä»¥ä¸ã®ãããªHandlerãæ¸ãã¾ãã
func (h interactionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { log.Printf("[ERROR] Invalid method: %s", r.Method) w.WriteHeader(http.StatusMethodNotAllowed) return } buf, err := ioutil.ReadAll(r.Body) if err != nil { log.Printf("[ERROR] Failed to read request body: %s", err) w.WriteHeader(http.StatusInternalServerError) return } jsonStr, err := url.QueryUnescape(string(buf)[8:]) if err != nil { log.Printf("[ERROR] Failed to unespace request body: %s", err) w.WriteHeader(http.StatusInternalServerError) return } var message slack.AttachmentActionCallback if err := json.Unmarshal([]byte(jsonStr), &message); err != nil { log.Printf("[ERROR] Failed to decode json message from slack: %s", jsonStr) w.WriteHeader(http.StatusInternalServerError) return } // Only accept message from slack with valid token if message.Token != h.verificationToken { log.Printf("[ERROR] Invalid token: %s", message.Token) w.WriteHeader(http.StatusUnauthorized) return } .... }
ä¸ã®ä¾ã¯Handlerã®ååã®å¦çã§ãï¼ããã¯ã©ã®Interactive Messageã§ãå ±éã«ãªãã¨æãã¾ãï¼ã以ä¸ã®ãããªå¦çãè¡ã£ã¦ãã¾ãã
POST
ã§ããããå¤å¥ãã- PayloadãUnescapeãã
- UnescapeããPayloadã
slack.AttachmentActionCallback
ã«Unmarshalãã - å¾ãããã¡ãã»ã¼ã¸ã®TokenãSlack Appãç»é²ããéã«çºè¡ãããVerification Tokenã¨ä¸è´ãããã確èªãã
ãã¨ã¯å¾ãããã¦ã¼ã¶ã®Actionã®çµæãå¦çããã ãã§ãããã®é¨åã¯ä»¥ä¸ã®ããã«æ¸ãã¾ãã
func (h interactionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { .... action := message.Actions[0] switch action.Name { case actionSelect: value := action.SelectedOptions[0].Value // Overwrite original drop down message. originalMessage := message.OriginalMessage originalMessage.Attachments[0].Text = fmt.Sprintf("OK to order %s ?", strings.Title(value)) originalMessage.Attachments[0].Actions = []slack.AttachmentAction{ { Name: actionStart, Text: "Yes", Type: "button", Value: "start", Style: "primary", }, { Name: actionCancel, Text: "No", Type: "button", Style: "danger", }, } w.Header().Add("Content-type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(&originalMessage) return case actionStart: title := ":ok: your order was submitted! yay!" responseMessage(w, message.OriginalMessage, title, "") return case actionCancel: title := fmt.Sprintf(":x: @%s canceled the request", message.User.Name) responseMessage(w, message.OriginalMessage, title, "") return default: log.Printf("[ERROR] ]Invalid action was submitted: %s", action.Name) w.WriteHeader(http.StatusInternalServerError) return } }
ããã§ã¯å¾ãããã¢ã¯ã·ã§ã³ã«ããããããå¦çãè¡ãã¾ããã¢ã¯ã·ã§ã³ã®å¤å¥ã¯AttachmentAction.Name
ã§å®ç¾©ãããã®ãå©ç¨ãã¾ããä»åã®ä¾ã§ã¯ä»¥ä¸ã®3ã¤ã®ã¢ã¯ã·ã§ã³ãå®ç¾©ãã¦ãã¾ãã
actionSelect
– Menuã®é¸æã®ã¢ã¯ã·ã§ã³actionStart
– 注æã確å®ããButtonã®ã¢ã¯ã·ã§ã³actionCancel
– 注æããã£ã³ã»ã«ããButtonã®ã¢ã¯ã·ã§ã³
ä¾ãã°actionSelect
ã®å ´åã¯ã¦ã¼ã¶ã®é¸æï¼ãã¼ã«ã®éæï¼ãåãåºãå®éã«æ³¨æãè¡ã£ã¦ãããã®ç¢ºèªãåã³Buttonã使ã£ã¦è¡ã£ã¦ãã¾ããactionCancel
ã®å ´åã¯æ³¨æããã£ã³ã»ã«ãããæ¨ãã¬ã¹ãã³ã¹ã¨ãã¦è¿çãã¦ãã¾ãã
Interactive Messageã§ã¯ã¦ã¼ã¶ã®ã¢ã¯ã·ã§ã³ã«å¯¾ããè¿çã¯æ¢ã«Postããã¦ããOriginalMessage
ã®ã¡ãã»ã¼ã¸ãä¸æ¸ãããããã«è¿çãè¡ãã¾ããããã¯ä¾ãã°Buttonãæ¼ãã¦ãButtonããã®ã¾ã¾è¡¨ç¤ºããã¦ãããæ°¸é ã«Buttonãæ¼ãããã¨ã«ãªãã®ãé²ãããã§ãããããè¡ãããã«ã¯ä»¥ä¸ã®ãããªé¢æ°ãæºåãã¦ããã¨ä¾¿å©ã§ããButtonãªã©ã®AttachmentAction
ãæ¶ãã¦æ°ããªã¡ãã»ã¼ã¸ã§ä¸æ¸ãããã¬ã¹ãã³ã¹ãè¿ãäºãã§ãã¾ãã
func responseMessage(w http.ResponseWriter, original slack.Message, title, value string) { original.Attachments[0].Actions = []slack.AttachmentAction{} // empty buttons original.Attachments[0].Fields = []slack.AttachmentField{ { Title: title, Value: value, Short: false, }, } w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(&original) }
ã¢ã¯ã·ã§ã³ã®ã¬ã¹ãã³ã¹ã«ã¯å¿ ã誰ããã®ã¢ã¯ã·ã§ã³ãè¡ã£ãããæ示ããã®ã大åã§ããããããªãã¨èª°ããã®ã¢ã¯ã·ã§ã³ãè¡ã£ãããå¾ã§è¿½ããªããªãããã§ããä»åã®Botã§ã¯ä»¥ä¸ã®ããã«èª°ãButtonãæ¼ããããåããããã«ãªã£ã¦ãã¾ãï¼Buttonãªã©ã使ã£ãInteractionã®ãã¶ã¤ã³ã«é¢ãã¦ã¯Guidelines for building messagesãåèã«ãªãã¾ãï¼ã
ã¾ã¨ã
æ¬è¨äºã§ã¯Golangã使ãInteractive Messageã使ã£ãBotã®éçºãæ¸ãæ¹æ³ã«ã¤ãã¦ç´¹ä»ãã¾ãããInteractive Messageã使ããã使ããããBotãéçºãã©ãã©ãèªååãã¦ããã¾ãããã
Mercariã§ã¯èªååã好ãGolangã好ããªSREãåéãã¦ãã¾ãã6/7ã«SRE Drink Meetupãããã¾ããèå³ãããæ¹ã¯æ¯éåå ãã¦ä¸ããã