3 Scripts That Make Working With AWS Realtime Gamelift Servers Easier

One of the hardest parts of working with a newer or niche service, platform, framework, or language is the smaller momentum behind it and the lack of information and tools published online. Gamelift, which was announced in 2016, is no exception to this rule. There are two ways of developing server-side code with Gamelift:

-Custom Server Build [c#,c++,etc.]

-Realtime Server Script [Javascript]

These scripts that I am sharing with you are useful for the 2nd method. This is an important distinction to understand. I am sharing these in the hopes that others find them and make use of them. Having some familiarity with python and AWS/boto3 will help you understand the scripts but isn't strictly needed. Hopefully these scripts save you time like they do for me!

Remotely Connecting to Fleet Instance

from zipfile import ZipFile
import os
from os.path import basename
import boto3
import sys, getopt
import base64
import json 
import subprocess

# sys.argv[1] = instance id
# Fleet Id
FLEET_ID = "FLEET_ID_GOES_HERE"
def main(argv):
    # creates instanceDetails.json
    os.system("aws gamelift get-instance-access --fleet-id "+FLEET_ID+" --instance-id "+sys.argv[1]+" --output json > instanceDetails.json")
    # creates MyPrivateKey.pem to connect to instance
    os.system("aws gamelift get-instance-access --fleet-id "+FLEET_ID+" --instance-id "+sys.argv[1]+" --query InstanceAccess.Credentials.Secret --output text > MyPrivateKey.pem")

    # Opening JSON file 
    f = open('instanceDetails.json',) 
    
    # returns JSON object as  
    # a dictionary 
    data = json.load(f)

    # connect to instance with ssh using MyPrivateKey.pem
    print("ssh -i MyPrivateKey.pem gl-user-remote@"+data['InstanceAccess']['IpAddress'])
    os.system("aws gamelift update-fleet-port-settings --fleet-id  "+FLEET_ID+" --inbound-permission-authorizations \"FromPort=22,ToPort=22,IpRange=0.0.0.0/0,Protocol=TCP\" --region us-east-2")
    # Closing file
    f.close()
if __name__ == "__main__":
   main(sys.argv[1])
from zipfile import ZipFile
import os
from os.path import basename
import boto3
import sys, getopt
import base64
import json 
import subprocess

# sys.argv[1] = instance id
# Fleet Id
FLEET_ID = "FLEET_ID_GOES_HERE"
def main(argv):
    # creates instanceDetails.json
    os.system("aws gamelift get-instance-access --fleet-id "+FLEET_ID+" --instance-id "+sys.argv[1]+" --output json > instanceDetails.json")
    # creates MyPrivateKey.pem to connect to instance
    os.system("aws gamelift get-instance-access --fleet-id "+FLEET_ID+" --instance-id "+sys.argv[1]+" --query InstanceAccess.Credentials.Secret --output text > MyPrivateKey.pem")

    # Opening JSON file 
    f = open('instanceDetails.json',) 
    
    # returns JSON object as  
    # a dictionary 
    data = json.load(f)

    # connect to instance with ssh using MyPrivateKey.pem
    print("ssh -i MyPrivateKey.pem gl-user-remote@"+data['InstanceAccess']['IpAddress'])
    os.system("aws gamelift update-fleet-port-settings --fleet-id  "+FLEET_ID+" --inbound-permission-authorizations \"FromPort=22,ToPort=22,IpRange=0.0.0.0/0,Protocol=TCP\" --region us-east-2")
    # Closing file
    f.close()
if __name__ == "__main__":
   main(sys.argv[1])
# Usage Example (Please make sure to update fleet id first within script)
python connectToFleet.py "INSTANCE_ID_GOES_HERE"

This is a python script that generates the ssh command to connect to an instance remotely. It will update the fleet's port settings successfully the first time you run it. The script also generates the permissions file correctly. Just make sure to update the fleet id. You just copy and run the ssh command that it prints.

Monitoring Script

// To Run: /local/NodeJS/bin/node ./monitor.js

// https://www.npmjs.com/package/tail
const Tail = require('tail').Tail;
const fs = require('fs');

let aTails = {};
let lastFiles = null;
let firstRun = true;

function showFiles() {
    let logDir = '/local/game/logs/';

    // Process a list of all the files
    try {
        let directories = fs.readdirSync(logDir);
        directories.sort(function(a, b) {
           return fs.statSync(logDir + a).mtime.getTime() -
                  fs.statSync(logDir + b).mtime.getTime();
        });

        for (var i = 0; i<directories.length; i++ ) {
            if (fs.lstatSync(logDir + directories[i]).isDirectory()) {
                let thisDir = logDir + directories[i] + '/';

                // Find the files
                let files = fs.readdirSync(thisDir);
                files.sort(function(a, b) {
                   return fs.statSync(thisDir + a).mtime.getTime() -
                          fs.statSync(thisDir + b).mtime.getTime();
                });

                for (var j = 0; j<files.length; j++ ) {
                    let thisFile = thisDir + files[j];
                    if (fs.lstatSync(thisFile).isFile()) {
                        if (!(thisFile in aTails)) {
                            console.log('Add ' + thisFile);
                            aTails[thisFile] = {
                                counter: Object.keys(aTails).length,
                                lastUpdate: 0,
                                tail: null
                            }
                            if (!firstRun) {
                                aTails[thisFile]['tail'] = startTail(thisFile);
                            }
                        }
                    }
                }
            }
        }

        for (var thisFile in aTails) {
            let aTailData = aTails[thisFile];
            if (aTailData.tail == null) {
                if (firstRun && aTailData.counter > Object.keys(aTails).length - 10) {
                    aTails[thisFile]['tail'] = startTail(thisFile);
                }
            }
        }

        firstRun = false;

    } catch (err) {
        console.log(err);
    }

    setTimeout(showFiles, 1000);
}

function startTail(thisFile) {
    console.log('Monitoring New File ' + thisFile);
    let tail = true;
    if (typeof Tail == 'undefined') {
        aTails[thisFile]['tail'] = true;
    } else {
        let options = {fromBeginning : true};
        tail = new Tail(thisFile, options);
        tail.on("line", function(data) {
          console.log(data);
        });
        tail.on("error", function(error) {
          console.log('ERROR: ', error);
        });
    }
    return tail;
}

showFiles();

Basically it “tails” all the logs (there are lots, especially if your app keeps crashing) so you get the log output in real time. I can't take credit for this script. It was given to me by Robbie over on the lumberyard forums.

Instructions (setup):

  1. Rename file as “.js” (I had to put as .txt to upload here)
  2. Put into the server app path so it gets uploaded to the “etag” directory one each deploy.
  3. Add https://www.npmjs.com/package/tail into package.json

Instructions (to run):

  1. SSH in to the instance to monitor.
  2. You need to run as root (sudo).
  3. cd to any “etag” (in /local/game - can be any of the etags, does not need to be the “latest” if you are doing multiple deploys)
  4. Run: /local/NodeJS/bin/node ./monitor.js

Notes:

  • Currently monitors last 10 files; each “server” (i.e. how many times your run your .js as set in console/CFN) creates a log file, so if you have more than 5 servers (on the single instance) then you might want to increase this. Of course there is performance hit, but for debugging you should be fine. [Monitor twice the files so you get the crash logs of the old ones)
  • Keep your etags clean with ls -td /local/game/etag-* | tail -n +3 | head -n -1 | sudo xargs rm -rf --
  • Keep your logs clean with cd /local/game/logs/ ls -t | tail -n +20 | sudo xargs rm -rf --

Upload Script

from zipfile import ZipFile
import os
from os.path import basename
import boto3
import sys, getopt

def main(argv):
    versInput = sys.argv[1]
    #initializes client for updating script in aws gamelift
    client = boto3.client('gamelift')

    #Where is the directory relative to the script directory. In this case, one folder dir lower and the contents of the RealtimeServer dir
    dirName = '../RealtimeServer'

    # create a ZipFile object
    with ZipFile('RealtimeServer.zip', 'w') as zipObj:
        # Iterate over all the files in directory
        for folderName, subfolders, filenames in os.walk(dirName):
            rootlen = len(dirName) + 1
            for filename in filenames:
                #create complete filepath of file in directory
                filePath = os.path.join(folderName, filename)
                # Add file to zip
                zipObj.write(filePath, filePath[rootlen:])

    with open('RealtimeServer.zip','rb') as f:
        contents = f.read()

    response = client.update_script(
        ScriptId="SCRIPT_ID_GOES_HERE",
        Version=sys.argv[1],
        ZipFile=contents
    )

if __name__ == "__main__":
   main(sys.argv[1])

This script above allows for uploading zip files of your Realtime server script. It assumes that this upload script is in a separate directory than this python script. You also need to make sure you have your AWS credentials setup to configure AWS services.

# Usage Example
python uploadScript.py "0.1.1"

Specify a version number as the argument to differentiate between uploads

Both of the above python scripts have the following dependencies:

boto3==1.16.5
botocore==1.19.5
jmespath==0.10.0
python-dateutil==2.8.1
s3transfer==0.3.3
six==1.15.0
urllib3==1.25.11

These scripts are relatively bare-bones but are a good starting point. The upload script can be pushed beyond it's 5mb upload limit by utilizing an s3 bucket but it may be more than what's needed to get started. If you have any questions or comments then please feel free to contact me.