Skip to content

Commit

Permalink
feat(gamelift): add Script L2 Construct for GameLift (#22343)
Browse files Browse the repository at this point in the history
Following [aws-cdk-rfcs](aws/aws-cdk-rfcs#436) I have written the first Script L2 resource which create a GameLift Script resource based on a S3 location.

----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
stevehouel authored Oct 5, 2022
1 parent b342566 commit da181ba
Show file tree
Hide file tree
Showing 16 changed files with 978 additions and 6 deletions.
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")
});
```
8 changes: 7 additions & 1 deletion packages/@aws-cdk/aws-gamelift/lib/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import { Content } from './content';
import { CfnBuild } from './gamelift.generated';

/**
* Represents a GameLift server build.
* Your custom-built game server software that runs on GameLift and hosts game sessions for your players.
* A game build represents the set of files that run your game server on a particular operating system.
* You can have many different builds, such as for different flavors of your game.
* The game build must be integrated with the GameLift service.
* You upload game build files to the GameLift service in the Regions where you plan to set up fleets.
*
* @see https://docs.aws.amazon.com/gamelift/latest/developerguide/gamelift-build-cli-uploading.html
*/
export interface IBuild extends cdk.IResource, iam.IGrantable {

Expand Down
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';
243 changes: 243 additions & 0 deletions packages/@aws-cdk/aws-gamelift/lib/script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
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';

/**
* 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
*/
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,
});

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

0 comments on commit da181ba

Please sign in to comment.