はじめに
はじめまして、@nahiro_tus です。突然ですが、皆さんはdiscordのslash commands機能をご存知ですか?
これはdiscordにコマンドを覚えさせることができる機能で、入力時にコマンドを補完をしてくれたり、コマンドに説明を追加したりできます。
今までのdiscord botはコマンドを覚えて根性で入力するしかありませんでしたが、この機能の登場によりbotを操作するハードルがグッと下がりました。今回はそんなslash commandsを利用してEC2の起動、停止、状態確認を実行できる機能を開発しました。
構成図
Lambda関数を分けて多段Lambda構成で実行しています。EC2の起動・停止・状態確認の処理に時間がかかるので、一度appでメッセージを返したあと、各Lambda関数から処理完了メッセージを送っています。具体的な処理内容については後述します。
この方法はクラスメソッドさんの記事を参考にしました。
実装
コード全文はこちらに上げています。(ちょくちょく改良しているので、若干変わっているかもしれません。)
前提条件は以下の様になっています。
- デプロイ可能なaws cliが設定されている
- 制御対象のEC2インスタンスがすでに作成されている
Discord Applications を作成する
portalページからbotを作成します。
「New Application」をクリックして新規アプリを作成します。
General Information で以下の2つを控えておきます。
- APPLICATION ID
- PUBLIC KEY
bot から create tokenを選択して、TOKEN を発行します。これも後ほど使うので控えておいてください。
(参考画像では、すでにtokenを発行していたのでReset tokenになっています。)
slash commands の登録
discordはslash commandsの登録がAPIでしか行えないので、以下のコードを実行してコマンドを登録します。
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のエンドポイントが発行されます。
こちらのURLをdiscord bot の INTERACTIONS ENDPOINT URL に登録します。
保存ボタンを押すとエンドポイントの認証確認が走ります。問題なければ、以下のような表示が出て設定が保存されます。
discordサーバへの招待
bot と applications.commands をつけて招待URLを発行します。
発行されたURLにアクセスして、サーバーにbotを招待します。
動作確認
インスタンスが起動しているか確認しましょう。
/hello status
と実行します。
EC2が停止していることが確認できます。
それでは、EC2を起動します。/hello start
と打ちます。
EC2のステータスチェックが完了するとパブリックIPが送られてきます。
マネジメントコンソールからも確認してみます。
指定したインスタンスIDのインスタンスが起動しています。
最後に、EC2を停止します。/hello stop
と打ちます。
停止しました。マネジメントコンソールからも確認します。
コード解説
量が多いので、Lambdaの非同期呼び出し部分とEC2の制御、非同期処理完了時のメッセージ送信3つにしぼって解説します。
他Lambda関数の非同期呼び出し
このアプリはappが別のlambda関数を呼び出す構成で成しています。スラッシュコマンドが実行されたとき、appがアクション(startやstopなど)の内容を読み取って、呼び出すlambda関数を分けています。
他のlambda関数の呼び出しにはboto3を利用しています。boto3はAWSのリソースを操作するPythonのライブラリの1つで、lambdaにはデフォルトでインストールされています。
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関数を実行するとき、InvocationType
をEvent
にすることで非同期で別のLambda関数を呼び出すことができます。呼び出すlambda関数に値を渡したいときは、Payload
に値を渡してあげると呼び出されたLambda関数ハンドラのevent
に値が格納されます。
また、slash commandsは一定時間内にレスポンスを返さないとエラーになるため、一度このLambda関数でメッセージを返してあげます。
return {
"type": 4, # InteractionResponseType.ChannelMessageWithSource
"data": {"content": text},
}
EC2の制御
各Lambda関数ではboto3を用いてEC2の起動・停止・状態の読み取りを行っています。
下のコードはEC2インスタンスを起動する部分です。wait_until_runnning()
でEC2が起動するまで待っています。
response = ec2_client.start_instances(InstanceIds=[instance_id])
print(response)
ec2_resource.wait_until_running()
起動時はインスタンスのインスタンスステータスとシステムステータスがOK
になるまで待機しています。
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}
にリクエストすることで一時的に返したメッセージへ返信することができます。
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コマンドで一発デプロイできるようにする
を予定しています。
参考