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

はじめに
最近チームで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つです。

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

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

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


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を開きます。今回は特に暗号化ヘルパーは使用していません。
下の方に次のようにキーがありますのでこの値を入力します。

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


JSONで設定してもどちらでもいいですが、IAM ARNとIPv4アドレスを指定して、この2箇所からしかアクセスできないようにします。IAM ARNにはLambdaの実行ロールで使用しているロールのARNを設定します。
Trello Webhookの登録
最後にTrelloのWebhookを登録します。
API keyとtokenの取得
また以下URLを開きます。
この中でページ上部にある開発者向け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のダッシュボードを開いてください。



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


ここにカードの情報が入っていれば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
-
前の記事
WordPressをEC2のt3.nanoで稼働してたらMySQLがOut of memoryになった話 2022.02.16
-
次の記事
記事がありません