はじめに
EC2は起動している時間単位で課金されるため、使う・使わないに応じてコマメに起動・停止するのが理想。
AWSコンソールで操作するのは手軽さに欠けるため、誰でも自由に操作できる環境を整備したい。
本ページではdiscordのメンバーが自由にEC2を起動・停止できるようにする他、
インスタンスタイプ(サーバー性能)を変更できるようにするbotを設定する。
サンプルコード↓
構成図
Herokuで動いているdiscord botから、APIGWを介してLambdaをキックして、EC2を起動・停止する。

やること
- EC2を起動・停止するLambdaを設置
 - EC2起動時に、インスタンスタイプを変更する処理を追加
 - discordのメンバーが送信した特定のemojiを検知してLambdaを起動するbotを設置
 
前提
- 初期設定済のEC2
 - discord botを実行する環境(本ページではHeroku)
 
もくじ
- Lambdaを設定する
 - discord botを設定する
 - 動作を確認する
 
1. Lambdaを設定する
1-1. APIGWとLambdaの設定方法
下記ページを参照。
1-2. Lambdaの中身(ec2_up)
EC2のインスタンスタイプを上書きした後、起動する。
INSTANCE_IDとALLOWED_INSTANCE_TYPESは各自の環境に合わせて書き換える必要がある。
import boto3
# YOUR OWN INSTANCE ID
INSTANCE_ID = 'i-00000000000000000'
ALLOWED_INSTANCE_TYPES = ['t3a.large', 'm5n.large']
def lambda_handler(event, context):
    new_instance_type = event['instance_type']
    if not new_instance_type in ALLOWED_INSTANCE_TYPES:
        return response_maker(400, "[ERROR!] invalid instance_type")
    ec2 = boto3.client("ec2")
    # only running instance will return
    description = ec2.describe_instance_status(
        InstanceIds=[INSTANCE_ID],
        IncludeAllInstances=True
    )
    instance_state = description['InstanceStatuses'][0]['InstanceState']['Name']
    if instance_state != 'stopped':
        return response_maker(400, "[ERROR!] ec2 instance is not in 'stopped' state")
    ec2.modify_instance_attribute(
        InstanceId=INSTANCE_ID,
        Attribute='instanceType',
        Value=new_instance_type
    )
    ec2.start_instances(InstanceIds=[INSTANCE_ID])
    return response_maker(200, "minecraft up up up ...")
def response_maker(status, message):
    return {
        "isBase64Encoded": True,
        "statusCode": status,
        "headers": {
            "Content-Type": "application/json",
        },
        "body": {
            "message": message
        }
    }
1-3. Lambdaの中身(ec2_down)
EC2を停止する。
instancesは各自の環境に合わせて書き換える必要がある。
import boto3
# YOUR OWN INSTANCE ID
instances = ['i-00000000000000000']
def lambda_handler(event, context):
    ec2 = boto3.client("ec2")
    ec2.stop_instances(InstanceIds=instances)
    return {
        "statusCode": 200,
        "headers": {
            "Content-Type": "application/json"
        },
        "body": {
            "message": "minecraft down down down ..."
        }
    }
1-4. APIGWの設定
/upと/downで異なるLambdaをinvokeするように紐付けておく。
2. discord botを設定する
2-1. discord botの運用環境
GCPでは最小インスタンスを1つまで無料で運用できる。
また、1ヶ月あたりの起動時間に制限こそあるが、用途によってはHerokuでも十分に無料運用できる。
Herokuにdiscord botをディプロイする方法は既にすばらしい記事が存在するため、深堀りしない↓
2-2. botの中身
discordサーバーの特定チャンネル内で、特定のemoji:ec2_down:, :ec2_up_common:, :ec2_up_boost:が送信されたとき、Lambdaをキックする。
emojiはメッセージ本文に含んだとき、リアクションとして押したとき、両方とも反応する。
押すスタンプの種類によって、インスタンスタイプの種類をt3a.largeとm5n.largeで切り替えている。
(高負荷の休日と、低負荷の平日でサーバー性能を使い分けるような想定。)
TOKEN, ID_CHANNEL_BOT, LAMBDA_INVOKE_PATHは各自の環境に合わせて書き換える必要がある。
import discord
import requests
# YOUR OWN DISCORD BOT TOKEN
TOKEN = 'AAAAAAAAAAAAAAAAAAAAAAAA.AAAAAA.AAAA_AAAAAAAAAAAAAAAAAAAAA'
# YOUR OWN DISCORD CHANNEL ID
ID_CHANNEL_BOT = 000000000000000000
EMOJI_EC2_DOWN = 'ec2_down'
EMOJI_EC2_UP_COMMON = 'ec2_up_common'
EMOJI_EC2_UP_BOOST = 'ec2_up_boost'
EC2_INSTANCE_TYPE_POOR = 't3a.large'
EC2_INSTANCE_TYPE_GOOD = 'm5n.large'
LAMBDA_INVOKE_PATH = 'https://${YOUR_OWN_APIGW_PATH}/dev/ec2'
STATUS_OK = 200
client = discord.Client()
async def down_ec2(user_name):
    channel = client.get_channel(ID_CHANNEL_BOT)
    r = requests.get(f'{LAMBDA_INVOKE_PATH}/down').json()
    status = r["statusCode"]
    if status == STATUS_OK:
        await channel.send(f'ec2 down down down ... by `{user_name}`')
    else:
        await channel.send(f'[ERROR!] lambda invocation error --> `{r["body"]}`')
async def up_ec2(user_name, ec2_instance_type):
    channel = client.get_channel(ID_CHANNEL_BOT)
    r = requests.get(
        f'{LAMBDA_INVOKE_PATH}/up?instance_type={ec2_instance_type}'
    ).json()
    status = r["statusCode"]
    if status == STATUS_OK:
        await channel.send(f'ec2 up up up ... by `{user_name}`\ninstance type is `{ec2_instance_type}`')
    else:
        await channel.send(f'[ERROR!] lambda invocation error --> `{r["body"]}`')
async def validate_message(message):
    if message.author.bot:
        return
    channel_id = message.channel.id
    if channel_id != ID_CHANNEL_BOT:
        return
    content = message.content
    author = message.author.name
    if EMOJI_EC2_DOWN in content:
        await down_ec2(author)
        return
    if EMOJI_EC2_UP_COMMON in content:
        await up_ec2(author, EC2_INSTANCE_TYPE_POOR)
        return
    if EMOJI_EC2_UP_BOOST in content:
        await up_ec2(author, EC2_INSTANCE_TYPE_GOOD)
        return
async def validate_reaction(payload):
    channel_id = payload.channel_id
    if channel_id != ID_CHANNEL_BOT:
        return
    emoji = payload.emoji.name
    author = payload.member
    if emoji == EMOJI_EC2_DOWN:
        await down_ec2(author)
        return
    if emoji == EMOJI_EC2_UP_COMMON:
        await up_ec2(author, EC2_INSTANCE_TYPE_POOR)
        return
    if emoji == EMOJI_EC2_UP_BOOST:
        await up_ec2(author, EC2_INSTANCE_TYPE_GOOD)
        return
@client.event
async def on_ready():
    channel = client.get_channel(ID_CHANNEL_BOT)
    await channel.send('hello!')
@client.event
async def on_message(message):
    await validate_message(message)
@client.event
async def on_raw_reaction_add(payload):
    await validate_reaction(payload)
client.run(TOKEN)
2-3. discordサーバーにemojiを追加
わかりやすい画像を用意して、よしなに追加する。
3. 動作を確認する
使うemojiによってインスタンスの性能を変更しつつ、EC2を起動・停止できた。
discordに参加しているメンバーなら、ワンボタンでEC2を操作できるようになった。


