<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Kevin Monvoisin]]></title><description><![CDATA[AWS Cloud Engineer, I am passionate about learning and trying new things. I like to learn, build, break and start over.]]></description><link>https://blog.mkevin.fr</link><generator>RSS for Node</generator><lastBuildDate>Mon, 18 May 2026 23:33:56 GMT</lastBuildDate><atom:link href="https://blog.mkevin.fr/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[In Real-Time notification system using WebSockets on AWS]]></title><description><![CDATA[In Real-Time notification system using WebSockets on AWS
⚡ TL:DR
We will cover how to efficiently create a WebSockets API Gateway and send real-time messages from our Lambda functions to our WebSockets client to make our applications more interactive...]]></description><link>https://blog.mkevin.fr/in-real-time-notification-system-using-websockets-on-aws</link><guid isPermaLink="true">https://blog.mkevin.fr/in-real-time-notification-system-using-websockets-on-aws</guid><category><![CDATA[AWS]]></category><category><![CDATA[websockets]]></category><category><![CDATA[API Gateway]]></category><category><![CDATA[notifications]]></category><dc:creator><![CDATA[Kévin Monvoisin]]></dc:creator><pubDate>Mon, 17 Mar 2025 13:26:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742382267120/5eb0cde7-9de0-4728-8b92-09e1b0b1dcd3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-in-real-time-notification-system-using-websockets-on-aws">In Real-Time notification system using WebSockets on AWS</h3>
<h3 id="heading-tldr">⚡ TL:DR</h3>
<p>We will cover how to efficiently create a WebSockets API Gateway and send real-time messages from our Lambda functions to our WebSockets client to make our applications more interactive.</p>
<h3 id="heading-introduction">🌍 Introduction</h3>
<p>In today’s fast-paced digital landscape, the demand for instant communication and real-time updates has become more and more important. Whether it’s receiving breaking news alerts, tracking deliveries, or staying connected with friends and colleagues, the need for timely information has never been greater. In such a dynamic environment, traditional methods of communication often fall short, unable to provide the immediacy and responsiveness required.</p>
<p>This is where real-time notification systems step in to bridge the gap. By leveraging technologies like WebSockets, these systems enable seamless, bidirectional communication between clients and servers, allowing for instantaneous updates and alerts.</p>
<p>In the past year, I’ve completed a project for a client that was using a fully event-driven architecture, and for me, it was missing a crucial feature, real-time notification.</p>
<h3 id="heading-goals-and-objectives">💪 Goals and Objectives</h3>
<p>In this article, we will dive into the creation of a simple system on AWS by providing an architecture diagram, a few principles, the limits of performing this architecture on AWS, and a little bit of code.</p>
<p>We will be using the following AWS services:</p>
<ul>
<li><p>AWS API Gateway (Rest API and WebSockets API)</p>
</li>
<li><p>AWS Lambda</p>
</li>
<li><p>AWS Simple Queue Service</p>
</li>
<li><p>AWS DynamoDB</p>
</li>
</ul>
<h3 id="heading-requirements">🛠️ Requirements</h3>
<p>In this article, we will mostly be using AWS so make sure you have an account ready to play with.</p>
<h3 id="heading-solution-architecting">🤔 Solution Architecting</h3>
<p>We have a simple web application that performs Rest API calls, some methods will create new AWS EC2 instances and take some time, we want to be notified through the whole creation process, at every step.</p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*0yHDoNcG5SvWhey4-QBxcg.gif" alt /></p>
<p>Here we are, with a blank page, tons of ideas in mind but can’t get them out. We know what we are looking for, a simple notification system. While some AWS services out there perform notification such as SNS (Simple Notification Service), it might not be our best fit for real-time notification.</p>
<p>When thinking about web and Rest API, an idea would be to store notifications in a database and make queries to our API that fetch stored notifications. But how should we fetch these notifications? Should we add a button to our web application that fetches these notifications? Should we create code that automatically makes the request to the right API endpoint every X seconds and collects the notifications? This is a solution, but not the one we are looking for.</p>
<p>What we want to do is real-time, a protocol that comes into my mind is WebSockets. WebSocket is a communication protocol that provides full-duplex communication channels over a single, long-lived connection between a client and a server. Unlike traditional HTTP connections, which are stateless and involve a request-response cycle meaning that the connection is terminated when the response is received, WebSockets enable persistent, low-latency communication.</p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*uwgsSijzrb32dbG3cDAC-A.png" alt /></p>
<p>Diagram explaining simply how WebSockets works</p>
<p>With that definition in mind, WebSockets seems to be a solid candidate for our problem. On top of that, the AWS API Gateway service allows us to create WebSockets API, just what we need.</p>
<p>We now have to think about how we correlate our Rest API requests and WebSockets communication. If we take a look at how WebSockets API Gateway works on AWS, we might find one solution. If we refer to the AWS documentation, to send a message to a WebSocket client, we need to perform an HTTP request to the following URL:</p>
<pre><code class="lang-javascript">POST https:<span class="hljs-comment">//{api-id}.execute-api.us-east-1.amazonaws.com/{stage}/@connections/{connection_id}</span>
</code></pre>
<p>Here, the interesting part of the URL is “{connection_id}”. That means that each time a user connects to the WebSockets API Gateway a connection ID is generated. Don’t we have a similar feature on Rest API Gateway? When requesting Rest API Gateway, a Request ID is generated, awesome! We now have a way to correlate WebSocket Connection and Rest API requests. For each request made to the Rest API Gateway, we will have to provide the WS connection ID. We can use a database solution to store these IDs and use them in our system to send messages later.</p>
<p>We have to find a way to send messages from AWS to our web application. Since we are dealing with an application that creates instances, we may want to send messages once instance creation starts, when instances are successfully created, and when instances are usable. Wouldn’t it be convenient to have a centralized way of delivering messages?</p>
<p>First thing first, we saw earlier that sending a message to one WebSocket client can be done by making a POST HTTP request to a specific URL. We can create a Lambda Function that will perform this HTTP request. In order to invoke that lambda function, we can either invoke the Lambda Function directly from other lambda functions, we can also create an SQS Queue that will receive every message and then send them to our Lambda Function, this is the solution that we will use because it decouples our architecture, it is more resilient to failure and it gives us more visibility about the number of messages to process.</p>
<p>We have one last topic to address, we want to send messages when the instance is created and ready to be used, but how? We can leverage Amazon EventBridge and catch those events.</p>
<p>At the end, we should have the following architecture:</p>
<p><img src="https://cdn-images-1.medium.com/max/1500/1*TIBoLAoz1TVld51AGbmsdg.png" alt /></p>
<p>Simple diagram (using Excalidraw) of the solution described above using API Gateway (Rest API/WebSockets), Lambda functions, DynamoDB and a SQS Queue</p>
<h3 id="heading-implementing-our-solution">🛠️ Implementing our solution</h3>
<p>Before actually starting to build our solution, let’s give some context here and define some boundaries:</p>
<ul>
<li><p>In the provided architecture diagram above, we have mentioned the use of an SQS queue. We won’t be implementing it here.</p>
</li>
<li><p>We’ve also suggested the use of DynamoDB to store Requests IDs and remove them on disconnection, we won’t be implementing them on here.</p>
</li>
<li><p>We won’t deploy a Rest API Gateway as well, we will simply deploy a WebSockets API Gateway and create a Lambda that we will trigger manually to send back messages to our WebSocket client.</p>
</li>
</ul>
<p>We have a simple web application that allows users to create VDI instances (Virtual Desktop Interface) on the go. The application is fairly simple, users are authenticated and can request their instance from the web interface.</p>
<p>Users would like to get in real-time notifications about their order of instance.</p>
<p><img src="https://cdn-images-1.medium.com/max/1500/1*YG0sGZ1vfSVbIITeGioKmA.png" alt /></p>
<p>Very simple web application design that demonstrate our use case</p>
<p>Here, users want to create new instances. Creation of EC2 instances can take some time due to many factors:</p>
<ul>
<li><p>EC2 creation time</p>
</li>
<li><p>Instance setup (using any automation solution)</p>
</li>
<li><p>Compliance check</p>
</li>
</ul>
<p>The user requested to get an update at each step of the instance creation process. Let’s create our notification system, starting on AWS!</p>
<p>First, let’s log in to AWS and head to the API Gateway service. Create a new WebSocket API Gateway.</p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*ayyFRihYZ0wzr283RINqZg.png" alt /></p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*x0NujN9sCSKz3GlFNI9xCw.png" alt /></p>
<p>When communicating to a WebSocket API Gateway, you send a payload, in this payload you will need to specify the action you are trying to perform. In this input, you set which key inside of your payload is the action you are trying to perform. We will come back to this later.</p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*e3yw0uQ1HNymuvy5IQs3oA.png" alt /></p>
<p>Add the $connect and “$disconnect” predefined routes to your API Gateway.</p>
<ul>
<li><p>$connect: This route is the one called when your client connects to the WebSocket API Gateway</p>
</li>
<li><p>$disconnect: This route is the one called when your client disconnects to the WebSocket API Gateway</p>
</li>
</ul>
<p>In our use-case, we don’t need to add any custom routes yet.</p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*AM0PbaiaQrGimLWAm9kZEg.png" alt /></p>
<p>Then you will want to create some Lambda Functions, one for the $connect route, and another one for the $disconnect route. This is the Lambda function that will be called when the client connect/disconnect to our API Gateway. Remember earlier when we said that we will have to match WebSocket connection ID and Rest API Request ID? Well, the $connect lambda function is a good place to write our logic that store the connection ID somewhere.</p>
<p>In the $disconnect lambda, we can write the logic that removes our stored connection ID.</p>
<p>We will write the lambda code later.</p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*TqrRsi7VMOpIIG5baisVOg.png" alt /></p>
<p>The final step is the stage. Here we are testing things, so we will simply create a first stage named “development”. Click on “Next” and deploy your API Gateway.</p>
<p>Once our WebSocket API Gateway is deployed, let’s try it. We will use Postman which supports WebSockets.</p>
<p>Grab your API Gateway WebSocket URL and open Postman.</p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*T7ActsRWfinUc2TInBBDlA.png" alt /></p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*jLJ215ai2Bdn8eVT-0F8gg.png" alt /></p>
<p>Click on Connect, and you shall see that the connection is successful. Great!</p>
<p>We will add some code to our $connect Lambda function to analyze what’s being passed to that lambda.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> json

def lambda_handler(event, context):

    print(json.dumps(event))

    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">'statusCode'</span>: <span class="hljs-number">200</span>,
        <span class="hljs-string">'body'</span>: json.dumps(<span class="hljs-string">'Hello from Lambda!'</span>)
    }
</code></pre>
<p>This code will simply dump every passed parameter to that Lambda. Let’s disconnect and then reconnect to our WebSocket API and see those parameters.</p>
<pre><code class="lang-javascript">{
    <span class="hljs-string">"headers"</span>: {
        <span class="hljs-string">"Host"</span>: <span class="hljs-string">"c9if5l9rgd.execute-api.eu-west-3.amazonaws.com"</span>,
        <span class="hljs-string">"Sec-WebSocket-Extensions"</span>: <span class="hljs-string">"permessage-deflate; client_max_window_bits"</span>,
        <span class="hljs-string">"Sec-WebSocket-Key"</span>: <span class="hljs-string">"q2ueME10A76weGCzBqgq2Q=="</span>,
        <span class="hljs-string">"Sec-WebSocket-Version"</span>: <span class="hljs-string">"13"</span>,
        <span class="hljs-string">"X-Amzn-Trace-Id"</span>: <span class="hljs-string">"Root=1-65feee73-002d1ca83f839f353a4c89b4"</span>,
        <span class="hljs-string">"X-Forwarded-For"</span>: <span class="hljs-string">"xxx"</span>,
        <span class="hljs-string">"X-Forwarded-Port"</span>: <span class="hljs-string">"443"</span>,
        <span class="hljs-string">"X-Forwarded-Proto"</span>: <span class="hljs-string">"https"</span>
    },
    <span class="hljs-string">"multiValueHeaders"</span>: {
        ...
    },
    <span class="hljs-string">"requestContext"</span>: {
        <span class="hljs-string">"routeKey"</span>: <span class="hljs-string">"$connect"</span>,
        <span class="hljs-string">"eventType"</span>: <span class="hljs-string">"CONNECT"</span>,
        <span class="hljs-string">"extendedRequestId"</span>: <span class="hljs-string">"VFoyGG1vDoEF2dg="</span>,
        <span class="hljs-string">"requestTime"</span>: <span class="hljs-string">"23/Mar/2024:15:00:03 +0000"</span>,
        <span class="hljs-string">"messageDirection"</span>: <span class="hljs-string">"IN"</span>,
        <span class="hljs-string">"stage"</span>: <span class="hljs-string">"development"</span>,
        <span class="hljs-string">"connectedAt"</span>: <span class="hljs-number">1711206003575</span>,
        <span class="hljs-string">"requestTimeEpoch"</span>: <span class="hljs-number">1711206003576</span>,
        <span class="hljs-string">"identity"</span>: {
            <span class="hljs-string">"sourceIp"</span>: <span class="hljs-string">"xxx"</span>
        },
        <span class="hljs-string">"requestId"</span>: <span class="hljs-string">"VFoyGG1vDoEF2dg="</span>,
        <span class="hljs-string">"domainName"</span>: <span class="hljs-string">"c9if5l9rgd.execute-api.eu-west-3.amazonaws.com"</span>,
        <span class="hljs-string">"connectionId"</span>: <span class="hljs-string">"VFoyGcaIjoECIjw="</span>,
        <span class="hljs-string">"apiId"</span>: <span class="hljs-string">"c9if5l9rgd"</span>
    },
    <span class="hljs-string">"isBase64Encoded"</span>: <span class="hljs-literal">false</span>
}
</code></pre>
<p>Did you notice the important parameter here? “<strong><em>connectionId</em></strong>” that has a value of “VFoyGcaIjoECIjw=”. Note that this number is unique and generated on the client connection.</p>
<p>We will now create a new Lambda Function that will send a message to a given WebSocket connectionId, to ensure that we are able to reach this connectionId.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> json
<span class="hljs-keyword">import</span> boto3

api_gw_id = <span class="hljs-string">"c9if5l9rgd"</span> # Change <span class="hljs-keyword">with</span> your WebSocket API Gateway ID
region = <span class="hljs-string">"eu-west-3"</span> # Change <span class="hljs-keyword">with</span> the region where your WebSocket API Gateway ID is

def send_message(connectionId: str, <span class="hljs-attr">message</span>: str, <span class="hljs-attr">stage</span>: str) -&gt; None:
    client = boto3.client(
        <span class="hljs-string">"apigatewaymanagementapi"</span>,
        endpoint_url=f<span class="hljs-string">"https://{api_gw_id}.execute-api.{region}.amazonaws.com/{stage}"</span>,
    )
    client.post_to_connection(
        ConnectionId=connectionId, Data=json.dumps(message).encode(<span class="hljs-string">"utf-8"</span>)
    )

def lambda_handler(event, context):  
    send_message(
        connectionId=event[<span class="hljs-string">"connectionId"</span>],
        message=event[<span class="hljs-string">"message"</span>],
        stage=event[<span class="hljs-string">"stage"</span>]
    )
    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">'statusCode'</span>: <span class="hljs-number">200</span>,
        <span class="hljs-string">'body'</span>: json.dumps(<span class="hljs-string">'Hello from Lambda!'</span>)
    }
</code></pre>
<p>Create a new test, and set these values:</p>
<pre><code class="lang-javascript">{
  <span class="hljs-string">"connectionId"</span>: <span class="hljs-string">"VFoyGcaIjoECIjw="</span>, # The Connection ID caught <span class="hljs-keyword">in</span> the $connect dump
  <span class="hljs-string">"stage"</span>: <span class="hljs-string">"development"</span>,
  <span class="hljs-string">"message"</span>: <span class="hljs-string">"Hello Medium"</span> # The message you want to send
}
</code></pre>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*w_G9OrN63m_PcP05EiHe0g.png" alt /></p>
<p>We are not done yet! We need to give additional permission to our Lambda Function otherwise we will face a permission denial while invoking our test!</p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*UHILr5EvGaM4KyPinXth1w.png" alt /></p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*wRcpqUejeVdx2EEpX9k4iQ.png" alt /></p>
<pre><code class="lang-javascript">{
    <span class="hljs-string">"Sid"</span>: <span class="hljs-string">"Statement1"</span>,
    <span class="hljs-string">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
    <span class="hljs-string">"Action"</span>: [
        <span class="hljs-string">"execute-api:ManageConnections"</span>
    ],
    <span class="hljs-string">"Resource"</span>: [
        <span class="hljs-string">"arn:aws:execute-api:YOUR_REGION:YOUR_ACCOUNT_ID:API_GATEWAY_ID/*/*"</span>
    ]
}
</code></pre>
<p>Now, execute your test. You should now see your message on Postman!</p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*NBFDQFmEwZo8FmmyEpJAIA.png" alt /></p>
<p>We’re not quite done yet. There is another thing to do, include theses messages in our web application.</p>
<p>There are tons of ways to perform WebSocket. Here we will use Vanilla JavaScript.</p>
<p>We will simply add the following code to our web application:</p>
<pre><code class="lang-javascript">&lt;script&gt;
<span class="hljs-keyword">const</span> socket = <span class="hljs-keyword">new</span> WebSocket(<span class="hljs-string">'ws://your-websocket-server-address'</span>);

<span class="hljs-comment">// Event listener for when the connection is established</span>
socket.addEventListener(<span class="hljs-string">'open'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'WebSocket connected'</span>);
});

<span class="hljs-comment">// Event listener for incoming messages</span>
socket.addEventListener(<span class="hljs-string">'message'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
    <span class="hljs-comment">// Display an alert with the received message</span>
    alert(<span class="hljs-string">'Message received: '</span> + event.data);
});

<span class="hljs-comment">// Function to send a message to the WebSocket server</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendMessage</span>(<span class="hljs-params">message</span>) </span>{
    <span class="hljs-keyword">if</span> (socket.readyState === WebSocket.OPEN) {
        socket.send(message);
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Message sent: '</span> + message);
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'WebSocket connection is not open.'</span>);
    }
}

&lt;/script&gt;
</code></pre>
<p>Once this code is added, we can refresh the page. Our web application will then send a connect request to our WebSocket API Gateway. On the AWS side, just like before, we will fetch the connection ID, and manually run our Lambda function.</p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*F5aoHgqnfnjWwTPK87uVMA.png" alt /></p>
<p>Screenshot of the web application with the message sent from AWS</p>
<p>And guess what? Our Web Application displays our message! We have made it!</p>
<h3 id="heading-room-for-improvements">👀 Room for improvements</h3>
<p>Well, as you are probably wondering, I have mentioned earlier the use of a REST API Gateway, an SQS queue but not above, why?</p>
<p>The general idea was to provide you with a simple use case of how to use WebSockets within the AWS environment.</p>
<p>On our Web Application, we have a small interface that allows users to create virtual desktop instances. When clicking on the “Request New Instance” button, an HTTP request is sent to our REST API Gateway. When sending this request, we will need to send the Connection ID into our initial payload, that way we will be able to send notifications later, during our instance initialization process.</p>
<p>Each Lambda Function has to send a message to our SQS queue, to ensure message ordering and delivery. We can then either configure a Lambda that’s triggered on message pushed that will send messages to our WebSocket client or having a scheduler that will invoke our Lambda function and fetch messages from our SQS Queue.</p>
<h3 id="heading-conclusion">🏁 Conclusion</h3>
<p>We have learned how to use WebSockets on AWS, and it wasn’t that hard, right? I hope that you have enough resources to dig on your own, and maybe implement some of my ideas in one of your projects! Feel free to comment on this post with a link to your project, I will happily take a look at it.</p>
]]></content:encoded></item><item><title><![CDATA[🎮 How I created a Minecraft server provider with Discord and AWS]]></title><description><![CDATA[⚡ TL:DR
We will cover how I have created my own Minecraft Server Provider using Discord to create the servers and ECS to run the servers.
🌍 Introduction
Since my teenage years, I have always been a fan of hosting my very own game servers. I used to ...]]></description><link>https://blog.mkevin.fr/how-i-created-a-minecraft-server-provider-with-discord-and-aws</link><guid isPermaLink="true">https://blog.mkevin.fr/how-i-created-a-minecraft-server-provider-with-discord-and-aws</guid><category><![CDATA[AWS]]></category><category><![CDATA[discord]]></category><category><![CDATA[minecraft]]></category><category><![CDATA[ECS]]></category><category><![CDATA[Docker]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Kévin Monvoisin]]></dc:creator><pubDate>Mon, 17 Mar 2025 13:24:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742382338833/df114b57-97c4-4dc8-92d2-ffccf1053d34.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-tldr">⚡ TL:DR</h3>
<p>We will cover how I have created my own Minecraft Server Provider using Discord to create the servers and ECS to run the servers.</p>
<h3 id="heading-introduction">🌍 Introduction</h3>
<p>Since my teenage years, I have always been a fan of hosting my very own game servers. I used to have a well-known French community (WayCaster.fr) on a game named Garry’s Mod. We had a few servers that were up and full most of the time. I was using some open-source game panels, but none of them were interesting to me. So, I ended up using the classic Linux shell to manage the servers. Then I learned programming. I started with web development using PHP, and I thought, “Hey, let’s create my own game server panel!” It never happened. 🙄</p>
<p>Today, I have realized that I no longer use web apps when an alternative exists, such as phone applications or bots (Telegram, WhatsApp, Discord, etc.). So here I am now, a fully grown man with knowledge in coding, solution architecture, and the same passion for building stuff.</p>
<p>I would like to build a solution that allows Minecraft Players from the Mineral Contest community to start a server on the go, using Discord.</p>
<p>What is Mineral Contest? Mineral Contest is a Minecraft game mode that went viral in France few years ago. A French Youtuber name “Squeezie” released a YouTube video of him and his friends playing this game mode and people loved it. (for the curious ones: <a target="_blank" href="https://www.youtube.com/watch?v=q_xALLkdwNA">https://www.youtube.com/watch?v=q_xALLkdwNA</a>)</p>
<p>In a few words; the main objective is to collect resources (Iron, Gold, Diamond, Emerald) and bring them to your base to earn some points. You are playing against 3 other teams, each team is usually composed of 4 players.</p>
<p>This is where I came in! The game mode was private, and everyone wanted to play it. So I decided to re-create it based on the YouTube Videos and release it publicly. The plugin quickly became popular, and is still active today!</p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*ypCr2IMLyzzDu3WIm-RzGQ.png" alt /></p>
<p>2024/08/08 — Current statistics for the plugin — available at <a target="_blank" href="https://mc.monvoisin-kevin.fr">https://mc.monvoisin-kevin.fr/</a></p>
<h3 id="heading-goals-and-objectives">💪 Goals and Objectives</h3>
<p>In this article, we will dive into the creation of a system that provides Minecraft Servers using AWS, Discord, and a NodeJS bot.</p>
<p>We will be using the following AWS services:</p>
<ul>
<li><p>AWS ECR (Elastic Container Registry)</p>
</li>
<li><p>AWS ECS (Elastic Container Service)</p>
</li>
<li><p>AWS DynamoDB</p>
</li>
</ul>
<p>We will also be using the discord.js library to build our bot and the AWS SDK for JavaScript.</p>
<h3 id="heading-requirements">🛠️ Requirements</h3>
<p>You will need an AWS Account, a Discord Application, Docker, NodeJS (version used in this article: 20.15.1).</p>
<p>The source code will also be provided, so you might need to use Terraform, Git, and the AWS CLI.</p>
<h3 id="heading-solution-architecting">🤔 Solution Architecting</h3>
<p>First thing first, what are we looking for? What are the requirements? What is the context?</p>
<p>Alright, so we are looking for a solution using Discord as our application client, we will be using AWS to host any virtualization service and database. We will also need to write a discord bot. Many choices to make here!</p>
<p>Firstly, for the Discord Bot, I would like to use NodeJS to practice this language. Lucky me, there is a well-known Discord library for Javascript named Discord.js. I want to restrict server creation to a specific set of users, to achieve this behavior, I will need to create a Discord Server Role.</p>
<p>How does the user should request its server? Should it be via a message sent to the moderation team? Via a server command? Here, I want the service to be fully automated, using a server command to provide automation.</p>
<p>Now that the user can request a server using a command, how does the user get his server details? I am not a fan of private messages, so this is not a suitable option. However, in my Discord Server, I am using a Ticketing tool. Users click on a button, it creates a new text channel that only the user and administration team can see; I like this idea a lot. Every user that requests a game server will have access to a newly created text channel that will contain every detail of its game server. Great!</p>
<p>Now, I need to think about server hosting, how am I going to create the game servers? We have many options here:</p>
<ol>
<li><p>EC2 instance: Either create a Golden AMI (an Amazon Machine Image that contains any dependencies and server binaries, pre-built that can be used without any further configuration) or use a configuration solution like AWS System Manager (Automation or RunCommand).</p>
</li>
<li><p>ECS Cluster: Using ECS Fargate we don’t have to manage any EC2, we only have to create our Docker Image, and specify the CPU and Memory needed.</p>
</li>
<li><p>EKS Cluster: Using Kubernetes, we don’t have to manage any EC2, we only have to create our Docker Image, and specify the CPU and Memory needed, however, this solution sounds a bit overkill for our usage and is expensive.</p>
</li>
</ol>
<p>On a side note, I do not need to have a persistent EBS volume since the game server will be temporary.</p>
<p>We now have to choose between an EC2 instance and an ECS Cluster. I already have experience using EC2 instances, coupled with SSM and I would like to learn and use this project to get knowledge about ECS so I will be selecting ECS.</p>
<p>We have to figure out how to create a game server on ECS. We have many possibilities here:</p>
<ul>
<li><p>Using a service: a service is a feature on ECS that guarantees a specific amount of containers up and running. Kinda like the AutoScaling Group on EC2. If a task fails or gets stopped, the service will automatically shut down the task and start a new one.</p>
</li>
<li><p>Using a task: a task contains a container to use, CPU and Memory to allocate, and Network configuration. This option is lighter than using a service and more suitable for what we are looking to achieve.</p>
</li>
</ul>
<p>Let’s recap what we’ve said so far:</p>
<ol>
<li><p>The user needs to have a special role</p>
</li>
<li><p>The user needs to use a Discord command to request a server creation</p>
</li>
<li><p>A new ECS task has to be created</p>
</li>
<li><p>The details of the ECS task need to be retrieved (IP Address, Current State, …)</p>
</li>
<li><p>A new Discord text channel needs to be created, only the administration team and the server owner can have access to it.</p>
</li>
<li><p>Send the details to the newly created text channel.</p>
</li>
</ol>
<p>We now have to think about data persistence. How do we ensure the bot has a list of active ECS tasks, knows when the task was created, how long before the tasks need to be killed, how to ensure the bot keeps this data stored somewhere “safe” (i.e: not in memory) in case the bot crash and has to restart?</p>
<p>We have here many solutions:</p>
<ol>
<li><p>Use a memory database (Redis, H2, …)</p>
</li>
<li><p>Use a SQL database (MariaDB, MySQL, SQL Server, …)</p>
</li>
<li><p>Use a No-SQL Database (MongoDB, DynamoDB, …)</p>
</li>
</ol>
<p>Since this is my own money, I want the solution to be cheap, nearly free. DynamoDB would be a solid choice since it’s managed, almost free for my usage, and runs outside of my bot application.</p>
<p>Let’s create a diagram showing what our solution would look like:</p>
<p><img src="https://cdn-images-1.medium.com/max/1500/0*rRd4evK-N9rCPrHH" alt /></p>
<p>Currently, I am happy with the choices we made, let’s begin implementing our solution!</p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*2oSk1yBvlp8jCobh2NC8CQ.gif" alt /></p>
<h3 id="heading-implementing-our-solution">🛠️ Implementing our solution</h3>
<p>Before actually implementing our solution, I just want to give some words. I won’t be going through every step required to build this project. I will go through basic things, and the whole project will be available at the end of this post. Thank you if you already made it that far!</p>
<p>Our first step here is to create our discord application. Simply head to <a target="_blank" href="https://discord.com/developers/applications">https://discord.com/developers/applications</a> and click on “New Application”. Generate a new Token, grab the Client ID, and save it somewhere, we will need it later.</p>
<p>We will now create the bot application logic. Based on the Discord.js documentation, installing the library is as easy as 1–2–3. Simply run the following command:</p>
<pre><code class="lang-bash">npm install discord.js dotenv
</code></pre>
<p>And now, create our file first:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// main.js</span>
<span class="hljs-keyword">import</span> { Client, GatewayIntentBits } <span class="hljs-keyword">from</span> <span class="hljs-string">'discord.js'</span>;
<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> Client({ <span class="hljs-attr">intents</span>: [GatewayIntentBits.Guilds] });

client.on(<span class="hljs-string">'ready'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Logged in as <span class="hljs-subst">${client.user.tag}</span>!`</span>);
});

client.login(<span class="hljs-string">"token"</span>);
</code></pre>
<p>Run the bot with the following command:</p>
<pre><code class="lang-plaintext">$ node main.js                                                                                                                                      N
Logged in as bot1271486725746720900#4545!
</code></pre>
<p>Awesome! We now have the basic code that allows our bot to log in! For those who want to explore a little bit more, the “Getting Started” page of Discord.Js is full of useful information, you can read more <a target="_blank" href="https://discord.js.org/docs/packages/discord.js/main">here</a>.</p>
<p>Let’s now create our application structure, with files and folder names. Here, I will be using the following structure:</p>
<pre><code class="lang-plaintext">medium-discord-bot-aws/
├── commands/
│   └── typeOfCommand/
│       ├── commandExecutor1.js
│       └── ...
├── events/
│   ├── onMessage.js
│   ├── onReady.js
│   └── ...
├── .env
├── deploy-commands.js
├── main.js
└── package.json
</code></pre>
<ul>
<li><p>In the commands folder, we will have every command that our bot will support.</p>
</li>
<li><p>In the events folder, we will define every event that our bot will listen to.</p>
</li>
<li><p>In the .env file, we will have every environment variable required and read by our bot.</p>
</li>
<li><p>In the deploy-command.js file, we will have the logic to register commands into our discord server.</p>
</li>
<li><p>In the main.js file, we will have our application code.</p>
</li>
<li><p>The package.json file will contain the project description, dependencies, the main script to execute, and a little bit more.</p>
</li>
</ul>
<p>With that structure in mind, let’s add and create the deploy-commands.js file.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { REST, Routes } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'discord.js'</span>);
<span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'node:fs'</span>);
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'node:path'</span>);
<span class="hljs-built_in">require</span>(<span class="hljs-string">'dotenv'</span>).config()


<span class="hljs-keyword">const</span> commands = [];
<span class="hljs-comment">// Grab all the command folders from the commands directory you created earlier</span>
<span class="hljs-keyword">const</span> foldersPath = path.join(__dirname, <span class="hljs-string">'commands'</span>);
<span class="hljs-keyword">const</span> commandFolders = fs.readdirSync(foldersPath);

<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> folder <span class="hljs-keyword">of</span> commandFolders) {
 <span class="hljs-comment">// Grab all the command files from the commands directory you created earlier</span>
 <span class="hljs-keyword">const</span> commandsPath = path.join(foldersPath, folder);
 <span class="hljs-keyword">const</span> commandFiles = fs.readdirSync(commandsPath).filter(<span class="hljs-function"><span class="hljs-params">file</span> =&gt;</span> file.endsWith(<span class="hljs-string">'.js'</span>));
 <span class="hljs-comment">// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment</span>
 <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> file <span class="hljs-keyword">of</span> commandFiles) {
  <span class="hljs-keyword">const</span> filePath = path.join(commandsPath, file);
  <span class="hljs-keyword">const</span> command = <span class="hljs-built_in">require</span>(filePath);
  <span class="hljs-keyword">if</span> (<span class="hljs-string">'data'</span> <span class="hljs-keyword">in</span> command &amp;&amp; <span class="hljs-string">'execute'</span> <span class="hljs-keyword">in</span> command) {
   commands.push(command.data.toJSON());
  } <span class="hljs-keyword">else</span> {
   <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`[WARNING] The command at <span class="hljs-subst">${filePath}</span> is missing a required "data" or "execute" property.`</span>);
  }
 }
}

<span class="hljs-comment">// Construct and prepare an instance of the REST module</span>
<span class="hljs-keyword">const</span> rest = <span class="hljs-keyword">new</span> REST().setToken(process.env.DISCORD_TOKEN);

<span class="hljs-comment">// and deploy your commands!</span>
(<span class="hljs-keyword">async</span> () =&gt; {
 <span class="hljs-keyword">try</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Started refreshing <span class="hljs-subst">${commands.length}</span> application (/) commands.`</span>);

  <span class="hljs-comment">// The put method is used to fully refresh all commands in the guild with the current set</span>
  <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> rest.put(
   Routes.applicationGuildCommands(process.env.DISCORD_CLIENT_ID, process.env.DISCORD_GUILD_ID),
   { <span class="hljs-attr">body</span>: commands },
  );

  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Successfully reloaded <span class="hljs-subst">${data.length}</span> application (/) commands.`</span>);
 } <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-comment">// And of course, make sure you catch and log any errors!</span>
  <span class="hljs-built_in">console</span>.error(error);
 }
})();
</code></pre>
<p>When you execute this script, the bot will automatically register any command location in our “commands” folder to our discord server. Great! For those wondering, I did not write all this by myself 👀, the code came from the Discord.js documentation available <a target="_blank" href="https://discordjs.guide/creating-your-bot/command-deployment.html#guild-commands">here</a>.</p>
<p>We will now edit our main.js file to include command processing event processing, and auto-completion on our commands.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'node:fs'</span>);
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'node:path'</span>);
<span class="hljs-keyword">const</span> { Client, Collection, GatewayIntentBits, Events } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'discord.js'</span>);

<span class="hljs-built_in">require</span>(<span class="hljs-string">'dotenv'</span>).config()

<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> Client({ <span class="hljs-attr">intents</span>: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] });

client.commands = <span class="hljs-keyword">new</span> Collection();
<span class="hljs-keyword">const</span> foldersPath = path.join(__dirname, <span class="hljs-string">'commands'</span>);
<span class="hljs-keyword">const</span> commandFolders = fs.readdirSync(foldersPath);

<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> folder <span class="hljs-keyword">of</span> commandFolders) {
 <span class="hljs-keyword">const</span> commandsPath = path.join(foldersPath, folder);
 <span class="hljs-keyword">const</span> commandFiles = fs.readdirSync(commandsPath).filter(<span class="hljs-function"><span class="hljs-params">file</span> =&gt;</span> file.endsWith(<span class="hljs-string">'.js'</span>));
 <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> file <span class="hljs-keyword">of</span> commandFiles) {
  <span class="hljs-keyword">const</span> filePath = path.join(commandsPath, file);
  <span class="hljs-keyword">const</span> command = <span class="hljs-built_in">require</span>(filePath);
  <span class="hljs-keyword">if</span> (<span class="hljs-string">'data'</span> <span class="hljs-keyword">in</span> command &amp;&amp; <span class="hljs-string">'execute'</span> <span class="hljs-keyword">in</span> command) {
   client.commands.set(command.data.name, command);
  } <span class="hljs-keyword">else</span> {
   <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`[WARNING] The command at <span class="hljs-subst">${filePath}</span> is missing a required "data" or "execute" property.`</span>);
  }
 }
}

<span class="hljs-keyword">const</span> eventsPath = path.join(__dirname, <span class="hljs-string">'events'</span>);
<span class="hljs-keyword">const</span> eventFiles = fs.readdirSync(eventsPath).filter(<span class="hljs-function"><span class="hljs-params">file</span> =&gt;</span> file.endsWith(<span class="hljs-string">'.js'</span>));

<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> file <span class="hljs-keyword">of</span> eventFiles) {
 <span class="hljs-keyword">const</span> filePath = path.join(eventsPath, file);
 <span class="hljs-keyword">const</span> event = <span class="hljs-built_in">require</span>(filePath);
 <span class="hljs-keyword">if</span> (event.once) {
  client.once(event.name, <span class="hljs-function">(<span class="hljs-params">...args</span>) =&gt;</span> event.execute(...args));
 } <span class="hljs-keyword">else</span> {
  client.on(event.name, <span class="hljs-function">(<span class="hljs-params">...args</span>) =&gt;</span> event.execute(...args));
 }
}

client.on(Events.InteractionCreate, <span class="hljs-keyword">async</span> interaction =&gt; {
 <span class="hljs-keyword">if</span> (interaction.isAutocomplete()) {
  <span class="hljs-keyword">const</span> command = interaction.client.commands.get(interaction.commandName);

  <span class="hljs-keyword">if</span> (!command) {
   <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`No command matching <span class="hljs-subst">${interaction.commandName}</span> was found.`</span>);
   <span class="hljs-keyword">return</span>;
  }

  <span class="hljs-keyword">try</span> {
   <span class="hljs-keyword">await</span> command.autocomplete(interaction);
  } <span class="hljs-keyword">catch</span> (error) {
   <span class="hljs-built_in">console</span>.error(error);
  }
 }
});



client.login(process.env.DISCORD_TOKEN);
</code></pre>
<p>Don’t worry, the whole code will be available at the end of this blog post. Now that we have the basics of the bot, let’s head into the AWS part.</p>
<p>To provision any resource, we will be using Terraform.</p>
<p>“Terraform is an infrastructure as code tool that lets you build, change, and version cloud and on-prem resources safely and efficiently.” — <a target="_blank" href="https://developer.hashicorp.com/terraform/intro">https://developer.hashicorp.com/terraform/intro</a></p>
<p>To make things right, we will start by creating an S3 Bucket, and a DynamoDB table to store our remote state. That way, the infrastructure state isn’t saved on our computer and we cannot remove it accidentally. Enable Object Versioning on your S3 bucket to ensure the terraform state file cannot be deleted accidentally and can be retrieved if any accident happens. While we are here, also create an ECR repository.</p>
<p>Here is our provider.tf file:</p>
<pre><code class="lang-plaintext">// provider.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~&gt; 5.0"
    }
  }

  backend "s3" {
    bucket = "mybucket"
    key    = "path/to/my/key"
    region = "eu-west-1"
  }
}

provider "aws" {
  region = local.region
}
</code></pre>
<p>I like to work with configuration files rather than terraform variables. This is my own opinion. Here I will be working with a YAML configuration file and locals.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># config.yaml</span>
<span class="hljs-attr">region:</span> <span class="hljs-string">eu-west-1</span>
<span class="hljs-attr">cluster_name:</span> <span class="hljs-string">MineralContest</span>
<span class="hljs-attr">ecr_repository:</span> <span class="hljs-string">your_repository_id.dkr.ecr.eu-west-1.amazonaws.com</span>
<span class="hljs-attr">image_name:</span> <span class="hljs-string">mineralcontest</span>
</code></pre>
<pre><code class="lang-plaintext">// locals.tf
locals {
  raw_data         = yamldecode(file("${path.module}/config.yaml"))
  region           = local.raw_data["region"]
  ecs_cluster_name = local.raw_data["cluster_name"]
  ecr_repository   = local.raw_data["ecr_repository"]
  image_name       = local.raw_data["image_name"]
}
</code></pre>
<p>Our first terraform resource will be the VPC. I will be using the AWS VPC Module. Here is the vpc.tf file.</p>
<pre><code class="lang-plaintext">// vpc.tf
module "vpc" {
  source = "terraform-aws-modules/vpc/aws"
  name = "mineralcontest-vpc"
  cidr = "10.0.0.0/16"
  azs            = ["${local.region}a"]
  public_subnets = ["10.0.1.0/24"]
}
</code></pre>
<p>Then, we will create our ECS cluster, the ECS task definition, and the security group that our task will use. Here is the ecs.tf file</p>
<pre><code class="lang-plaintext">// ecs.tf 
resource "aws_ecs_cluster" "cluster" {
  name = local.ecs_cluster_name
}

resource "aws_ecr_repository" "repository" {
  name = local.ecr_repository
  image_tag_mutability = "MUTABLE"
  image_scanning_configuration {
    scan_on_push = false # Disable image scanning
  }
}

resource "aws_ecs_task_definition" "mineralcontest_task" {
  family                   = "mineralcontest"
  task_role_arn            = aws_iam_role.ecs_task_role.arn
  execution_role_arn       = aws_iam_role.ecs_task_role.arn
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = "2048"
  memory                   = "4096"
  container_definitions = jsonencode(
    [
      {
        name      = "mineralcontest",
        image     = "${local.ecr_repository}/${local.image_name}:latest",
        cpu       = 0,
        memory    = 128,
        essential = true,
        portMappings = [
          {
            containerPort = 25565,
            hostPort      = 25565,
          },
        ]
      },
    ]
  )

}

resource "aws_security_group" "mineralcontest" {
  vpc_id = module.vpc.vpc_id
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 25565
    to_port     = 25565
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 25565
    to_port     = 25565
    protocol    = "udp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
</code></pre>
<p>Now, let’s create our iam.tf file that will contain every needed policies and roles.</p>
<pre><code class="lang-plaintext">// iam.tf
resource "aws_iam_instance_profile" "ec2_ssm_instance_profile" {
  name = "EC2-SSM-Instance-Profile"
  role = aws_iam_role.ec2_ssm_instance_role.name
}

resource "aws_iam_role" "ec2_ssm_instance_role" {
  name               = "EC2-SSM-Instance-role"
  assume_role_policy = &lt;&lt;EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ec2.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
EOF
}


resource "aws_iam_role" "ecs_task_role" {
  name               = "ECS-Task-role"
  assume_role_policy = &lt;&lt;EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ecs-tasks.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
EOF
}

resource "aws_iam_policy_attachment" "ecs_attachment" {
  name       = "ECS-Task-attachment"
  roles      = [aws_iam_role.ecs_task_role.name]
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
</code></pre>
<p>We’ve also mentioned the use of a DynamoDB table to store our current servers up and running, so let’s create the table.</p>
<pre><code class="lang-plaintext">// dynamodb.tf
resource "aws_dynamodb_table" "tasks" {
    name           = "mineralcontest_tasks"
    billing_mode   = "PAY_PER_REQUEST"
    hash_key       = "taskId"
    attribute {
        name = "taskId"
        type = "S"
    }
}
</code></pre>
<p>We are done with the AWS part. Now, we need to create our game server docker container image. In my use case, I want users to be able to become administrator by themself. So I have written a simple Java plugin that allows an user to become an admin by submitting a game command. I have also created a small script that is the entry point of the container and pass the token to the game server (for the user to become admin), plus the configuration required to make the server “official” or “non-official” (enabling or not non-official game version). Here is my dockerfile.</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># Build the Spigot Server</span>
<span class="hljs-keyword">FROM</span> amazoncorretto:<span class="hljs-number">17</span>-alpine3.<span class="hljs-number">16</span> as SPIGOT_BUILDER
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /spigot</span>
<span class="hljs-keyword">ARG</span> version=<span class="hljs-number">1.19</span>.<span class="hljs-number">4</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apk update &amp;&amp; apk add curl git</span>
<span class="hljs-keyword">RUN</span><span class="bash"> curl -o BuildTools.jar https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar</span>
<span class="hljs-keyword">RUN</span><span class="bash"> java -jar BuildTools.jar --rev <span class="hljs-variable">$version</span> --final-name server.jar</span>


<span class="hljs-comment"># Build the MineralContest Plugin</span>
<span class="hljs-keyword">FROM</span> maven:<span class="hljs-number">3.9</span>.<span class="hljs-number">8</span>-amazoncorretto-<span class="hljs-number">17</span>-al2023 as MINERALCONTEST_BUILDER
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /mineralcontest</span>
<span class="hljs-keyword">RUN</span><span class="bash"> yum install -y git</span>
<span class="hljs-keyword">RUN</span><span class="bash"> <span class="hljs-built_in">cd</span> /mineralcontest &amp;&amp; git <span class="hljs-built_in">clone</span> https://github.com/synchroneyes/mineralcontest</span>
<span class="hljs-keyword">RUN</span><span class="bash"> <span class="hljs-built_in">cd</span> /mineralcontest/mineralcontest &amp;&amp; mvn clean install</span>

<span class="hljs-comment"># Create the final image</span>
<span class="hljs-keyword">FROM</span> amazoncorretto:<span class="hljs-number">17</span>-alpine3.<span class="hljs-number">16</span> as MC_SERVER
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /server</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=SPIGOT_BUILDER /spigot/server.jar server.jar</span>
<span class="hljs-keyword">COPY</span><span class="bash"> MinecraftPlugins/OPRedeem.jar plugins/OPRedeem.jar</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=MINERALCONTEST_BUILDER /mineralcontest/mineralcontest/target/MineralContest.jar plugins/MineralContest.jar</span>
<span class="hljs-keyword">COPY</span><span class="bash"> files/server.properties server.properties</span>
<span class="hljs-keyword">COPY</span><span class="bash"> scripts/init.sh init.sh</span>


<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">25565</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"/server/init.sh"</span>]</span>
</code></pre>
<p>We now have to build, tag, and push our image to the ECR repository. The ECR service gives us the command required to build, tag, and push. Let’s execute them.</p>
<pre><code class="lang-bash">aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin your_repository_id.dkr.ecr.eu-west-1.amazonaws.com
docker build -t mineralcontest .
docker tag mineralcontest:latest your_repository_id.dkr.ecr.eu-west-1.amazonaws.com/mineralcontest:latest
docker push your_repository_id.dkr.ecr.eu-west-1.amazonaws.com/mineralcontest:latest
</code></pre>
<p>And voila! Our image is now pushed into our ECR repository and is ready to be used!</p>
<p>The (almost) final steps in our project are to create the command handler for the server creation, server stop, and automatic server shutdown after a specific amount of time. I won’t be covering every command otherwise this post will be much longer (it’s already really long, sorry about that 🙄). The whole code will be available at this end, so feel free to dig and take what’s interesting to you.</p>
<p>Finally, we need to package our bot application. I want this project to be as cheap as possible, for the bot hosting, I will be using a server that I am already renting and has Docker installed on it. Here is the dockerfile and the docker-compose.yaml file for the bot application</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:<span class="hljs-number">20.15</span>-alpine
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npm install</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npm run deploy-commands</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"npm"</span>, <span class="hljs-string">"start"</span>]</span>
</code></pre>
<pre><code class="lang-yaml"><span class="hljs-string">//</span> <span class="hljs-string">docker-compose.yaml</span>
<span class="hljs-attr">version:</span> <span class="hljs-string">'3'</span>
<span class="hljs-attr">services:</span>
  <span class="hljs-attr">mineralcontest_bot:</span>
    <span class="hljs-attr">build:</span>
      <span class="hljs-attr">context:</span> <span class="hljs-string">.</span>
      <span class="hljs-attr">dockerfile:</span> <span class="hljs-string">Dockerfile</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">network_mode:</span> <span class="hljs-string">host</span>
</code></pre>
<p>Now, simply run docker-compose up -d and the bot is now running! Awesome, we did it, and everything is working smoothly. Let’s talk about cost optimization.</p>
<p>As mentioned earlier, this game server is running the game mode Mineral Contest, which last approximately 60 minutes. It can be more or less depending on the user’s settings. We can implement a feature that automatically terminates an ECS task after a specified amount of seconds. Here is the code I wrote for this feature:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { getLocalDatabase } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'../services/serverDatabase'</span>);
<span class="hljs-keyword">const</span> { killServer } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'../services/killServer'</span>);

<span class="hljs-built_in">require</span>(<span class="hljs-string">'dotenv'</span>).config()


<span class="hljs-comment">/**
 * Watch servers and kill them when maximum run time is reached
 * <span class="hljs-doctag">@returns </span>Interval handler
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">watchServerRunTime</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">setInterval</span>(<span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> localDatabase = getLocalDatabase();
      <span class="hljs-keyword">for</span>(<span class="hljs-keyword">let</span> taskId <span class="hljs-keyword">in</span> localDatabase){
          <span class="hljs-keyword">let</span> task = localDatabase[taskId];

          <span class="hljs-keyword">let</span> creationDate = task.creationDate;
          <span class="hljs-keyword">let</span> currentDate = <span class="hljs-built_in">Date</span>.now();
          <span class="hljs-keyword">if</span>(currentDate &gt; creationDate + process.env.GAMESERVER_MAX_DURATION){
              <span class="hljs-keyword">await</span> killServer(taskId);
          }

      }
  }, <span class="hljs-number">1000</span>);
}

<span class="hljs-built_in">module</span>.exports = { watchServerRunTime };
</code></pre>
<p>Every second, the bot will fetch the local database (a copy of the DynamoDB table stored locally, to avoid excessive API calls) and check when the server was created. If the server was created more than 90 minutes ago, then the ECS task is killed.</p>
<p>I want to implement another feature. I want to ensure the server has at least 4 players in it. If this requirement is not met, then the server is terminated. To implement this feature, I have used a JavaScript library that allows Minecraft server queries to fetch the current player count. When the minimum player count threshold is not met, a warning is sent to the user’s Discord text channel. After 3 warnings, the server is automatically killed.</p>
<h3 id="heading-room-for-improvements">👀 Room for improvements</h3>
<p>That was a long post, wasn’t it? A first improvement would be to cut this post into subsidiary posts and add a “previous” or “next” link. Let me know your thoughts about this idea!</p>
<p>About this project, the first improvement would be within the game server container. Currently, I am using environment variables. I could improve it by using Secrets which is supported using Docker Compose which is a feature I am currently using. <a target="_blank" href="https://docs.docker.com/compose/use-secrets/">https://docs.docker.com/compose/use-secrets/</a>. I would have to make some changes to my application and read secrets from /run/secrets/&lt;secret_name&gt;.</p>
<p>For the build of the game server container, I could use a CI/CD to automate the build and deployment. Since I am cloning a Git Repository, the version my container has of this repository could be outdated. I can use CodeBuild to build my docker images and push them to my ECR repository and EventBridge Scheduler to schedule the build.</p>
<p><img src="https://cdn-images-1.medium.com/max/1000/1*f5wMWKDP2jB-xZh9aOqRug.png" alt /></p>
<p>As you can see in the diagram above, I have added the SNS Service. Currently; we lack some observability in our application. We could add some monitoring, for instance when a server creation fails, when a new Docker image build fails or succeeds, and when anything goes wrong, a notification can save us a lot of time.</p>
<p>Also, within the CI/CD, it would be nice to have some sort of testing, code scanning, and convention. We could do the following:</p>
<ol>
<li><p>Ensure the code is formatted everywhere and use the same naming convention (snakecase/camelcase/…)</p>
</li>
<li><p>Ensure there are no over-permissive policies that could lead to big issues</p>
</li>
<li><p>Local deployment to ensure everything is ready to be used</p>
</li>
</ol>
<p>Currently, the application only supports one type of game server. It would be a nice idea to add a way to support multiple games. What’s possible is to define some sort of game server template. You would define your game server settings in a JSON file, and upload the file into the Discord Server. The Bot application would parse the file, store the details in a DynamoDB table, and make the game server available for creation.</p>
<p>The bot application now! It was written using SDKv2 instead of SDKv3. AWS will end support for SDKv2 in 2025, meaning this project would need some parts to be rewritten in 2025.</p>
<h3 id="heading-conclusion-amp-source-code">🏁 Conclusion &amp; Source Code</h3>
<p>This was a really fun project where I have learned a lot. I had no knowledge of ECS and very poor coding skills in Javascript. I actually enjoyed this project a lot.</p>
<p>The application is now live within my community, and some servers have already been provided to players, they are happy, and I am happy, what else?</p>
<p>Thank you for reading this whole article, I hope that wasn’t too boring to read, the source code is available below, feel free to read the README.md file to understand how to deploy the solution.</p>
<p>Source code: <a target="_blank" href="https://github.com/Synchroneyes/gameserver-discord-aws-mc">https://github.com/Synchroneyes/gameserver-discord-aws-mc</a></p>
<p>I am excited to read your remarks!</p>
]]></content:encoded></item></channel></rss>