For fans of Valheim, the immersive Norse-inspired survival game, dedicated servers have become essential for persistent worlds and seamless multiplayer experiences. However, running a dedicated server can be costly and inconvenient, especially for groups who play intermittently. This article delves into an innovative solution: leveraging Discord slash commands to control an on-demand Valheim server hosted on Amazon Web Services (AWS). This approach not only optimizes costs but also provides a user-friendly way to manage your server directly from your Discord community.
Before we dive into the technical details, let’s understand the motivation behind this project. Like many Valheim enthusiasts, my friends and I enjoy playing in the evenings a few times a week. Coordinating server uptime and managing costs for a server that’s not always in use presented a challenge. Existing dedicated server services offer convenience, but the continuous costs can add up. The official Valheim Discord server and online guides provide valuable resources for self-hosting, but I wanted to explore a more customized and cost-effective solution using AWS and Infrastructure as Code (IaC).
Enter AWS Cloud Development Kit (CDK), a powerful IaC tool that allows you to define and deploy AWS infrastructure using familiar programming languages like Python. CDK enables the creation of reusable, high-level components, making infrastructure management more efficient and accessible. This article will guide you through setting up a Valheim server on AWS, controlled by Discord slash commands, using CDK and the cdk-valheim
construct, offering a blend of technical depth and practical application.
Why Discord and AWS for Your Valheim Server?
The combination of Discord and AWS offers a powerful and flexible solution for managing your Valheim server. Let’s break down the advantages of each component:
Discord for Server Management:
Discord, already a central hub for gaming communities, provides an intuitive interface for server control. Integrating Discord slash commands offers several benefits:
- User-Friendly Control: Slash commands are easy to use, allowing server admins and authorized players to start, stop, and check the server status directly from their Discord channels. No need to SSH into servers or use complex web interfaces.
- Community Integration: Server management becomes part of your existing community workflow. Players can easily request server status or start the server when they are ready to play, fostering better coordination.
- Access Control: Discord’s role and permission system allows you to control who can manage the server, ensuring only authorized users can execute commands.
AWS for Server Hosting:
Amazon Web Services provides a robust and scalable platform for hosting game servers, with key advantages including:
- Cost Optimization: AWS’s on-demand infrastructure allows you to pay only for the resources you consume. By controlling server uptime via Discord, you can significantly reduce costs compared to always-on server solutions.
- Scalability and Reliability: AWS infrastructure is designed for high availability and scalability. Services like Elastic Container Service (ECS) and Fargate ensure your server can handle player load and maintain stable performance.
- Flexibility and Customization: AWS offers a wide range of services and configuration options, allowing for deep customization of your server environment to meet specific needs.
- Global Reach: AWS’s global infrastructure allows you to choose server locations closer to your player base, minimizing latency and improving the gaming experience.
By combining Discord’s user-friendly interface with AWS’s powerful and cost-effective infrastructure, we create a smart and efficient solution for running a Valheim server on demand.
Project Overview: Valheim Server on Demand
This project utilizes a serverless architecture on AWS, orchestrated by Discord slash commands. Here’s a breakdown of the components and how they interact, visualized in the diagram below:
Diagram
Key Components:
- Discord Client (14): Players interact with the server through Discord using slash commands (13) within their community server.
- Discord Interactions (15): When a slash command is invoked (e.g.,
/vh server start
), Discord sends aPOST
request (15) to a pre-configured endpoint. - API Gateway (16): Amazon API Gateway acts as the entry point, receiving the
POST
request from Discord and routing it to the Lambda function. - Lambda Function (17): An AWS Lambda function, running a lightweight Flask application, processes the request. It verifies the request’s authenticity using security headers from Discord and then interprets the command (start, stop, status).
- AWS SDK (boto3) (18): The Lambda function uses the boto3 library to interact with AWS services, specifically ECS.
- Elastic Container Service (ECS) (6): Amazon ECS manages the Valheim server container. The Lambda function scales the ECS service’s desired task count to 1 (start) or 0 (stop). We utilize Fargate, a serverless compute engine for containers, eliminating the need to manage EC2 instances.
- Elastic File System (EFS) (7): Amazon EFS provides persistent storage for the Valheim world data. This ensures that game progress is saved between server start and stop cycles.
cdk-valheim
Construct (10): This CDK construct simplifies the setup of the ECS service, Fargate, EFS, and optional AWS Backup (8). It provides a high-level abstraction for deploying the Valheim server infrastructure.- AWS CDK CLI (3): The CDK Command Line Interface is used to define and deploy the entire infrastructure as code from your local machine (1) or a CI/CD pipeline (2), such as GitLab CI.
Workflow:
- A user types a slash command in Discord (13).
- Discord sends an Interaction
POST
request (15) to the API Gateway endpoint (16). - API Gateway triggers the Lambda function (17).
- The Lambda function authenticates the request and executes the corresponding action:
- Start: Scales the ECS service to 1 task, starting the Valheim server.
- Stop: Scales the ECS service to 0 tasks, stopping the server and reducing costs.
- Status: Queries ECS for the server status (desired, running, pending tasks) and responds to the Discord user with the information.
- The Valheim server runs within an ECS container (6), using EFS for persistent world storage (7).
- Players connect to the server using the public IP of the ECS task (5) and the server password.
This architecture ensures that the Valheim server is only running when needed, controlled directly from Discord, minimizing AWS costs while providing a seamless gaming experience for your community.
Setting Up Your Discord Application for Server Control
To enable Discord slash command control, you first need to create and configure a Discord application. Follow these steps:
-
Create a Discord Server (if you don’t have one): You’ll need admin privileges on a Discord server to integrate the bot.
-
Navigate to Discord Developer Portal: Go to https://discord.com/developers/applications and log in with your Discord account.
-
Create a New Application: Click “New Application” and give your application a name (e.g., “Valheim Server Control”).
-
General Information:
- Note down the APPLICATION ID (Public Key). You’ll need this later.
- Go to
Server Settings > Widget
in your Discord server and note the SERVER ID (Guild ID).
-
OAuth2 Tab:
- In the “OAuth2” tab, under “Scopes,” select
bot
andapplications.commands
. - Copy the generated OAuth2 authorization link. Open this link in a new browser tab. You might see an error message initially.
- In the “OAuth2” tab, under “Scopes,” select
-
Bot Tab:
- Navigate to the “Bot” tab and click “Add Bot.” Confirm by clicking “Yes, do it!”.
- Important: Turn off the “Public Bot” option to restrict bot access to your server. Save changes.
- Click “Reset Token” to generate a BOT TOKEN. Note this down securely, as it provides access to your bot. Add this to your
.env
file along withGUILD_ID
andAPPLICATION_ID
:
export GUILD_ID=YOUR_GUILD_ID export APPLICATION_ID=YOUR_APPLICATION_ID export BOT_TOKEN=YOUR_BOT_TOKEN
-
Authorize Bot to Your Server:
- Return to the “OAuth2” tab, select
bot
andapplications.commands
scopes again, and copy the updated OAuth2 authorization link. - Open the link in your browser. Select your server from the dropdown menu and click “Authorize.” Complete the captcha if prompted.
- You should see an “Authorized” message and a notification in your Discord server that your bot has joined.
- Return to the “OAuth2” tab, select
-
Create the Interaction (Slash Command):
- Currently, Discord Interactions (slash commands) are registered via HTTP
POST
requests. Use the following Python script to register the/vh
command:
""" https://discord.com/developers/docs/interactions/slash-commands#registering-a-command """ import os import requests APPLICATION_ID = os.environ.get("APPLICATION_ID") GUILD_ID = os.environ.get("GUILD_ID") BOT_TOKEN = os.environ.get("BOT_TOKEN") url = f"https://discord.com/api/v8/applications/{APPLICATION_ID}/guilds/{GUILD_ID}/commands" json = { "name": "vh", "description": "Start, stop or get the status of the Valheim server", "options": [ { "name": "valheim_server_controls", "description": "What do you want to do?", "type": 3, "required": True, "choices": [ { "name": "status", "value": "status" }, { "name": "start", "value": "start" }, { "name": "stop", "value": "stop" } ] }, ] } headers = { "Authorization": f"Bot {BOT_TOKEN}" } if __name__ == "__main__": r = requests.post(url, headers=headers, json=json) print(r.content)
- Save this script as
register_bot.py
. Before running it, source your.env
file:
source .env
- Execute the script:
python3 register_bot.py
- A successful registration will return a JSON response containing command details.
- Currently, Discord Interactions (slash commands) are registered via HTTP
-
Test the Slash Command: Type
/
in any channel on your Discord server. You should see thevh
command in the autocomplete options. If you run any command now, it will likely respond with “This interaction failed” as the Interactions Endpoint URL is not yet configured.
Setting Up AWS Infrastructure with CDK
Now, let’s move to the AWS side and set up the infrastructure using CDK.
-
Prerequisites: Ensure you have Node.js, npm, and AWS CDK CLI installed.
npm i -g aws-cdk
-
Initialize CDK Project: Create a directory for your project and initialize a Python CDK app within a
cdk
subdirectory:mkdir valheim-discord-cdk && cd valheim-discord-cdk mkdir cdk && cd cdk && cdk init app --language=python
-
Add CDK Dependencies: Modify the
install_requires
section incdk/setup.py
to include necessary CDK libraries and thecdk-valheim
construct:install_requires=[ "aws-cdk.core==1.92.0", "aws-cdk.aws_applicationautoscaling==1.92.0", "aws-cdk.aws_datasync==1.92.0", "aws-cdk.aws_lambda==1.92.0", "aws-cdk.aws_s3==1.92.0", "aws-cdk.aws_apigateway==1.92.0", "cdk-valheim==0.0.16", ],
Run
pip install -r requirements.txt
in thecdk
directory to install these dependencies. -
Define Valheim World Construct: Replace the content of
cdk/cdk/cdk_stack.py
with the following code, which defines theValheimWorld
construct:from aws_cdk import core as cdk from aws_cdk import ( core, aws_datasync as datasync, aws_iam as iam, aws_lambda as _lambda, aws_apigateway as apigw, aws_applicationautoscaling as appScaling, aws_s3 as s3, ) from cdk_valheim import ValheimWorld, ValheimWorldScalingSchedule import os class CdkStack(cdk.Stack): def __init__(self, scope: cdk.Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) self.valheim_world = ValheimWorld( self, 'ValheimWorld', cpu=2048, memory_limit_mib=4096, schedules=[ValheimWorldScalingSchedule( start=appScaling.CronOptions(hour='12', week_day='1-5'), stop=appScaling.CronOptions(hour='1', week_day='1-5'), )], environment={ "SERVER_NAME": os.environ.get("SERVER_NAME", "CDK Valheim"), "WORLD_NAME": os.environ.get("WORLD_NAME", "Amazon"), "SERVER_PASS": os.environ.get("SERVER_PASS", "fargate"), "BACKUPS": 'false', }) self.env_vars = { "APPLICATION_PUBLIC_KEY": os.environ.get("APPLICATION_PUBLIC_KEY"), "ECS_SERVICE_NAME": self.valheim_world.service.service_name, "ECS_CLUSTER_ARN": self.valheim_world.service.cluster.cluster_arn } self.flask_lambda_layer = _lambda.LayerVersion( self, "FlaskAppLambdaLayer", code=_lambda.AssetCode("./layers/flask"), compatible_runtimes=[_lambda.Runtime.PYTHON_3_8, ], ) self.flask_app_lambda = _lambda.Function( self, "FlaskAppLambda", runtime=_lambda.Runtime.PYTHON_3_8, code=_lambda.AssetCode('./lambda/functions/interactions'), function_name="flask-app-handler", handler="lambda-handler.handler", layers=[self.flask_lambda_layer], timeout=core.Duration.seconds(60), environment={**self.env_vars}, ) self.flask_app_lambda.role.add_managed_policy( iam.ManagedPolicy.from_managed_policy_arn( self, 'ECS_FullAccessPolicy', managed_policy_arn='arn:aws:iam::aws:policy/AmazonECS_FullAccess' ) ) self.request_templates = { "application/json": '''{ "method": "$context.httpMethod", "body" : $input.json("$"), "headers": { #foreach($param in $input.params().header.keySet()) "$param": "$util.escapeJavaScript($input.params().header.get($param))" #if($foreach.hasNext),#end #end } }''' } self.apigateway = apigw.RestApi( self, 'FlaskAppEndpoint', ) self.apigateway.root.add_method("ANY") self.discord_interaction_webhook = self.apigateway.root.add_resource("discord") self.discord_interaction_webhook_integration = apigw.LambdaIntegration( self.flask_app_lambda, request_templates=self.request_templates ) self.discord_interaction_webhook.add_method( 'POST', self.discord_interaction_webhook_integration )
-
Update CDK App Entry Point: Modify
cdk/app.py
to name your stack and pass environment variables:#!/usr/bin/env python3 import os from aws_cdk import core as cdk from cdk.cdk_stack import CdkStack aws_region = os.environ.get("AWS_DEFAULT_REGION", "us-east-1") aws_account = os.environ.get("AWS_ACCOUNT_ID", "") app = cdk.App() CdkStack(app, "valheim-server-stack", env={"region": aws_region, "account": aws_account}) app.synth()
-
Lambda Function Code: Create the directory structure
lambda/functions/interactions
and place thelambda-handler.py
andrequirements.txt
files (provided earlier) in this directory. Createlayers/flask
and an emptylayers/flask/python
directory. -
GitLab CI for Automated Deployments: In the project root, create a
.gitlab-ci.yml
file with the following content:stages: - build - deploy image: python:3.8 pip_install: stage: build rules: - if: "$CI_COMMIT_TAG" when: always artifacts: paths: - layers/flask/python script: - pip install -r lambda/functions/interactions/requirements.txt -t layers/flask/python cdk_deploy: stage: deploy rules: - if: "$CI_COMMIT_TAG" when: always before_script: - apt-get -qq update && apt-get -y install nodejs npm - npm i -g aws-cdk - pip3 install -e cdk script: - cdk bootstrap --app cdk/app.py aws://$AWS_ACCOUNT_ID/$AWS_DEFAULT_REGION - cdk deploy --app cdk/app.py --require-approval never
-
GitLab Repository and Variables:
- Initialize a Git repository in your project root (
git init
). Remove the.git
directory insidecdk
if it exists (rm -rf cdk/.git
). - Create a GitLab repository and add it as a remote (
git remote add origin ...
). - In your GitLab project’s
Settings > CI/CD > Variables
, add the following protected variables:AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_DEFAULT_REGION
AWS_ACCOUNT_ID
APPLICATION_PUBLIC_KEY
SERVER_PASS
SERVER_NAME
WORLD_NAME
- Protect tags in
Settings > Repository > Protected Tags
by adding a wildcard (*
).
- Initialize a Git repository in your project root (
-
Deploy via GitLab CI: Commit your changes, tag a release, and push to GitLab:
git add . git commit -m "Initial commit with CDK and Discord integration" git tag v0.0.1 git push origin v0.0.1
Monitor your GitLab CI pipeline. If successful, it will deploy the Valheim server infrastructure to your AWS account.
Lambda Function for Handling Discord Interactions
The Lambda function, written in Python using Flask, acts as the bridge between Discord slash commands and AWS ECS.
Code Breakdown (lambda-handler.py
):
import os
import logging
import awsgi
import boto3
from discord_interactions import verify_key_decorator
from flask import (
Flask, jsonify, request
)
client = boto3.client('ecs')
PUBLIC_KEY = os.environ.get('APPLICATION_PUBLIC_KEY')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
app = Flask(__name__)
@app.route('/discord', methods=['POST'])
@verify_key_decorator(PUBLIC_KEY)
def index():
if request.json["type"] == 1: # Respond to Discord PING
return jsonify({"type": 1})
else:
logger.info(request.json)
try:
interaction_option = request.json["data"]["options"][0]["value"]
except KeyError:
logger.info("Could not parse the interaction option")
interaction_option = "status"
logger.info("Interaction:")
logger.info(interaction_option)
content = ""
if interaction_option == "status":
try:
resp = client.describe_services(
cluster=os.environ.get("ECS_CLUSTER_ARN", ""),
services=[
os.environ.get("ECS_SERVICE_NAME", ""),
]
)
desired_count = resp["services"][0]["desiredCount"]
running_count = resp["services"][0]["runningCount"]
pending_count = resp["services"][0]["pendingCount"]
content = f"Desired: {desired_count} | Running: {running_count} | Pending: {pending_count}"
except Exception as e:
content = "Could not get server status"
logger.info("Could not get the server status")
logger.info(e)
elif interaction_option == "start":
content = "Starting the server"
resp = client.update_service(
cluster=os.environ.get("ECS_CLUSTER_ARN", ""),
service=os.environ.get("ECS_SERVICE_NAME", ""),
desiredCount=1
)
elif interaction_option == "stop":
content = "Stopping the server"
resp = client.update_service(
cluster=os.environ.get("ECS_CLUSTER_ARN", ""),
service=os.environ.get("ECS_SERVICE_NAME", ""),
desiredCount=0
)
else:
content = "Unknown command"
logger.info(resp)
return jsonify({
"type": 4, # Respond to channel message
"data": {
"tts": False,
"content": content,
"embeds": [],
"allowed_mentions": {"parse": []}
}
})
def handler(event, context):
return awsgi.response(app, event, context, base64_content_types={"image/png"})
Key Functionality:
- Flask Application: A simple Flask app handles the
/discord
POST endpoint. verify_key_decorator
: This decorator from thediscord-interactions
library verifies the authenticity of requests from Discord using your application’s public key, ensuring security.- Command Handling: The
index()
function parses the incoming JSON payload from Discord, identifies the slash command option (status, start, stop), and performs the corresponding action using boto3 to interact with ECS. - ECS Interaction: The function uses
boto3.client('ecs')
to:describe_services()
: To get the current status of the ECS service (for thestatus
command).update_service()
: To scale the ECS service’sdesiredCount
to 1 (forstart
) or 0 (forstop
).
- Response to Discord: The function returns a JSON response in the format Discord expects, displaying the server status or command confirmation in the Discord channel.
awsgi.response
: Thehandler()
function wraps the Flask app withawsgi.response
to make it compatible with API Gateway and Lambda.
Configuring API Gateway Endpoint
API Gateway acts as the public-facing endpoint for your Discord slash commands, routing requests to the Lambda function.
CDK Configuration (cdk_stack.py
):
The CDK code in cdk_stack.py
(provided earlier) sets up the API Gateway with the following key configurations:
apigw.RestApi
: Creates a new REST API.apigateway.root.add_resource("discord")
: Defines a resource path/discord
under your API Gateway endpoint.apigw.LambdaIntegration
: Integrates the/discord
resource with yourflask_app_lambda
function.request_templates
: This crucial section defines a request template in Apache Velocity Template Language (VTL). It ensures that all headers from the incomingPOST
request from Discord, including the security-critical signature headers, are passed through to the Lambda function. Without this, the@verify_key_decorator
in the Lambda function would fail to validate the requests.
Deployment and Final Configuration
-
Retrieve API Gateway URL: After a successful GitLab CI deployment, check the pipeline logs for the API Gateway endpoint URL. It will be in the format:
https://YOUR_API_GATEWAY_ID.execute-api.YOUR_REGION.amazonaws.com/prod/
. -
Configure Interactions Endpoint URL in Discord:
- Go back to your Discord Application’s General Information page (https://discord.com/developers/applications/).
- In the “Interactions Endpoint URL” field, enter your API Gateway URL, appending
/discord
to the end:https://YOUR_API_GATEWAY_ID.execute-api.YOUR_REGION.amazonaws.com/prod/discord
. - Save changes. Discord will automatically verify the URL. If verification fails, double-check your API Gateway configuration and Lambda function code, especially the public key and header handling.
-
Test Discord Slash Commands: Now, in your Discord server, use the
/vh
slash command./vh status
: Should return the current status of your Valheim server (Desired, Running, Pending tasks)./vh start
: Should start the Valheim server. It might take a few minutes for the server to become fully online./vh stop
: Should stop the Valheim server, reducing AWS costs.
Conclusion: Serverless Valheim Fun with Discord Control
This project demonstrates a powerful and cost-effective way to run a Valheim server using Amazon AWS and Discord slash commands. By leveraging serverless technologies and Infrastructure as Code, you gain:
- Cost Efficiency: Pay only for server uptime, significantly reducing expenses compared to always-on solutions.
- On-Demand Server: Start and stop your server as needed, directly from Discord, enhancing convenience for intermittent play sessions.
- Community Integration: Server management becomes seamless within your Discord community, improving coordination and accessibility.
- Scalability and Reliability: AWS infrastructure ensures your server is robust and can handle your player base.
- Customization and Extensibility: The CDK and AWS ecosystem provide ample opportunities for further customization and feature additions.
This solution not only addresses the practical challenges of running a Valheim server but also showcases the potential of combining Discord’s community platform with the power of cloud computing for innovative gaming experiences. You can further enhance this setup by exploring features like automated backups, billing reports, and contributing to the open-source cdk-valheim
project to improve and expand its capabilities for the community.
Further Reading and Resources
- GitLab Repository: https://gitlab.com/briancaffey/valheim-cdk-discord-interactions – Code repository for this project.
cdk-valheim
Construct: https://github.com/gotodeploy/cdk-valheim – GitHub repository for the CDK construct used for Valheim server deployment.- Discord Developer Documentation – Interactions: https://discord.com/developers/docs/interactions/slash-commands – Official Discord documentation on slash commands and interactions.