
Slackと連携していたWebhook通知をTeamsに連携させる
カシオでサーバ開発に携わっている高橋です。
この記事では、Slackと連携させていたWebhook通知をMicrosoft Teamsに連携させるためにおこなったことについて記載をします。
通知先のTeamsのチーム・チャンネルは作成済の前提です。
背景
これまでチーム内のコミュニケーションや開発に関する様々なやり取りを、Slackでおこなっていました。
カシオとアシックスが共同開発したランニング向けスマートフォンアプリのRunmetrix、ウォーキング向けパーソナルコーチングアプリのWalkmetrixのバックエンドで動いているMetrixサーバの開発・運用においても、下記を参考にしてAWS上で稼働しているMetrixサーバでのエラー発生や、開発中のコード修正のプルリクエスト(PR)発行などをSlackにWebhook通知していました。
諸事情によりSlackが使えなくなりTeamsに移行する必要が発生したため、Webhook通知しているものについてもTeamsへの移行が必要となりました。
また一から作るのは既存リソースがもったいないので、下記の3点を要件に設定してTeamsに移行することにしました。
Slackへの通知で使っていたAWSリソース(EventBridge, SNS, Lambda)はなるべく流用する
Slackに通知していたメッセージのフォーマットはTeamsでも基本的に変更しない
エラー通知:Webhook通知されたメッセージはそのままチャンネルに投稿する
※他チームの方がエラー解析に使っており、フォーマットを変更してほしくない要望があったPR通知:イベントの種類(PR発行/更新など)、リポジトリ、ブランチ、説明など
PRの通知についてはレビュワーにメンションされるようにする
※ Slackのときに取り入れていたため継続したい
参考:システム構成
Metrixサーバのエラー通知やコード修正時のPR通知は、おおよそ下記のようなシステム構成でSlackに届いていました。
今回の対応により、Slackの場所がTeamsに差し替わるイメージです。

Slackには、下記のようなメッセージフォーマットで通知が届いていました。
エラー発生時:

PR時:

エラー発生時のWebhook通知とPR時のWebhook通知で行ったことが違う箇所があるので、以降それぞれに記載します。
エラー発生時のWebhook通知をTeams移行する
実際に移行するにあたって実施したことは下記の3点です。
Teamsワークフローの作成、Webhook用URLの取得
LambdaのWebhook通知先URLを変更
Teamsワークフローを修正し、チャンネルにメッセージが流れるようにする
以降、順を追って説明します。
1. Teamsワークフローの作成、Webhook用URLの取得
Teamsの左側に並んでいるアイコンから 、"Workflows" を選択します。
※なければ、三点リーダーのアイコンから探す

"新しいフロー"を選択し、"Webhook要求を受信するとチャネルに投稿する"を選択します。

表示されるダイアログでフロー名を入力、Teamsチーム・チャネルを選択してフローを作成すると、Webhook用URLを取得することができます。
後で使うので控えておきましょう。

2. LambdaのWebhook通知先URLを変更
Webhook通知するLambdaからSlackに通知するのに使っているURLを1.で取得したURLに差し替えればOKです。
3. Teamsワークフローを修正し、チャンネルにメッセージが流れるようにする
2までの作業で、作成したTeamsワークフローにメッセージが到達するようになるので、Teamsチャンネルにメッセージが投稿されるようにワークフローを修正します。
Teamsのワークフローの画面に戻り、1.で作成したワークフローを編集します。
例として、"{text: {key1: value1, key2: value2}}"というフォーマットのリクエストボディをWebhookで通知するものとします。
ワークフロー作成時点で入っている"Send each adaptive card"は削除
「新しいステップ」から"JSONの解析"を追加(「組み込み」タブから探すとすぐ見つかる)
コンテンツ:本文 ※「動的なコンテンツの追加」から追加
スキーマ:(「サンプルから作成」で送信するメッセージのサンプルを貼り付ければ生成してくれる)
続けて、「新しいステップ」から"チャットまたはチャネルでメッセージを投稿する"を追加
投稿者:フローボット
投稿先:チャネル
チーム:(投稿先チャネルのチーム)
チャネル:(投稿先チャネル)
メッセージ:text(メッセージのルートキー) ※「動的なコンテンツの追加」から追加
最終形のワークフローは下記のような感じになります。
properties.text.typeをobjectにして内部構造を記述すればtext内部も解析してくれますが、textの文字列がそのままTeamsの投稿に表示されればよいのでstringにしています。

通知されたメッセージをそのままTeamsの投稿に出すだけなら、ワークフローはシンプルです。
実際に通知させてみた結果は下記です。

Slackに通知していたときと、おおよそ同じにできました。
投稿者が「Workflow経由の〇〇(ワークフロー作成者の名前)」になっているのが個人的にイマイチですが、ここまででWebhook通知をTeamsで受け取ることはできます。
PR時のWebhook通知をTeams移行する
PR時のWebhook通知については、
イベントの種類(PR発行/更新など)、リポジトリ、ブランチ、説明などをわかりやすく表示する
レビュワーにメンションされるようにする
の2点を満たす必要があります。
また、PRに関しては「発行」「更新」「コメント追加」「マージ」「却下」それぞれに応じてメッセージを変えたいので、追加作業が必要です。
PR時のWebhook通知の移行にあたっては下記4点を実施しました。
Teamsワークフローの作成、Webhook用URLの取得
LambdaのWebhook通知先URLを変更
Lambdaのコード変更
通知メッセージを解析し、チャンネルに流れるメッセージを整形するようにTeamsワークフローを修正する
1.と2.まではエラー発生時の通知とやることが同じなので割愛して、以降では3.から説明します。
3. Lambdaのコード変更
プルリクエスト通知用のLambdaのコードを下記サイトを参考に書きます。
コードは下記のような感じ。
lambda_handler:Lambda実行時のエントリーポイント
get_message:EventBridgeから渡されたイベントを解釈してTeamsに通知するメッセージを整形
post_teams:TeamsにWebhook通知
import json
import os
import re
import requests
import sys
from logging import getLogger, INFO ,StreamHandler
logger = getLogger(__name__)
logger.setLevel(INFO)
TEAMS_WEBHOOK_URL = os.environ['WEBHOOK_URL']
REGION = os.environ['REGION']
REPOSITORY_URL_BASE = os.environ['REPOSITORY_URL_BASE']
def lambda_handler(event, context) -> None:
logger.info('event:')
logger.info(event)
message_dict_str = json.dumps(event, ensure_ascii=False)
logger.info('message:')
logger.info(message_dict_str)
msg_title, msg_title_fixed, detail = get_message(event)
post_teams(msg_title, msg_title_fixed, detail)
def get_message(message: dict) -> (str, str, list):
msg_title = message['detail-type']
event = message['detail']['event']
# PRへのコメント
if (event == "commentOnPullRequestCreated" or event == "commentOnPullRequestUpdated"):
# msg_title
msg_title_fixed = 'プルリクエストにコメントされました'
# detail
user_arn = message['detail']['callerUserArn']
user_arn_splitted = user_arn.split('/')
user_name = user_arn_splitted[1] + '/' + user_arn_splitted[2]
repo_name = message['detail']['repositoryName']
pull_request_id = message['detail']['pullRequestId']
url = REPOSITORY_URL_BASE + repo_name + '/pull-requests/' + message['detail']['pullRequestId'] + '/activity' + REGION
detail = {
'event':msg_title_fixed,
'repo_name':repo_name,
'id':pull_request_id,
'author':user_name,
'url':url
}
# PR作成/更新
elif (event == "pullRequestCreated" or event == "pullRequestSourceBranchUpdated"):
# msg_title
msg_title_fixed = 'プルリクエストが作成されました' if message['detail']['event'] == "pullRequestCreated" else 'プルリクエストが更新されました'
# detail
repo_name = message['detail']['repositoryNames'][0]
src_branch = message['detail']['sourceReference'][10:] # ref/heads/を削る
dst_branch = message['detail']['destinationReference'][10:] # ref/heads/を削る
author_arn = message['detail']['author']
author_arn_splitted = author_arn.split('/')
author_name = author_arn_splitted[1] + '/' + author_arn_splitted[2]
description = message['detail']['description'] if 'description' in message['detail'] else 'No Description'
pull_request_id = message['detail']['pullRequestId']
url = REPOSITORY_URL_BASE + repo_name + '/pull-requests/' + message['detail']['pullRequestId'] + REGION
title = message['detail']['title'] if 'title' in message['detail'] else 'No Title'
title_split = title.split() if 'title' in message['detail'] else 'No Title'
reviewer = set_reviewer(title_split[0])
title_fixed = title_split[1] if reviewer != '' else title
detail = {
'event': msg_title_fixed,
'title': title_fixed,
'reviewer': reviewer,
'repo_name':repo_name,
'id':pull_request_id,
'src_branch':src_branch,
'dst_branch':dst_branch,
'author':author_name,
'url':url,
'description':description
}
# PRマージ/却下
else:
# msg_title
msg_title_fixed = ''
if event == "pullRequestMergeStatusUpdated":
msg_title_fixed = 'プルリクエストがマージされました'
elif event == "pullRequestStatusChanged":
msg_title_fixed = 'プルリクエストが却下されました'
else:
msg_title_fixed = event
# detail
repo_name = message['detail']['repositoryNames'][0]
pull_request_id = message['detail']['pullRequestId']
url = REPOSITORY_URL_BASE + repo_name + '/pull-requests/' + message['detail']['pullRequestId'] + REGION
user_arn = message['detail']['callerUserArn']
user_arn_splitted = user_arn.split('/')
user_name = user_arn_splitted[1] + '/' + user_arn_splitted[2]
description = message['detail']['description'] if 'description' in message['detail'] else 'No Description'
detail = {
'event': msg_title_fixed,
'repo_name':repo_name,
'id':pull_request_id,
'author':user_name,
'url':url,
'description':description,
}
return msg_title, msg_title_fixed, detail
def set_reviewer(user):
# メールアドレスとslackのときのメンション名が違う人がいたのでその置換用(なくていいが使い勝手のために残した)
user_list = {
'@xxxxx': '@yyyyy'
}
user = user.replace("@", "@")
reviewer = user_list.get(user,user).replace("@", "") + "@casio.co.jp"
return reviewer if user.startswith('@') else ''
def post_teams(msg_title: str, msg_title_fixed: str, detail: list) -> None:
payload = {
'@type': 'MessageCard',
"@context": "http://schema.org/extensions",
"themeColor": "0076D7",
"summary": f'{msg_title}',
"sections": {
"activityTitle": 'CodeCommit Notifiction',
"activitySubtitle": f'{msg_title}',
"facts": detail,
"markdown": 'true',
}
}
logger.info('post message:')
logger.info(json.dumps(payload))
try:
response = requests.post(TEAMS_WEBHOOK_URL, data=json.dumps(payload), headers={'Content-Type': 'application/json'})
except requests.exceptions.RequestException as e:
print(e)
else:
print(response.status_code)
※Lambdaの環境変数に下記を設定しています
WEBHOOK_URL:控えておいたWebhook用URL
REGION:?region=ap-northeast-1
REPOSITORY_URL_BASE:https://ap-northeast-1.console.aws.amazon.com/codesuite/codecommit/repositories/
上記コードで扱っているイベント以外に何があるか、EventBridgeから渡されるデータ構造が知りたい方は下記を参照ください。
実際にPRを発行した場合、Teamsに到達するリクエストボディは下記のようなフォーマットになります。
更新などの際は"summary"と"sections"の中身が変わります。
{
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"themeColor": "0076D7",
"summary": "CodeCommit Pull Request State Change",
"sections": {
"activityTitle": "CodeCommit Notifiction",
"activitySubtitle": "CodeCommit Pull Request State Change",
"facts": {
"event": "プルリクエストが作成されました",
"title": "タイトル",
"reviewer": "レビュワーのメールアドレス",
"repo_name": "リポジトリ名",
"id": "PRのID",
"src_branch": "ソースブランチ名",
"dst_branch": "ターゲットブランチ名",
"author": "発行した人",
"url": "PRのURL",
"description": "PR内容に関する説明"
},
"markdown": "true"
}
}
CodeCommitでPR発行する際にタイトルを"@xxxx タイトル"(xxxxの部分はメールアドレスの@より前)のようにすることで、Lambdaコードのset_reviewer関数において"reviewer"にレビュワーのメールアドレスが入るようにしており、これをメンションのために使います。 ("@xxxx"を付けなければ"reviewer"がnullになり、メンションされない)
4. 通知メッセージを解析し、チャンネルに流れるメッセージを整形するようにTeamsワークフローを修正する
上で挙げた、
イベントの種類(PR発行/更新など)、リポジトリ、ブランチ、説明などをわかりやすく表示する
レビュワーにメンションされるようにする
を実現するために、ワークフローを修正していきます。
条件分岐がいくつか必要で、メンションされるようにする必要もあるため、ワークフローを頑張って作ります。
箇条書きで説明すると長くなるので、まず最終形のワークフローの画像から。
条件分岐させるために"条件"、メンションのために"ユーザーの@mentionトークンを取得する"のアクションを使います。

JSONの解析のスキーマは、下記のようになります。
長いですが、Teamsに届いたメッセージをコピーして「サンプルから作成」でペーストすれば勝手に作ってくれますのでご安心を。
{
"type": "object",
"properties": {
"@@type": {
"type": "string"
},
"@@context": {
"type": "string"
},
"themeColor": {
"type": "string"
},
"summary": {
"type": "string"
},
"sections": {
"type": "object",
"properties": {
"activityTitle": {
"type": "string"
},
"activitySubtitle": {
"type": "string"
},
"facts": {
"type": "object",
"properties": {
"event": {
"type": "string"
},
"title": {
"type": "string"
},
"reviewer": {
"type": "string"
},
"repo_name": {
"type": "string"
},
"id": {
"type": "string"
},
"src_branch": {
"type": "string"
},
"dst_branch": {
"type": "string"
},
"author": {
"type": "string"
},
"url": {
"type": "string"
},
"description": {
"type": "string"
}
}
},
"markdown": {
"type": "string"
}
}
}
}
}
ポイントは下記の2点です。
JSON解析で"sections"配下も解析させ、その結果から個々のイベントに合わせて表示したいキーを抜き出してメッセージを整形
"ユーザーの@mentionトークンを取得する"のアクションにレビュワーのメールアドレスを渡すようにし、メッセージに"@mention"を含めることでレビュワーの人にメンションされるようにする
実際にTeamsに投稿されるメッセージは、下記のような感じになります。
Slackのときのメッセージに近くなり、メンションも無事付いています。

当たり前ですが、通知されたメッセージをそのまま投稿するよりもわかりやすいです。
本当はURLやDescriptionでリンクを挿入できるようにしたかったのですが、文字列扱いで表示されてしまいました・・・
とりあえずコピーしてブラウザに貼り付ければいいので、保留しました。
おわりに
以上の作業を行うことで、無事SlackからTeamsに移行することができました。
メッセージの投稿者名やURLにうまくリンクが張られないなど、個人的に納得がいっていないところもありますが、そのうち時間があるときにまた調べてみようかなと思います。
【前回のテックブログはコチラ】
カシオの「G-SHOCK MOVE」や「CASIO WATCHES」という時計につながるスマートフォンアプリの保守や仕様設計の担当者が、さらにカシオの時計を楽しく使っていただくため、スマートフォンアプリの機能の一つである「ワールドタイム機能」について、紹介しています。
【カシオのソフトウェア採用についてはコチラ】