Skip to content

Latest commit

 

History

History
1014 lines (674 loc) · 36 KB

API.md

File metadata and controls

1014 lines (674 loc) · 36 KB

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 '../../cdk-game-server/src';

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: 15000, protocol: Protocol.UDP },
        { portNumber: 15777, protocol: Protocol.UDP }
      ],
      mountTarget: "/home/steam/.config/Epic/FactoryGame/Saved/SaveGames"
    });
  }
}

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

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 :)

API Reference

Constructs

GameServer

Builds a game server, running on ECS Fargate.

This is designed to run as cheaply as possible, which means some availability and reliability has been sacrificed.

Default configuration: Single AZ with a Single Public Subnet Fargate Spot capacity provider EFS General performance file system for storage

Initializers

import { GameServer } from '@raykrueger/cdk-game-server'

new GameServer(scope: Construct, id: string, props: GameServerProps)
Name Type Description
scope constructs.Construct No description.
id string No description.
props GameServerProps No description.

scopeRequired
  • Type: constructs.Construct

idRequired
  • Type: string

propsRequired

Methods

Name Description
toString Returns a string representation of this construct.

toString
public toString(): string

Returns a string representation of this construct.

Static Functions

Name Description
isConstruct Checks if x is a construct.

isConstruct
import { GameServer } from '@raykrueger/cdk-game-server'

GameServer.isConstruct(x: any)

Checks if x is a construct.

xRequired
  • Type: any

Any object.


Properties

Name Type Description
node constructs.Node The tree node.
cluster aws-cdk-lib.aws_ecs.ICluster No description.
containerEnv {[ key: string ]: string} No description.
containerInsights boolean No description.
containerSecrets {[ key: string ]: aws-cdk-lib.aws_ecs.Secret} No description.
cpu number No description.
gamePorts GamePort[] No description.
image aws-cdk-lib.aws_ecs.ContainerImage No description.
memoryLimitMiB number No description.
mountTarget string No description.
service aws-cdk-lib.aws_ecs.IService No description.
additionalArgs string[] No description.
autoShutdownConfig AutoShutdownProps No description.
discord DiscordCommandOptions No description.
dnsConfig DomainProps No description.
logging aws-cdk-lib.aws_ecs.LogDriver No description.
steamArgs string No description.
vpc aws-cdk-lib.aws_ec2.IVpc No description.

nodeRequired
public readonly node: Node;
  • Type: constructs.Node

The tree node.


clusterRequired
public readonly cluster: ICluster;
  • Type: aws-cdk-lib.aws_ecs.ICluster

containerEnvRequired
public readonly containerEnv: {[ key: string ]: string};
  • Type: {[ key: string ]: string}

containerInsightsRequired
public readonly containerInsights: boolean;
  • Type: boolean

containerSecretsRequired
public readonly containerSecrets: {[ key: string ]: Secret};
  • Type: {[ key: string ]: aws-cdk-lib.aws_ecs.Secret}

cpuRequired
public readonly cpu: number;
  • Type: number

gamePortsRequired
public readonly gamePorts: GamePort[];

imageRequired
public readonly image: ContainerImage;
  • Type: aws-cdk-lib.aws_ecs.ContainerImage

memoryLimitMiBRequired
public readonly memoryLimitMiB: number;
  • Type: number

mountTargetRequired
public readonly mountTarget: string;
  • Type: string

serviceRequired
public readonly service: IService;
  • Type: aws-cdk-lib.aws_ecs.IService

additionalArgsOptional
public readonly additionalArgs: string[];
  • Type: string[]

autoShutdownConfigOptional
public readonly autoShutdownConfig: AutoShutdownProps;

discordOptional
public readonly discord: DiscordCommandOptions;

dnsConfigOptional
public readonly dnsConfig: DomainProps;

loggingOptional
public readonly logging: LogDriver;
  • Type: aws-cdk-lib.aws_ecs.LogDriver

steamArgsOptional
public readonly steamArgs: string;
  • Type: string

vpcOptional
public readonly vpc: IVpc;
  • Type: aws-cdk-lib.aws_ec2.IVpc

Structs

AutoShutdownProps

Initializer

import { AutoShutdownProps } from '@raykrueger/cdk-game-server'

const autoShutdownProps: AutoShutdownProps = { ... }

Properties

Name Type Description
cpuUtilizationMin number No description.
evaluationPeriods number No description.

cpuUtilizationMinOptional
public readonly cpuUtilizationMin: number;
  • Type: number

evaluationPeriodsOptional
public readonly evaluationPeriods: number;
  • Type: number

DiscordCommandOptions

Initializer

import { DiscordCommandOptions } from '@raykrueger/cdk-game-server'

const discordCommandOptions: DiscordCommandOptions = { ... }

Properties

Name Type Description
commandName string No description.
secretName string No description.

commandNameRequired
public readonly commandName: string;
  • Type: string

secretNameRequired
public readonly secretName: string;
  • Type: string

DomainProps

Initializer

import { DomainProps } from '@raykrueger/cdk-game-server'

const domainProps: DomainProps = { ... }

Properties

Name Type Description
domainName string No description.
hostzedZone string No description.
assumedRole string No description.

domainNameRequired
public readonly domainName: string;
  • Type: string

hostzedZoneRequired
public readonly hostzedZone: string;
  • Type: string

assumedRoleOptional
public readonly assumedRole: string;
  • Type: string

GamePort

Initializer

import { GamePort } from '@raykrueger/cdk-game-server'

const gamePort: GamePort = { ... }

Properties

Name Type Description
portNumber number No description.
protocol aws-cdk-lib.aws_ecs.Protocol No description.

portNumberRequired
public readonly portNumber: number;
  • Type: number

protocolRequired
public readonly protocol: Protocol;
  • Type: aws-cdk-lib.aws_ecs.Protocol

GameServerProps

Initializer

import { GameServerProps } from '@raykrueger/cdk-game-server'

const gameServerProps: GameServerProps = { ... }

Properties

Name Type Description
gamePorts GamePort[] No description.
image aws-cdk-lib.aws_ecs.ContainerImage The container image to run.
mountTarget string No description.
additionalArgs string[] No description.
autoShutdownConfig AutoShutdownProps No description.
containerEnv {[ key: string ]: string} No description.
containerInsights boolean Do we want to enable Cloudwatch Container Insights, and incur additional cost?
containerSecrets {[ key: string ]: aws-cdk-lib.aws_ecs.Secret} No description.
cpu number vCpu amout to be granted to ECS Fargate task.
discord DiscordCommandOptions No description.
dnsConfig DomainProps No description.
logging aws-cdk-lib.aws_ecs.LogDriver Logging driver to use.
memoryLimitMiB number Memory limit in 1024 incrmements.
vpc aws-cdk-lib.aws_ec2.IVpc Provide an existing VPC to deploy into.

gamePortsRequired
public readonly gamePorts: GamePort[];

imageRequired
public readonly image: ContainerImage;
  • Type: aws-cdk-lib.aws_ecs.ContainerImage

The container image to run.


mountTargetRequired
public readonly mountTarget: string;
  • Type: string

additionalArgsOptional
public readonly additionalArgs: string[];
  • Type: string[]

autoShutdownConfigOptional
public readonly autoShutdownConfig: AutoShutdownProps;

containerEnvOptional
public readonly containerEnv: {[ key: string ]: string};
  • Type: {[ key: string ]: string}

containerInsightsOptional
public readonly containerInsights: boolean;
  • Type: boolean
  • Default: false

Do we want to enable Cloudwatch Container Insights, and incur additional cost?


containerSecretsOptional
public readonly containerSecrets: {[ key: string ]: Secret};
  • Type: {[ key: string ]: aws-cdk-lib.aws_ecs.Secret}

cpuOptional
public readonly cpu: number;
  • Type: number
  • Default: DEFAULT_VCPU

vCpu amout to be granted to ECS Fargate task.

https://aws.amazon.com/fargate/pricing/


discordOptional
public readonly discord: DiscordCommandOptions;

dnsConfigOptional
public readonly dnsConfig: DomainProps;

loggingOptional
public readonly logging: LogDriver;
  • Type: aws-cdk-lib.aws_ecs.LogDriver
  • Default: undefined

Logging driver to use.

The Cloudwatch logging driver will incur addtional costs.


Example

logging: new ecs.AwsLogDriver({ streamPrefix: 'EventDemo' })
memoryLimitMiBOptional
public readonly memoryLimitMiB: number;
  • Type: number
  • Default: DEFAULT_VCPU

Memory limit in 1024 incrmements.

https://aws.amazon.com/fargate/pricing/


vpcOptional
public readonly vpc: IVpc;
  • Type: aws-cdk-lib.aws_ec2.IVpc

Provide an existing VPC to deploy into.

If none is given a default ec2.VPC will be created.