Retty Tech Blog

実名口コミグルメサービスRettyのエンジニアによるTech Blogです。プロダクト開発にまつわるナレッジをアウトプットして、世の中がHappyになっていくようなコンテンツを発信します。

API Gateway と Lambda で deploy bot を作った

この記事は Retty Advent Calendar Part2の25日目の記事です。 Part1はこちらです。

はじめに

Retty インフラチームの中西です。

Retty のサービスでは主にAWS ECSを利用しており、リリース作業の際は Slack チャンネル上でメッセージを入力するとBot がデプロイを実行するというChatOps での運用を行っています。

このデプロイを行う Slack Bot (deploy bot) には課題、懸念点がありました。

deploy bot の課題

既存のdeploy bot には以下の課題がありました。

  • EC2 インスタンスで稼働している
    • シングル構成となっているのでインスタンスに不具合があればリリース作業に影響がある
  • Slack カスタムインテグレーションでの作成となっており現在は非推奨になっている
    • 将来、強制的に廃止される可能性が高い

解決策として、サーバレスでの稼働とカスタムインテグレーションに替わる Slack Api での再作成を行うために Amazon API gateway + Lambda を採用しました。

Amazon API gateway + Lambda の採用に関しては参考記事をはじめ、
設定事例が多かったのが決めてとなりました。

deploy bot で行うこと

既存の deploy bot が行っている処理の踏襲となりますが、要件をまとめました。

  • Slack チャンネル上での Bot へのメンション時に特定のメッセージ(コマンド)に反応して以下の処理を行う
    • 1.デプロイ対象のコンテナのイメージタグの一覧を表示する
    • 2.デプロイ用の コンテナのイメージタグを設定する
    • 3.デプロイ用のイメージタグ設定後にECSサービスのアップデートを行う
      • 実行前に Yes , No のボタンを表示してデプロイ実行者に確認を促すようにする

行ったこと

基本的に参考記事と同じとなります。 (非常に懇切丁寧な内容で大変助かりました)

1 . Lambda function の作成

作成後のRole に必要なIAMポリシーを付与します。 今回はECR、ECSの操作権限を付与しました。

またコードは Slack App 作成前に作成をしておきます。 以下は一部となります。

def lambda_handler(event, context):
    # 受信データをCloud Watchログに出力
    logging.info(json.dumps(event))

    # SlackのEvent APIの認証
    if "challenge" in event:
        return event["challenge"]

    # tokenのチェック
    if not is_verify_token(event):
        return "OK"    

    # ボットへのメンションでない場合
    if not is_app_mention(event):
        return "OK"    
    
    channel = event.get("event").get("channel")
    sgmes = event.get("event").get("text")
    sguser = event.get("event").get("user")
    
    # メッセージ毎に処理を行う
    if "show-ecr-tags" in sgmes:
        show_ecr_tags(channel)
        return 'OK'

    elif "set-release-version-tag" in sgmes:
        set_relese_version_tags(channel)
        retrun 'OK'

    elif "release-to-ecs-production" in sgmes:
        slack.post_message_to_button(event.get("event").get("channel"))
        return 'OK'

    else:
        message = "usage..."
        slack.post_message_to_channel(channel, message)

    return 'OK'

2. API Gateway の作成

1 で作成した Lambda function からトリガーを設定します。


デフォルトでANYメソッドが作成されていますが、今回は不要です。 ANYメソッドを削除して POSTメソッド単体を作成します。


アクション メニューからAPIのデプロイ を行います。


デプロイ後のURLは Slack App 作成時に必要となるので控えておきます。


3. Slack Api の作成

Slack Api にアクセスして Create New App を行います。

  • From scratch を選択します。


App 作成後に Basic Information を選択して Verification Token の値を控えます


Event Subscriptions を選択して以下の設定を行います。

  • Enable Events を On
  • Request URL に API Gateway でデプロイした URLを入力
  • Subscribe to bot events で Add Bot User Event ボタンから app_mention を選択、追加
  • Save Changes で保存


OAuth & Permissions で以下の設定を行います。

  • Scopes の Add an OAuth Scope ボタンから chat:write を選択、追加
  • Bot User OAuth Token の値を控える
  • Install to Workspace 又は Reinstall to Workspace ボタンを選択して Slackワークスペースへのインストールを行う


アクセス権限のリクエストを確認して 許可する ボタンを選択します。


4. Lambda の設定、動作確認

3.の Slack Api 作成時に控えた値を環境背変数として設定します。

  • SLACK_BOT_USER_ACCESS_TOKEN: Bot User OAuth Token の値
  • SLACK_BOT_VERIFY_TOKEN: Verification Token の値

1.の Lambda function の作成で予めコードは作成してるので動作確認を行います

これで要件で書いた 1.デプロイ対象のコンテナのイメージタグの一覧を表示する の処理が確認できました


Slack Interactive Messages の設定

上記 1 - 4 の操作で 要件 1 と 2は実装できるようになりました。 3.デプロイ用のイメージタグ設定後にECSサービスのアップデートを行う には以下の処理が必要になります

  • YesNo 操作確認を行うボタンを表示する
  • 押したボタンの内容によって処理を行いレスポンスを返す

ボタンの表示

ボタンの表示はドキュメントを参考に json 内の attachments: で作成しました。

import json
import urllib.request
import os
import logging

def post_message_to_button(channel):
 
    url = "https://slack.com/api/chat.postMessage"
    headers = {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer {0}".format(os.environ["SLACK_BOT_USER_ACCESS_TOKEN"])
    }
    data = {
        "token": os.environ["SLACK_BOT_VERIFY_TOKEN"],
        "channel": channel,
        #"text": message,
        "attachments": [
        {
            "text": "Do you want to release ? (Yes or No)",
            "fallback": "no_push_button",
            "callback_id": "release_button",
            "color": "#3AA3E3",
            "attachment_type": "default",
            "actions": [
                {
                    "name": "Yes",
                    "text": "Yes",
                    "type": "button",
                    "value": "Yes-release",
                    "action_id": "button",
                    "confirm": {
                        "title": "reconfirmation",
                        "text": "Do you really want to do it?",
                        "ok_text": "Yes",
                        "dismiss_text": "No"
                    }
                },
                {
                    "name": "No",
                    "text": "No",
                    "type": "button",
                    "value": "No-release",
                    "action_id": "button"
                },

            ]
        }
        ]
    }
    
    req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), method="POST", headers=headers)
    urllib.request.urlopen(req)

これで YesNo ボタンが表示されました。

Yes ボタンを押すと再確認のメニューも表示されてます。


Slack Interactive Component の設定

ボタンを押した後のレスポンスを設定する為に再度作成した Slack Api で Interactive Component の設定を行います。

Interactivity & Shortcuts を選択して Interactivity を On にします。
ここで Request URL の設定が必要となりますが、このURLにボタンを押された際にリクエストが送信されます。

Request URL 用のAPI Gateway + Lambda の作成

最初に作ったものとは別の Lambda Function を作成します。
また環境変数SLACK_BOT_VERIFY_TOKEN を設定します。

API Gateway は同じものを設定して別のリソースを作成します。
注意点として POST メソッド作成時は Lambda proxy を有効にする必要があります。
(有効にしないとリクエスト内のボタンを押した時のデータが受信できませんでした)

Lambda function での実装

以下の様な処理となります。

  • リクエストデータを整形
  • 整形したデータから token が一致しているか確認
  • ボタンを押した時の値を確認して処理を実施
  • 正常時に http status 200 を返す
    • 200を返さないと Slack のチャンネル上でエラーが表示された為

以下は一部コードのサンプルです

def lambda_handler(event, context):

    ## slackからのデータを整形
    decode_body = urllib.parse.unquote(json.dumps(event['body']))
    data = decode_body[1:-1].replace('payload=',"")
    dict_data = json.loads(data)
    response_url = dict_data['response_url']
    channel = dict_data['channel']['name']
    button_value = dict_data['actions'][0]['value']
    token = dict_data['token']

    ## slack token を確認
    if not is_verify_token(token):
        return "NG"
    
    ## button タップ時の値を取得して処理を行う
    if "Yes-release" in button_value:
        
        ecs_release()
        message = "OK. ECS release is started."
        
        post_message_response_button(response_url,channel, message)
        
        return {'statusCode': 200,}


    elif "No-release" in button_value:
        message = "ECS release is cancel."
        post_message_response_button(response_url,channel, message)
        return {'statusCode': 200,}

    else:
        message = "other response"
        post_message_response_button(response_url,channel, message)
        return {'statusCode': 200,}

動作確認

これでボタンを押した時にメッセージが表示されるようになりました。

Yes を選択した場合


No を選択した場合

残作業としては、ボタン毎にECSデプロイといった任意の処理を実装していけば作成は完了となります。

最後に

deploy bot のリプレースは行おうと考えていたのですが優先順位の問題で後回しになっていました。 アドベントカレンダーの題材として一気に進めることができて良かったです。

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

参考記事

AWS(API Gateway + Lambda(Python)) + Slack APIを使ったBot作成 https://nmmmk.hatenablog.com/entry/2018/10/10/001548