Skip to content

CDK Construct Library for launching game servers on AWS Fargate

License

Notifications You must be signed in to change notification settings

raykrueger/cdk-game-server

Repository files navigation

cdk-game-server

This AWS CDK Construct Library is designed to run dedicated game servers on Amazon Elastic Container Service (Amazon ECS) and AWS Fargate as cheaply as possible. However, it will not be free.

The simple description is we run a single instance of a game server container on AWS Fargate. We scale the desired tasks to 0 if the CPU utilization drops below 5% for 30 minutes. We use a Discord /slash command to start the server when you want to play. We deploy no Load Balancers, we rely on public IP addresses.

A slightly deeper description... We keep costs down by making a few architectural decisions. Firstly, we are going to use a VPC with only public subnets (similar to the default VPC in a new account). This eliminates the use of NAT Gateways in a proper VPC. Secondly, we do not deploy any load balancers and simply expose the Fargate task with a public IP Address. The cdk-fargate-public-dns library is used to add optional DNS updates for the Fargate public IP address. Thirdly, we deploy a Cloudwatch Alarm that triggers if CPU Utilization falls below 5% (configurable). The action for that alarm is an AWS Lambda function (via an Amazon SNS Topic) that sets the desired tasks on the ECS Service to 0. Game servers will have an EFS Filesystem created that is mounted game save data. Note that game server files should be stored in the container, and not on EFS. Files inside the container are basically free as far as Fargate is concerned. Storing files on EFS is not free, so we only mount the save game paths. Finally, and optionally, a Discord slash command bot is deployed via AWS Step Functions and AWS Lambda. The Discord bot is entirely serverless and is used to start the server if it is stopped, or check if the server is up.

We will publish instructions for a few game servers we have tested, namely Valheim, Satisfactory, and Factorio.

Prerequisites

This library requires working knowledge of the AWS Cloud. You will have to create IAM Users, and navigate the AWS web console at times. If these are not subjects you are comfortable with, this likely isn't the library for you.

This software is released without warranty. There is no commitment that the costs of running this will be acceptable to your individual budget.

This software is released in a 0.0.x version state. Which means there are no guarantees of backwards compatibility with future changes. It also means there will be bugs.

You will be deploying this at your own risk.

Ok, let's go!

Getting Started

You will need an AWS account, and an IAM User with API Access. Optionally, you may want the AWS CLI installed.

You will need NodeJS installed, anything greater than 16.x should be fine. Now make a directory, initialize a new cdk app, and install this library.

mkdir cdk-my-server
cd cdk-my-server
npx cdk init app --language=typescript
npm install --save @raykrueger/cdk-game-server

Currently, we are only releasing the Typescript version of the library, in the future, we may release Python support. The Typescript version should work with a Javascript-based application, but we'll use Typescript for this doc.

Next, we'll edit bin/cdk-my-server.ts and remove all the boilerplate. Replace that with the following sample. We'll use Satisfactory as an example.

#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { Stack, Tags } from 'aws-cdk-lib';
import { Vpc } from 'aws-cdk-lib/aws-ec2';
import { AwsLogDriver, ContainerImage, Protocol } from 'aws-cdk-lib/aws-ecs';
import { RetentionDays } from 'aws-cdk-lib/aws-logs';
import 'source-map-support/register';
import { GameServer } from '@raykrueger/cdk-game-server';

class GameStack extends Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    new GameServer(this, 'Satisfactory', {
      cpu: 2048, // 2 vcpu
      memoryLimitMiB: 8192, // 8 gb
      image: ContainerImage.fromRegistry("raykrueger/satisfactory-dedicated-server"),
      gamePorts: [
        { portNumber: 7777, protocol: Protocol.UDP },
        { portNumber: 7777, protocol: Protocol.TCP },
      ],
      mountTarget: {
        mountTarget: "/home/steam/.config/Epic/FactoryGame/Saved/SaveGames",
        aclGroupId: 1000,
        aclUserId: 1000
      },
    });
  }
}

const app = new cdk.App();
new GameStack(app, "Satisfactory", { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-2' } });

This will deploy Satisfactory using the raykrueger/satisfactory-dedicated-server container. We will initially give it 2 vCPUs and 4gb of memory (which will be fine to start with for Satisfactory). We expose the necessary ports for the server, which all use UDP. We then specify the mountTarget, which is where the container stores the game save files.

Now let's deploy the application.

npx cdk deploy

You will be prompted to accept the security changes that occur, including creating IAM roles and Security Groups. If you accept that prompt, the deployment will commence and it will take a while.

Once that deployment is complete, you can open the AWS Console and look at your Amazon ECS Clusters. You should see a long Satisfactory-something-something name. All of the names are randomly generated from prefixes. Most aren't pretty.

  1. Click into the Cluster, it will have one Service.
  2. Click into the Service, and click on the "Configuration and tasks" tab
  3. There will be one running Task. Click on the task, it will have an ID like 98adc2c0d39d428e81868e8e35bdf9ab.
  4. In the "Configuration" table, on the right you will see the Public IP. Copy that IP Address.

Use that IP Address to connect to your server!

Autoshutdown Support

Note that the server will shutdown in 30 minutes if it is idle. If it does, the desired tasks count will be changed to 0 when that happens. To start the server again just update that desired count to 1.

Adding DNS Support

Copying the public IP address every time we start the server is annoying, so let's add DNS Support.

If you have a Hosted Zone configured in Amazon Route 53, the cdk-fargate-public-dns library support can update a DNS record for you. In your Hosted Zone, create a new A Record and set the value to the Public IP from above (or 1.1.1.1 if the server isn't running). Be sure to set the TTL to 300 seconds (5 minutes), this ensures you don't have to wait forever, but is also reasonable.

You'll add to the configuration above, after the mountTarget. The domainName is your fully qualified A Record you created previously. The hostedZone is the ID from Route 53, it usually starts with a Z.

//previous code cut for brevity
mountTarget: "/home/steam/.config/Epic/FactoryGame/Saved/SaveGames",
dnsConfig: {
  domainName: 'satisfactory.example.com',
  hostzedZone: 'ZXXXXXXXXXXXXXXXXXXXX',
  //optional: Delete this if using Route 53 the same account
  //assumedRole: 'arn:aws:iam::111111111111:role/cross-account-r53-update'
},

Optionally, we can update Route 53 in a different account, see cdk-fargate-public-dns docs for more details.

Setting up Discord

Create a Discord Bot

Logging into the AWS console and updating the desired tasks count, or doing it from the AWS CLI, works, but we can do better. The cdk-game-sever construct library can support deploying a Discord slash command bot that can start the server for us. You'll need a Discord Server and permission to add bots to do this.

You need to create a bot in Discord first. Log into the Discord Developer Portal and Create an application. After accepting the agreement, you'll be presented with the General Information page for your Application.

Continuing with Satisfactory as our example, fill in the Name as Satisfactory. You'll want to start a set of notes at this point to gather a few values. Collect the Application Id and Public Key for your application.

Now, click on the Bot navigation link on the left. Click the Add Bot button. Once you have completed the Add Bot request, click on the Copy button for the Token. Add the Bot Token to your notes.

Now, invite the bot to your server. Expand the Oauth2 dropdown in the left navigation and click on URL Generator. We are only clicking two checkboxes here, the bot and applications.commands scope. At the bottom of the page, next to the Generated URL field, click the Copy button. Open a new tab in your browser, paste that url, and hit enter. Invite your new bot to your server and accept the permissions.

The last thing you need is the Guild Id for your server (Guild is what Discord calls their servers). To get the server ID for the first parameter, open Discord, go to Settings > Advanced, and enable developer mode. Then, right-click on the server title and select "Copy ID" to get the guild ID. Add that to your notes.

Create an AWS Secret

Now we'll create a secret in AWS Secrets manager. Open the AWS Secrets Manager console. Be sure to select the region where you are deploying your game server. Click Store a new secret and choose Other Type of Secret. This will present us with the option to create a Key/value type secret.

You're going to create a secret with the following Keys. Note that the key names have to be exact, so copy them from here.

Key Value
PublicKey < The Public key from your Application >
AppId < Your Application ID >
GuildId <Your Guild ID, copied from the server >
BotToken < The Token we copied from the Bot page in Discord >
Authorization Bot < Token >

Note: The Authorization Value is the word "Bot", a space, and your bot token. Yes, currently this requires two keys.

For the Encryption key you can choose the default, unless you know you want something else.

Click Next.

For Secret Name enter "SatisfactoryBotSecret", or whatever you want, and remember that name for later.

Click Next.

You're not doing anything with this screen, so click Next again.

Now you're on the Review page, just click Store.

Your new secret may not show up on the list page right away, just refresh and it will show up.

Deploy the Discord Bot

//previous code cut for brevity
mountTarget: "/home/steam/.config/Epic/FactoryGame/Saved/SaveGames",
discord: {
  commandName: 'satisfactory',
  secretName: 'SatisfactoryBotSecret'
},

Where commandName is the command as it will appear in Discord, so "/satisfactory" in the example above. For secretName, that is going to be the name of the secret created in the previous step.

That's it. Let's deploy our server.

npx cdk deploy

If you accept the security changes the Bot will be deployed to your account. This will create an API Gateway, a few AWS Lambda Functions, and a state machine in AWS Step Functions. Additionally, some Lambda functions are deployed that act as Custom Resources in Cloudformation to register your slash commands with Discord.

When the deployment completes, you'll see some Outputs mentioned. We need the API Gateway output, it will have a somewhat nonsensical name like Satisfactory.SatisfactoryDiscordBot.....

For example:

Outputs:
Satisfactory.SatisfactoryDiscordBotDiscordBotListenerLambdaRestApiEndpointCF7F987E = https://randomnumbers.execute-api.us-east-2.amazonaws.com/prod/

Copy that URL and go back to your Application in the Discord Developer Portal. On the General Information page, paste that url into the INTERACTIONS ENDPOINT URL field.

Click Save Changes. Discord will hit your API and make sure everything is deployed correctly. If you get an error, go back and check your secret names.

Congratulations, your discord bot should be active now.

There are only two bot commands /{commandName} start and /{commandName} status and they aren't customizable at this time. The start command will start the server (by setting the Desired Task count to 1), or tell you if the server is already running. The status command will simply tell you whether the server is up or down.

Adding Logging

If your chosen game server container is giving you trouble, you can add logging

//previous code cut for brevity
mountTarget: "/home/steam/.config/Epic/FactoryGame/Saved/SaveGames",
logging: new AwsLogDriver({
  streamPrefix: 'SatisfactoryLogs',
  logRetention: RetentionDays.THREE_DAYS,
}),

This will generate a default log group in CloudWatch, and keep those logs for 3 days. A shorter retention time will keep costs down. You can set your own log group name by adding a logGroup: "MyGameServerLogs" if you want.

Tweaking Autoshutdown

If you want to raise or lower the CPU Utilization target for the autoshutdown, or increase the evaluation period you can add the following.

//previous code cut for brevity
mountTarget: "/home/steam/.config/Epic/FactoryGame/Saved/SaveGames",
autoShutdownConfig: {
  cpuUtilizationMin: 5,
  evaluationPeriods: 6
}

If CPU Utilization of your container falls below cpuUtilizationMin for evaluationPeriods the server will be shutdown by setting the Desired Tasks to 0. The evaluationPeriods is 5-minute periods. So in the defaults, Six 5-minute periods is 30 minutes.

So if your CPU Utilization is below 5% for 30 minutes, the server is stopped.

Troubleshooting

We will add to this section over time.

Game Server Not Starting

The best thing you can do for troubleshooting your game is to enable Logging

Everything Else

Open a github issue :)