【入門】Slack のコマンドを作ってみよう!(同期実行版)

Serverless Framework を利用したカスタム Slack コマンドの作り方を共有します。
2019.12.26

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

皆さん、Slack コマンドを作ったことはありますか?つい先日、Slack コマンドを作る機会があったのですが、Serverless Framework を利用することで、割と簡単に Slack コマンドを作成することが出来たので、備忘録として共有させていただきます。この冬休み(余暇)を利用して、Slack コマンドを作ってみてはいかがでしょうか?

その前に、「Slack コマンドってそもそも何なの?」という方に向けて簡単にご紹介しておきます。

Slack コマンドって何?

ビジネス向けのチャットツールとして知名度の高い Slack というチャットツールの中でメッセージ作成ボックスから文字列を入力することにより、アプリケーションを呼び出すことができる機能です。

アプリケーションを作成しなくとも、標準で Slack が用意しているコマンドが最初から利用できます。(ビルトインのスラッシュコマンドと呼ばれます)例えば、、、

  • /mute チャンネルをミュートする
  • /search [任意のテキスト] Slack のメッセージやファイルを検索する
  • /status ステータスを削除、または新しく設定する

などのコマンドが用意されており、他にも公式ページで紹介されています。

それでは、実際に作っていきましょう。

Slack コマンド作ってみた

本記事のタイトルにもあるとおり、入門編として一番簡単なパターンで実装していきます。 なお、筆者の環境は以下のとおりです。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.2
BuildVersion:   19C57
$ sls --version
Framework Core: 1.58.0
Plugin: 3.2.5
SDK: 2.2.1
Components Core: 1.1.2
Components CLI: 1.4.0
$ pipenv --version
pipenv, version 2018.11.26
$ aws --version
aws-cli/1.16.263 Python/3.7.4 Darwin/19.2.0 botocore/1.12.253

まずは、下記のリンクから Slack アプリを作成します。

表示されたウィンドウ内の App Name にアプリケーション名を、Development Slack Workspace に、このアプリケーションを利用する Slack のワークスペースを選択します。

1d4456f2bd58a964d6fb1b91c9b5ee19.png

定番ネタではありますが、Hello World アプリを作成します。Create App ボタンをクリックしましょう。

e776a0cafd394042e4b34d758f68919b-640x458.png

アプリケーションが作成されたら、Basic Information 画面が表示されます。まずはじめに必要な情報として、App Credentials の Signing Secret を取得します。少し下にスクロールしてください。

4a35a8d60d8c03d7233bdd3942b2b120-640x620.png

Signing Secret の横に設置された Show ボタンをクリックして、シークレット情報をコピーしておいてください。取得したシークレットは、Lambda から利用するために AWS Secrets Manager に保存します。利用している PC のターミナルを開いて、AWS CLI でシークレットを作成します。(acbde... となってる部分を、取得したシークレット情報に置き換えてください)

$ cat <<EOF > secret.json
{
    "key": "abcdefghijklmnopqrstu123456789"
}
EOF
$ aws secretsmanager create-secret \
--region ap-northeast-1 \
--name slack/secret \
--secret-string file://secret.json
{
    "ARN": "arn:aws:secretsmanager:ap-northeast-1:012345678901:secret:slack/secret-QF4hfm",
    "Name": "slack/secret",
    "VersionId": "8612e5ec-c620-4d4c-9249-xxxxxxxxxxxx"
}
$ rm -f secret.json

次に Slack コマンドからのリクエストを受け取る API Gateway と Lambda を作成していきます。必要なファイルは以下の2つだけです。GitHub に Push しておきましたので、clone してご利用ください。

$ git clone https://github.com/yuji-shimoda/hello-slack-command.git
  • handler.py
import os
import json
import boto3
import hmac
import hashlib
import datetime
from urllib import parse
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
TOKYO = 'ap-northeast-1'
# Get credentials
secret_name = os.environ['SLACK_API_SIGNING_SECRET']
secretsmanager = boto3.client('secretsmanager', region_name=TOKYO)
resp = secretsmanager.get_secret_value(SecretId=secret_name)
secret = json.loads(resp['SecretString'])


def verify(headers, body):
    try:
        signature = headers["X-Slack-Signature"]
        request_ts = int(headers["X-Slack-Request-Timestamp"])
        now_ts = int(datetime.datetime.now().timestamp())
        message = "v0:{}:{}".format(headers["X-Slack-Request-Timestamp"], body)
        expected = "v0={}".format(hmac.new(
                        bytes(secret['key'], 'UTF-8'),
                        bytes(message, 'UTF-8'),
                        hashlib.sha256).hexdigest())
    except Exception:
        return False
    else:
        if (abs(request_ts - now_ts) > (60 * 5)
                or not hmac.compare_digest(expected, signature)):
            return False
        return True


def request(event, context):
    if verify(event['headers'], event['body']):
        text = parse.parse_qs(event['body'])['text'][0]
        payload = {
            "text": 'Hello' + text,
        }
        response = {
            "statusCode": 200,
            "body": json.dumps(payload)
        }
        return response
    else:
        logger.info("Error: verify request")
        return {"statusCode": 400}
  • serverless.yml
service: hello
provider:
  name: aws
  runtime: python3.7
  stage: ${opt:stage, 'dev'}
  region: ${opt:region, 'ap-northeast-1'} 
  timeout: 30
  memorySize: 512
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "secretsmanager:GetSecretValue"
      Resource:
        - "*"
plugins:
  - serverless-python-requirements
functions:
  hello:
    handler: handler.request
    environment: 
      SLACK_API_SIGNING_SECRET: slack/secret
    events:
      - http:
          path: '/'
          method: post
custom:
  pythonRequirements:
    usePipenv: true

あとは、serverless-python-requirements プラグインや boto3 等の必要なライブラリをインストールしデプロイするだけです。

$ npm install -g serverless
$ brew install pipenv
$ sls plugin install -n serverless-python-requirements
$ pipenv install boto3
$ sls deploy
:
Serverless: Stack update finished...
Service Information
service: hello
stage: dev
region: ap-northeast-1
stack: hello-dev
resources: 10
api keys:
  None
endpoints:
  POST - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/
functions:
  hello: hello-dev-hello

Serverless Framework により、API Gateway と Lambda が作成されました。POST - の後にある URL(https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/)が API Gateway のエンドポイントになります。Slack アプリ側で設定を行うためメモしておきましょう。

再度、Slack アプリ側の設定ページに戻り Slack コマンドを作成していきます。

e5e66060aa6b67a5d520fe3c127d0067-640x458.png

Slash Commands をクリックします。

6c6fa8909c2a6dadd9b47315501631f0-640x213.png

Create New Command ボタンをクリックします。

6d797d797f539ef846525c647a85c149.png

必須項目を埋めていきましょう。

項目 入力値
Command コマンド名
Request URL API Gateway のエンドポイント
Short Description コマンドの簡単な説明

パラメータ入力後は、実際の利用イメージとしてプレビュー画面が更新表示されます。

f692ec19c7a71759b070d65a9e1d5a6f-640x235.png

Save ボタンをクリックしたら Slack コマンドの準備は完了です。 最後に Slack アプリをワークスペース内で有効化することで、コマンドが使用可能になります。

abb20f58a9ad825e8340cb3835ace016-640x185.png

左袖メニューの「Install App」から、「Install App to Workspace」ボタンをクリックして、権限のリクエストを許可してあげてください。

55eef1d42792a5b2fff08e670d9d3ddd.png

Slack のメッセージ作成ボックスから /hello と入力してみましょう。

149b14b202b9d3e24ca450a3dc9697b9-640x159.png

Slack コマンドにテキストを入力してみます。

8593d1ceeae5ecbcc7beeb01bc692709-640x62.png

送信ボタンを押すと

d6eb62be11b09233e9f72bc8a80e790c.png

"Hello" + 入力した文字列の応答が返ってきました。(半角スペースを入れるのを忘れてますが、、、(笑)

さいごに

こちらのソースコードをベースに Slack コマンドから何らかの入力値を受け取り、希望する処理を行うことも可能です。(先日、下記のようなコードを利用して雑に EC2 インスタンスを起動する Slack コマンドを試してみました。

    if verify(event['headers'], event['body']):
        instance_id = parse.parse_qs(event['body'])['text'][0]
        try:
            ec2 = boto3.client('ec2', region_name=TOKYO)
            ec2.start_instances(
                InstanceIds=[instance_id]
            )
        except Exception as e:
            logger.exception("ec2 start_instances {}".format(e))
        else:
            payload = {
                "text": 'EC2 を起動しました、しばらくお待ち下さい',
            }
            response = {
                "statusCode": 200,
                "body": json.dumps(payload)
            }
            return response

Slack のスラッシュコマンドの仕様上、3秒 以内に処理を完了し Slack コマンドへレスポンスを返却する必要があります。ただし、外部の API を叩いたり、複雑な仕事をさせなければ 3秒以内に処理を終えレスポンスを返却することは可能かと思われます。標準のアプリアイコンでは素っ気ないので、 Slack アプリアイコンを設定したり、Python コードを修正して何らかの処理を追加する等、自分なりの Slack コマンドを育ててみてはいかがでしょうか? もし、3秒以上掛かる処理を実行したい!(非同期実行版のブログ期待している)という方がおられましたら、次回策をご期待いただけますと幸いです。ではでは

参考文献