Discord から Amazon EC2 の起動・停止を管理する【コード・テンプレート付き】

こんにちは、SCSKのひるたんぬです。

もう4月になりますね…2024年度の幕開けです。
昨年の4月に新入社員としてSCSKに入社し社会人デビューをした身としては、あっという間の一年だったなぁ…と思うと同時に、「もう後輩が来るんですか!?」というのが正直なところです。少しでも先輩らしい(?)振る舞いができるようになりたいものです。

今回は、仕事上…というよりもプライベートで取り組んでみたことについてご紹介いたします。

背景

私は学生時代の友人と、「まれによく¹」Minecraftをして遊びます。
学生時代はサーバやインフラに関する知識などは全くと言っていいほどなかったため、
月額固定料金のサーバを契約 → Minecraftサーバとしての初期設定 → 一定期間遊ぶ → 飽きる → 解約する
を繰り返していました。この「まれによく」という頻度がくせ者でして、

  • よく遊ぶ…固定料金を契約して好きなだけ遊ぶ
  • まれに遊びたくなる…その気持ちをこらえて、忘却を待つ

とできないのです。皆さんも今回の話に限らず、このようなことありませんか…?
ある日、私がクラウド関係の仕事をしていると友人に話したところ、

マイクラ(Minecraft)って、急に無性に遊びたくなるんだよねぇ…
今までみたいにいちいち契約して設定して使うっていうのは面倒くさい・時間かかるし、かと言ってずーーっと契約して遊ぶほどでもないんだよね。お金の無駄だし。なんとかならない??
※若干脚色しております。
…なんとわがままなのでしょうか。
しかし、言っていることはごもっともであり、そういうことなら何か仕組みを作ってみよう!ということで、今回の取り組みが幕を開けました。

¹ 「まれによく」という言葉は正しい日本語ではないですが、この頻度を適切に表す表現が他に見当たらないためこの表現を用いています。こちらの図解「1. よくある(頻発する)ことが稀に起こる状態」が私が表現したい頻度です。

 

取り組み概要

今回は「友人と普段の通話やチャットで用いているDiscord」と「私が業務で触れているAWS」を組み合わせて、先程の友人のわがままを叶えようと考えました。調べたところ、既に同様のことに取り組まれている方々がおり、それらを参考に以下のようなアーキテクチャを構築しました。

ここでの特徴は「Lambdaが多段構成」となっている点です。このようにすることで、EC2に関する処理に時間がかかってもタイムアウトとなることを防いでいます。

もう少し詳しく説明いたします。
通常は下図に示すアーキテクチャが一般的だと思われます。

こちらの場合ですと、「App」のLambdaがServerとのやり取りをし、その結果をAPI Gateway経由でDiscordに返すという処理の流れになります。しかし、Discordのドキュメントを確認すると、

Interaction tokens are valid for 15 minutes and can be used to send followup messages but you must send an initial response within 3 seconds of receiving the event. If the 3 second deadline is exceeded, the token will be invalidated.
引用元:Discord Developper Potal 「Interactions: Receiving and Responding

と記載があります。つまり、Discordに対して3秒以内に一度応答を返さないとエラーになってしまうのです。そのため、通常の構成ですとEC2の起動や終了の命令を出すことはできても、EC2側においてそれらの処理が正常に完了したかを確認する時間がありません。

そこで、最初に示したようにLambdaを多段構成にすることで、「App」が対応するLambdaを非同期で呼び出し、仮の応答(ACK)をDiscordに返します。そして、対応するLambdaがそれぞれの処理を実行し、メッセージを更新するという形で仮の応答を更新します。これにより、タイムアウトのエラーを防ぎ、適切な応答を返すことができるようになります。

記事の構成上、参考サイト等は記事の末尾に記載しております。

 

構築手順

ここからはアーキテクチャの構築手順について、Discord Botの作成からAWSでのリソース構築までを画像での解説を交えながら順に説明いたします。
少々長い内容とはなりますが、最後までお付き合いいただけますと幸いです。

Discord Botの作成

Discord Developer Portalにアクセス

  1. 「New Application」を押下し、Botのアカウント名を入力し「Create」を押下する。
  2. 遷移先の画面(General Infomation)にて、
    ・APPLICATION ID
    ・PUBLIC KEY
    を控えておく。
  3. 左ペインより「Bot」を選択し、「Reset Token」を押下してトークンを発行する。
    発行されたトークンを控えておく。
                          

    このタイミングで「USERNAME」を編集しても問題ありません。
    ここで設定する名前が、Discordの表示名となります。
    発行されたトークンは画面遷移などにより再表示できなくなる場合があるので、発行したタイミングで必ず控えておくようにしてください。
    控え忘れてしまった場合は再度「Reset Token」を押下してトークンを発行してください。
  4. スラッシュコマンドの登録
    実際にDiscordからBotを呼ぶ際に、多くの方は「/XXXXX」という形式でコマンドを入力すると思います。

    今回作成するBotもこのような形で利用するため、コマンドの登録を行います。
    以下のコードの「認証情報」を自身の情報に置換した上、Python(実行環境は問いません)で実行してください。
    “Registration Completed!!”と表示されれば正常に処理が行われています。                                      

    # register-slash-commands.py
    
    import requests
    import json
    
    # 認証情報
    BOT_ACCESS_TOKEN = "トークンを記載"
    APPLICATION_ID = "APPLICATION IDを記載"
    
    commands = {
        "name": "server",
        "description": "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/{APPLICATION_ID}/commands"
        headers = {
            "Authorization": f'Bot {BOT_ACCESS_TOKEN}',
            "Content-Type": "application/json",
        }
        try:
            res = requests.post(url, headers=headers, data=json.dumps(commands))
            res.raise_for_status()
            print("Registration Completed!!")
        except Exception as e:
            print("Error occurred.", e)
    
    if __name__ == "__main__":
        main()
     

リソースの構築

事前準備

今回は前提条件として、サーバとなるEC2インスタンスは既に作成されているものとします。
そのため、この時点でEC2インスタンスを作成していない方は作成してください。

今回の要件においては、EC2インスタンスを作成するのみで問題ありません。
サーバとしての設定等は不要です。
また、下記に示す4つのPythonコードを指定名で保存し、一つのzipファイルに圧縮してください。zipファイル名は任意です。
そして、そのzipファイルを適当なS3バケットにアップロードしてください。
※…コメントアウト等で処理内容を残すべきですが、コードの量が多いため削除しております。
# ファイル名 -> app.py

import os
import json
import boto3
import logging
from nacl.signing import VerifyKey
logger = logging.getLogger()
logger.setLevel("INFO")

def lambda_handler(event: dict, context: dict):
    try:
        headers: dict = {k.lower(): v for k, v in event["headers"].items()}
        rawBody: str = event.get("body", "")
        signature: str = headers.get("x-signature-ed25519", "")
        timestamp: str = headers.get("x-signature-timestamp", "")
        if not verify(signature, timestamp, rawBody):
            return {
                "cookies": [],
                "isBase64Encoded": False,
                "statusCode": 401,
                "headers": {},
                "body": "",
            }
        req: dict = json.loads(rawBody)
        if req["type"] == 1:
            return {"type": 1}
        elif req["type"] == 2:
            action = req["data"]["options"][0]["value"]
            if action == "start":
                token = req.get("token", "")
                parameter = {
                    "token": token,
                    "DISCORD_APP_ID": os.environ["APPLICATION_ID"],
                }
                payload = json.dumps(parameter)
                boto3.client("lambda").invoke(
                    FunctionName="DiscordSlashCommandController-Start",
                    InvocationType="Event",
                    Payload=payload,
                )
            elif action == "stop":
                token = req.get("token", "")
                parameter = {
                    "token": token,
                    "DISCORD_APP_ID": os.environ["APPLICATION_ID"],
                }
                payload = json.dumps(parameter)
                boto3.client("lambda").invoke(
                    FunctionName="DiscordSlashCommandController-Stop",
                    InvocationType="Event",
                    Payload=payload,
                )
            elif action == "status":
                token = req.get("token", "")
                parameter = {
                    "token": token,
                    "DISCORD_APP_ID": os.environ["APPLICATION_ID"],
                }
                payload = json.dumps(parameter)
                boto3.client("lambda").invoke(
                    FunctionName="DiscordSlashCommandController-Status",
                    InvocationType="Event",
                    Payload=payload,
                )
            else:
                Exception("Unexpected Command")
        return {
            "type": 5,
        }
    except Exception as e:
        logger.error(str(e))
        return e

def verify(signature: str, timestamp: str, body: str) -> bool:
    PUBLIC_KEY = os.environ["PUBLIC_KEY"]
    verify_key = VerifyKey(bytes.fromhex(PUBLIC_KEY))
    try:
        verify_key.verify(
            f"{timestamp}{body}".encode(), bytes.fromhex(signature)
        )
    except Exception as e:
        logger.warning(f"failed to verify request: {e}")
        return False
    return True
# ファイル名 -> start.py

import os
import json
import requests
import boto3
import logging
logger = logging.getLogger()
logger.setLevel("INFO")

def lambda_handler(event, context):
    logger.info("Starting script")
    instance_id = os.environ["INSTANCE_ID"]
    result = start_ec2(instance_id)
    application_id = event["DISCORD_APP_ID"]
    interaction_token = event["token"]
    message = {}
    if result["status"] == 1:
        message = {"content": "Server is already running\n```\n{}\n```".format(result["ip"])}
    elif result["status"] == 0:
        message = {"content": "Server becomes ready!\n```\n{}\n```".format(result["ip"])}
    elif result["status"] == 2:
        message = {"content": "Starting instance process failed"}
    else:
        message = {"content": "Unexpected error at starting process"}
    payload = json.dumps(message)
    r = requests.post(
        url=f"https://discord.com/api/v10/webhooks/{application_id}/{interaction_token}",
        data=payload,
        headers={"Content-Type": "application/json"},
    )
    logger.debug(r.text)
    logger.info("Finished starting script")
    return r.status_code

def start_ec2(instance_id: str) -> dict:
    try:
        logger.info("Starting instance: " + str(instance_id))
        region = os.environ["AWS_REGION"]
        ec2_client = boto3.client("ec2", region_name=region)
        status_response = ec2_client.describe_instances(InstanceIds=[instance_id])
        if (status_response["Reservations"][0]["Instances"][0]["State"]["Name"] == "running"):
            logger.info("Instance is already running: " + str(instance_id))
            return {"status": 1, "ip": get_public_ip(status_response)}
        else:
            logger.info("Start instance: " + str(instance_id))
            response = ec2_client.start_instances(InstanceIds=[instance_id])
            logger.debug(response)
            logger.info("Waiting for Instance to be ready: " + str(instance_id))
            try:
                waiter_running = ec2_client.get_waiter("instance_running")
                waiter_status = ec2_client.get_waiter("instance_status_ok")
                waiter_running.wait(InstanceIds=[instance_id])
                waiter_status.wait(InstanceIds=[instance_id])
                logger.info("Starting instance: " + str(instance_id))
                return {"status": 0, "ip": get_public_ip(ec2_client.describe_instances(InstanceIds=[instance_id]))}
            except Exception as e:
                logger.error("Starting instance process failed.")
                logger.error(str(e))
                return {"status": 2}
    except Exception as e:
        logger.error(str(e))
        return {"status": 3}

def get_public_ip(response: dict) -> str:
    try:
        ip_address = response["Reservations"][0]["Instances"][0]["PublicIpAddress"]
    except KeyError as e:
        logger.warning("Failed Extract ip address.")
        ip_address = "XXX.XXX.XXX.XXX"
    except Exception as e:
        logger.error(str(e))
        ip_address = "XXX.XXX.XXX.XXX"
    return ip_address
# ファイル名 -> stop.py

import os
import json
import requests
import boto3
import logging
logger = logging.getLogger()
logger.setLevel("INFO")

def lambda_handler(event, context):
    logger.info("Starting script")
    instance_id = os.environ["INSTANCE_ID"]
    result = stop_ec2(instance_id)
    application_id = event["DISCORD_APP_ID"]
    interaction_token = event["token"]
    message = {}
    if result["status"] == 1:
        message = {"content": "Server is already stopped"}
    elif result["status"] == 0:
        message = {"content": "Server stopped!"}
    else:
        message = {"content": "Unexpected error at stopping process"}
    payload = json.dumps(message)
    r = requests.post(
        url=f"https://discord.com/api/v10/webhooks/{application_id}/{interaction_token}",
        data=payload,
        headers={"Content-Type": "application/json"},
    )
    logger.debug(r.text)
    logger.info("Finished stopping script")
    return r.status_code

def stop_ec2(instance_id: str) -> None:
    try:
        logger.info("Stopping instance: " + str(instance_id))
        region = os.environ["AWS_REGION"]
        ec2_client = boto3.client("ec2", region_name=region)
        status_response = ec2_client.describe_instances(InstanceIds=[instance_id])
        if (status_response["Reservations"][0]["Instances"][0]["State"]["Name"] == "stopped"):
            logger.info("Instance is already stopped: " + str(instance_id))
            return {"status": 1}
        else:
            logger.info("Stop instance: " + str(instance_id))
            response = ec2_client.stop_instances(InstanceIds=[instance_id])
            logger.debug(response)
            logger.info("Waiting for Instance to be stopped: " + str(instance_id))
            try:
                waiter_stopped = ec2_client.get_waiter("instance_stopped")
                waiter_stopped.wait(InstanceIds=[instance_id])
                logger.info("Stopped instance: " + str(instance_id))
                return {"status": 0}
            except Exception as e:
                logger.error("Stopping instance process failed.")
                logger.error(str(e))
                return {"status": 2}
    except Exception as e:
        logger.error(str(e))
        return {"status": 3}
# ファイル名 -> status.py

import os
import json
import requests
import boto3
import logging
logger = logging.getLogger()
logger.setLevel("INFO")

def lambda_handler(event, context):
    logger.info("Starting script")
    instance_id = os.environ["INSTANCE_ID"]
    result = status_ec2(instance_id)
    application_id = event["DISCORD_APP_ID"]
    interaction_token = event["token"]
    message = {}
    if result["status"] == 1:
        message = {"content": "Server: Stopped"}
    elif result["status"] == 0:
        message = {"content": "Server: Running\n```\n{}\n```".format(result["ip"])}
    elif result["status"] == 2:
        message = {"content": "Server: Pending"}
    elif result["status"] == 3:
        message = {"content": "Unexpected instance status"}
    else:
        message = {"content": "Unexpected error at status check process"}
    payload = json.dumps(message)
    r = requests.post(
        url=f"https://discord.com/api/v10/webhooks/{application_id}/{interaction_token}",
        data=payload,
        headers={"Content-Type": "application/json"},
    )
    logger.debug(r.text)
    logger.info("Finished status checking script")
    return r.status_code

def status_ec2(instance_id: str) -> None:
    result = {}
    try:
        region = os.environ["AWS_REGION"]
        ec2_client = boto3.client("ec2", region_name=region)
        status_response = ec2_client.describe_instances(InstanceIds=[instance_id])
        status = status_response["Reservations"][0]["Instances"][0]["State"]["Name"]
        if (status == "running"):
            logger.info("Instance is running: " + str(instance_id))
            result["status"] = 0
            result["ip"] = status_response["Reservations"][0]["Instances"][0]["PublicIpAddress"]
        elif (status == "stopping" or status == "stopped"):
            logger.info("Instance is stoppping: " + str(instance_id))
            result["status"] = 1
        elif (status == "pending"):
            logger.info("Instance is pending: " + str(instance_id))
            result["status"] = 2
        else:
            logger.warning("Unexpected instance status")
            result["status"] = 3
        return result
    except Exception as e:
        logger.error(str(e))
        result["status"] = 4
        return result

アップロード後、zipファイルのオブジェクトを開き、キーを控えておいてください。
   

Layerの作成

先ほどのLambda関数において、外部ライブラリを使用します。
それにあたり、必要なパッケージを用意します。

私はCloud9にてPython 3.12を使用し作成しました。
そのため、デプロイするLambdaのランタイムもPython 3.12にしてあります。
下記引用に記載があるように、Lambdaのレイヤー作成にはLinux環境が必要です。
Windowsやmac OSでレイヤーファイルを作成した場合、Lambdaが動作しないケースがあります。
※…私はWindowsで作成してしまい、一度引っかかりました。

The first step to creating a layer is to bundle all of your layer content into a .zip file archive. Because Lambda functions run on Amazon Linux, your layer content must be able to compile and build in a Linux environment. If you build packages on your local Windows or Mac machine, you’ll get output binaries for that operating system by default.These binaries may not work properly when you upload them to Lambda.
引用元:AWS Lambda Document「Packaging your layer content」※…日本語版は機械翻訳のため、英語版サイトから引用しています。

  1. カレントディレクトリ上に「requirements.txt」を作成します。テキストファイル内には以下を入力してください。
    PyNaCl==1.5.0
    requests
    urllib3<2
  2. 以下のコマンドを実行します。
    $ mkdir python
    $ pip install -r requirements.txt -t ./python
    $ zip -r9 layer.zip python
  3. カレントディレクトリに「layer.zip」ができていることを確認し、そのzipファイルを適当なS3バケットにアップロードしてください。アップロード後、コードと同様にzipファイルのオブジェクトを開き、キーを控えておいてください。
レイヤーファイルのアップロード場所は、先程コードをアップロードしたバケットと同一でも問題ありません。

CloudFormationによる構築

今回構築するAPI Gateway, Lambda, IAMのCloudFormationテンプレートを用意しました。
以下のYAMLファイルを使用し、リソースをデプロイしてください。
パラメータには

  • ApplicationID…Discord Bot作成時の「APPLICATION ID」
  • CodeS3BucketName…事前準備で格納したコードの宛先S3バケット名
  • CodeS3Key…コードのキー
  • EC2InstanceID…制御したいEC2インスタンスのID
  • LayerS3BucketName…事前準備で格納したレイヤーの宛先S3バケット名
  • LayerS3Key…レイヤーのキー
  • PublicKey…Discord Bot作成時の「PUBLIC KEY」

をそれぞれ入力してください。

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  ApplicationID:
    Type: String
  PublicKey:
    Type: String
  EC2InstanceID:
    Type: String
  CodeS3BucketName:
    Type: String
  CodeS3Key:
    Type: String
  LayerS3BucketName:
    Type: String
  LayerS3Key:
    Type: String

Resources:
  # API Gateway
  ControlAPIGateway:
    Type: AWS::ApiGatewayV2::Api
    Properties:
      Name: DiscordSlashCommandController-Api
      ProtocolType: HTTP
  ControlAPIGatewayIntegration:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref ControlAPIGateway
      IntegrationType: AWS_PROXY
      IntegrationUri: !GetAtt AppFunction.Arn
      PayloadFormatVersion: 2.0
  ControlAPIGatewayRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref ControlAPIGateway
      RouteKey: POST /
      Target: !Sub integrations/${ControlAPIGatewayIntegration}
  ControlAPIGatewayStage:
    Type: AWS::ApiGatewayV2::Stage
    Properties:
      ApiId: !Ref ControlAPIGateway
      StageName: $default
      AutoDeploy: true

  # Lambda layer
  LambdaLayer:
    Type: "AWS::Lambda::LayerVersion"
    Properties:
      CompatibleRuntimes:
        - python3.12
      Content:
        S3Bucket: !Ref LayerS3BucketName
        S3Key: !Ref LayerS3Key
      LayerName: DiscordSlashCommandController-Layer

  # Lambda function
  AppFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref CodeS3BucketName
        S3Key: !Ref CodeS3Key
      Handler: app.lambda_handler
      Runtime: python3.12
      FunctionName: DiscordSlashCommandController-App
      Layers:
        - !Ref LambdaLayer
      MemorySize: 1024
      Timeout: 10
      Environment:
        Variables:
          PUBLIC_KEY: !Ref PublicKey
          APPLICATION_ID: !Ref ApplicationID
      Role: !GetAtt LambdaExecutionRole.Arn
  StartFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref CodeS3BucketName
        S3Key: !Ref CodeS3Key
      Handler: start.lambda_handler
      Runtime: python3.12
      FunctionName: DiscordSlashCommandController-Start
      Layers:
        - !Ref LambdaLayer
      MemorySize: 1024
      Timeout: 600
      Environment:
        Variables:
          INSTANCE_ID: !Ref EC2InstanceID
      Role: !GetAtt LambdaExecutionRole.Arn
  StopFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref CodeS3BucketName
        S3Key: !Ref CodeS3Key
      Handler: stop.lambda_handler
      Runtime: python3.12
      FunctionName: DiscordSlashCommandController-Stop
      Layers:
        - !Ref LambdaLayer
      MemorySize: 1024
      Timeout: 60
      Environment:
        Variables:
          INSTANCE_ID: !Ref EC2InstanceID
      Role: !GetAtt LambdaExecutionRole.Arn
  StatusFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref CodeS3BucketName
        S3Key: !Ref CodeS3Key
      Handler: status.lambda_handler
      Runtime: python3.12
      FunctionName: DiscordSlashCommandController-Status
      Layers:
        - !Ref LambdaLayer
      MemorySize: 1024
      Timeout: 10
      Environment:
        Variables:
          INSTANCE_ID: !Ref EC2InstanceID
      Role: !GetAtt LambdaExecutionRole.Arn

  # Lambda execution log groups
  AppFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${AppFunction}
      RetentionInDays: 30
  StartFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${StartFunction}
      RetentionInDays: 30
  StopFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${StopFunction}
      RetentionInDays: 30
  StatusFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${StatusFunction}
      RetentionInDays: 30

  # Lambda invoke permission
  ApiGWInvokeAppFunction:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt AppFunction.Arn
      Principal: apigateway.amazonaws.com
      SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ControlAPIGateway}/*

  # Lambda IAM Role
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
      Policies:
        - PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - lambda:InvokeFunction
                  - ec2:DescribeInstances
                  - ec2:DescribeInstanceStatus
                  - ec2:StopInstances
                  - ec2:StartInstances
                Effect: Allow
                Resource: "*"
          PolicyName: IPL-DiscordSlashCommandController
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      RoleName: IRL-DiscordSlashCommandController

Outputs:
  EndpointURL:
    Value: !Sub https://${ControlAPIGateway}.execute-api.${AWS::Region}.${AWS::URLSuffix}/

構築が完了すると、出力にエンドポイントのURLがありますので、そちらを控えておきます。

Discord Botの利用

Discord Developer Portalに再びアクセスし、先程作成したApplicationをクリックします。
中段にある「INTERACTIONS ENDPOINT URL」に、先程控えたエンドポイントURLを貼り付けます。

「Save Changes」を押下すると、接続テストが行われ正常に疎通確認が取れるとエンドポイントURLが保存されます。

ここで何かしらのエラーが出る場合は、APIが正常に構築できていない・貼り付け先のBOTを間違えている可能性があります。

Botの招待

左ペインより「OAuth2」を選択し、下段「OAuth2 URL Generator – SCOPES」から「bot」と「applications.commands」にチェックを入れます。その後、その下に表示されている「GENERATED URL」をコピーします。

コピーしたURLにアクセスして、サーバーにBotを招待します。

Botをサーバへ招待するには、サーバの管理者など「サーバ管理権限」を持つアカウントでの操作が必要です。

動作確認

Botが招待されたサーバのテキストチャンネルにて、スラッシュコマンドを入力します。
下記画像では「/server」を入力したあとに、コマンドの候補や説明が適切に表示されていることが分かります。

それぞれのコマンド(start, stop, status)を入力し、所望の応答が得られることを確認します。

  • コマンド入力直後(共通)
  • サーバ開始時(/server start)
  • サーバ終了時(/server stop)
  • サーバステータス確認時(/server status)
IPアドレス欄のみコードブロックにて独立して配置することで、IPアドレスのコピーを容易にしています。

 

参考サイト

今回の実装にあたり、以下のサイトを参考にさせていただきました。

  • 構成や実際のコードを参考にいたしました。
  • 応答方法の処理について参考にいたしました。
  • Layerを作成する際に参考にいたしました。
  • CloudFormationテンプレートを作成する際に参考にいたしました。

この他にも、API GatewayやLambdaのCloudFormationに関するドキュメント、Discord Developer Potalの公式ドキュメントも参考にしております。

 

終わりに

今回は、インフラのことを知らない人でもDiscord上から簡単にEC2インスタンスの開始・停止を行うための仕組みをご紹介いたしました。インスタンスを使いたい人が自分の好きなタイミングで開始や停止ができるので、運用面や費用面でも大きなメリットがあるのではないかなと思っております。
個人的にも一からこのような仕組みを構築することができ、達成感や学ぶことが多くありました。
(アイデアやコードは多くのサイトを参考にさせていただきました。)

とても長くなってしまいましたが、最後までご覧いただきありがとうございました!

余談:私はDiscordやVS Codeをライトテーマで使うタイプです。

タイトルとURLをコピーしました