BUZZ BUZZ BUZZ
My phone is getting blown up in the middle of the night with calls, texts, and pings from my friends wanting to hop on our Minecraft server. Turns out I had accidentally turned off my ancient laptop that was hosting it. I begrudgingly get up from my cozy bed to boot up the server. Interrupted sleep, all to preserve the sacred annual 2-week Minecraft phase...
So I put my thinking cap on. Is there a way that I could host the server in the cloud and let my friends turn it on and off themselves?
In this post, I'll show you how to build a Discord bot that can interact with your AWS resources. We'll set up a Minecraft server on EC2 as our example, but this same pattern works for any AWS service.
Before we dive in, here's what you'll need:
- AWS Account
- Discord Developer Account
- Docker (for packaging dependencies in a Linux environment)
- Python 3.12
- Minecraft Account
1. Create Discord Application
- Go to Discord Developer Portal
- Create New Application
- Go to Bot → then click Reset Token and save it securely
- Go to OAuth2 → URL Generator → Select
bot+applications.commands - Copy Generated URL and add bot to your server
- Note your Application ID and Public Key from General Information
2. Create PyNaCl Lambda Layer
Discord requires signature verification on every request. The PyNaCl library handles this, but it has native dependencies — meaning we can't just pip install it on our Mac/Windows and upload to Lambda. We need to build it for Amazon Linux.
Build the layer zip (requires Docker)
Open up your terminal of choice and run:
mkdir -p pynacl_layer
docker run --platform linux/amd64 --rm -v "$(pwd)/pynacl_layer:/out" \
public.ecr.aws/sam/build-python3.12 bash -c \
"pip install PyNaCl -t /tmp/python/lib/python3.12/site-packages/ && cd /tmp && zip -r /out/pynacl_layer.zip python"
Make sure Docker Desktop is running first!
You'll get a "Cannot connect to Docker daemon" error otherwise.
Upload it to Lambda
Head to the Lambda console and navigate to Layers → Create layer.
-
Name:
pynacl-layer -
Upload: Choose the
pynacl_layer.zipfile from your local machine - Compatible runtimes: Python 3.12
Click Create.
3. Create Lambda Function
Lambda lets us run code on-demand in the cloud. This is where we'll handle incoming Discord commands and control our EC2 instance.
Now go to Functions → Create function.
Basic settings:
-
Function name:
discord-bot(or whatever you like) - Runtime: Python 3.12
- Architecture: x86_64 (must match your layer!)
Click Create function.
Once created, scroll to the bottom of the page. You'll see a Layers section. Click Add a layer, select Custom layers, pick pynacl-layer from the dropdown, and hit Add.
Add your Discord public key:
- Go to Configuration tab → Environment variables
- Click Edit → Add environment variable
- Key: DISCORD_PUBLIC_KEY
- Value: your public key from Discord Developer Portal (General Information page)
- Click Save
Increase the timeout:
- Still in Configuration → General configuration
- Click Edit
- Set Timeout to 15 seconds (to reduce risk if we encounter latency)
- Click Save
Lambda Code (lambda_function.py)
Paste this into the Lambda code editor. It verifies that requests are actually coming from Discord, then starts, stops, or checks the status of your EC2 instance based on the command. We're using boto3, the AWS SDK for Python.
import json
import os
import boto3
from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError
PUBLIC_KEY = os.environ['DISCORD_PUBLIC_KEY']
# INSTANCE_ID = os.environ['INSTANCE_ID'] # Uncomment in step 8
ec2 = boto3.client('ec2')
def verify_signature(event):
"""Verify the request is actually from Discord"""
body = event.get("body", "")
headers = event.get("headers", {})
signature = headers.get("x-signature-ed25519") or headers.get("X-Signature-Ed25519", "")
timestamp = headers.get("x-signature-timestamp") or headers.get("X-Signature-Timestamp", "")
verify_key = VerifyKey(bytes.fromhex(PUBLIC_KEY))
verify_key.verify(f"{timestamp}{body}".encode(), bytes.fromhex(signature))
def handle_server(action):
"""Start, stop, or check the EC2 instance"""
if action == "start":
ec2.start_instances(InstanceIds=[INSTANCE_ID])
return "🟢 Starting server..."
elif action == "stop":
ec2.stop_instances(InstanceIds=[INSTANCE_ID])
return "🔴 Stopping server..."
elif action == "status":
response = ec2.describe_instances(InstanceIds=[INSTANCE_ID])
state = response['Reservations'][0]['Instances'][0]['State']['Name']
emoji = "🟢" if state == "running" else "🔴" if state == "stopped" else "🟡"
return f"{emoji} Server is {state}"
return "Unknown action"
def lambda_handler(event, context):
"""Entry point for Lambda"""
try:
verify_signature(event)
except Exception:
return {"statusCode": 401, "body": "Invalid signature"}
body = json.loads(event.get("body", "{}"))
# Discord PING check
if body.get("type") == 1:
return {"statusCode": 200, "body": json.dumps({"type": 1})}
# Slash command
if body.get("type") == 2:
command_name = body.get("data", {}).get("name")
if command_name == "server":
options = body.get("data", {}).get("options", [])
action = options[0].get("value") if options else None
message = handle_server(action)
return {"statusCode": 200, "body": json.dumps({"type": 4, "data": {"content": message}})}
return {"statusCode": 200, "body": json.dumps({"type": 4, "data": {"content": "Unknown command"}})}
Hit Deploy to save your changes. That's it for the Lambda function for now. We'll come back to it later once we set up our EC2 instance.
4. Create API Gateway
Discord needs a public URL to send commands to. API Gateway gives us that. It acts as the front door to our Lambda function and exposes it as an HTTPS endpoint.
Head to the API Gateway console and create a REST API. Name it something like discord-endpoint.
- Create resource
/event - Create POST method on
/event - Check Lambda proxy integration
- Select your Lambda function
- Deploy API to a stage (e.g., "prod")
- Note the Invoke URL:
https://xxx.execute-api.YOUR_REGION.amazonaws.com/prod/event
5. Register Discord Endpoint
Now we tell Discord where to send commands when someone uses our bot.
- Go to Discord Developer Portal → Your App → General Information
- Set Interactions Endpoint URL to your API Gateway URL
- Discord will send a test PING - if verification passes, it saves
6. Register Slash Commands
Discord doesn't know what commands our bot supports yet. We need to register them.
To find your Guild ID, right-click your server name in Discord and click Copy Server ID. If you don't see that option, enable Developer Mode first: Settings → Advanced → Developer Mode.
Run this locally in your terminal:
curl -X POST "https://discord.com/api/v10/applications/YOUR_APP_ID/guilds/YOUR_GUILD_ID/commands" \
-H "Authorization: Bot YOUR_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "server", "type": 1, "description": "Control game server", "options": [{"name": "action", "description": "What to do", "type": 3, "required": true, "choices": [{"name": "start", "value": "start"}, {"name": "stop", "value": "stop"}, {"name": "status", "value": "status"}]}]}'
7. Create EC2 Instance
EC2 is a virtual machine in the cloud. We'll set up a Minecraft server on it, but you could run whatever you want here.
Head to the EC2 console and launch a new instance.
- AMI: Amazon Linux 2023
- Instance type: t3.medium (go larger for more demanding games)
- Key pair: Create or select one for SSH access
- Security group: Under Network settings, create a new security group. By default, SSH has Anywhere selected — change this to My IP
Expand Advanced details and paste this into the User data field (this is a script that runs automatically when the instance is first created). First, go to minecraft.net/download/server and right-click the minecraft_server.jar link to copy the URL. Replace PASTE_THE_URL_HERE with it:
#!/bin/bash
dnf install -y java-21-amazon-corretto
mkdir -p /opt/minecraft
cd /opt/minecraft
curl -O PASTE_THE_URL_HERE
echo "eula=true" > eula.txt
chown -R ec2-user:ec2-user /opt/minecraft
cat > /etc/systemd/system/minecraft.service <<EOF
[Unit]
Description=Minecraft Server
After=network.target
[Service]
WorkingDirectory=/opt/minecraft
ExecStart=/usr/bin/java -Xmx1G -Xms1G -jar server.jar nogui
Restart=always
User=ec2-user
[Install]
WantedBy=multi-user.target
EOF
systemctl enable minecraft
systemctl start minecraft
Click Launch instance, then note your Instance ID (e.g., i-0abc123def456). You'll need it for the next step.
Configure security group for Minecraft:
Click on your instance, then click the Security tab and click on the security group link. Go to Inbound rules → Edit inbound rules and add a rule:
| Type | Port | Source |
|---|---|---|
| Custom TCP | 25565 (Minecraft port) | Anywhere (0.0.0.0/0) |
Assign an Elastic IP:
Every time you stop and start your EC2 instance, the public IP changes. That means your friends would need a new IP every time. To fix this, assign an Elastic IP.
- In the EC2 console, go to Network & Security → Elastic IPs
- Click Allocate Elastic IP address → Allocate
- Select the new Elastic IP, click Actions → Associate Elastic IP address
- Choose your instance and click Associate
Now your server always has the same IP. This is the address you'll share with your friends.
8. Connect Lambda to Your EC2 Instance
Now that you have your EC2 instance, head back to your Lambda function.
Add the Instance ID environment variable:
- Go to Configuration → Environment variables
- Click Edit → Add environment variable
- Key:
INSTANCE_ID - Value: your EC2 instance ID
- Click Save
- Back in the code editor, uncomment the
INSTANCE_IDline and hit Deploy
Add IAM permissions so Lambda can control your instance. IAM (Identity and Access Management) is how AWS controls what services are allowed to do. Go to Configuration → Permissions, click the role name, hit the Add permissions dropdown, and create an inline policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:StartInstances",
"ec2:StopInstances"
],
"Resource": "arn:aws:ec2:YOUR_REGION:YOUR_ACCOUNT_ID:instance/YOUR_INSTANCE_ID"
},
{
"Effect": "Allow",
"Action": "ec2:DescribeInstances",
"Resource": "*"
}
]
}
This gives Lambda permission to start, stop, and check the status of your EC2 instance. Notice we're scoping StartInstances and StopInstances to your specific instance ARN instead of using *, so Lambda can only touch this one instance. DescribeInstances uses * because it doesn't support resource-level permissions.
Want to restrict who can use the bot commands? Discord lets server admins control that through Server Settings → Integrations. Check out Discord's command permissions docs for details.
9. Try It Out!
Head over to your Discord server and test your bot:
- Type
/server startand wait for the instance to boot up - Type
/server statusto confirm it's running - Open Minecraft, connect to your instance's public IP, and play!
- When you're done, do
/server stopto shut down and save money.
Lambda and API Gateway fall under the AWS Free Tier. The main cost to watch is EC2 if you size up for more demanding games.
We've now got our Minecraft server up and running. Let's hop in! Join the Minecraft server using your EC2 public IP as the server address: <your-ec2-public-ip>:25565
Your friends can start or stop the server at any time with the Discord bot.
Happy crafting!
Appendix: Cleanup
If you want to tear everything down, delete these resources:
- EC2 instance
- Elastic IP address
- Lambda function and layer
- API Gateway
- IAM inline policy on the Lambda role



Top comments (0)