AWS SAM + Slack Bolt for PythonでSlack botをつくる
書いたこと
- Lazy Listenerを利用し、ackが必要なイベントかつ3秒以上時間がかかる処理を実現する
- Lambda Function URLを利用してHTTP Endpointをつくる
Slack Bolt for PythonでSlack botをつくる
このチュートリアルを参照しつつ、つくります。割愛。
ある程度、チュートリアルに沿って、ローカルでSocket Mode有効でつくってから、Lambdaで動かすように次のLazy Listernerを有効にするように自分はしています。
Lazy Listenerの利用
Boltで受けられるイベントは各種ありますが、アクション(action)、コマンド(command)、ショートカット(shortcut)、オプション(options)、およびモーダルからのデータ送信(view_submission)を処理する場合は、Slack側からのリクエストに対して3秒以内に ack()
を返す必要があります。
ただ、Lambdaで動かす場合、HTTPレスポンスを返却したタイミングでプロセスが終了されるため、ackを返したあとに処理を継続させることができません。この対応として、Slack Bolt for Pythonでは、Lazy Listenerという機能が提供されています。
これは、HTTPでSlackからリクエストを受け取ったLambdaプロセスが、時間のかかる処理を実行するLambdaを非同期で呼び出して、ackレスポンスを返却することで実現されています。
例にあるように、ack
と、 lazy
の引数にそれぞれack処理と、時間のかかる処理を分けて書けます。lazyには複数の関数を渡せ、それぞれ並列に実行されます。
lazyに1つ処理を指定した場合、1つのイベントに対してack処理とlazy処理でLambdaが2度invokeされることになります。上に書いてあるackが必要なイベント以外をハンドルする場合はackが不要なので、その場合Lazy Listenerを使うと無駄にinvokeの回数が増えるので、この場合はLazy Listenerを有効にしないほうがよいです。
以下は、行頭にHelloもしくはhelloという文字列があったときにHello!を発言する場合です。 (messageイベントはackが不要なのでlazyを使う必要はないです)
import logging import re from slack_bolt import App, Say from slack_bolt.adapter.aws_lambda import SlackRequestHandler SlackRequestHandler.clear_all_log_handlers() logging.basicConfig(level=logging.INFO) app = App(process_before_response=True) def handle_message(message, say: Say): print(message) say("Hello!") rule = re.compile("^[Hh]ello") app.message(rule)( ack=lambda ack: ack(), lazy=[handle_message] ) def handler(event, context): print("invoked", event) slack_handler = SlackRequestHandler(app=app) return slack_handler.handle(event, context)
とにかくackだけ返したければ
ack=lambda ack: ack(),
と書けばOK
呼び出しをCloudWatch Metricsにputする
Slack botを作った場合、Slackからの呼び出しは同じLambda Functionが呼び出されるだけなので、複数のイベントをハンドリングする場合、どのイベントがどのくらい呼び出されているのかを調べるのが困難です。このときCloudWatch metricsにメトリックを記録すると便利です。
以下のように定義して、handle_shortcut
ではackしつつmodalを開いたりし、do_something
では時間のかかる処理を行い、step_handle_shortcut
ではCloudWatch metricsにメトリックを記録します。
do_something
と step_handle_shortcut
は並列にinvokeされます。
app.shortcut("shortcut")(
ack=handle_shortcut,
lazy=[do_something, metric.step_handle_shortcut]
)
metric.py
では
import boto3 import os import datetime def put(step: str): client = boto3.client("cloudwatch") env = os.getenv("ENV") # 別途定義する response = client.put_metric_data( Namespace=f"FooBarSlackApp/{env}", MetricData=[ { "MetricName": "Request", "Dimensions": [ { "Name": "Step", "Value": step }, ], "Timestamp": datetime.datetime.utcnow(), "Value": 1, }, ] ) print(response) def step_handle_shortcut(): put("handle_shortcut")
SAMでデプロイ
最低限必要なのは Lambda Function と、HTTP EndpointとしてのLambda Function URLです。 HTTP EndpointとしてAPI Gateway (v1, v2) を利用している例があったりしますが、特に高度な定義も必要なければ、Lambda Function URLを使用するほうが、定義が簡単で、かつ、利用料金もLambda側に含まれるので安価になります。(大抵はまぁ誤差みたいな金額だと思いますが)
src/
配下にコードを配置した場合、以下のようなコードで実現できます。
AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: Slack bot sample Resources: HandlerFunction: Type: AWS::Serverless::Function Properties: CodeUri: src/ Handler: app.handler Runtime: python3.11 Timeout: 30 Policies: - AWSLambdaRole FunctionUrlConfig: AuthType: NONE Outputs: HandlerFunctionUrl: Value: !GetAtt HandlerFunctionUrl.FunctionUrl
FunctionUrlConfig
を定義してやると、Lambda Function URLが有効になります。SlackからのリクエストにIAM認証を利用することはできないので、AuthType: NONE
になります。(bolt内でSlackの特定のアプリから来た正当なリクエストか signing_secret
を利用して検証しています)
Slack appの Interactivity eventsのURL、Slash commandのURL、Event SubscriptionsのURL (それぞれ有効にするかは使うイベント次第) にこのLambda Function URLのURLを指定してやる必要があるので、Outputs
で出力をしています。
SAMでリソースが勝手に作られて、その名前が Lambda Function名に Url
を足したものになるので、Lambda Functionが HandlerFunction
の場合、 HandlerFunctionUrl
になります。
botoでSample IMDB Movie DataをDynamoに挿入する
CloudSearch用のサンプルデータとして、IMDBのMovie Data Sampleが公開されています。
これを検証用にDynamoDBに入れたかったのでざっくりコードを書いた。 HashKeyをstring指定でテーブルを作り、実行。
# -*- coding: utf-8 -*- import decimal import json import boto.dynamodb2 import boto.dynamodb2.table conn = boto.dynamodb2.connect_to_region('ap-northeast-1') imdb = boto.dynamodb2.table.Table('imdb', connection=conn) with open('moviedata2.json') as f: records = json.load(f, parse_float=decimal.Decimal) with imdb.batch_write() as batch: for record in records: data = record['fields'] data['id'] = record['id'] for key in ['directors', 'genres', 'actors']: if key in data: data[key] = set(data[key]) # テスト用なので上書きOKにしてる batch.put_item(data, overwrite=True)
これでうまく挿入はされるが、release_date
はunixtimeとかで入れるべきである。
米国アマゾンのデジタルコンテンツビジネス戦略2012 (CD+冊子)
- 作者: インターネットメディア総合研究所
- 出版社/メーカー: インプレスR&D
- 発売日: 2011/10/27
- メディア: 単行本(ソフトカバー)
- クリック: 1回
- この商品を含むブログを見る
awscli s3apiでの日本語を含むオブジェクト操作
日本語などnon-ascii文字列を含むオブジェクト操作のメモ
"nonあascii" (unicode: \u3042) というオブジェクト名を想定します。
aws s3 rm
ふつうに日本語で実行できる。 (おそらく実行環境に依る)
# Key does not exist aws s3 rm "s3://bucket/non\u3042ascii" # delete successful aws s3 rm "s3://bucket/nonあascii"
s3api delete-object
こちらも同様。 ただ、s3コマンドと違ってファイルの存在を確認しないため、バージョニングが有効だと存在しないファイルにデリートマーカをつくる。
# successful aws s3api delete-object --bucket "bucket" --key "nonあascii" # fail (create delete marker to non\\u3042ascii) aws s3api delete-object --bucket "bucket" --key "non\u3042ascii"
s3api delete-objects
複数指定したい場合は、delete-objects。
以下は失敗パターン。
defaultencoding=asciiの環境だと 'ascii' codec can't encode character u'\u3042' in position 24: ordinal not in range(128)
のエラーが出る。
aws s3api delete-objects --bucket "bucket" \ --delete '{"Objects":[{"Key":"non\u3042ascii"}],"Quiet":false}' aws s3api delete-objects --bucket "bucket" \ --delete '{"Objects":[{"Key":"nonあascii"}],"Quiet":false}'
sitecustomize.pyでデフォルトエンコーディングをUTF-8に変更するとどちらもXML不正エラーが出る。
A client error (MalformedXML) occurred when calling the DeleteObjects operation: The XML you provided was not well-formed or did not validate against our published schema
何が起きているかというと、XMLがおかしい。
0x0000: 4500 0074 8db1 4000 4006 10a5 ac1f 18d5 E..t..@.@....... 0x0010: 36e7 a052 bab8 0050 ddd0 5f96 019d f618 6..R...P.._..... 0x0020: 5018 0073 9c94 0000 3c44 656c 6574 653e P..s....<Delete> 0x0030: 3c4f 626a 6563 743e 3c4b 6579 3e6e 6f6e <Object><Key>non 0x0040: e381 8261 7363 6969 3c2f 4b65 793e 3c2f ...ascii</Key></ 0x0050: 4f62 6a65 6374 3e3c 5175 6965 743e 6661 Object><Quiet>fa 0x0060: 6c73 653c 2f51 7569 6574 3e3c 2f44 656c lse</Quiet></Del 0x0070: 6574 653e ete>
ということで、XML実体参照で送る。
aws s3api delete-objects --bucket "bucket" \ --delete '{"Objects":[{"Key":"nonあascii"}],"Quiet":false}'
まとめ
- s3apiの引数でJSONを指定する場合、non-asciiな文字列はXML実体参照で入れる
JSONとXMLの変換の闇でした。
押しやすいドメインを探す
短くて覚えやすいドメインは取得されてしまっていることが多いです。とはいえ、jpドメインだと3文字が割と残っているので、ここから「キーボードで押しやすい」ドメインを探してみるスクリプトをリハビリがてら書いてみた。 これは、ガラケー時代のケータイ配列で押しやすいと同じ感じで、意味をこじつけるなどして覚えてもらえば、十分価値のあるドメインになると思います。
押しやすい定義
以下の定義を考えましたが、今回は一番上の配置が近い文字のみを見ています。
- QWERTYキーボードにおいて、キー配置が近い文字は押しやすい
- 文字数が小さいほど押しやすい
- 左手と右手を両方使いたくなる配列はよくない
- 誤認しやすい文字はあまりよくない (0 and o, 1 and l)