【AWS】EC2を起動&停止&性能変更するdiscord bot

# はじめに

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

サンプルコード↓

https://github.com/uni-ezobafun/ocean_ec2_manager_bot

### 構成図

Herokuで動いているdiscord botから、APIGWを介してLambdaをキックして、EC2を起動・停止する。
![Untitled presentation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/cf024e69-5abf-92de-d146-50392c874154.png)



### やること

* EC2を起動・停止するLambdaを設置
* EC2起動時に、インスタンスタイプを変更する処理を追加
* discordのメンバーが送信した特定のemojiを検知してLambdaを起動するbotを設置



### 前提

* 初期設定済のEC2
* discord botを実行する環境（本ページではHeroku）



# もくじ

1. Lambdaを設定する 
1. discord botを設定する
1. 動作を確認する



# 1. Lambdaを設定する

### 1-1. APIGWとLambdaの設定方法

下記ページを参照。

https://qiita.com/uni_ezobafun/items/a5a92e6afef21e239f48

### 1-2. Lambdaの中身(ec2_up)

EC2のインスタンスタイプを上書きした後、起動する。
`INSTANCE_ID`と`ALLOWED_INSTANCE_TYPES`は各自の環境に合わせて書き換える必要がある。

```index.py
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`は各自の環境に合わせて書き換える必要がある。

```index.py
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するように紐付けておく。

![Screen Shot 2021-07-28 at 23.25.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/69640acb-5ee5-de71-3af9-c64fc2bb9549.png)




# 2. discord botを設定する

### 2-1. discord botの運用環境
GCPでは最小インスタンスを1つまで無料で運用できる。
また、1ヶ月あたりの起動時間に制限こそあるが、用途によってはHerokuでも十分に無料運用できる。

Herokuにdiscord botをディプロイする方法は既にすばらしい記事が存在するため、深堀りしない↓

https://qiita.com/1ntegrale9/items/aa4b373e8895273875a8

### 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`は各自の環境に合わせて書き換える必要がある。

```EC2ManagerBot.py
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を追加

わかりやすい画像を用意して、よしなに追加する。

![Screen Shot 2021-07-28 at 23.10.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/c73c43e4-e115-17f7-7c41-30affcccceb6.png)



# 3. 動作を確認する

使うemojiによってインスタンスの性能を変更しつつ、EC2を起動・停止できた。
discordに参加しているメンバーなら、ワンボタンでEC2を操作できるようになった。

![Screen Shot 2021-07-28 at 23.49.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/b0b3fd93-2eae-5b93-127b-82005d4f0c59.png)