おぎろぐはてブロ

なんだかんだエンジニアになって10年以上

AWS SAM + Slack Bolt for PythonでSlack botをつくる

書いたこと

  • Lazy Listenerを利用し、ackが必要なイベントかつ3秒以上時間がかかる処理を実現する
  • Lambda Function URLを利用してHTTP Endpointをつくる

Slack Bolt for PythonでSlack botをつくる

このチュートリアルを参照しつつ、つくります。割愛。

slack.dev

ある程度、チュートリアルに沿って、ローカルで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_somethingstep_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 になります。

AWS CLIのS3接続先の選択動作とaddressing_styleオプション

AWS CLI がS3にアクセスする際にどのように Path-Style と Virtual Hosted-Style を使い分けるかをまとめます。

  • S3のアクセス形式2つ
    • 名前解決動作
    • バケット作成直後に Virtual Hosted–Style でアクセスした場合の問題
  • AWS CLI はどちらの形式でS3にアクセスするか?
    • DNS Compatible Check とは?
      • DNS Incompatible なバケット名
      • dotを含むバケット名の対応
  • s3.addressing_style オプション
    • リージョン違いのフォールバック
  • まとめ
続きを読む

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+冊子)

米国アマゾンのデジタルコンテンツビジネス戦略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&#x3042;ascii"}],"Quiet":false}'

まとめ

  • s3apiの引数でJSONを指定する場合、non-asciiな文字列はXML実体参照で入れる

JSONとXMLの変換の闇でした。

押しやすいドメインを探す

短くて覚えやすいドメインは取得されてしまっていることが多いです。とはいえ、jpドメインだと3文字が割と残っているので、ここから「キーボードで押しやすい」ドメインを探してみるスクリプトをリハビリがてら書いてみた。 これは、ガラケー時代のケータイ配列で押しやすいと同じ感じで、意味をこじつけるなどして覚えてもらえば、十分価値のあるドメインになると思います。

押しやすい定義

以下の定義を考えましたが、今回は一番上の配置が近い文字のみを見ています。

  • QWERTYキーボードにおいて、キー配置が近い文字は押しやすい
  • 文字数が小さいほど押しやすい
  • 左手と右手を両方使いたくなる配列はよくない
  • 誤認しやすい文字はあまりよくない (0 and o, 1 and l)
続きを読む