update to hetzner deployment

This commit is contained in:
Evan
2025-03-10 17:02:15 -07:00
parent de35f50e3b
commit 316129b70b
8 changed files with 65 additions and 42 deletions
+4
View File
@@ -27,6 +27,9 @@ if [ $# -ne 1 ] || ([ "$1" != "staging" ] && [ "$1" != "prod" ]); then
exit 1
fi
# TODO: fix this - need to build before creating the image
bun run build-prod
ENV=$1
VERSION_TAG="latest"
DOCKER_REPO=""
@@ -71,6 +74,7 @@ GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
echo "Git commit: $GIT_COMMIT"
docker buildx build \
--no-cache \
--platform linux/amd64 \
--build-arg GIT_COMMIT=$GIT_COMMIT \
-t $DOCKER_USERNAME/$DOCKER_REPO:$VERSION_TAG \
+4
View File
@@ -100,6 +100,10 @@ export interface ServerConfig {
env(): GameEnv;
adminToken(): string;
adminHeader(): string;
r2Bucket(): string;
r2Endpoint(): string;
r2AccessKey(): string;
r2SecretKey(): string;
}
export interface Config {
+10
View File
@@ -22,6 +22,16 @@ import { pastelTheme } from "./PastelTheme";
import { pastelThemeDark } from "./PastelThemeDark";
export abstract class DefaultServerConfig implements ServerConfig {
r2Endpoint(): string {
return process.env.R2_ENDPOINT;
}
r2AccessKey(): string {
return process.env.R2_ACCESS_KEY;
}
r2SecretKey(): string {
return process.env.R2_SECRET_KEY;
}
abstract r2Bucket(): string;
adminHeader(): string {
return "x-admin-key";
}
+3
View File
@@ -5,6 +5,9 @@ import { GameEnv, ServerConfig } from "./Config";
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
export class DevServerConfig extends DefaultServerConfig {
r2Bucket(): string {
return "openfront-staging";
}
adminToken(): string {
return "WARNING_DEV_ADMIN_KEY_DO_NOT_USE_IN_PRODUCTION";
}
+3
View File
@@ -2,6 +2,9 @@ import { GameEnv } from "./Config";
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
export const preprodConfig = new (class extends DefaultServerConfig {
r2Bucket(): string {
return "openfront-staging";
}
env(): GameEnv {
return GameEnv.Preprod;
}
+3
View File
@@ -2,6 +2,9 @@ import { GameEnv } from "./Config";
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
export const prodConfig = new (class extends DefaultServerConfig {
r2Bucket(): string {
return "openfront-prod";
}
numWorkers(): number {
return 6;
}
+37 -28
View File
@@ -1,6 +1,5 @@
import { GameRecord, GameID } from "../core/Schemas";
import { S3 } from "@aws-sdk/client-s3";
import { RedshiftData } from "@aws-sdk/client-redshift-data";
import {
GameEnv,
getServerConfigFromServer,
@@ -8,22 +7,31 @@ import {
const config = getServerConfigFromServer();
const s3 = new S3({ region: "eu-west-1" });
// R2 client configuration
const r2 = new S3({
region: "auto", // R2 ignores region, but it's required by the SDK
endpoint: config.r2Endpoint(), // You'll need to add this to your config
credentials: {
accessKeyId: config.r2AccessKey(), // You'll need to add these
secretAccessKey: config.r2SecretKey(), // credential methods to your config
},
});
const gameBucket = "openfront-games";
const analyticsBucket = "openfront-analytics";
const bucket = config.r2Bucket();
const gameFolder = "games";
const analyticsFolder = "analytics";
export async function archive(gameRecord: GameRecord) {
try {
// Archive to Redshift Serverless
await archiveAnalyticsToS3(gameRecord);
// Archive to R2
await archiveAnalyticsToR2(gameRecord);
// Archive to S3 if there are turns
// Archive full game if there are turns
if (gameRecord.turns.length > 0) {
console.log(
`${gameRecord.id}: game has more than zero turns, attempting to write to full game to S3`,
`${gameRecord.id}: game has more than zero turns, attempting to write to full game to R2`,
);
await archiveFullGameToS3(gameRecord);
await archiveFullGameToR2(gameRecord);
}
} catch (error) {
console.error(`${gameRecord.id}: Final archive error: ${error}`, {
@@ -35,8 +43,8 @@ export async function archive(gameRecord: GameRecord) {
}
}
async function archiveAnalyticsToS3(gameRecord: GameRecord) {
// Create analytics data object (similar to what was going to Redshift)
async function archiveAnalyticsToR2(gameRecord: GameRecord) {
// Create analytics data object
const analyticsData = {
id: gameRecord.id,
env: config.env(),
@@ -60,17 +68,17 @@ async function archiveAnalyticsToS3(gameRecord: GameRecord) {
// Store analytics data using just the game ID as the key
const analyticsKey = `${gameRecord.id}.json`;
await s3.putObject({
Bucket: analyticsBucket,
Key: analyticsKey,
await r2.putObject({
Bucket: bucket,
Key: `${analyticsFolder}/${analyticsKey}`,
Body: JSON.stringify(analyticsData),
ContentType: "application/json",
});
console.log(`${gameRecord.id}: successfully wrote game analytics to S3`);
console.log(`${gameRecord.id}: successfully wrote game analytics to R2`);
} catch (error) {
console.error(
`${gameRecord.id}: Error writing game analytics to S3: ${error}`,
`${gameRecord.id}: Error writing game analytics to R2: ${error}`,
{
message: error?.message || error,
stack: error?.stack,
@@ -82,7 +90,7 @@ async function archiveAnalyticsToS3(gameRecord: GameRecord) {
}
}
async function archiveFullGameToS3(gameRecord: GameRecord) {
async function archiveFullGameToR2(gameRecord: GameRecord) {
// Create a deep copy to avoid modifying the original
const recordCopy = JSON.parse(JSON.stringify(gameRecord));
@@ -93,9 +101,9 @@ async function archiveFullGameToS3(gameRecord: GameRecord) {
});
try {
await s3.putObject({
Bucket: gameBucket,
Key: recordCopy.id,
await r2.putObject({
Bucket: bucket,
Key: `${gameFolder}/${recordCopy.id}`,
Body: JSON.stringify(recordCopy),
ContentType: "application/json",
});
@@ -104,15 +112,15 @@ async function archiveFullGameToS3(gameRecord: GameRecord) {
throw error;
}
console.log(`${gameRecord.id}: game record successfully written to S3`);
console.log(`${gameRecord.id}: game record successfully written to R2`);
}
export async function readGameRecord(gameId: GameID): Promise<GameRecord> {
try {
// Check if file exists and download in one operation
const response = await s3.getObject({
Bucket: gameBucket,
Key: gameId,
const response = await r2.getObject({
Bucket: bucket,
Key: `${gameFolder}/${gameId}`, // Fixed - needed to include gameFolder
});
// Parse the response body
@@ -121,7 +129,8 @@ export async function readGameRecord(gameId: GameID): Promise<GameRecord> {
return gameRecord as GameRecord;
} catch (error) {
console.error(`${gameId}: Error reading game record from S3: ${error}`, {
// Log the error for monitoring purposes
console.error(`${gameId}: Error reading game record from R2: ${error}`, {
message: error?.message || error,
stack: error?.stack,
name: error?.name,
@@ -133,9 +142,9 @@ export async function readGameRecord(gameId: GameID): Promise<GameRecord> {
export async function gameRecordExists(gameId: GameID): Promise<boolean> {
try {
await s3.headObject({
Bucket: gameBucket,
Key: gameId,
await r2.headObject({
Bucket: bucket,
Key: `${gameFolder}/${gameId}`, // Fixed - needed to include gameFolder
});
return true;
} catch (error) {
+1 -14
View File
@@ -31,10 +31,6 @@ if [ -f /root/.env ]; then
export $(grep -v '^#' /root/.env | xargs)
fi
# Set the Loki URL
LOKI_URL=${LOKI_URL:-"http://localhost:3100/loki/api/v1/push"}
echo "Using Loki URL: ${LOKI_URL}"
echo "Pulling latest image from Docker Hub..."
docker pull $FULL_IMAGE_NAME
@@ -90,17 +86,9 @@ docker run -d -p 80:80 \
--restart=always \
$VOLUME_MOUNTS \
$NETWORK_FLAGS \
--env APP_ENV=${ENV} \
--env GAME_ENV=${ENV} \
--env-file /root/.env \
--name ${CONTAINER_NAME} \
--log-driver=loki \
--log-opt loki-url="${LOKI_URL}" \
--log-opt loki-batch-size="400" \
--log-opt loki-min-backoff="100ms" \
--log-opt loki-max-backoff="10s" \
--log-opt loki-retries="5" \
--log-opt loki-timeout="10s" \
--log-opt loki-external-labels="job=openfront,env=${ENV},container=${CONTAINER_NAME}" \
$FULL_IMAGE_NAME
if [ $? -eq 0 ]; then
@@ -121,5 +109,4 @@ echo "======================================================"
echo "✅ SERVER UPDATE COMPLETED SUCCESSFULLY"
echo "Container name: ${CONTAINER_NAME}"
echo "Image: ${FULL_IMAGE_NAME}"
echo "Logs: Streaming to Loki at ${LOKI_URL}"
echo "======================================================"