LoginSignup
5
4
この記事誰得? 私しか得しないニッチな技術で記事投稿!

discord の slash commands からEC2を操作できるようにしてみた

Posted at

はじめに

はじめまして、@nahiro_tus です。突然ですが、皆さんはdiscordのslash commands機能をご存知ですか?

これはdiscordにコマンドを覚えさせることができる機能で、入力時にコマンドを補完をしてくれたり、コマンドに説明を追加したりできます。
今までのdiscord botはコマンドを覚えて根性で入力するしかありませんでしたが、この機能の登場によりbotを操作するハードルがグッと下がりました。今回はそんなslash commandsを利用してEC2の起動、停止、状態確認を実行できる機能を開発しました。

構成図

Untitled (1).png
Lambda関数を分けて多段Lambda構成で実行しています。EC2の起動・停止・状態確認の処理に時間がかかるので、一度appでメッセージを返したあと、各Lambda関数から処理完了メッセージを送っています。具体的な処理内容については後述します。
この方法はクラスメソッドさんの記事を参考にしました。

実装

コード全文はこちらに上げています。(ちょくちょく改良しているので、若干変わっているかもしれません。)

前提条件は以下の様になっています。

  • デプロイ可能なaws cliが設定されている
  • 制御対象のEC2インスタンスがすでに作成されている

Discord Applications を作成する

portalページからbotを作成します。

「New Application」をクリックして新規アプリを作成します。

Untitled (2).png

General Information で以下の2つを控えておきます。

  • APPLICATION ID
  • PUBLIC KEY

Untitled (3).png

bot から create tokenを選択して、TOKEN を発行します。これも後ほど使うので控えておいてください。
(参考画像では、すでにtokenを発行していたのでReset tokenになっています。)

Untitled (4).png

slash commands の登録

discordはslash commandsの登録がAPIでしか行えないので、以下のコードを実行してコマンドを登録します。

register-slash-commands.py
import requests
import os
import json

commands = {
    "name": "hello",
    "description": "control ec2",
    "options": [
        {
            "name": "action",
            "description": "start/stop/status",
            "type": 3,
            "required": True,
            "choices": [
                {"name": "start", "value": "start"},
                {"name": "stop", "value": "stop"},
                {"name": "status", "value": "status"},
            ],
        },
    ],
}

def main():
    url = f"https://discord.com/api/v10/applications/{os.environ['APPLICATION_ID']}/commands"
    headers = {
        "Authorization": f'Bot {os.environ["BOT_ACCESS_TOKEN"]}',
        "Content-Type": "application/json",
    }
    res = requests.post(url, headers=headers, data=json.dumps(commands))
    print(res.content)

if __name__ == "__main__":
    main()
export BOT_ACCESS_TOKEN="発行したアクセストークン"
export APPLICATION_ID="**APPLICATION ID**"
python register-slash-commands.py

AWSへデプロイ

今回はserverless frameworkを使用してデプロイします。
serverless frameworkの説明は割愛しますが、簡単に言うとサーバレスアプリケーションの構成管理・デプロイツールです。詳しくはこちら
以下のコマンドをserverless.ymlがある環境で実行します。

npm install -g serverless
sls plugin install -n serverless-python-requirements
export BOT_ACCESS_TOKEN="発行したアクセストークン"
export APPLICATION_ID="控えておいたAPPLICATION ID"
export INSTANCE_ID="操作したいインスタンスのID"
export PUBLIC_KEY="控えておいたPUBLIC KEY"
sls deploy

デプロイに成功するとAPI gatewayのエンドポイントが発行されます。
Untitled (5).png

こちらのURLをdiscord bot の INTERACTIONS ENDPOINT URL に登録します。

Untitled (6).png

保存ボタンを押すとエンドポイントの認証確認が走ります。問題なければ、以下のような表示が出て設定が保存されます。
Untitled (8).png

discordサーバへの招待

botapplications.commands をつけて招待URLを発行します。

発行されたURLにアクセスして、サーバーにbotを招待します。

Untitled (7).png

動作確認

インスタンスが起動しているか確認しましょう。
/hello status と実行します。
Untitled (9).png
EC2が停止していることが確認できます。
それでは、EC2を起動します。/hello startと打ちます。
EC2のステータスチェックが完了するとパブリックIPが送られてきます。

Untitled (10).png

マネジメントコンソールからも確認してみます。

Untitled (11).png

指定したインスタンスIDのインスタンスが起動しています。
最後に、EC2を停止します。/hello stopと打ちます。

Untitled (13).png

停止しました。マネジメントコンソールからも確認します。

Untitled (12).png
きちんと停止していますね。

コード解説

量が多いので、Lambdaの非同期呼び出し部分とEC2の制御、非同期処理完了時のメッセージ送信3つにしぼって解説します。

他Lambda関数の非同期呼び出し

このアプリはappが別のlambda関数を呼び出す構成で成しています。スラッシュコマンドが実行されたとき、appがアクション(startやstopなど)の内容を読み取って、呼び出すlambda関数を分けています。
他のlambda関数の呼び出しにはboto3を利用しています。boto3はAWSのリソースを操作するPythonのライブラリの1つで、lambdaにはデフォルトでインストールされています。

app.py
token = req.get("token", "")
parameter = {
    "token": token,
    "DISCORD_APP_ID": os.environ["APPLICATION_ID"],
}
payload = json.dumps(parameter)
boto3.client("lambda").invoke(
    FunctionName="discord-slash-command-dev-minecraft-ec2-start",
    InvocationType="Event",
    Payload=payload,
)
# async_ec2_start()
text = "hi " + username + ", server starting up …"

boto3でlambda関数を実行するとき、InvocationTypeEventにすることで非同期で別のLambda関数を呼び出すことができます。呼び出すlambda関数に値を渡したいときは、Payloadに値を渡してあげると呼び出されたLambda関数ハンドラのeventに値が格納されます。
また、slash commandsは一定時間内にレスポンスを返さないとエラーになるため、一度このLambda関数でメッセージを返してあげます。

app.py
return {
            "type": 4,  # InteractionResponseType.ChannelMessageWithSource
            "data": {"content": text},
        }

EC2の制御

各Lambda関数ではboto3を用いてEC2の起動・停止・状態の読み取りを行っています。
下のコードはEC2インスタンスを起動する部分です。wait_until_runnning()でEC2が起動するまで待っています。

minecraft-ec2-start.py
response = ec2_client.start_instances(InstanceIds=[instance_id])
            print(response)
            ec2_resource.wait_until_running()

起動時はインスタンスのインスタンスステータスとシステムステータスがOKになるまで待機しています。

minecraft-ec2-start.py
while cont:
  status_response = ec2_client.describe_instance_status(
      InstanceIds=[instance_id]
  )
  if (
      status_response["InstanceStatuses"][0]["InstanceStatus"][
          "Status"
      ]
      == "ok"
      and status_response["InstanceStatuses"][0]["SystemStatus"][
          "Status"
      ]
      == "ok"
  ):
      cont = False
  else:
      time.sleep(10)
      total += 10
return {"status": 0, "ip": ec2_resource.public_ip_address}

非同期処理完了時のメッセージ送信

discord は https://discord.com/api/v10/webhooks/{application_id}/{interaction_token}にリクエストすることで一時的に返したメッセージへ返信することができます。

minecraft-ec2-start.py
requests.post(
        url=f"https://discord.com/api/v10/webhooks/{application_id}/{interaction_token}",
        data=payload,
        headers={
            "Content-Type": "application/json",
        },
    )

まとめ

今回はslash commandsからEC2の起動・停止・状態確認を行えるようにしました。今までのdiscord botはサーバを1台立てて常にdicordと接続している必要がありましたが、slash commandsの登場により大幅にコストを抑えれるようになりました。元々こちらの記事で書いている制御用サーバのサーバーレス化のためにしたものですが、作っていると他にもいろんなことに流用できそうだと思いました。

今後は

  • minecraftのサーバの起動・停止が行えるようにする
  • minecraftサーバの構築、discord用エンドポイントの構築までterraformコマンドで一発デプロイできるようにする

を予定しています。

参考

5
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4