Backlog更新通知をTeamsに連携してみた(ちょっとLambdaでコード書きます)

Teamsチャネルのメールアドレスを利用して通知を行うのが、ノンコーディングでお手軽だと思いました。
2021.07.07

複数のコラボレーションツールを使い、プロジェクトに携わる機会があると思います。これまで参画してきたプロジェクトにも、チャットコミュニケーションはTeamsで、チケット管理はBacklogというような使い分けをしているプロジェクトもありました。常に全てのツールを確認できていればいいのですが、実際のところメインで確認するツールは限られてくると思います。例えばTeamsをメインで確認していて、Backlog上の更新に気が付かなかったなんてことがあると思います。 今回はそんなお悩みを解決すべく、Backlogの更新通知をTeamsに連携してみたいと思います。現在、TeamsはBacklogのインテグレーション対象ではないため、作り込みや何かしらの設定が必要になります。

先に結論

Teamsにどのような通知を行いたいかにもよりますが、通知の作り込みはそれなりに時間がかかると思います。Teams側の該当チャネルでメールアドレスを取得し、Backlogプロジェクトにユーザとして参加させることでBacklog上の通知をTeamsに連携することが可能です。手間も少なく、リッチな通知(通常のBacklogメール通知同様)なのでTeamsで更新通知を受け取る際はこの方法がいいのではないでしょうか。

▲ Backlogユーザ(チャネルアドレス)をプロジェクトに招待

▲ Teams側への通知はこんな感じです

一番お伝えしたかった事はこの結論ではありますが、作り込みもしてみましたので以降で紹介させてください。

Backlog更新通知をTeamsに連携してみた

構成

BacklogからWebhookでAPIにリクエストを送り、Teamsに通知するような構成です。

Incoming Webhook作成(Teamsでの作業)

TeamsのチャネルでIncoming Webhookを有効にするとHTTPSエンドポイントが公開されます。そのエンドポイント宛てにリクエストを送信すると、Webhookを有効にしたチャネルにメッセージが投稿されます。Incoming Webhookはチャネルのメニューより数クリックで作成できます。手順については以下ドキュメントを参照ください。

▲ エンドポイントが公開されます

Incoming Webhook作成後に、ローカル環境からテストメッセージを送信してみました。(エンドポイントはマスクしてます)

$ curl -H 'Content-Type: application/json' -d '{"text": "Hello World"}' https://xxx/IncomingWebhook/xxx

該当のチャネルにメッセージが投稿されました。

これでTeams側の設定は完了です。

AWS構築

AWSではAPI GatewayとLambda Functionを作成します。SAMテンプレートを用意しましたので、おおまかに解説します。

SAMテンプレート

TeamsのIncoming Webhook URLをパラメータに指定し、Lambda Functionの環境変数に設定するつくりにしました。パラメータはSAM Deploy時に指定してください。SAMの利用方法等については以下を参考にしてください。

API Gateway

今回の用途ではAPI GatewayとLambdaを統合できればよく、API Gatewayのコア機能のみ利用しますので、低レイテンシ/コストで利用できる、HTTP APIを利用しました。

▲ 後ほどこのURLをBacklogのWebhookに設定します

HTTP APIとREST APIで機能の違いが気になる方は以下を参照ください。

Lambda Function

少々ステップ数がありますので折りたたんでいます。

app.py
import logging
import json
import os
import requests

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

def lambda_handler(event, context):
    teams_incoming_webhook = os.environ['TEAMS_INCOMING_WEBHOOK']     # 環境変数よりTeams Incoming WebhookのURL取得
    bl_body=json.loads(event['body'])
    #イベントデータなどロギング
    logger.debug("Event Data:" + json.dumps(dict(event)))
    logger.debug("Backlog Body:" + json.dumps(bl_body))

    # Backlogで定義された更新の種別
    bl_issue_type = {1:"課題の追加", 2:"課題の更新", 3:"課題にコメント", 4:"課題の削除",14:"課題をまとめて更新",17:"コメントにお知らせを追加"}
    # Backlogで操作した更新
    bl_type = bl_body['type']

    # Backlog課題に関するイベントのみ許可(キーがなければ例外発生)
    if bl_type in bl_issue_type.keys():
        # Teamsに通知するタイトル
        teams_title = bl_issue_type[bl_type]
    else:
        raise KeyError

    #更新種別を判別して通知メッセージ作成
    if bl_type == 1:      #課題追加
        payload_dic = issue_add(teams_title,bl_body)
    elif bl_type == 2:    #課題更新
        payload_dic = issue_update(teams_title,bl_body)
    elif bl_type == 3:    #課題コメント
        payload_dic = issue_comment(teams_title,bl_body)
    elif bl_type == 4:    #課題削除
        payload_dic = issue_delete(teams_title,bl_body)
    elif bl_type == 14:    #課題複数更新
        payload_dic = issue_multi_update(teams_title,bl_body)
    elif bl_type == 17:    #コメントお知らせ追加
        payload_dic = issue_comment_notice_add(teams_title,bl_body)
    else:
        raise Exception

    response = requests.post(teams_incoming_webhook, data=json.dumps(payload_dic))
    # ステータスコードを判定してロギング
    if response.status_code == requests.codes.ok:
        logger.info("Status Code :" + str(response.status_code) + " Response Header:" + json.dumps(dict(response.headers)))
        return response.status_code
    else:
        logger.info("Status Code :" + str(response.status_code) + " Response Header:" + json.dumps(dict(response.headers)))
        raise Exception

#課題追加時のメッセージ作成
def issue_add(teams_title,bl_body):
    bl_summary = bl_body['content']['summary']
    bl_description = bl_body['content']['description']
    bl_created = bl_body['created']
    teams_text = '件名:{0}<br/>課題の詳細:{1}<br/>日時:{2}'.format(bl_summary,bl_description,bl_created)
    #Teamsへの通知本文作成
    payload_dic = {
        "title": teams_title,
        "text": teams_text,
    }
    return payload_dic

#課題更新時のメッセージ作成
def issue_update(teams_title,bl_body):
    bl_summary = bl_body['content']['summary']
    bl_description = bl_body['content']['description']
    bl_created = bl_body['created']
    teams_text = '件名:{0}<br/>課題の詳細:{1}<br/>日時:{2}'.format(bl_summary,bl_description,bl_created)
    #Teamsへの通知本文
    payload_dic = {
        "title": teams_title,
        "text": teams_text,
    }
    return payload_dic

#課題コメント時のメッセージ作成
def issue_comment(teams_title,bl_body):
    bl_summary = bl_body['content']['summary']
    bl_comment = bl_body['content']['comment']['content']
    bl_created = bl_body['created']
    teams_text = '件名:{0}<br/>コメント:{1}<br/>日時:{2}'.format(bl_summary,bl_comment,bl_created)
    #Teamsへの通知本文
    payload_dic = {
        "title": teams_title,
        "text": teams_text,
    }
    return payload_dic

#課題削除時のメッセージ作成
def issue_delete(teams_title,bl_body):
    bl_created = bl_body['created']
    teams_text = '日時:{0}'.format(bl_created)
    #Teamsへの通知本文
    payload_dic = {
        "title": teams_title,
        "text": teams_text,
    }
    return payload_dic

#課題複数更新時のメッセージ作成
def issue_multi_update(teams_title,bl_body):
    bl_created = bl_body['created']
    #纏めて更新した要素数
    testt = range(len(bl_body['content']['link']))

    teams_text = ""
    for i in testt:
        print(bl_body['content']['link'][i]['title'])
        teams_text += '件名:{0}<br/>'.format(bl_body['content']['link'][i]['title'])
    teams_text += '日時:{0}'.format(bl_created)

    #Teamsへの通知本文
    payload_dic = {
        "title": teams_title,
        "text": teams_text,
    }
    return payload_dic

#コメントお知らせ追加時のメッセージ作成
def issue_comment_notice_add(teams_title,bl_body):
    bl_summary = bl_body['content']['summary']
    bl_comment = bl_body['content']['comment']['content']
    bl_created = bl_body['created']
    teams_text = '件名:{0}<br/>課題の詳細:{1}<br/>日時:{2}'.format(bl_summary,bl_comment,bl_created)
    #Teamsへの通知本文作成
    payload_dic = {
        "title": teams_title,
        "text": teams_text,
    }
    return payload_dic

Lambdaハンドラーでは、Backlogからのイベントデータ(JSON)より、Backlogプロジェクト上で行われたら操作種別を取得、判別します。操作種別の内容については以下ドキュメントに記載があります。

次に操作種別毎に作成した関数(issue_addなど)を呼び出し、Teamsチャネルに投稿するメッセージを作成します。その後、TeamsのIncoming Webhookエンドポイントにリクエストを送信しています。Teamsへのメッセージ送信については以下が参考になります。

機能と直接関係ありませんが、イベントデータなどロギングするようにしていますので、必要に応じレベルを変更して出力してください。

Webhook作成(Backlogでの作業)

Webhookは「課題の追加」など、Backlogプロジェクト上でのイベントをリアルタイムに指定されたURLへPOSTする機能です。今回は課題に関するイベントのみ設定しました。なお、Lambda Functionも課題に関するイベントのみ受け付けるように作成していますので、通知するイベントを変更する際はLambda Functionも変更が必要になります。

▲ API GatewayのURLをWebhook URLに設定します

Webhookは、Backlogのプロジェクト設定より数クリックで作成できます。手順については以下ドキュメントを参照ください。

動作確認

実際にBacklog上で操作を行い、Teamsへ通知(メッセージ投稿)してみたいと思います。

▲ Backlog課題を追加しました

Incoming Webhookを有効にしたTeamsチャネルに通知されました。

課題の更新など、Backlog通知設定を行った課題に関するアクションを行いました。想定通り通知が行われました。

さいごに

今回は作り込みで通知設定を行いましたが、それなりに手間がかかることがわかりました。冒頭で記載した結論の通り、BacklogからTeamsへの通知は、Teamsチャネルのメールアドレスを取得し、プロジェクトにユーザとして追加した方が手間も少なく通知要件を満たせるのではないでしょうか。

▲ (再掲)Teamsチャネルアドレスを利用した通知例

Teamsチャネルのメールアドレスで対応する際は以下を参考にしてください。

余談となりますが、BacklogのWebhook URLにTeamsのIncoming WebhookのURLを設定すれば通知が行えるかも?と思いましたが、そうはいきませんでした。

▲ エラーになりました(Backlogインテグレーション送信履歴)

Backlog上から送信されたイベントをコピーし、ローカルコマンドラインからもIncoming Webhookにリクエストを送りましたが、Summary or Text is required.でエラーとなりました。Teamsのドキュメントからは読み取れませんでしたが、Incoming Webhookに対するリクエストにSummaryかTextキーの指定が必要なようです。

$ curl -H 'Content-Type: application/json' \
-d '{"id":98021310,xxxxxxxxxxxxxxxxxxxxxxxx,}' \
-i https://xxx/IncomingWebhook/xxx

HTTP/2 400
cache-control: no-cache
	・
	・

Summary or Text is required.

Zapierや、Power Automate等であればその辺りのキーを意識でき、ノンコードで対応できそう *1ですが、Webhookがプレミアム機能だったりするので、実際に利用するには固定の料金がかかりそうです。 現状、BacklogとTeams連携には何かしらのつくり込みが必要ですが、機能追加の要望は多そうなので、アップデートに期待したいと思います。

脚注

  1. 本記事では扱いませんでしたが、ZapierをつかってBacklogからTeamsへの通知は行えました。