見出し画像

AWSを使ったslackチャットbotの構築

Arumon Advent Calendar 2021 の24日目の記事です。

はじめに

はじめまして。寺田です。
逃げてきた技術習得にようやく本腰を入れて、社内の活動に積極的に参加しようとしている最中、同期の前原に「Arumon Advent Calendar書いてみようぜ」と言われたのをきっかけに記事も書いたことがない、技術初心者の自分が一歩踏み出すことを決めました。
決意表明も兼ねて、対外発表初投稿ということで稚拙ではありますが、一本投稿させていただきます。
これがArumon入会の条件です。嘘です。

今回の内容を記事にする目的

今回の内容は社内のAWS re:Inventの振り返りと題したLT大会において、「AWS初心者がrecapLTのためにSlackChatbotを作ってみた」というタイトルで発表した内容の詳細を記載したものになります。
内容としてはありふれたものになってしまいますが、発表だけでは伝えることができなかった部分を伝えることや、同じ悩みを持ってる人の手助けになればと記事にすることにしました。

今回構築する構成

今回はAWSのAPIGatewayとLambda、Slackを使ってSlackで発した労いの言葉に反応して、チャットbotがお礼を言ってくれる構成にしました。
構成図は下図です。

  1. SlackからのOutgoingWebhookを使ってAPIGatewayに接続

  2. APIGatewayからのeventをトリガにLambdaが起動

  3. Lambdaのコードが実行されIncomingWebhookを使ってSlackにレスポンスを投げる

Webhookって?という方も少なからずいると思います。少なくとも私がそうでした。なので、簡単ではありますが、説明引用します…。

Webアプリケーションでイベントが実行された際、外部サービスにHTTP で通知する仕組み

https://kintone-blog.cybozu.co.jp/developer/000283.html

つまり、なにかイベントが実行されたら(今回だと特定の文言がSlack上に投稿されたら)、自動的に動いてくれる(自動的にAPIGatewayにリクエストしてくれる)仕組みです。
APIとの違いは?と皆さんはなるかわかりませんが、私はなりました。ので説明引用すると…

例えば、お気に入りのブログの記事が更新されたとすると、APIの場合はこちら側からリクエストをしないとレスポンスしてくれませんが、Webhookの場合は「更新されたら通知する」という設定をすると更新したことをトリガに通知をしてくれます。そのため、更新されるたびに通知が来るようになります。

https://it-kyujin.jp/article/detail/402/

作ってみた

Lambda→Slack

Webhookについては解決できたので、本題に戻り、実際に構築していきます。
まずは、Lambda→Slackから。
やること…
 ・SlackのIncomingWebhookの設定
 ・Lambdaの構築
 ・疎通確認

(1)SlackのIncomingWebhookの設定
Slack上でIncomingWebhookの設定をします。
ここはLambdaから送られてきたデータを受け取る部分になります。
IncomingWebhookの設定で発行されたURLをLambdaのコードで宛先に指定することで、LambdaからSlackにデータを送ることができるようになります。
「Slack>設定と管理>App管理 ⇒ 上部の検索窓」から「IncomingWebhook」と検索し、「Slackに追加」すると設定できるようになります。
機能を使うために以下をやっておきましょう。
 ・投稿するチャンネルと投稿する際の名前を指定する。
 ・webhookURLをメモしておく。

(2)Lambdaの構築
AWSでLambdaの構築を行います。
 ・一から作成を選択
 ・今回はpythonを使います
 ・作成
コードは技術力がないので書けないので他のWebサイトから引用してきたものを使います。
先程のIncomingWebhookの設定で取得したURLを宛先にするようにします。

import json 
import urllib.request 
import ssl 
 
# 内向きのウェブフックのURL ここに内向きのウェブフックで払い出したURLを指定する 
WEBHOOK_URL = "https://hooks.slack.com/services/XXXXXXXXXXXX" 
 
def lambda_handler(event, context): 
    # リクエストヘッダをJSONにする。 
    req_headers = { 
        "Content-Type": "application/json"
    } 
    # JSONにメッセージをつめる。 
    req_json = { 
        "text": "test"
    } 
 
    # リクエストを生成してSlackへ投げる。 
    req = urllib.request.Request(WEBHOOK_URL, json.dumps(req_json).encode(), req_headers) 
    urllib.request.urlopen(req) 
 

(3)疎通確認
Lambdaのテスト実行で今回の構成をテストしてみます。
Slack上に「test」と表示されれば成功です。

成功しました!!!
ここまででLambda→Slack間の接続は完了です。

Slack → APIGateway → Lambda → Slack

APIGatewayを入れて、完成させていきます。
やること…
 ・APIGatewayの構築
 ・OutgoingWebhookの設定
 ・Lambdaのコード修正
 ・疎通確認

(1)APIGatewayの構築
まずはAPIGatewayの構築です。
APIGatewayの作成は以下の手順で実施していきます。
 ・APIの作成
 ・リソースの作成
 ・メソッドの作成
 ・デプロイ ⇒ 最終的にデプロイされたときのURLにリクエストを飛ばす

APIの作成
リソースの作成
メソッドの作成
デプロイ

デプロイが完了すると、ステージというところに置かれます。
遷移して、メソッドごとにURLが発行されているためそれを(2)で使います。

ここで少し一工夫を入れないといけないです。
現在Lambdaが受け取れるのは「JSON」形式ですが、Slackから送られてくるのは「application/x-www-form-urlencoded」形式です。
そこで、APIGatewayで適切にマッピングさせてあげる必要があります。
そのときに利用するのが統合リクエスト機能です。
これもネットに落ちていたテンプレートをコピペして以下のように設定します。

## convert HTML POST data or HTTP GET query string to JSON

## get the raw post data from the AWS built-in variable and give it a nicer name
#if ($context.httpMethod == "POST")
 #set($rawAPIData = $input.path('$'))
#elseif ($context.httpMethod == "GET")
 #set($rawAPIData = $input.params().querystring)
 #set($rawAPIData = $rawAPIData.toString())
 #set($rawAPIDataLength = $rawAPIData.length() - 1)
 #set($rawAPIData = $rawAPIData.substring(1, $rawAPIDataLength))
 #set($rawAPIData = $rawAPIData.replace(", ", "&"))
#else
 #set($rawAPIData = "")
#end

## first we get the number of "&" in the string, this tells us if there is more than one key value pair
#set($countAmpersands = $rawAPIData.length() - $rawAPIData.replace("&", "").length())

## if there are no "&" at all then we have only one key value pair.
## we append an ampersand to the string so that we can tokenise it the same way as multiple kv pairs.
## the "empty" kv pair to the right of the ampersand will be ignored anyway.
#if ($countAmpersands == 0)
 #set($rawPostData = $rawAPIData + "&")
#end

## now we tokenise using the ampersand(s)
#set($tokenisedAmpersand = $rawAPIData.split("&"))

## we set up a variable to hold the valid key value pairs
#set($tokenisedEquals = [])

## now we set up a loop to find the valid key value pairs, which must contain only one "="
#foreach( $kvPair in $tokenisedAmpersand )
 #set($countEquals = $kvPair.length() - $kvPair.replace("=", "").length())
 #if ($countEquals == 1)
  #set($kvTokenised = $kvPair.split("="))
  #if ($kvTokenised[0].length() > 0)
   ## we found a valid key value pair. add it to the list.
   #set($devNull = $tokenisedEquals.add($kvPair))
  #end
 #end
#end

## next we set up our loop inside the output structure "{" and "}"
{
#foreach( $kvPair in $tokenisedEquals )
  ## finally we output the JSON for this pair and append a comma if this isn't the last pair
  #set($kvTokenised = $kvPair.split("="))
 "$util.urlDecode($kvTokenised[0])" : #if($kvTokenised[1].length() > 0)"$util.urlDecode($kvTokenised[1])"#{else}""#end#if( $foreach.hasNext ),#end
#end
}

(2)OutgoingWebhookの設定
Slackから出ていく側の設定をしていきます。
IncomingWebhookと同じようにSlackのApp管理から検索して「Slackに追加」します。
 ・投稿するチャンネル
 ・トリガ文言 ※空白はAny。Lambdaで制御。
 ・APIGatewayの宛先を指定します。 ※(1)で取得済み
この設定を行うことで「対象のチャンネルに、どんな言葉でもいいから投稿されたら自動的にAPIGateway宛にリクエストを投げる」というWebhookの機能が使えるようになります。

(3)Lambdaのコード修正
APIGatewayとOutgoingWebhookの設定が完了したので、投稿されたら自動的にAPIGatewayを通ってLambdaまでデータが届いている状態です。
では最後にLambdaのコードを修正して、「どんな言葉」ではなく「労いの言葉」に対してのみ反応するようにしていきます。
具体的には以下のコードを使いますが、そもそも「lambda_handler」ってなんだろって疑問に思ったことはありませんか?
もし思っている方がいましたら、大丈夫です、私もわからなかったので、説明します。

def lambda_handler(event, context): 

公式には以下のように記載があります。

AWS公式ページ

つまり、Lambdaが呼ばれたらとりあえずここの「lambda_handler」が動くよということ。
ちなみに、この部分はランタイムメソッドで定義されていて、実は名前を変更することができます。
ハンドラに記載の内容は、「lambda_function.pyのlambda_handler」という意味なので、この関係性を崩さなければ「terada.pyのterada_handler」にだってできます。このときハンドラは「terada.terada_handler」とします。ハンドラ、ソースファイル名、メソッド名のいずれかでも整合性が合わないと機能しません。

じゃあ2つの引数(event, context)の意味は何なんだろう…

eventにはJSON形式のデータが乗ってきます。つまり今回だとSlackから投げられた内容がAPIGatewayで変換されてJSONになります。このJSONデータがeventに該当します。
contextはあまり利用しないみたいですが、IDなどの情報が入ってくるみたいです。詳しくはここに。

Lambdaの説明が若干長くなりましたが、以下がコードです。
がんばれ,やるじゃん,いいね,ナイス,おつかれ」に反応してくれるbotができました。

import json 
import urllib.request 
import ssl
import logging
 
# 内向きのウェブフックのURL 
WEBHOOK_URL_slack = "https://hooks.slack.com/services/XXXXXXXXXXXXX"

# CloudwatchLogsのログ記憶ライブラリの使用
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context): 

    # CloudwatchLogsで詳細なログを取得
    logging.info(json.dumps(event))
    
    if 'user_name' not in event:
        msg = '??????'
    else:
        user_name = event['user_name']
        text = event['text']
        user_id = event['user_id']
        msg = '<@' + user_id + '>'
        
        # Slackbotの言葉には反応しないようにする
        if user_name == 'slackbot':
            return 0
        else:
            if 'がんばれ' in text:
                msg += 'がんばります!'
            elif 'やるじゃん' in text:
                msg = 'え。まじですか。嬉しい。'
            elif 'いいね' in text:
                msg += ':+1:'
            elif 'ナイス' in text:
                msg += 'ありがとうございます:star-struck:'
            elif 'おつかれ' in text:
                msg += 'さんもおつかれさまです!!'
            else:
                msg = '労ってくれると嬉しいです…!'
    
    # リクエストヘッダをJSONにする。
    req_headers = { 
        "Content-Type": "application/json", 
    } 

    # JSONにメッセージをつめる。 
    req_json_slack = { 
        "text": msg 
    } 

    #リクエストを生成してSlackへ投げる。 
    req_slack = urllib.request.Request(WEBHOOK_URL_slack, json.dumps(req_json_slack).encode(), req_headers) 
    urllib.request.urlopen(req_slack) 

(4)疎通確認
早速実行します!
ちゃんと反応してくれました!

ブラッシュアップ

LT-demoさんの写真が可愛くないので、変えてみたいと思います。
この内容は発表でもしてなかったので、やり方をサーチしました…!
と言ってもかなり直感的にできて、先程設定したIncomingWebhookの「アイコンをカスタマイズする」という項目で設定します。

Mr.childrenが大好きなので、著作権的にどうかと思いますが、ミスチルくんを設定します。

設定後、投稿してみると…

できました!!

おわりに

今回始めて記事を書いてみての感想
 ・人に伝えるって難しい。何が必要で何が冗長なんだろう…
 ・これが人に読まれて色んな意見をもらえたら楽しんだろうなあ。と。
  皆さんお願いします。
ということで、とても良い経験をすることができました。
今後もArumonの一員として(ちゃんと合格したかな…?)自分なりにがんばっていきたいと思います!

最後までお読みいただきありがとうございました。




この記事が気に入ったらサポートをしてみませんか?