hero_picture

【使う時だけ起動!】discordでサーバーon,off AWS EC2でpalworldの占有サーバーを料金安くする方法で構築してみた

2024/09/04

こんにちは。株式会社シーズの倉岡です。

前回は、EC2 spot instanceを使用してパルワールドの占有サーバーを安くする方法を紹介しました。

このまま使用しても良いのですが、、サーバー使わない時は停止しておきたいですよね。

使う時にサーバー起動!

使わない時はサーバー停止!にしたいです。

今回は、discord経由でサーバーを起動、停止させる方法を紹介したいと思います。

前回の記事:【月額1000円!?安価】AWS EC2でpalworldの占有サーバーを料金安くする方法で構築してみた

手順

起動テンプレートの修正

前回作成した起動テンプレートを修正します。

EC2サービスの起動テンプレートから前回作った起動テンプレート選択します。

その後アクションから「テンプレートを変更」を選択します。

特に変更点はなく、前回と同様の設定をしてください。

追加で一点「リソースタグ」で以下のタグを追加してください。

その後オレンジボタンの「新しいバージョン」を押して新しいバージョンのものを作成します。

discordで webhookの作成

discordのサーバー設定から「アプリ」の「連携サービス」を選択します。

「ウェブフック」を選択し、「新しいウェブフック」を選択します。その後、botの名前、発言するチャンネルを選択し、「ウェブフックURLをコピー」します。

(名称:minecraftになってますが、適宜palwordなどに変えてください!)

これで準備は完了です。

準備としてIAMで権限設定

まずはプログラムを作る前に権限の設定が必要です。

IAMと入力して「IAM」サービスに遷移します。

その後、ロールを選択

右のオレンジの「ロールを作成」をクリックします。

ページが遷移したら一つ目はこのような感じ「Lambda」で使うのでユースケースはLambdaを選択します。

2ページ目は「AmazonEC2FullAccess」と「AWSCloudFormationFullAccess」を付与します。

この権限は正直過剰な権限です。今回は検証のためこちらの権限を付与してます。

名前は任意です。今回は「lambda-ec2-start-stop-role」にしました。

これで下まで確認して行って問題なければオレンジの作成を押してロールを作成してください。

続いて以下のポリシーも付与します。

ロールから自分の作成した「lambda-ec2-start-stop-role」を選択。その後、「許可」のタブから「許可の追加」を押してインラインポリシーを選択します。

インラインポリシーでは以下のポリシーをjsonで入力します。

アカウントIDの「123456789」の部分を自分のIDにしてください。

※「aws-ec2-spot-fleet-tagging-role」スポットリクエストで使ったロール、「SSM-role」EC2のサーバーに接続するためのロールそれぞれ名称を自分で付けている人はその名称に変更してください。

例えば前回の記事でロールの名前「SSM-role」で作ってなくて「ec2-role」で作ったという人は

「arn:aws:iam::123456789:role/SSM-role」から「arn:aws:iam::自分のアカウントID:role/ec2-role」とかにしてください。

1{
2    "Version": "2012-10-17",
3    "Statement": [
4        {
5            "Effect": "Allow",
6            "Action": "iam:PassRole",
7            "Resource": "arn:aws:iam::123456789:role/aws-ec2-spot-fleet-tagging-role"
8        },
9        {
10            "Effect": "Allow",
11            "Action": "iam:PassRole",
12            "Resource": "arn:aws:iam::123456789:role/SSM-role"
13        },
14        
15        {
16            "Effect": "Allow",
17            "Action": "lambda:InvokeFunction",
18            "Resource": "arn:aws:lambda:ap-northeast-1:123456789:function:pal-world-ec2-start"
19        }
20    ]
21}

完了したら、次へを押して、ポリシー名を決めます。

これは任意の名前なので好きな名前をつけてください。

こんな感じの3つ権限が付与できればOKです。

起動停止にはAWS Lambdaを使う

AWS サービスの検索窓からlambdaと検索してLambdaサービスを選択します。

Lambdaサービスで関数を選択して一覧表示部分に遷移後、オレンジの「関数作成」を選択します。

※東京リージョンにしっかりなってるか気をつけて!(右上の名称が「東京」になってない人は東京にしましょう!)

オレンジを押した後は、以下のような形で作成してください。

関数名は任意です。

ランタイムはpython3.12

アーキはx86

を使用します。

下の「デフォルトの実行ロールの変更」をクリックして、先ほど作ったroleを付与させます。

これで準備完了です。オレンジをクリックして作成します。

作成したらまず、「設定」タブの「一般設定」より編集を押します。タイムアウトの値が3秒になっているのでこれを1分にしておきます。(プログラムの実行時間の最大値を変更します。)

続いて、コードタブの下にレイヤーというものがあります。そちらでレイヤーを追加します。

レイヤーは本来自分でファイルをアップロードしてといったことをしますが今回は下記のレイヤー配布しているサイトからrequestsモジュールを追加して動かしたいと思います。

https://api.klayers.cloud//api/v2/p3.12/layers/latest/ap-northeast-1/html

ARNを指定して下記の「arn:aws:lambda:ap-northeast-1:770693421928:layer:Klayers-p312-requests:8」をコピペ検証を押します。

※ちなみにこのレイヤー現在は存在しますが、存在しない場合もありますので、適宜上記のサイトからrequestsモジュールのものをコピペすることをお勧めします。

オレンジのボタンを押して追加します。

下準備は完了です!いよいよプログラムの出番です。

コードのタブの「lambda_function」の部分にプログラムを書いていきます。

以下がプログラムになります。

DISCORD_WEBHOOK_URL

template_bodyの中の

IamFleetRole

LaunchTemplateId

SubnetId

VCpuCount

MemoryMiB

こちらは各自の値を入れてください。

1import boto3
2import time
3import requests
4
5# 必要なAWSクライアント
6cf = boto3.client('cloudformation')
7ec2 = boto3.client('ec2')
8
9# CloudFormationスタック名
10STACK_NAME = 'MySpotFleetStack'
11DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/***********"  # Webhook URLを設定
12
13# CloudFormationテンプレートのYAML
14template_body = '''
15AWSTemplateFormatVersion: '2010-09-09'
16Resources:
17  MySpotFleet:
18    Type: 'AWS::EC2::SpotFleet'
19    Properties:
20      SpotFleetRequestConfigData:
21        IamFleetRole: 'arn:aws:iam::123456789:role/aws-ec2-spot-fleet-tagging-role'
22        AllocationStrategy: 'priceCapacityOptimized'
23        TargetCapacity: 1
24        TerminateInstancesWithExpiration: true
25        LaunchTemplateConfigs:
26        - LaunchTemplateSpecification:
27            LaunchTemplateId: 'lt-0f**********'
28            Version: '$Latest'
29          Overrides:
30          - SubnetId: 'subnet-*********,subnet-*************'
31            InstanceRequirements:
32              VCpuCount:
33                Min: 4
34              MemoryMiB:
35                Min: 16384
36        Type: 'request'
37'''
38
39def lambda_handler(event, context):
40    try:
41        # CloudFormationスタックを作成
42        cf.create_stack(
43            StackName=STACK_NAME,
44            TemplateBody=template_body,
45            Capabilities=['CAPABILITY_IAM']
46        )
47        print(f"CloudFormationスタック {STACK_NAME} が作成されました")
48        
49        # スタック作成が完了するまで待機
50        waiter = cf.get_waiter('stack_create_complete')
51        waiter.wait(StackName=STACK_NAME)
52
53    except cf.exceptions.AlreadyExistsException:
54        print(f"スタック {STACK_NAME} は既に存在しています")
55    except Exception as e:
56        # CloudFormationの作成失敗時にDiscordにエラーメッセージを通知
57        error_message = f"CloudFormationスタック {STACK_NAME} の作成に失敗しました: {str(e)}"
58        requests.post(DISCORD_WEBHOOK_URL, json={"content": error_message})
59        print(error_message)
60        return {
61            'statusCode': 500,
62            'body': error_message
63        }
64
65    # 起動したインスタンスIDを取得
66    try:
67        response = ec2.describe_instances(Filters=[
68            {'Name': 'tag:game', 'Values': ['palworld']},
69            {'Name': 'instance-state-name', 'Values': ['running']}
70        ])
71
72        instances = response['Reservations'][0]['Instances']
73        if instances:
74            instance_id = instances[0]['InstanceId']
75            public_ip = instances[0]['PublicIpAddress']
76            
77            # Discord Webhookで通知
78            message = f"EC2インスタンス {instance_id} が起動しました。Public IP: {public_ip}"
79            requests.post(DISCORD_WEBHOOK_URL, json={"content": message})
80            print(f"Discordに通知しました: {message}")
81        else:
82            # インスタンスが見つからなかった場合の通知
83            error_message = "起動したインスタンスが見つかりませんでした。"
84            requests.post(DISCORD_WEBHOOK_URL, json={"content": error_message})
85            print(error_message)
86
87    except Exception as e:
88        # EC2インスタンスの取得に失敗した場合にDiscordにエラーメッセージを通知
89        error_message = f"EC2インスタンスの取得中にエラーが発生しました: {str(e)}"
90        requests.post(DISCORD_WEBHOOK_URL, json={"content": error_message})
91        print(error_message)
92        return {
93            'statusCode': 500,
94            'body': error_message
95        }
96
97    return {
98        'statusCode': 200,
99        'body': "Spot Fleet request completed."
100    }
101

プログラムを実行する

プログラムを実行する時に水色の「test」を押すのですが、その時に下記のテストイベントが出てきます。イベント名を適当について、オレンジで保存して再度水色の「test」を押すことで実行可能です。

うまくいったみたいです。

ディスコードの方にもwebhookを設定したチャンネルに通知が来ているみたいですね。

ストップさせる関数

停止させるようのLambdaも上と同じように作成します。

プログラムだけ変えます。

以下がプログラムです。

1import boto3
2import requests
3
4# 必要なAWSクライアント
5cf = boto3.client('cloudformation')
6
7# 削除対象のCloudFormationスタック名
8STACK_NAME = 'MySpotFleetStack'
9DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/***********************"  # Webhook URLを設定
10
11def lambda_handler(event, context):
12    try:
13        # CloudFormationスタックを削除
14        print(f"CloudFormationスタック {STACK_NAME} を削除しています...")
15        cf.delete_stack(StackName=STACK_NAME)
16
17        # スタック削除が完了するまで待機
18        waiter = cf.get_waiter('stack_delete_complete')
19        waiter.wait(StackName=STACK_NAME)
20        print(f"CloudFormationスタック {STACK_NAME} が正常に削除されました")
21
22        # Discord通知
23        message = f"CloudFormationスタック {STACK_NAME} が正常に削除され、関連するEC2インスタンスが終了しました。"
24        requests.post(DISCORD_WEBHOOK_URL, json={"content": message})
25        print(f"Discordに通知しました: {message}")
26
27    except Exception as e:
28        error_message = f"CloudFormationスタック {STACK_NAME} の削除中にエラーが発生しました: {str(e)}"
29        print(error_message)
30        
31        # エラー時のDiscord通知
32        requests.post(DISCORD_WEBHOOK_URL, json={"content": error_message})
33        return {
34            'statusCode': 500,
35            'body': error_message
36        }
37
38    return {
39        'statusCode': 200,
40        'body': f"CloudFormationスタック {STACK_NAME} が正常に削除されました"
41    }
42

このlambdaを使って問題なく起動停止できれば次のステップに移動してください。

関数URLの発行を行う

関数URLを発行します。各lambdaの以下のタブに関数のURLが発行できる部分があります。

こちらを押してください。

「NONE」を選択してオレンジのボタンを押して作成してください。

作成するとこのようなURLが出てきます。

後はこのURLをdiscordのチャンネル「サーバー起動停止」チャンネルなどを作りURLを貼っておきます。

これでdiscordからURLをぽちっと押すだけで起動停止することができました。

スマホでURLアクセスすると30秒くらいまたないといけませんが、こんな感じで表示されます。

discordの通知もいい感じに来てますね。

起動した後にすぐに停止してみました。

まとめ

いかがでしょうか。discordからURLを押すだけでサーバー起動停止ができ、他のゲームのサーバー立ち上げなどにも活用できる手段かと思います。

こちら実はまだ、仮完成ぐらいの部分です。

こちらを使用するとみなさん感じると思います。

  • 起動するURLにアクセスしてからが長い
  • ログは出てきているけど、本当にサーバーは起動しているのか

次回の記事では、URLアクセス後すぐにレスポンスが返るように修正をする方法と実際にサーバーが動いているのか検知するLambdaとEvent bridgeの作成方法を記載します。

理想は下記のような画像です。

1分おきに「サーバー状態」というところでpalworldのサーバーが起動しているか確認してくれるようにしておきます。

こうすることで、起動URL押した後に本当に立ち上がっているのか停止しているのかがわかるようになります!

次回の記事更新までお待ちください。それでは!