Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gamelift): add Script L2 Construct for GameLift #22343

Merged
merged 8 commits into from
Oct 5, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-gamelift/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ junit.xml
!**/*.integ.snapshot/**/asset.*/**

#include game build js file
!test/my-game-build/*.js
!test/my-game-build/*.js
!test/my-game-script/*.js
22 changes: 21 additions & 1 deletion packages/@aws-cdk/aws-gamelift/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ your game server files. This section provides guidance on preparing and uploadin
files or Realtime Servers server script files. When you upload files, you create a GameLift build or script resource, which
you then deploy on fleets of hosting resources.

### Upload a custom server build to GameLift
To troubleshoot fleet activation problems related to the server script, see [Debug GameLift fleet issues](https://docs.aws.amazon.com/gamelift/latest/developerguide/fleets-creating-debug.html).

#### Upload a custom server build to GameLift

Before uploading your configured game server to GameLift for hosting, package the game build files into a build directory.
This directory must include all components required to run your game servers and host game sessions, including the following:
Expand All @@ -89,3 +91,21 @@ new gamelift.Build(this, 'Build', {
content: gamelift.Content.fromBucket(bucket, "sample-asset-key")
});
```

#### Upload a realtime server Script

Your server script can include one or more files combined into a single .zip file for uploading. The .zip file must contain
all files that your script needs to run.

You can store your zipped script files in either a local file directory or in an Amazon Simple Storage Service (Amazon S3)
bucket or defines a directory asset which is archived as a .zip file and uploaded to S3 during deployment.

After you create the script resource, GameLift deploys the script with a new Realtime Servers fleet. GameLift installs your
server script onto each instance in the fleet, placing the script files in `/local/game`.

```ts
declare const bucket: s3.Bucket;
new gamelift.Script(this, 'Script', {
content: gamelift.Content.fromBucket(bucket, "sample-asset-key")
});
```
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-gamelift/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './content';
export * from './build';
export * from './script';

// AWS::GameLift CloudFormation Resources:
export * from './gamelift.generated';
237 changes: 237 additions & 0 deletions packages/@aws-cdk/aws-gamelift/lib/script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import * as iam from '@aws-cdk/aws-iam';
import * as s3 from '@aws-cdk/aws-s3';
import * as s3_assets from '@aws-cdk/aws-s3-assets';
import * as cdk from '@aws-cdk/core';
import { Construct } from 'constructs';
import { Content } from './content';
import { CfnScript } from './gamelift.generated';

/**
* Represents a GameLift realtime server script.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the explanation from the "About" page communicates, in a short paragraph, what a script is:

Suggested change
* Represents a GameLift realtime server script.
* Your configuration and custom game logic for use with Realtime Servers.
* Realtime Servers are provided by GameLift to use instead of a custom-built
* game server. You configure Realtime Servers for your game clients by
* creating a script using JavaScript, and add custom game logic as
* appropriate to host game sessions for your players. You upload the Realtime
* script to the GameLift service in the Regions where you plan to set up
* fleets.
*
* @see https://docs.aws.amazon.com/gamelift/latest/developerguide/realtime-script-uploading.html

I suggest you also copy the one for builds to the Build class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Update done

*/
export interface IScript extends cdk.IResource, iam.IGrantable {

/**
* The Identifier of the realtime server script.
*
* @attribute
*/
readonly scriptId: string;

/**
* The ARN of the realtime server script.
*
* @attribute
*/
readonly scriptArn: string;
}

/**
* Base class for new and imported GameLift realtime server script.
*/
export abstract class ScriptBase extends cdk.Resource implements IScript {
/**
* The Identifier of the realtime server script.
*/
public abstract readonly scriptId: string;
public abstract readonly scriptArn: string;

public abstract readonly grantPrincipal: iam.IPrincipal;
}

/**
* Represents a Script content defined outside of this stack.
*/
export interface ScriptAttributes {
/**
* The ARN of the realtime server script
*/
readonly scriptArn: string;
/**
* The IAM role assumed by GameLift to access server script in S3.
* @default - undefined
*/
readonly role?: iam.IRole;
}

/**
* Properties for a new realtime server script
*/
export interface ScriptProps {
/**
* Name of this realtime server script
*
* @default No name
*/
readonly scriptName?: string;

/**
* Version of this realtime server script
*
* @default No version
*/
readonly scriptVersion?: string;

/**
* The game content
*/
readonly content: Content;

/**
* The IAM role assumed by GameLift to access server script in S3.
* If providing a custom role, it needs to trust the GameLift service principal (gamelift.amazonaws.com) and be granted sufficient permissions
* to have Read access to a specific key content into a specific S3 bucket.
* Below an example of required permission:
* {
* "Version": "2012-10-17",
* "Statement": [{
* "Effect": "Allow",
* "Action": [
* "s3:GetObject",
* "s3:GetObjectVersion"
* ],
* "Resource": "arn:aws:s3:::bucket-name/object-name"
* }]
*}
*
* @see https://docs.aws.amazon.com/gamelift/latest/developerguide/security_iam_id-based-policy-examples.html#security_iam_id-based-policy-examples-access-storage-loc
*
* @default - a role will be created with default permissions.
*/
readonly role?: iam.IRole;
}

/**
* A GameLift script, that is installed and runs on instances in an Amazon GameLift fleet. It consists of
* a zip file with all of the components of the realtime game server script.
*
* @see https://docs.aws.amazon.com/gamelift/latest/developerguide/realtime-script-uploading.html
*
* @resource AWS::GameLift::Script
*/
export class Script extends ScriptBase {

/**
* Create a new realtime server script from s3 content
*/
static fromBucket(scope: Construct, id: string, bucket: s3.IBucket, key: string, objectVersion?: string) {
return new Script(scope, id, {
content: Content.fromBucket(bucket, key, objectVersion),
});
}

/**
* Create a new realtime server script from asset content
*/
static fromAsset(scope: Construct, id: string, path: string, options?: s3_assets.AssetOptions) {
return new Script(scope, id, {
content: Content.fromAsset(path, options),
});
}

/**
* Import a script into CDK using its ARN
*/
static fromScriptArn(scope: Construct, id: string, scriptArn: string): IScript {
return this.fromScriptAttributes(scope, id, { scriptArn });
}

/**
* Import an existing realtime server script from its attributes.
*/
static fromScriptAttributes(scope: Construct, id: string, attrs: ScriptAttributes): IScript {
const scriptArn = attrs.scriptArn;
const scriptId = extractIdFromArn(attrs.scriptArn);
const role = attrs.role;

class Import extends ScriptBase {
public readonly scriptArn = scriptArn;
public readonly scriptId = scriptId;
public readonly grantPrincipal:iam.IPrincipal;
public readonly role = role

constructor(s: Construct, i: string) {
super(s, i, {
environmentFromArn: scriptArn,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really used yet but I wanted to ensure Script env was linked to information provide as parameter (here the ARN)

});

this.grantPrincipal = this.role || new iam.UnknownPrincipal({ resource: this });
}
}

return new Import(scope, id);
}

/**
* The Identifier of the realtime server script.
*/
public readonly scriptId: string;

/**
* The ARN of the realtime server script.
*/
public readonly scriptArn: string;

/**
* The IAM role GameLift assumes to acccess server script content.
*/
public readonly role: iam.IRole;

/**
* The principal this GameLift script is using.
*/
public readonly grantPrincipal: iam.IPrincipal;

constructor(scope: Construct, id: string, props: ScriptProps) {
super(scope, id, {
physicalName: props.scriptName,
});

if (props.scriptName && !cdk.Token.isUnresolved(props.scriptName)) {
if (props.scriptName.length > 1024) {
throw new Error(`Script name can not be longer than 1024 characters but has ${props.scriptName.length} characters.`);
}
}
this.role = props.role ?? new iam.Role(this, 'ServiceRole', {
assumedBy: new iam.ServicePrincipal('gamelift.amazonaws.com'),
});
this.grantPrincipal = this.role;
const content = props.content.bind(this, this.role);

const resource = new CfnScript(this, 'Resource', {
name: props.scriptName,
version: props.scriptVersion,
storageLocation: {
bucket: content.s3Location && content.s3Location.bucketName,
key: content.s3Location && content.s3Location.objectKey,
objectVersion: content.s3Location && content.s3Location.objectVersion,
roleArn: this.role.roleArn,
},
});

this.scriptId = this.getResourceNameAttribute(resource.ref);
this.scriptArn = this.getResourceArnAttribute(resource.attrArn, {
service: 'gamelift',
resource: `script/${this.physicalName}`,
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
});
}
}

/**
* Given an opaque (token) ARN, returns a CloudFormation expression that extracts the script
* identifier from the ARN.
*
* Script ARNs look like this:
*
* arn:aws:gamelift:region:account-id:script/script-identifier
*
* ..which means that in order to extract the `script-identifier` component from the ARN, we can
* split the ARN using ":" and select the component in index 5 then split using "/" and select the component in index 1.
*
* @returns the script identifier from his ARN
*/
function extractIdFromArn(arn: string) {
const splitValue = cdk.Fn.select(5, cdk.Fn.split(':', arn));
return cdk.Fn.select(1, cdk.Fn.split('/', splitValue));
}
5 changes: 2 additions & 3 deletions packages/@aws-cdk/aws-gamelift/test/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import * as s3 from '@aws-cdk/aws-s3';
import * as cdk from '@aws-cdk/core';
import * as cxapi from '@aws-cdk/cx-api';
import * as gamelift from '../lib';
import { OperatingSystem } from '../lib';

describe('build', () => {
const buildId = 'test-identifier';
Expand Down Expand Up @@ -207,13 +206,13 @@ describe('build', () => {
build = new gamelift.Build(stack, 'BuildWithName', {
...defaultProps,
buildName: buildName,
operatingSystem: OperatingSystem.AMAZON_LINUX_2,
operatingSystem: gamelift.OperatingSystem.AMAZON_LINUX_2,
buildVersion: '1.0',
});

Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Build', {
Name: buildName,
OperatingSystem: OperatingSystem.AMAZON_LINUX_2,
OperatingSystem: gamelift.OperatingSystem.AMAZON_LINUX_2,
Version: '1.0',
});
});
Expand Down
13 changes: 13 additions & 0 deletions packages/@aws-cdk/aws-gamelift/test/integ.script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as path from 'path';
import * as cdk from '@aws-cdk/core';
import * as gamelift from '../lib';

const app = new cdk.App();

const stack = new cdk.Stack(app, 'aws-gamelift-script');

new gamelift.Script(stack, 'Script', {
content: gamelift.Content.fromAsset(path.join(__dirname, 'my-game-script')),
});

app.synth();
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('Hello World');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('Hello World');
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"version": "21.0.0",
"files": {
"6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7": {
"source": {
"path": "asset.6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7",
"packaging": "zip"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7.zip",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
},
"9c561e93c7a2947a15dba683670660e922cf493e17b2a6f8ca03cf221442c222": {
"source": {
"path": "aws-gamelift-script.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "9c561e93c7a2947a15dba683670660e922cf493e17b2a6f8ca03cf221442c222.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
}
},
"dockerImages": {}
}
Loading