Skip to content

Commit 607f09f

Browse files
authored
feature: use docker to run game servers (#111)
* feature: use docker to run game servers * use docker image from game version * migrate game deployment query from mysql to graphql * use ports from game_version
1 parent 3bf22df commit 607f09f

32 files changed

+461
-63
lines changed

async-server-provisioner/config/custom-environment-variables.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ hcloudServer:
1515
datadog:
1616
enabled: DD_ENABLED
1717
api_key: DD_API_KEY
18+
19+
cloudGame:
20+
apiUrl: CLOUD_GAME_API_URL
21+
apiToken: CLOUD_GAME_API_TOKEN

async-server-provisioner/config/default.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ hetzner:
1111

1212
hcloudServer:
1313
type: cx31
14-
image: '162583525'
14+
image: '162606073'
1515
location: nbg1
1616

1717
datadog:
1818
enabled: false
1919
api_key: placeholder
20+
21+
cloudGame:
22+
apiUrl: http://host.docker.internal:1337
23+
apiToken: placeholder
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as config from 'config';
2+
3+
const apiUrl = config.get<string>('cloudGame.apiUrl');
4+
5+
export async function gqlQuery(query: string, data: any): Promise<any> {
6+
const res = await fetch(`${apiUrl}/graphql`, {
7+
method: 'POST',
8+
headers: {
9+
'Content-Type': 'application/json',
10+
Accept: 'application/json',
11+
Authorization: config.get<string>('cloudGame.apiToken'),
12+
},
13+
body: JSON.stringify({
14+
query,
15+
variables: data,
16+
}),
17+
});
18+
return res.json();
19+
}

async-server-provisioner/src/entities/CloudInstance.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ export interface CloudInstance {
77

88
export function cloudInstanceFactory(row: any): CloudInstance {
99
return {
10-
provider: row.ci_provider,
11-
apiName: row.ci_api_name,
12-
costPerHour: row.ci_cost_per_hour,
13-
region: row.ci_region,
10+
provider: row.data.attributes.provider,
11+
apiName: row.data.attributes.api_name,
12+
costPerHour: row.data.attributes.cost_per_hour,
13+
region: row.data.attributes.region,
1414
};
1515
}

async-server-provisioner/src/entities/GameDeployment.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,26 @@ export enum GameDeploymentStatus {
88

99
export interface GameDeployment {
1010
id: number;
11-
workspaceName: string;
1211
consumerUUID: string;
1312
status: GameDeploymentStatus;
1413
cloudInstance: CloudInstance;
1514
gameInstance: GameInstance;
1615
}
1716

18-
function generateTFWorkspaceName(row: any): string {
19-
const rawString = `${row.gi_id}-${row.gi_name}`;
17+
export function generateTFWorkspaceName(deploy: GameDeployment): string {
18+
const rawString = `${deploy.id}-${deploy.gameInstance.name}`;
2019
return rawString.replace(/([\s-_])/g, '-').toLowerCase();
2120
}
2221

23-
export function gameDeploymentFactory(row: any): GameDeployment {
24-
const workspaceName = generateTFWorkspaceName(row);
22+
export function gameDeploymentFactory(consumerUid: string, row: any): GameDeployment {
23+
const gameDeployment = row.data.gameDeployment;
24+
const cloudInstance = gameDeployment.data.attributes.cloud_instance;
25+
const gameInstance = gameDeployment.data.attributes.game_instance;
2526
return {
26-
workspaceName,
27-
id: row.gd_id,
28-
consumerUUID: row.gd_consumer_uuid,
29-
status: (GameDeploymentStatus as any)[row.gd_status],
30-
cloudInstance: cloudInstanceFactory(row),
31-
gameInstance: gameInstanceFactory(row),
27+
id: gameDeployment.data.id,
28+
consumerUUID: consumerUid,
29+
status: (GameDeploymentStatus as any)[gameDeployment.data.attributes.status],
30+
cloudInstance: cloudInstanceFactory(cloudInstance),
31+
gameInstance: gameInstanceFactory(gameInstance),
3232
};
3333
}
Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
export interface GameInstance {
22
name: string;
33
dockerImage: string;
4+
ports: GameInstancePort[];
5+
}
6+
7+
export enum GameInstancePortType {
8+
UDP = 'UDP',
9+
TCP = 'TCP',
10+
}
11+
12+
export interface GameInstancePort {
13+
name: string;
14+
port: number;
15+
type: GameInstancePortType;
416
}
517

618
export function gameInstanceFactory(row: any): GameInstance {
19+
const gameVersion = row.data.attributes.game_version;
20+
return {
21+
name: row.data.attributes.name,
22+
dockerImage: gameVersion.data.attributes.docker_image,
23+
ports: gameVersion.data.attributes.ports.map(parseGameInstancePort),
24+
};
25+
}
26+
27+
function parseGameInstancePort(raw: any): GameInstancePort {
728
return {
8-
name: row.gi_name,
9-
dockerImage: row.gv_docker_image,
29+
name: raw.name,
30+
port: raw.port,
31+
type: (GameInstancePortType as any)[raw.type],
1032
};
1133
}

async-server-provisioner/src/entities/MinecraftTFConfig.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as config from 'config';
2-
import { GameDeployment } from './GameDeployment';
2+
import { GameDeployment, generateTFWorkspaceName } from './GameDeployment';
33

44
export interface MinecraftTFConfig {
55
metadata: {
@@ -10,6 +10,11 @@ export interface MinecraftTFConfig {
1010
type: string;
1111
image: string;
1212
docker_image: string;
13+
ports: {
14+
proto: string;
15+
port: string;
16+
description: string;
17+
}[];
1318
};
1419
datadog: {
1520
enabled: boolean;
@@ -26,13 +31,18 @@ export function mcTFConfToTFArgs(mfConfig: MinecraftTFConfig): string {
2631
export function createMinecraftTFConfigFromGameConfig(mfConfig: GameDeployment): MinecraftTFConfig {
2732
return {
2833
metadata: {
29-
name: mfConfig.workspaceName,
34+
name: generateTFWorkspaceName(mfConfig),
3035
location: mfConfig.cloudInstance.region,
3136
},
3237
server: {
3338
type: mfConfig.cloudInstance.apiName,
3439
image: config.get('hcloudServer.image'),
35-
docker_image: 'cloudgame/minecraft:vanilla-1.18.2',
40+
docker_image: mfConfig.gameInstance.dockerImage,
41+
ports: mfConfig.gameInstance.ports.map((port) => ({
42+
proto: port.type.toLowerCase(),
43+
port: `${port.port}`,
44+
description: port.name,
45+
})),
3646
},
3747
datadog: {
3848
enabled: config.get('datadog.enabled'),

async-server-provisioner/src/repositories/GameDeploymentRepository.ts

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,49 @@ import MySqlAdapter from '../adapters/MySqlAdapter';
22
import { GameDeployment, gameDeploymentFactory } from '../entities/GameDeployment';
33
import { v4 } from 'uuid';
44
import { TerraformGSOutput } from '../services/TerraformService';
5+
import { gqlQuery } from '../adapters/GraphQlAdapter';
6+
7+
const query = `
8+
query($id: ID) {
9+
gameDeployment(id: $id) {
10+
data {
11+
id
12+
attributes {
13+
status
14+
cloud_instance {
15+
data {
16+
attributes {
17+
api_name
18+
provider
19+
region
20+
cost_per_hour
21+
}
22+
}
23+
}
24+
game_instance {
25+
data {
26+
attributes {
27+
name
28+
game_version {
29+
data {
30+
attributes {
31+
docker_image
32+
ports {
33+
name
34+
port
35+
type
36+
}
37+
}
38+
}
39+
}
40+
}
41+
}
42+
}
43+
}
44+
}
45+
}
46+
}
47+
`;
548

649
export default class GameDeploymentRepository {
750
constructor(
@@ -24,14 +67,8 @@ export default class GameDeploymentRepository {
2467

2568
const rows = await this.dirtyMysqlAdapter.query(
2669
`
27-
SELECT gd.id as gd_id, gd.status as gd_status, gd.consumer_uuid as gd_consumer_uuid, ci.provider as ci_provider, ci.api_name as ci_api_name, ci.cost_per_hour as ci_cost_per_hour, ci.region as ci_region, gi.id as gi_id, gi.name as gi_name, gv.docker_image as gv_docker_image
70+
SELECT gd.id as gd_id
2871
FROM game_deployments gd
29-
INNER JOIN game_deployments_game_instance_links gil ON gil.game_deployment_id = gd.id
30-
INNER JOIN game_instances gi ON gil.game_instance_id = gi.id
31-
INNER JOIN game_instances_game_version_links gvl ON gvl.game_instance_id = gi.id
32-
INNER JOIN game_versions gv ON gv.id = gvl.game_version_id
33-
INNER JOIN game_deployments_cloud_instance_links cil ON cil.game_deployment_id = gd.id
34-
INNER JOIN cloud_instances ci ON ci.id = cil.cloud_instance_id
3572
WHERE consumer_uuid = ?;
3673
`,
3774
[uuid]
@@ -41,7 +78,11 @@ export default class GameDeploymentRepository {
4178
return null;
4279
}
4380

44-
return gameDeploymentFactory(rows[0]);
81+
const data = await gqlQuery(query, { id: rows[0].gd_id });
82+
const gameDeployment = gameDeploymentFactory(uuid, data);
83+
// eslint-disable-next-line no-console
84+
console.log(JSON.stringify(gameDeployment, null, 2));
85+
return gameDeployment;
4586
}
4687

4788
public async failedDeployment(): Promise<void> {

async-server-provisioner/src/services/GameDeploymentService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Logger } from 'pino';
22
import GameDeploymentRepository from '../repositories/GameDeploymentRepository';
33
import { TerraformService } from './TerraformService';
44
import { HetznerCloudRepository } from '../repositories/HetznerCloudRepository';
5-
import { GameDeploymentStatus } from '../entities/GameDeployment';
5+
import { GameDeploymentStatus, generateTFWorkspaceName } from '../entities/GameDeployment';
66

77
export interface GameDeploymentServiceConfig {
88
timeoutMillis?: number;
@@ -61,7 +61,7 @@ export class GameDeploymentService {
6161
this.logger.info('received new message %s', JSON.stringify(res));
6262
try {
6363
if (res.status === GameDeploymentStatus.STOPPING) {
64-
await this.shutdownHetznerServer(res.workspaceName);
64+
await this.shutdownHetznerServer(generateTFWorkspaceName(res));
6565
}
6666

6767
this.logger.info('start terraform execution');

async-server-provisioner/src/services/TerraformService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Logger } from 'pino';
22
import { ShellAdapter, ShellResult } from '../adapters/ShellAdapter';
3-
import { GameDeployment, GameDeploymentStatus } from '../entities/GameDeployment';
3+
import { GameDeployment, GameDeploymentStatus, generateTFWorkspaceName } from '../entities/GameDeployment';
44
import { GameDeploymentLogRepository } from '../repositories/GameDeploymentLogRepository';
55
import { createMinecraftTFConfigFromGameConfig, mcTFConfToTFArgs, MinecraftTFConfig } from '../entities/MinecraftTFConfig';
66

@@ -21,7 +21,7 @@ export class TerraformService {
2121
public async execute(config: GameDeployment): Promise<TerraformGSOutput | null> {
2222
const tfVars = createMinecraftTFConfigFromGameConfig(config);
2323
await this.init(config.id);
24-
await this.changeWorkspace(config.id, config.workspaceName);
24+
await this.changeWorkspace(config.id, generateTFWorkspaceName(config));
2525

2626
switch (config.status) {
2727
case GameDeploymentStatus.STARTING:

docker-compose.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ services:
4444
- MYSQL_HOST=db
4545
- TERRAFORM_PATH=/opt/terraform/02-game-server
4646
- HCLOUD_TOKEN=${HCLOUD_TOKEN}
47+
- CLOUD_GAME_API_TOKEN=${CLOUD_GAME_API_TOKEN}
4748
volumes:
4849
- ~/.aws:/root/.aws
4950
- ./async-server-provisioner:/usr/src/app

infrastructure/ansible/roles/base-server/tasks/prepare-server.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
- name: Install common packages
33
apt:
44
pkg:
5-
- telnet
6-
- vim
7-
- tcpdump
8-
- gnupg2
5+
- telnet
6+
- vim
7+
- tcpdump
8+
- gnupg2
9+
- git
910
state: present
1011
update_cache: true
1112

infrastructure/terraform/02-game-server/files/startup.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ watcher:
1919
port: 8080
2020
EOF
2121

22-
ansible-playbook /root/ansible/game-server-start.yml --extra-vars "@/root/ansible/vars/game-server.yaml"
22+
git clone -b ${ansible_branch} https://github.com/dhenkel92/cloud-gameserver.git /tmp/cloud-gameserver
23+
ansible-playbook /tmp/cloud-gameserver/infrastructure/ansible/game-server-start.yml --extra-vars "@/root/ansible/vars/game-server.yaml"

infrastructure/terraform/02-game-server/main.tf

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
locals {
2-
tags = merge(var.tags, {})
2+
tags = merge(var.tags, {})
3+
default_rules = [{ proto = "tcp", port = "22", description = "ssh" }]
4+
firewall_rules = concat(local.default_rules, var.server.ports)
35
}
46

57
data "terraform_remote_state" "aws_platform" {
@@ -24,23 +26,28 @@ module "firewall" {
2426
source = "../modules/firewall"
2527

2628
name = var.metadata.name
27-
rules = [
28-
{
29-
proto = "tcp"
30-
port = "22"
31-
source_ips = ["0.0.0.0/0"]
32-
},
33-
{
34-
proto = "tcp"
35-
port = "25565"
36-
source_ips = ["0.0.0.0/0"]
37-
},
38-
{
39-
proto = "tcp"
40-
port = "8080"
41-
source_ips = ["0.0.0.0/0"]
42-
}
43-
]
29+
rules = [for rule in local.firewall_rules : {
30+
proto = rule.proto
31+
port = rule.port
32+
description = rule.description
33+
source_ips = ["0.0.0.0/0"]
34+
}]
35+
# rules = [
36+
# {
37+
# proto = "tcp"
38+
# port = "22"
39+
# },
40+
# {
41+
# proto = "tcp"
42+
# port = "25565"
43+
# source_ips = ["0.0.0.0/0"]
44+
# },
45+
# {
46+
# proto = "tcp"
47+
# port = "8080"
48+
# source_ips = ["0.0.0.0/0"]
49+
# }
50+
# ]
4451
}
4552

4653
module "game_server" {
@@ -69,6 +76,7 @@ module "game_server" {
6976
datadog_api_key = var.datadog.api_key
7077
aws_access_key_id = data.terraform_remote_state.aws_platform.outputs.access_keys["game_user.cloud-game"].access_key_id
7178
aws_secret_access_key = data.terraform_remote_state.aws_platform.outputs.access_keys["game_user.cloud-game"].secret_access_key
79+
ansible_branch = var.ansible_branch
7280
}
7381
}
7482
}

0 commit comments

Comments
 (0)