- AWS SAMとは
- Slack botをサーバーレスアプリケーションとして構築する理由
- AWS SAMを用いたSlack botの作成
- SAMを用いてSlack botを作成したときに発生する可能性のある課題
- SAM CLIでの後片付け
- おわりに
こんにちは!スマートキャンプ株式会社の松下です。今日はAWS SAMを用いてSlack botを作成する方法と、発生する可能性のある課題について紹介したいと思います。
この記事は以下のZenn記事を統合し、再編集したものです。 https://zenn.dev/smartcamp/articles/f222ef915bc826 https://zenn.dev/smartcamp/articles/ead9a00fe79cab
AWS SAMとは
AWS Serverless Application Model(AWS SAM)は、Infrastructure as Code(IaC)を使用してサーバーレスアプリケーションを構築するためのオープンソースフレームワークです。AWS SAMの短縮構文を使用すると、デベロッパーはデプロイ中にインフラストラクチャに変換されるAWS CloudFormationリソースと特殊なサーバーレスリソースを宣言します。
Slack botをサーバーレスアプリケーションとして構築する理由
https://api.slack.com/quickstart
Slack botを実装する場合の流れは基本的には下記のような形になります。
- Slackのプラットフォーム上でAppを作成
- Appの設定に基づいてSlackが送信してくるリクエストに対して、レスポンスを返すAPIを実装
2.のようなシンプルなAPIを作成したい場合において、サーバーレスアプリケーションはインフラをあまり考慮する必要がないという観点でメリットがあります。また、botの利用頻度があまり高くない場合、費用面でもメリットが出てくるでしょう。
AWS SAMを用いたSlack botの作成
それでは早速、AWS SAMを使用してSlack botを作成する手順を見ていきましょう。
SAM CLI のセットアップ
上記公式ドキュメントを参照してください。IAM Userなど必要なセットアップを行い、AWS CLIが実行できる状態であることが前提です。
インストール後は下記コマンドで、インストールが成功しているか、パスが通っているかを確認します。
$ which sam /usr/local/bin/sam $ sam --version SAM CLI, <latest version>
SAM CLIによるプロジェクトの初期化
sam init
コマンドを実行すると、SAMで作成したいアプリケーションについて、対話的に設定を行なうことができます。
$ sam init
1 - AWS Quick Start Templates
1 - Hello World Example
Use the most popular runtime and package type? (Python and zip)
→y
- ディレクトリ名 →
sample-sam-slack-app
対話式のインターフェースで上記のように選択していくと、下記のようなファイルテンプレート群が生成されます。
$ cd sample-sam-slack-app/ $ tree . ├── README.md ├── __init__.py ├── events │ └── event.json ├── hello_world │ ├── __init__.py │ ├── app.py │ └── requirements.txt ├── samconfig.toml ├── template.yaml └── tests ├── __init__.py ├── integration │ ├── __init__.py │ └── test_api_gateway.py ├── requirements.txt └── unit ├── __init__.py └── test_handler.py
注目すべきは下記のファイルです。
samconfig.toml
: SAM CLIを実行する際に参照されるSAMの設定ファイルtemplate.yaml
: SAMのデプロイ時に実行されるCloudFormationのテンプレートファイル
Slack App の実装
sam init
で生成したコードにSlack Appとしての実装を行なっていきます。生成されたコードに含まれる実装を削除してしまいましょう。
$ rm -rf events/ hello_world/ tests/
次に src
ディレクトリを作成し、アプリケーションのロジックを書く app.py
を作成します。
$ mkdir src $ touch src/app.py
app.pyではBolt for PythonというSlackが提供しているフレームワークを利用します。
https://slack.dev/bolt-python/ja-jp/tutorial/getting-started
このようなフレームワークを利用することで、認証を自前で実装する必要がなくなります。
Bolt for Pythonのドキュメントに書いてあるとおり、ここではvenvを利用して仮想環境を作成して作業します。(Python環境のセットアップについては省略します)
$ python3 -m venv .venv $ source .venv/bin/activate $ which python3 /Users/${ユーザー名}/sample-sam-application/sample-sam-slack-app/.venv/bin/python3
次に仮想環境内にBolt for Pythonをインストールします。
$ pip install slack_bolt
なお、SAM CLIではビルド時にrequirements.txtを参照するためあらかじめrequirements.txtも出力しておきます。
$ pip freeze > src/requirements.txt
次に src/app.py
を編集します。
import os from slack_bolt import App from slack_bolt.adapter.aws_lambda import SlackRequestHandler app = App( token=os.environ["SLACK_BOT_TOKEN"], signing_secret=os.environ["SLACK_SIGNING_SECRET"], process_before_response=True, ) @app.event("app_mention") def say_hello(event, say): user_id = event["user"] say(f"Hi, <@{user_id}>!") def lambda_handler(event, context): slack_handler = SlackRequestHandler(app=app) return slack_handler.handle(event, context)
botに対してメンションをすると、メンションをしたユーザーに対して「Hi, @user_id」と返答メッセージを送信するシンプルなロジックになっています。環境変数の設定については後述します。
SAM CLIでのビルド
ここまで設定した段階で一度SAM CLIでのビルドを試してみましょう。 sam init
コマンドで生成したディレクトリ構成を変更しているため、 template.yaml
の変更が必要です。
template.yaml
を下記のように修正してください。
@@ -12,31 +12,31 @@ MemorySize: 128 Resources: - HelloWorldFunction: + SampleSAMSlackAppFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: - CodeUri: hello_world/ + CodeUri: src/ Handler: app.lambda_handler Runtime: python3.9 Architectures: - x86_64 Events: - HelloWorld: + SampleSAMSlackApp: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: - Path: /hello - Method: get + Path: /slack/events + Method: post Outputs: # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function # Find out more about other implicit resources you can reference within SAM # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api - HelloWorldApi: - Description: "API Gateway endpoint URL for Prod stage for Hello World function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" - HelloWorldFunction: - Description: "Hello World Lambda Function ARN" - Value: !GetAtt HelloWorldFunction.Arn - HelloWorldFunctionIamRole: - Description: "Implicit IAM Role created for Hello World function" - Value: !GetAtt HelloWorldFunctionRole.Arn + SampleSAMSlackAppApi: + Description: "API Gateway endpoint URL for Prod stage for Sample SAM Slack App function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/slack/events" + SampleSAMSlackAppFunction: + Description: "Sample SAM Slack App Lambda Function ARN" + Value: !GetAtt SampleSAMSlackAppFunction.Arn + SampleSAMSlackAppFunctionIamRole: + Description: "Implicit IAM Role created for Sample SAM Slack App function" + Value: !GetAtt SampleSAMSlackAppFunctionRole.Arn
SAM CLIでのデプロイ
ここまで変更できたら、デプロイを試してみましょう。
$ sam deploy
を実行しましょう。CloudFormationのStackのChangeSetが表示されます。今回は初めて作成することになるので新規作成のような表示になっていると思います。
Deploy this changeset? [y/N]
と聞かれるため、y
を入力してデプロイを開始します。
Successfully created/updated stack - sample-sam-slack-app in ap-northeast-1
と表示されればデプロイ成功です。
CloudFormation outputs from deployed stack
には template.yaml
で設定した出力(例:API GatewayのURL)などが出力されていると思います。 SampleSAMSlackAppApi
の値は、Slack Appの設定に必要であるためメモしておきましょう。
ここで試しに表示されたURLについてCurlでリクエストを投げてみましょう。
$ curl -X POST '${URL}' {"message": "Internal server error"}
環境変数の設定がされておらず、認証も当然通らないためInternal server errorが返ってきますが、APIの処理自体が動いていることはわかります。
Slack Appの設定
ここからSlackプラットフォーム上にSlack Appを作成して設定していきます。
https://slack.dev/bolt-python/ja-jp/tutorial/getting-started
上記のBolt for PythonのドキュメントにSlack Appの作成方法が記載されているので、Slack Appを作成してください。
その後、Slack Appの「OAuth & Permissions」を開いてください。
「Scopes」までスクロールし、bot tokenに下記のようにScopeを追加します。
Scopeの追加後には、「Install to Workspace」ボタンからインストールしましょう。
Bot Tokenが取得できるようになります。
これがLambda関数に設定したい SLACK_BOT_TOKEN
の値になります。
次に、Basic Informationを開きましょう。
「App Credentials」の中の、「Signing Secret」が SLACK_SIGNING_SECRET
の値になります。
template.yaml
を編集して環境変数として設定しましょう。今回は便宜上、値を直接記載しますが、うっかりコミットしないように気をつけてください。(この問題に関しては後述します)
@@ -20,6 +20,10 @@ Runtime: python3.9 Architectures: - x86_64 + Environment: + Variables: + SLACK_BOT_TOKEN: "${トークンの値}" + SLACK_SIGNING_SECRET: "${シークレットの値}" Events: SampleSAMSlackApp: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
次にSlackからのイベントを受け取るための設定をします。
「Event Subscriptions」を開き、Onにします。
「Request URL」にデプロイ時に出力されたAPI GatewayのURLを、パス(/slack/events
)まで含めて入力します。URLを入力した時点で、Slack側からVerifyのリクエストが発行され、うまくいっている場合はVerifiedと表示されます。
また、その下の「Subscribe to bot events」にも設定が必要です。「Add Bot User Event」ボタンを押して app:mention
を設定しておきましょう。
この設定を行なうと、画面下部の「Save Changes」が有効になり、設定を保存できるようになったと思います。
ここまで来ればもうSlack Appは動く状態になっています。任意のSlackチャンネルにSlack Appを追加して、メンションを飛ばしてみましょう。
SAMを用いてSlack botを作成したときに発生する可能性のある課題
次に、SAMを用いてSlack botを作成したときに発生する可能性のある課題について説明します。
- クレデンシャルをセキュアに設定する方法がわからない
- botのレスポンスが何回も実行される
クレデンシャルをセキュアに設定する方法がわからない
「Slack Appの設定」の項目では、 template.yaml
に直接クレデンシャルを記載していました。実際の本番アプリケーションとして運用する場合、クレデンシャルをコミットすることはセキュリティ上好ましくありません。
解決策
今回は、AWS System ManagerのParameter Storeを利用する方法を紹介します。
AWS Systems Manager の一機能である Parameter Store は、設定データ管理と機密管理のための安全な階層型ストレージを提供します。パスワード、データベース文字列、Amazon Machine Image (AMI) ID、ライセンスコードなどのデータをパラメータ値として保存することができます。
AWSのコンソールから手動で値を入れ、SAMのデプロイ時にCloudFormationの処理で値を解決するようにします。Parameter Storeを開き、一覧の右上にあるCreate parameterボタンをクリックして、パラメータを作成します。
次に template.yaml
を修正します。
(省略) Environment: Variables: SLACK_BOT_TOKEN: !Sub "{{resolve:ssm:/sample-sam-slack-app/slack-bot-token:1}}" SLACK_SIGNING_SECRET: !Sub "{{resolve:ssm:/sample-sam-slack-app/slack-signing-secret:1}}" (省略)
なお、CloudFormationの実行ユーザーがParameter Storeにアクセスできる必要があるため、ロールに適切なポリシーを設定してください。 https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/sysman-paramstore-access.html
bot のレスポンスが何回も実行される
今回設定した template.yaml
では、Lambdaのタイムアウトを3秒に設定していましたが、時間がかかる処理を行なう場合があるためいったん30秒に伸ばしてみます。
(省略) Globals: Function: Timeout: 30 MemorySize: 128 (省略)
その後、 app.py
を編集します。時間がかかる処理を擬似的に再現するために、time.sleep(10)
で10秒間のスリープを追加してみます。
(省略) @app.event("app_mention") def say_hello(event, say): time.sleep(10) user_id = event["user"] say(f"Hi, <@{user_id}>!") (省略)
その後変更をビルド・デプロイして、Slack Appにメンションを送ってみます。
すると、一度メンションを送っただけにも関わらず、3回もレスポンスが返ってきてしまいます。
https://api.slack.com/interactivity/handling#acknowledgment_response
This must be sent within 3 seconds of receiving the payload. If your app doesn't do that, the Slack user who interacted with the app will see an error message, so ensure your app responds quickly.
これはSlack側の仕様で3秒以内にSlack側からのリクエストにレスポンスを返すことができないと、エラーになるためです。エラーになった後は何度かリトライを行なう挙動になっているようです。
解決策
3秒以内にレスポンスを返す必要があるため、以下の二段階に処理を分けるような実装を考えます。
- Slackからのリクエストに対してひとまずすぐにレスポンスを返す
- 時間がかかる処理を行なってから、処理結果を含むレスポンスを返す
Bolt for PythonなどのBoltフレームワークでは1.を実現するために、 ack
という関数が実現されています。
(省略) @app.event("app_mention") def say_hello(event, say, ack): ack() time.sleep(10) user_id = event["user"] say(f"Hi, <@{user_id}>!") (省略)
ack
関数を用いた上記のコードは、うまく動作しない例です。 AWS Lambdaは一度レスポンスを返してしまうと、その後の処理が継続することがどうやら保証されていないため、ack()
で一度リクエストに対してレスポンスを返してしまうと、その後の処理が継続されないことがあるようです。
そこでBolt for Pythonに実装されているLazy Listnerを利用します。一度レスポンスを返した後に、処理を行なうための仕組みです。 https://slack.dev/bolt-python/ja-jp/concepts#lazy-listeners
まずは、ライブラリをインストールします。
$ pip install python-lambda
その後、Lambdaに適用しているIAMロールの権限として "lambda:InvokeFunction"
と "lambda:GetFunction"
を付与してやる必要があると公式ドキュメントには記載があります。これは、Lazy Listnerの実装で、もう一度Lambdaそのものを実行し直す実装になっているからだと思われます。
(省略) Resources: SampleSAMSlackAppFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: src/ Handler: app.lambda_handler Runtime: python3.9 Architectures: - x86_64 Environment: Variables: SLACK_BOT_TOKEN: !Sub "{{resolve:ssm:/sample-sam-slack-app/slack-bot-token:1}}" SLACK_SIGNING_SECRET: !Sub "{{resolve:ssm:/sample-sam-slack-app/slack-signing-secret:1}}" Role: !GetAtt SampleSAMSlackAppFunctionRole.Arn Events: SampleSAMSlackApp: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: Path: /slack/events Method: post SampleSAMSlackAppFunctionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: LambdaBasicExecution PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: "*" - PolicyName: LambdaInvokePolicy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - lambda:InvokeFunction - lambda:GetFunction Resource: "*" (省略)
このようにして明示的にIAM Roleを新しく定義し、Lambdaに指定するようにします。ログ出力のためにCloudWatch Logs用の権限も必要なため、これも含めて定義します。
その後、下記のようにコードも修正します。
(省略) def say_hello(event, say, ack): time.sleep(10) user_id = event["user"] say(f"Hi, <@{user_id}>!") app.event("app_mention")(ack=lambda ack: ack(), lazy=[say_hello]) (省略)
lazyの引数として、後から実行したい処理の関数を配列で渡してやります。
ただし、Lambdaの実行シチュエーションによっては、 そもそものack()を3秒以内に返せない場合もあります。 このようなときは、本質的ではありませんが、下記のようにSlackが送ってくるリトライリクエストを無視するという方法もあります。
(省略) def ignore_retry_request(request, ack, next): if "x-slack-retry-num" in request.headers: return ack() next() app.use(ignore_retry_request) (省略)
app.use()
を利用することでミドルウェア的にすべてのリクエストに対して処理を通すことができます。
SAM CLIでの後片付け
$ sam delete
上記のコマンドを実行することで、SAM CLIによって作成されたCloudFormationのスタックが削除されます。
Slackプラットフォーム上のSlack Appは手動で削除してください。
おわりに
以上、AWS SAMを用いてSlack botを作成する方法と、発生する可能性のある課題について紹介しました。AWS SAMを使用してSlack botを作成することで、サーバーレスアプリケーションの利点を活かしつつ、比較的簡単にbotを実装できます。しかし、クレデンシャル管理や非同期処理の扱いなど、いくつかの課題にも直面する可能性があります。これらの課題に対してはAWS Systems Manager Parameter Storeの活用やBolt for PythonのLazy Listenerの使用など、適切な解決策を実装することで対処できます。
本記事が、AWS SAMを用いたSlack bot開発の参考になれば幸いです。