TrelloのWebhookを使ってAWS OpenSearch Serviceにカード情報を入れる

  • 2022.04.29
  • AWS
TrelloのWebhookを使ってAWS OpenSearch Serviceにカード情報を入れる

はじめに

最近チームでTrelloを使うようになったのですが、Trelloのカード操作の情報を分析したくて良いBIツールがないか検討していたのですが、せっかくなのでAWS OpenSearch Serviceを使ってみることにしました。構成は以下の通りです。

OpenSearch Serviceのセキュリティを高めるならVPC内にドメインを構築することが推奨されているのですが、その場合Proxy用のサーバーが必要になってきて、そこで余分なコストがかかってしまうため、今回はパブリックな構成で構築しました。

DashboardはグローバルIPで絞っていて、他のIPからはDashboardを参照できないようにしています。

また、OpenSearch Serviceの操作はIPに加えて、ある特定のLambdaからも操作できるように権限設定をしています。

Trelloのセキュリティについて考える

TrelloのWebhookを使用してAPI Gatewayにカード情報を送るときにAPI GatewayのAPIキーを使えると良かったのですが、どうもうまく渡せませんでした。そこで、API GatewayではRateLimitだけ制御して、Lambda側でTrelloのWebhook Signaturesを使用して、ちゃんとTrelloから送られてきたデータなのかをチェックすることにしました。この辺りは実装部分で説明します。

API Gatewayの設定

APIGatewayの設定ポイントとしては、次の3つです。

まずは、リソースはHEADとPOSTの2つ作ることです。TrelloのWebhookを登録するときはHEADリクエストがくるので、HEADで200を返してやる必要があります。

もう一つはLambda プロキシ統合の使用にチェックを入れることです。

Lambda プロキシ統合を使うことで、LambdaにTrelloからのheader情報を簡単に渡すことができます。

あとは、使用量プランでリミットを設けておきます。

OpenSearchドメインの作成

OpenSearchはコストを下げるために最低限のスペックでドメインを作りました。

変えているのは名前と、Deployment typeを[開発およびテスト]にしているぐらいです。
インスタンスタイプをt3.small.searchに変更しています。
ここは特に変更していません。

マスターユーザーを作成します。

ドメインアクセスポリシーは[きめ細かなアクセスコントロールのみを使用]を選択します。

あとは特に変更していません。ここまで設定したらドメインを作成します。
作成には15分ぐらいはかかりました。

Lambdaレイヤーの作成

LambdaはPython3.9で作りました。そこで必要となってくるのが次のモジュールです。

  • requests
  • requests_aws4auth

レイヤー作成の方法は色々あると思いますが、私はよくDockerを使ってます。こんな感じの簡単なDockerfileでいいのでAmazonLinuxのコンテナを作ります。

FROM amazonlinux:latest

RUN yum install -y automake libcurl-devel zlib-devel git gcc make bzip2 tar zip python3
WORKDIR /root/python
RUN pip3 install requests requests_aws4auth -t .
WORKDIR /root
RUN zip -r layer.zip python

docker imageをbuildします。

docker build -t python-layer:1 .

コンテナを起動して、モジュールをまとめたzipファイルをコピーしておきます。

docker run -itd --name python-layer python-layer:1
docker cp python-layer:/root/layer.zip .
docker rm -f python-layer

あとはこのzipファイルを使ってLambdaレイヤーを作ります。

設定はこんな感じで大丈夫です。

Lambda関数の作成

Lambda関数は次のコードで作成します。

このコードのポイントとしては、Trelloから送られてくるヘッダーの中にx-trello-webhookという情報があります。x-trello-webhookは、hmacのsha1ハッシュのbase64ダイジェストとなっており、送られてきたbodyデータとcallback urlをくっつけた値をhmacのsha1ハッシュのbase64ダイジェストした値と比較し一致していれば正しいデータだということが分かります。これやっておかないと、適当なリクエスト投げられて変な操作されまくってしまう可能性があるので危険です。

もう一つは、ある特定のLambdaからのみ操作を許可する場合、OpenSearch側にIAMロールARNを設定するだけではダメで、ここで使っているrequests_aws4authのAWS4Authを使う必要があるということです。

これらの情報は少なくって結構ハマりました。

import json
import hmac
import hashlib
import base64
import os
import requests
import boto3
from requests.auth import HTTPBasicAuth
from requests_aws4auth import AWS4Auth

region = 'ap-northeast-1'
service = 'es'
credentials = boto3.Session().get_credentials()
awsauth = AWS4Auth(credentials.access_key, credentials.secret_key, region, service, session_token=credentials.token)


OPEN_SEARCH_URL = 'https://< endpoint >.ap-northeast-1.es.amazonaws.com'

def is_from_trello(hashed_header, request_body):
    double_hashed_body = base64.b64encode(hmac.new(key=os.environ['TRELLO_OAUTH_SECRET'].encode(),
                                                   msg=(request_body + os.environ['TRELLO_CALLBACK_URL']).encode(),
                                                   digestmod=hashlib.sha1,).digest())
    return hmac.compare_digest(hashed_header.encode(), double_hashed_body)

def translate_data(json_data):
    data = json.loads(json_data)
    
    ret = {}
    # model
    ret['board-id'] = data['model']['id']
    ret['board-name'] = data['model']['name']
    ret['board-url'] = data['model']['url']

    # archive
    if 'closed' in data['action']['data']['card']:
        ret['archive'] = True
    else:
        ret['archive'] = False

    # card
    ret['card-name'] = data['action']['data']['card']['name']

    # list
    if 'list' in data['action']['data']:
        ret['list-id'] = data['action']['data']['list']['id']
        ret['list-name'] = data['action']['data']['list']['name']

    # type
    ret['type'] = data['action']['type']

    # timestamp
    ret['@timestamp'] = data['action']['date']

    # member
    ret['member-name'] = data['action']['memberCreator']['fullName']
    ret['member-username'] = data['action']['memberCreator']['username']
    ret['member-id'] = data['action']['memberCreator']['id']

    # checklist
    if 'checklist' in data['action']['data']:
        ret['checklist-id'] = data['action']['data']['checklist']['id']
        ret['checklist-name'] = data['action']['data']['checklist']['name']

    if 'checkItem' in data['action']['data']:
        ret['checkitem-id'] = data['action']['data']['checkItem']['id']
        ret['checkitem-name'] = data['action']['data']['checkItem']['name']
        ret['checkitem-state'] = data['action']['data']['checkItem']['state']
        
    return ret

def put_opensearch(data):
    ret = requests.post(OPEN_SEARCH_URL + '/trello/_doc/',
                       headers={'Content-Type': 'application/json'},
                       auth=awsauth,
                       data=json.dumps(translate_data(data)))
    print(ret.text)
    
def lambda_handler(event, context):
    if is_from_trello(event['headers']['x-trello-webhook'], event['body']):
        put_opensearch(event['body'])

    return {'statusCode': '200'}

< endpoint > は OpenSearch Serviceからエンドポイントを調べてその内容を記述してください。

あとは環境変数に次の2つを作ります。

  • TRELLO_OAUTH_SECRET ・・・ これは後ほど説明します。
  • TRELLO_CALLBACK_URL ・・・ API GatewayのURLです。API Gatewayのステージのところから調べられます。

Trello OAuth SECRETの調べ方

Lambdaに設定するTrelloのOAuth Secretですが、次のURLを開きます。今回は特に暗号化ヘルパーは使用していません。

https://trello.com/app-key

下の方に次のようにキーがありますのでこの値を入力します。

OpenSearch Serviceのセキュリティ設定

OpenSearchのセキュリティ設定をします。

JSONで設定してもどちらでもいいですが、IAM ARNとIPv4アドレスを指定して、この2箇所からしかアクセスできないようにします。IAM ARNにはLambdaの実行ロールで使用しているロールのARNを設定します。

Trello Webhookの登録

最後にTrelloのWebhookを登録します。

API keyとtokenの取得

また以下URLを開きます。

https://trello.com/app-key

この中でページ上部にある開発者向けAPIキーが以降で使用するAPI Keyになります。

次に[トークン]のリンクを開いてください。

下の方にある[許可]をクリックするとtokenを発行できます。

board idの取得

次のpythonスクリプトを使ってboard idを取得します。

import requests
import json
import pprint

base_url = "https://api.trello.com/1/"
member_id = '<  メンバーID  >'

params = {
    'key': '<  API Key   >',
    'token': '<   token    >'
}

response = requests.request(
    "GET",
    '{}/members/{}/boards'.format(base_url, member_id),
    params=params
)



for board in json.loads(response.text):
    print('id:{} board:{}'.format(board['id'], board['name']))

メンバーIDはTrelloのプロフィールから確認できます。デフォルトだと@userで始まるIDです。

API Key と token は先ほど調べたものを入れてください。

pythonスクリプトを実行すると、boardとidのリストが表示されますので、webhookを使いたいboard id を調べておきます。

webhookの登録

次のPythonスクリプトでwebhookを登録します。

import requests
import json

url = "https://api.trello.com/1/webhooks/"

headers = {
   "Accept": "application/json"
}

query = {
   'callbackURL': '<  callback url  >',
   'idModel': '<  board id   >',
   'key': '<  API Key  >',
   'token': '< token  >'
}

response = requests.request(
   "POST",
   url,
   headers=headers,
   params=query
)

print(json.dumps(json.loads(response.text), sort_keys=True, indent=4, separators=(",", ": ")))

callback urlは、API Gatewayのエンドポイントです。

board id は先ほど調べたidで、API Keyとtokenは先ほどと同じものです。

これで一通り設定完了です。

動作確認

では、正常に動作するか確認してみます。

Trelloのカードを作ったりアーカイブ してみます。

次にOpenSearchのダッシュボードを開いてください。

ここでUsernameとPasswordが必要となりますが、これはOpenSearchドメインを作成したときに作ったマスターアカウントのものとなります。

ログインできたら、[Query Wrokbench] を開きます。

そのまま[Run]を押してみます。その中にtrelloというテーブルができているか確認します。

こんな感じで情報を確認することができます。

ここにカードの情報が入っていればOKです。

参考URL

https://developer.atlassian.com/cloud/trello/guides/rest-api/webhooks/

https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/what-is.html

https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/search-example.html