From 53964a8644d73d97e1fa5ac2766abc8039399eb2 Mon Sep 17 00:00:00 2001 From: Matt McClean Date: Thu, 13 Jun 2019 17:40:15 +0100 Subject: [PATCH 1/6] feat(sagemaker): added new L2 resource for NotebookInstance --- packages/@aws-cdk/aws-sagemaker/lib/index.ts | 2 + .../aws-sagemaker/lib/notebook-instance.ts | 212 ++++++++++++++++++ packages/@aws-cdk/aws-sagemaker/package.json | 6 + .../aws-sagemaker/test/test.notebook.ts | 131 +++++++++++ .../aws-sagemaker/test/test.sagemaker.ts | 9 - 5 files changed, 351 insertions(+), 9 deletions(-) create mode 100644 packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts create mode 100644 packages/@aws-cdk/aws-sagemaker/test/test.notebook.ts delete mode 100644 packages/@aws-cdk/aws-sagemaker/test/test.sagemaker.ts diff --git a/packages/@aws-cdk/aws-sagemaker/lib/index.ts b/packages/@aws-cdk/aws-sagemaker/lib/index.ts index 4c40a31057568..4cf8cb86958c3 100644 --- a/packages/@aws-cdk/aws-sagemaker/lib/index.ts +++ b/packages/@aws-cdk/aws-sagemaker/lib/index.ts @@ -1,2 +1,4 @@ +export * from './notebook-instance'; + // AWS::SageMaker CloudFormation Resources: export * from './sagemaker.generated'; diff --git a/packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts b/packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts new file mode 100644 index 0000000000000..d9363f952dc4d --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts @@ -0,0 +1,212 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import iam = require('@aws-cdk/aws-iam'); +import kms = require('@aws-cdk/aws-kms'); +import { Construct, Fn, Lazy, Resource } from '@aws-cdk/cdk'; +import { CfnNotebookInstance, CfnNotebookInstanceLifecycleConfig } from './sagemaker.generated'; + +/** + * @experimental + */ +export interface NotebookInstanceProps { + + /** + * Enable the notebook to have direct internet access. + * + * @default true + */ + readonly enableDirectInternetAccess?: boolean; + + /** + * Instance type of the notebook. + * + * @default m1.t2.medium + */ + readonly instanceType?: ec2.InstanceType; + + /** + * Encryption key for the EBS volume attached to the notebook instance. + * + * @default none + */ + readonly kmsKeyId?: kms.IKey; + + /** + * Name of the notebook instance. + * + * @default none + */ + readonly notebookInstanceName?: string; + + /** + * Role to provide to the Sagemaker service to access other AWS services. + * + * @default a new role for Amazon SageMaker. + */ + readonly role?: iam.IRole; + + /** + * Enable the root access to the notebook instance. + * + * @default true + */ + readonly enableRootAccess?: boolean; + + /** + * Security groups attached to the notebook instance. + * + * @default none + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * Subnet where the notebook instance is deployed to in the VPC. + * + * @default none + */ + readonly subnet?: ec2.ISubnet; + + /** + * Tags for the notebook instance. + * + * @default none + */ + readonly tags?: {[key: string]: any}; + + /** + * Size of the notebook volume in GB. + * + * @default 5 GB + */ + readonly volumeSizeInGB?: number; +} + +/** + * @experimental + */ +export class NotebookInstance extends Resource implements ec2.IConnectable { + + /** + * Allows specify security group connections for instances of this fleet. + */ + public readonly connections: ec2.Connections = new ec2.Connections(); + + /** @attribute */ + public readonly notebookInstanceName: string; + + private readonly role: iam.IRole; + private readonly instanceType: string; + private readonly onCreateLines = new Array(); + private readonly onStartLines = new Array(); + private readonly tags: {[key: string]: any} = {}; + + constructor(scope: Construct, id: string, props?: NotebookInstanceProps) { + super(scope, id); + + if (!props) { + props = {}; + } + + // set the instance type if defined otherwise set to 'ml.t2.medium' + if (props.instanceType) { + this.validateInstanceType(props.instanceType.toString()); + this.instanceType = 'ml.' + props.instanceType.toString(); + } else { + // default to 'ml.t2.medium' if undefined + this.instanceType = 'ml.t2.medium'; + } + + // set the notebook instance name + if (props.notebookInstanceName) { + this.notebookInstanceName = props.notebookInstanceName; + } + + // set the notebook instance tags + this.tags = (props.tags) ? (props.tags) : {}; + + // add the security groups to the connections object + if (props.securityGroups) { + props.securityGroups.forEach(sg => this.connections.addSecurityGroup(sg)); + } + + // validate the value of volumeSizeInGB + if (props.volumeSizeInGB) { + this.validateVolumeSizeInGb(props.volumeSizeInGB); + } + + // set the sagemaker role or create new one + this.role = props.role || new iam.Role(this, 'SagemakerRole', { + assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'), + managedPolicyArns: [ + new iam.AwsManagedPolicy('AmazonSageMakerFullAccess', scope).policyArn + ] + }); + + // create the Lifecycle Config resource + const onCreateToken = Lazy.stringValue( {produce: () => Fn.base64(this.createScript(this.onCreateLines)) }); + const onStartToken = Lazy.stringValue( {produce: () => Fn.base64(this.createScript(this.onStartLines)) }); + const lifecycleConfig = new CfnNotebookInstanceLifecycleConfig(this, 'LifecycleConfig', { + onCreate: [{content: onCreateToken}], + onStart: [{content: onStartToken}], + }); + + // create the CfnNotebookInstance resource + new CfnNotebookInstance(this, 'NotebookInstance', { + roleArn: this.role.roleArn, + instanceType: this.instanceType, + lifecycleConfigName: lifecycleConfig.notebookInstanceLifecycleConfigName, + notebookInstanceName: this.notebookInstanceName, + tags: (Object.keys(this.tags).length > 0) ? (Object.keys(this.tags).map(key => ({ key, value: this.tags[key] }))) : undefined, + directInternetAccess: props.enableDirectInternetAccess !== undefined ? + (props.enableDirectInternetAccess ? 'Enabled' : 'Disabled') : undefined, + volumeSizeInGb: props.volumeSizeInGB, + subnetId: props.subnet !== undefined ? props.subnet.subnetId : undefined, + securityGroupIds: props.securityGroups !== undefined ? props.securityGroups.map(sg => (sg.securityGroupId)) : undefined, + rootAccess: props.enableRootAccess !== undefined ? (props.enableRootAccess ? 'Enabled' : 'Disabled') : undefined, + kmsKeyId: props.kmsKeyId !== undefined ? props.kmsKeyId.keyArn : undefined, + }); + } + + /** + * Add command to the on create script of the notebook instance. + * The command must be in a scripting language supported by Linux. + */ + public addOnCreateScript(...scriptLines: string[]) { + scriptLines.forEach(scriptLine => this.onCreateLines.push(scriptLine)); + } + + /** + * Add command to the on start script of the notebook instance. + * The command must be in a scripting language supported by Linux. + */ + public addOnStartScript(...scriptLines: string[]) { + scriptLines.forEach(scriptLine => this.onStartLines.push(scriptLine)); + } + + /** + * Creates the script to be attached to the lifecycle configuration resource. + */ + private createScript(scripts: string[]): string { + return '#!/bin/bash\n' + scripts.join('\n'); + } + + /** + * Validates the provided instance type. + * @param instanceType the instance type of the notebook instance + */ + private validateInstanceType(instanceType: string) { + // check if a valid sagemaker instance type + if (!['c4', 'c5', 'm4', 'm5', 'p2', 'p3', 't2', 't3'].some(instanceClass => instanceType.indexOf(instanceClass) >= 0)) { + throw new Error(`Invalid instance type for a Sagemaker notebook instance: ${instanceType}`); + } + } + + /** + * Validates the provided EVS volume size. + * @param volumeSizeInGb the volume size in GB + */ + private validateVolumeSizeInGb(volumeSizeInGb: number) { + if (volumeSizeInGb < 5 || volumeSizeInGb > 16384) { + throw new Error("VolumeSizeInGb value must be between 5 and 16384 GB"); + } + } +} diff --git a/packages/@aws-cdk/aws-sagemaker/package.json b/packages/@aws-cdk/aws-sagemaker/package.json index 1661b6544830c..4f7501ec21bbe 100644 --- a/packages/@aws-cdk/aws-sagemaker/package.json +++ b/packages/@aws-cdk/aws-sagemaker/package.json @@ -70,9 +70,15 @@ "pkglint": "^0.34.0" }, "dependencies": { + "@aws-cdk/aws-ec2": "^0.34.0", + "@aws-cdk/aws-iam": "^0.34.0", + "@aws-cdk/aws-kms": "^0.34.0", "@aws-cdk/cdk": "^0.34.0" }, "peerDependencies": { + "@aws-cdk/aws-ec2": "^0.34.0", + "@aws-cdk/aws-iam": "^0.34.0", + "@aws-cdk/aws-kms": "^0.34.0", "@aws-cdk/cdk": "^0.34.0" }, "engines": { diff --git a/packages/@aws-cdk/aws-sagemaker/test/test.notebook.ts b/packages/@aws-cdk/aws-sagemaker/test/test.notebook.ts new file mode 100644 index 0000000000000..61e37d7b86b92 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/test.notebook.ts @@ -0,0 +1,131 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import iam = require('@aws-cdk/aws-iam'); +import kms = require('@aws-cdk/aws-kms'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import sagemaker = require('../lib'); + +export = { + "When creating Sagemaker Notebook Instance": { + "with only required properties set, it correctly sets default properties"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + // create the notebook instance + new sagemaker.NotebookInstance(stack, 'Notebook'); + + // THEN + expect(stack).to(haveResource("AWS::SageMaker::NotebookInstance", { + InstanceType: "ml.t2.medium", + RoleArn: { + "Fn::GetAtt": [ "NotebookSagemakerRole48081954", "Arn" ] + }, + LifecycleConfigName: { + "Fn::GetAtt": [ "NotebookLifecycleConfig48B89718", "NotebookInstanceLifecycleConfigName" ] + } + })); + + test.done(); + }, + "with all properties set, it correctly configures the resource"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, "Vpc"); + const subnets = vpc.privateSubnets; + const sg = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc, allowAllOutbound: true }); + const key = new kms.Key(stack, 'Key'); + const role = new iam.Role(stack, 'Role', { assumedBy: new iam.ServicePrincipal("sagemaker.amazonaws.com") } ); + const instanceType = new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge); + + // create the notebook instance + new sagemaker.NotebookInstance(stack, 'Notebook', { + kmsKeyId: key, + role, + instanceType, + notebookInstanceName: "mynotebook", + tags: { + Name: "myname", + Project: "myproject" + }, + subnet: subnets[0], + securityGroups: [ sg ], + enableDirectInternetAccess: true, + enableRootAccess: false, + volumeSizeInGB: 100, + }); + + // THEN + expect(stack).to(haveResource("AWS::SageMaker::NotebookInstance", { + InstanceType: "ml.m4.xlarge", + RoleArn: { + "Fn::GetAtt": [ "Role1ABCC5F0", "Arn" ] + }, + LifecycleConfigName: { + "Fn::GetAtt": [ "NotebookLifecycleConfig48B89718", "NotebookInstanceLifecycleConfigName" ] + }, + NotebookInstanceName: "mynotebook", + Tags: [ + { Key: "Name", Value: "myname" }, + { Key: "Project", Value: "myproject" }, + ], + SubnetId: { + Ref: "VpcPrivateSubnet1Subnet536B997A" + }, + SecurityGroupIds: [ + { "Fn::GetAtt": [ "SecurityGroupDD263621", "GroupId" ] }, + ], + VolumeSizeInGB: 100, + DirectInternetAccess: 'Enabled', + RootAccess: 'Disabled', + KmsKeyId: { + "Fn::GetAtt": [ "Key961B73FD", "Arn" ] + } + })); + + test.done(); + }, + "it configures the lifecycle config object"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const notebook = new sagemaker.NotebookInstance(stack, 'Notebook'); + notebook.addOnCreateScript( + 'echo "Creating Notebook"', + ); + notebook.addOnStartScript( + 'echo "Starting Notebook"', + ); + // THEN + expect(stack).to(haveResource("AWS::SageMaker::NotebookInstanceLifecycleConfig", { + OnCreate: [ + { + Content: { "Fn::Base64": "#!/bin/bash\necho \"Creating Notebook\""} + } + ], + OnStart: [ + { + Content: { "Fn::Base64": "#!/bin/bash\necho \"Starting Notebook\""} + } + ] + })); + test.done(); + }, + "it throws error when incorrect volume size given"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + // WHEN + test.throws(() => new sagemaker.NotebookInstance(stack, 'Notebook', { + volumeSizeInGB: 1 + })); + test.done(); + }, + "it throws error when incorrect instance type given"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + // WHEN + test.throws(() => new sagemaker.NotebookInstance(stack, 'Notebook', { + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.X1, ec2.InstanceSize.XLarge32) + }), /Invalid instance type/); + test.done(); + }, + } +}; diff --git a/packages/@aws-cdk/aws-sagemaker/test/test.sagemaker.ts b/packages/@aws-cdk/aws-sagemaker/test/test.sagemaker.ts deleted file mode 100644 index 59232198316cd..0000000000000 --- a/packages/@aws-cdk/aws-sagemaker/test/test.sagemaker.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Test, testCase } from 'nodeunit'; -import {} from '../lib'; - -export = testCase({ - notTested(test: Test) { - test.ok(true, 'No tests are specified for this package.'); - test.done(); - } -}); From f337483345f62c668e4c2f908ab42282dd80da7a Mon Sep 17 00:00:00 2001 From: Matt McClean Date: Sun, 16 Jun 2019 22:26:06 +0100 Subject: [PATCH 2/6] feat(sagemaker): added new L2 resources - Model and Endpoint --- .../@aws-cdk/aws-sagemaker/lib/endpoint.ts | 222 ++++++++++++ packages/@aws-cdk/aws-sagemaker/lib/index.ts | 2 + packages/@aws-cdk/aws-sagemaker/lib/model.ts | 318 ++++++++++++++++++ .../aws-sagemaker/lib/notebook-instance.ts | 87 +++-- .../aws-sagemaker/test/test.endpoint.ts | 170 ++++++++++ .../@aws-cdk/aws-sagemaker/test/test.model.ts | 97 ++++++ .../aws-sagemaker/test/test.notebook.ts | 12 +- 7 files changed, 874 insertions(+), 34 deletions(-) create mode 100644 packages/@aws-cdk/aws-sagemaker/lib/endpoint.ts create mode 100644 packages/@aws-cdk/aws-sagemaker/lib/model.ts create mode 100644 packages/@aws-cdk/aws-sagemaker/test/test.endpoint.ts create mode 100644 packages/@aws-cdk/aws-sagemaker/test/test.model.ts diff --git a/packages/@aws-cdk/aws-sagemaker/lib/endpoint.ts b/packages/@aws-cdk/aws-sagemaker/lib/endpoint.ts new file mode 100644 index 0000000000000..07aa5949141a4 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/lib/endpoint.ts @@ -0,0 +1,222 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import kms = require('@aws-cdk/aws-kms'); +import { Construct, Lazy, Resource, Tag } from '@aws-cdk/cdk'; +import { IModel } from './model'; +import { CfnEndpoint, CfnEndpointConfig } from './sagemaker.generated'; + +/** + * Name tag constant + */ +const NAME_TAG: string = 'Name'; + +/** + * Construction properties for a SageMaker Endpoint. + */ +export interface EndpointProps { + + /** + * Name of the endpoint. + * + * @default none + */ + readonly endpointName?: string; + + /** + * Name of the endpoint configuration. + * + * @default same as the endpoint name with 'Config' appended. + */ + readonly configName?: string; + + /** + * Optional KMS encryption key associated with this stream. + * + * @default none + */ + readonly kmsKey?: kms.IKey; + + /** + * List of production variants. + * + * @default none + */ + readonly productionVariants?: ProductionVariant[]; + +} + +/** + * Construction properties for the Production Variant. + */ +export interface ProductionVariant { + + /** + * Optional accelerateor type. + * + * @default none + */ + readonly acceleratorType?: AcceleratorType; + + /** + * Initial number of instances to be deployed. + * + * @default 1 + */ + readonly initialInstanceCount?: number; + + /** + * Inital weight of the production variant. + * + * @default 100 + */ + readonly initialVariantWeight?: number; + + /** + * Instance type of the production variant. + * + * @default ml.c4.xlarge instance type. + */ + readonly instanceType?: ec2.InstanceType; + + /** + * The model aligned to the production variant. + * + */ + readonly model: IModel; + + /** + * Name of the production variant. + * + * @default same name as the model name + */ + readonly variantName?: string; +} + +export enum AcceleratorType { + + Large = 'ml.eia1.large ', + + Medium = 'ml.eia1.medium', + + XLarge = 'ml.eia1.xlarge', +} + +/** + * Defines a SageMaker Endpoint and associated configuration resource. + */ +export class Endpoint extends Resource { + + /** + * The name of the endpoint. + * + * @attribute + */ + public readonly endpointName: string; + + /** + * Name of the endpoint configuration. + * + * @attribute + */ + public readonly endpointConfigName: string; + + private readonly productionVariants: ProductionVariant[] = []; + + constructor(scope: Construct, id: string, props: EndpointProps= {}) { + super(scope, id); + + // check that the production variants are defined + if (props.productionVariants && props.productionVariants.length > 0) { + this.productionVariants = props.productionVariants; + } + + // apply a name tag to the endpoint resource + this.node.applyAspect(new Tag(NAME_TAG, this.node.path)); + + // create the endpoint configuration resource + const endpointConfig = new CfnEndpointConfig(this, 'EndpointConfig', { + kmsKeyId: (props.kmsKey) ? props.kmsKey.keyArn : undefined, + endpointConfigName: (props.configName) ? props.configName : (props.endpointName) ? props.endpointName + "Config" : undefined, + productionVariants: Lazy.anyValue({ produce: () => this.renderProductionVariants(this.productionVariants) }) + }); + this.endpointConfigName = endpointConfig.endpointConfigName; + + // create the endpoint resource + const endpoint = new CfnEndpoint(this, 'Endpoint', { + endpointConfigName: endpointConfig.endpointConfigName, + endpointName: (props.endpointName) ? props.endpointName : undefined, + }); + this.endpointName = endpoint.endpointName; + } + + /** + * Add production variant to the endpoint configution. + * + * @param productionVariant: The production variant to add. + */ + public addProductionVariant(productionVariant: ProductionVariant): void { + this.productionVariants.push(productionVariant); + } + + protected validate(): string[] { + const result = super.validate(); + // check we have at least one production variant + if (this.productionVariants.length === 0) { + result.push("Must have at least one Production Variant"); + } + + // check instance count is greater than zero + this.productionVariants.forEach(v => { + if (v.initialInstanceCount && v.initialInstanceCount < 1) { + result.push("Must have at least one instance"); + } + }); + + // check variant weight is not negative + this.productionVariants.forEach(v => { + if (v.initialVariantWeight && v.initialVariantWeight < 0) { + result.push("Cannot have negative variant weight"); + } + }); + + // validate the instance type + this.productionVariants.forEach(v => { + if (v.instanceType) { + this.validateInstanceType(v.instanceType.toString(), result); + } + }); + return result; + } + + /** + * Render the list of production variants. Set default values if not defined. + * @param productionVariants the variants to render + */ + private renderProductionVariants(productionVariants: ProductionVariant[]): CfnEndpointConfig.ProductionVariantProperty[] { + return productionVariants.map(v => ( + { + // get the initial instance count. Set to 1 if not defined + initialInstanceCount: (v.initialInstanceCount) ? v.initialInstanceCount : 1, + // get the initial variant weight. Set to 100 if not defined + initialVariantWeight: (v.initialVariantWeight) ? v.initialVariantWeight : 100, + // get the instance type. Set to 'ml.c4.xlarge' if not defined + instanceType: 'ml.' + ((v.instanceType) ? (v.instanceType.toString()) : + new ec2.InstanceTypePair(ec2.InstanceClass.C4, ec2.InstanceSize.XLarge)), + modelName: v.model.modelName, + // get the variant name. Set to the model name if not defined + variantName: (v.variantName) ? v.variantName : v.model.modelName, + } + )); + } + + /** + * Validates the provided instance type. + * @param instanceType the instance type of the SageMaker endpoint production variant. + */ + private validateInstanceType(instanceType: string, result: string[]) { + // check if a valid sagemaker instance type + if (!['c4', 'c5', 'm4', 'm5', 'p2', 'p3', 't2'].some(instanceClass => instanceType.indexOf(instanceClass) >= 0)) { + result.push(`Invalid instance type for a Sagemaker Endpoint Production Variant: ${instanceType}`); + } + } + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sagemaker/lib/index.ts b/packages/@aws-cdk/aws-sagemaker/lib/index.ts index 4cf8cb86958c3..fb3b433681fa1 100644 --- a/packages/@aws-cdk/aws-sagemaker/lib/index.ts +++ b/packages/@aws-cdk/aws-sagemaker/lib/index.ts @@ -1,3 +1,5 @@ +export * from './endpoint'; +export * from './model'; export * from './notebook-instance'; // AWS::SageMaker CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-sagemaker/lib/model.ts b/packages/@aws-cdk/aws-sagemaker/lib/model.ts new file mode 100644 index 0000000000000..39a43fddca7f5 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/lib/model.ts @@ -0,0 +1,318 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import iam = require('@aws-cdk/aws-iam'); +import { Construct, Context, IResource, Lazy, Resource, Stack, Tag, Token } from '@aws-cdk/cdk'; +import { CfnModel } from './sagemaker.generated'; + +/** + * Name tag constant + */ +const NAME_TAG: string = 'Name'; + +/** + * Interface that defines a Model resource. + */ +export interface IModel extends IResource { + /** + * Returns the ARN of this model. + * + * @attribute + */ + readonly modelArn: string; + + /** + * Returns the name of this model. + * + * @attribute + */ + readonly modelName: string; +} + +/** + * Construction properties for a generic container image. + */ +export interface GenericContainerProps { + + /** + * A map of region to ECS Container Registry URI. + */ + readonly amiMap: {[region: string]: string}; + + /** + * A map of enviornment variables to pass into the container. + * + * @default none + */ + readonly environment?: {[key: string]: string}; + + /** + * Hostname of the container. + * + * @default none + */ + readonly containerHostname?: string; + + /** + * S3 path to the model artefacts. + * + * @default none + */ + readonly modelDataUrl?: string; +} + +/** + * Interface that defines a container definition. + */ +export interface IContainerDefinition { + /** + * Return the container image to use in the given context + */ + getImage(scope: Construct): string; + + /** + * Return the container envionment map. + * @param scope the Construct scope. + */ + getEnvironment(scope?: Construct): {[key: string]: string} | undefined; + + /** + * Return the URL of the model artefacts + * @param scope the Construct scope. + */ + getModelDataUrl(scope?: Construct): string | undefined; + + /** + * Return the container hostname. + * @param scope the Construct scope. + */ + getContainerHostname(scope?: Construct): string | undefined; + +} + +/** + * Construct an ECR Container Image URI from a map of region names to ECR container URIs. + * + */ +export class GenericContainerDefinition implements IContainerDefinition { + + private readonly amiMap: {[region: string]: string} = {}; + + constructor(private readonly props?: GenericContainerProps) { + if (props) { this.amiMap = props.amiMap; } + } + + public getImage(scope: Construct): string { + let region = Stack.of(scope).region; + if (Token.isUnresolved(region)) { + region = Context.getDefaultRegion(scope); + } + + const uri = region !== 'test-region' ? this.amiMap[region] : '123456789012.dkr.ecr.us-west-2.amazonaws.com/mymodel:latest'; + if (!uri) { + throw new Error(`Unable to find ECR Container Image URI in map: no URI specified for region '${region}'`); + } + return uri; + } + + public getEnvironment(_scope: Construct): {[key: string]: string} | undefined { + if (!(this.props)) { return undefined; } + return this.props.environment; + } + + public getModelDataUrl(_scope: Construct): string | undefined { + if (!(this.props)) { return undefined; } + return this.props.modelDataUrl; + } + + public getContainerHostname(_scope: Construct): string | undefined { + if (!(this.props)) { return undefined; } + return this.props.containerHostname; + } + } + +/** + * Construction properties for a SageMaker Model. + */ +export interface ModelProps { + + /** + * The IAM role that the Amazon SageMaker service assumes. + * + * @default a new IAM role will be created. + */ + readonly role?: iam.IRole; + + /** + * Name of the SageMaker Model. + * + * @default none + */ + readonly modelName?: string; + + /** + * The VPC to deploy the endpoint to. + * + * @default none + */ + readonly vpc?: ec2.IVpc; + + /** + * The VPC subnets to deploy the endpoints. + * + * @default none + */ + readonly vpcSubnets?: ec2.SubnetSelection; + + /** + * Primary container definition + * + * @default none + */ + readonly primaryContainer?: IContainerDefinition; +} + +/** + * Defines a SageMaker Model. + */ +export class Model extends Resource implements IModel, ec2.IConnectable { + + /** + * Creates a SageMaker model from a name. + * @param scope the Construct scope. + * @param id the resource id. + * @param modelName the name of the model. + */ + public static fromModelName(scope: Construct, id: string, modelName: string): IModel { + class Import extends Resource implements IModel { + public modelName = modelName; + public modelArn = Stack.of(this).formatArn({ + service: 'sagemaker', + resource: 'model', + resourceName: this.modelName + }); + } + + return new Import(scope, id); + } + + /** + * Returns the ARN of this model. + * @attribute + */ + public readonly modelArn: string; + + /** + * Returns the name of the model. + * @attribute + */ + public readonly modelName: string; + + /** + * Allows specify security group connections for instances of this fleet. + */ + public readonly connections: ec2.Connections; + + private readonly vpc: ec2.IVpc; + private readonly role: iam.IRole; + private readonly securityGroup: ec2.ISecurityGroup; + private readonly securityGroups: ec2.ISecurityGroup[] = []; + private readonly subnets: ec2.SelectedSubnets; + private readonly primaryContainer: IContainerDefinition; + private readonly containers: IContainerDefinition[] = []; + + constructor(scope: Construct, id: string, props: ModelProps = {}) { + super(scope, id); + + // set the model if if defined + if (props.modelName) { this.modelName = props.modelName; } + if (props.primaryContainer) { this.primaryContainer = props.primaryContainer; } + + // configure networking + if (props.vpc) { + this.vpc = props.vpc; + // create a security group + this.securityGroup = new ec2.SecurityGroup(this, 'ModelSecurityGroup', { + vpc: props.vpc + }); + this.connections = new ec2.Connections({ securityGroups: [this.securityGroup] }); + this.securityGroups.push(this.securityGroup); + this.subnets = props.vpc.selectSubnets(props.vpcSubnets); + } + + // set the sagemaker role or create new one + this.role = props.role || new iam.Role(this, 'SagemakerRole', { + assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'), + managedPolicyArns: [ + new iam.AwsManagedPolicy('AmazonSageMakerFullAccess', scope).policyArn + ] + }); + + this.node.applyAspect(new Tag(NAME_TAG, this.node.path)); + + const model = new CfnModel(this, 'Model', { + executionRoleArn: this.role.roleArn, + modelName: this.modelName, + primaryContainer: Lazy.anyValue({ produce: () => (this.containers.length === 0) ? + this.renderPrimaryContainer(scope, this.primaryContainer) : undefined }), + vpcConfig: Lazy.anyValue({ produce: () => this.renderVpcConfig() }), + containers: Lazy.anyValue({ produce: () => this.renderContainerList(scope, this.containers) }) + }); + this.modelName = model.modelName; + this.modelArn = model.modelArn; + } + + /** + * Add the security group to all instances via the launch configuration + * security groups array. + * + * @param securityGroup: The security group to add + */ + public addSecurityGroup(securityGroup: ec2.ISecurityGroup): void { + this.securityGroups.push(securityGroup); + } + + /** + * Add the container definition to the model. + * + * @param container: The container to add + */ + public addContainer(container: IContainerDefinition): void { + this.containers.push(container); + } + + protected validate(): string[] { + const result = super.validate(); + if (!(this.primaryContainer) && (this.containers.length === 0)) { + result.push("Must define either Primary Container or list of inference containers"); + } + return result; + } + + private renderPrimaryContainer(scope: Construct, container?: IContainerDefinition): CfnModel.ContainerDefinitionProperty | undefined { + if (!container) { return undefined; } + return { + image: container.getImage(scope), + containerHostname: container.getContainerHostname(), + environment: container.getEnvironment(), + modelDataUrl: container.getModelDataUrl(), + }; + } + + private renderVpcConfig(): CfnModel.VpcConfigProperty | undefined { + if (!this.vpc) { return undefined; } + return { + subnets: this.subnets.subnetIds, + securityGroupIds: this.securityGroups.map(sg => sg.securityGroupId), + }; + } + + private renderContainerList(scope: Construct, containers?: IContainerDefinition[]): CfnModel.ContainerDefinitionProperty[] | undefined { + if (!(containers) || containers.length === 0) { return undefined; } + return containers.map(c => ( + { + image: c.getImage(scope), + containerHostname: c.getContainerHostname(), + environment: c.getEnvironment(), + modelDataUrl: c.getModelDataUrl(), + })); + } +} diff --git a/packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts b/packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts index d9363f952dc4d..036c5d8ff7f36 100644 --- a/packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts +++ b/packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts @@ -5,6 +5,8 @@ import { Construct, Fn, Lazy, Resource } from '@aws-cdk/cdk'; import { CfnNotebookInstance, CfnNotebookInstanceLifecycleConfig } from './sagemaker.generated'; /** + * Construction properties for a SageMaker Notebook instance + * * @experimental */ export interface NotebookInstanceProps { @@ -52,11 +54,11 @@ export interface NotebookInstanceProps { readonly enableRootAccess?: boolean; /** - * Security groups attached to the notebook instance. + * VPC to deploy the notebook instance. * * @default none */ - readonly securityGroups?: ec2.ISecurityGroup[]; + readonly vpc?: ec2.IVpc; /** * Subnet where the notebook instance is deployed to in the VPC. @@ -65,13 +67,6 @@ export interface NotebookInstanceProps { */ readonly subnet?: ec2.ISubnet; - /** - * Tags for the notebook instance. - * - * @default none - */ - readonly tags?: {[key: string]: any}; - /** * Size of the notebook volume in GB. * @@ -81,6 +76,8 @@ export interface NotebookInstanceProps { } /** + * Defines a SageMaker Notebook instance. + * * @experimental */ export class NotebookInstance extends Resource implements ec2.IConnectable { @@ -88,24 +85,35 @@ export class NotebookInstance extends Resource implements ec2.IConnectable { /** * Allows specify security group connections for instances of this fleet. */ - public readonly connections: ec2.Connections = new ec2.Connections(); + public readonly connections: ec2.Connections; - /** @attribute */ + /** + * Notebook instance name. + * + * @attribute + */ public readonly notebookInstanceName: string; + /** + * Notebook instance ARN. + * + * @attribute + */ + public readonly notebookInstanceArn: string; + + private readonly vpc: ec2.IVpc; + private readonly securityGroup: ec2.ISecurityGroup; + private readonly securityGroups: ec2.ISecurityGroup[] = []; + private readonly subnet: ec2.ISubnet; + private readonly role: iam.IRole; private readonly instanceType: string; private readonly onCreateLines = new Array(); private readonly onStartLines = new Array(); - private readonly tags: {[key: string]: any} = {}; - constructor(scope: Construct, id: string, props?: NotebookInstanceProps) { + constructor(scope: Construct, id: string, props: NotebookInstanceProps = {}) { super(scope, id); - if (!props) { - props = {}; - } - // set the instance type if defined otherwise set to 'ml.t2.medium' if (props.instanceType) { this.validateInstanceType(props.instanceType.toString()); @@ -120,12 +128,26 @@ export class NotebookInstance extends Resource implements ec2.IConnectable { this.notebookInstanceName = props.notebookInstanceName; } - // set the notebook instance tags - this.tags = (props.tags) ? (props.tags) : {}; - - // add the security groups to the connections object - if (props.securityGroups) { - props.securityGroups.forEach(sg => this.connections.addSecurityGroup(sg)); + // setup the networking of the notebook instance + if (props.vpc) { + this.vpc = props.vpc; + // create a security group + this.securityGroup = new ec2.SecurityGroup(this, 'NotebookSecurityGroup', { + vpc: props.vpc + }); + this.connections = new ec2.Connections({ securityGroups: [this.securityGroup] }); + this.securityGroups.push(this.securityGroup); + if (props.subnet) { + this.subnet = props.subnet; + } else { + if (this.vpc.privateSubnets.length > 0) { + this.subnet = this.vpc.privateSubnets[0]; + } else if (this.vpc.publicSubnets.length > 0) { + this.subnet = this.vpc.publicSubnets[0]; + } else { + throw new Error("No available subnets to deploy notebook instance"); + } + } } // validate the value of volumeSizeInGB @@ -150,20 +172,22 @@ export class NotebookInstance extends Resource implements ec2.IConnectable { }); // create the CfnNotebookInstance resource - new CfnNotebookInstance(this, 'NotebookInstance', { + const notebook = new CfnNotebookInstance(this, 'NotebookInstance', { roleArn: this.role.roleArn, instanceType: this.instanceType, lifecycleConfigName: lifecycleConfig.notebookInstanceLifecycleConfigName, notebookInstanceName: this.notebookInstanceName, - tags: (Object.keys(this.tags).length > 0) ? (Object.keys(this.tags).map(key => ({ key, value: this.tags[key] }))) : undefined, directInternetAccess: props.enableDirectInternetAccess !== undefined ? (props.enableDirectInternetAccess ? 'Enabled' : 'Disabled') : undefined, volumeSizeInGb: props.volumeSizeInGB, - subnetId: props.subnet !== undefined ? props.subnet.subnetId : undefined, - securityGroupIds: props.securityGroups !== undefined ? props.securityGroups.map(sg => (sg.securityGroupId)) : undefined, + subnetId: this.subnet !== undefined ? this.subnet.subnetId : undefined, + securityGroupIds: Lazy.listValue( {produce: () => + (this.securityGroups !== undefined ? this.securityGroups.map(sg => (sg.securityGroupId)) : undefined)}), rootAccess: props.enableRootAccess !== undefined ? (props.enableRootAccess ? 'Enabled' : 'Disabled') : undefined, kmsKeyId: props.kmsKeyId !== undefined ? props.kmsKeyId.keyArn : undefined, }); + this.notebookInstanceName = notebook.notebookInstanceName; + this.notebookInstanceArn = notebook.notebookInstanceArn; } /** @@ -182,6 +206,15 @@ export class NotebookInstance extends Resource implements ec2.IConnectable { scriptLines.forEach(scriptLine => this.onStartLines.push(scriptLine)); } + /** + * Add the security group to the notebook instance. + * + * @param securityGroup: The security group to add + */ + public addSecurityGroup(securityGroup: ec2.ISecurityGroup): void { + this.securityGroups.push(securityGroup); + } + /** * Creates the script to be attached to the lifecycle configuration resource. */ diff --git a/packages/@aws-cdk/aws-sagemaker/test/test.endpoint.ts b/packages/@aws-cdk/aws-sagemaker/test/test.endpoint.ts new file mode 100644 index 0000000000000..e6b9ec7fefad4 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/test.endpoint.ts @@ -0,0 +1,170 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import kms = require('@aws-cdk/aws-kms'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import sagemaker = require('../lib'); + +export = { + "When creating Sagemaker Endpoint": { + "with only required properties set, it correctly sets default properties"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const model = sagemaker.Model.fromModelName(stack, 'Model', "mymodel"); + + // WHEN + new sagemaker.Endpoint(stack, 'Endpoint', { + productionVariants: [ { model } ], + }); + + // THEN + expect(stack).to(haveResource("AWS::SageMaker::EndpointConfig", { + ProductionVariants: [ + { + InitialInstanceCount: 1, + InitialVariantWeight: 100, + InstanceType: 'ml.c4.xlarge', + ModelName: "mymodel", + VariantName: "mymodel", + } + ] + })); + expect(stack).to(haveResource("AWS::SageMaker::Endpoint", { + EndpointConfigName: { "Fn::GetAtt": [ "EndpointEndpointConfig5871F635", "EndpointConfigName" ] }, + Tags: [ + { Key: "Name", Value: "Endpoint" } + ] + })); + + test.done(); + }, + "with many properties set, it correctly creates the endpoint resources"(test: Test) { + // GIVEN + const region = 'test-region'; // hardcode the region + const stack = new cdk.Stack(undefined, undefined, { + env: { + region + }, + }); + const containerImage = new sagemaker.GenericContainerDefinition(); + // create the notebook instance + const model = new sagemaker.Model(stack, 'Model', { + primaryContainer: containerImage, + }); + const kmsKey = new kms.Key(stack, "Key"); + + // WHEN + const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { + configName: "myconfig", + productionVariants: [ + { + model, + variantName: "myvariant", + initialInstanceCount: 10, + initialVariantWeight: 20, + instanceType: new ec2.InstanceType('p3.2xlarge'), + } + ], + kmsKey, + }); + endpoint.node.applyAspect(new cdk.Tag("Project", "myproject")); + + // THEN + expect(stack).to(haveResource("AWS::SageMaker::EndpointConfig", { + EndpointConfigName: "myconfig", + ProductionVariants: [ + { + InitialInstanceCount: 10, + InitialVariantWeight: 20, + InstanceType: 'ml.p3.2xlarge', + ModelName: { "Fn::GetAtt": [ "Model2AD80A05", "ModelName" ]}, + VariantName: "myvariant", + } + ], + KmsKeyId: { "Fn::GetAtt": [ "Key961B73FD", "Arn" ] } + })); + expect(stack).to(haveResource("AWS::SageMaker::Endpoint", { + EndpointConfigName: { "Fn::GetAtt": [ "EndpointEndpointConfig5871F635", "EndpointConfigName" ] }, + Tags: [ + { Key: "Name", Value: "Endpoint" }, + { Key: "Project", Value: "myproject" }, + ] + })); + + test.done(); + }, + "it throws error when no production variant given"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const endpoint = new sagemaker.Endpoint(stack, 'Endpoint'); + + const errors = validate(stack); + + test.equal(errors.length, 1); + const error = errors[0]; + test.same(error.source, endpoint); + test.equal(error.message, "Must have at least one Production Variant"); + + test.done(); + }, + "it throws error when incorrect instance count given"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const model = sagemaker.Model.fromModelName(stack, 'Model', "mymodel"); + + // WHEN + const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { + productionVariants: [ + { + initialInstanceCount: -1, + initialVariantWeight: 100, + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.C4, ec2.InstanceSize.XLarge), + model, + variantName: "production", + } + ] + }); + const errors = validate(stack); + + test.equal(errors.length, 1); + const error = errors[0]; + test.same(error.source, endpoint); + test.equal(error.message, "Must have at least one instance"); + + test.done(); + }, + "it throws error when incorrect variant weight given"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const model = sagemaker.Model.fromModelName(stack, 'Model', "mymodel"); + + // WHEN + const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { + productionVariants: [ + { + initialInstanceCount: 1, + initialVariantWeight: -100, + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.C4, ec2.InstanceSize.XLarge), + model, + variantName: "production", + } + ] + }); + const errors = validate(stack); + + test.equal(errors.length, 1); + const error = errors[0]; + test.same(error.source, endpoint); + test.equal(error.message, "Cannot have negative variant weight"); + + test.done(); + }, + } +}; + +function validate(construct: cdk.IConstruct): cdk.ValidationError[] { + cdk.ConstructNode.prepare(construct.node); + return cdk.ConstructNode.validate(construct.node); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sagemaker/test/test.model.ts b/packages/@aws-cdk/aws-sagemaker/test/test.model.ts new file mode 100644 index 0000000000000..0cc937c4de138 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/test.model.ts @@ -0,0 +1,97 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import iam = require('@aws-cdk/aws-iam'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import sagemaker = require('../lib'); + +export = { + "When creating Sagemaker Model": { + "with only required properties set, it correctly sets default properties"(test: Test) { + // GIVEN + const region = 'test-region'; // hardcode the region + const stack = new cdk.Stack(undefined, undefined, { + env: { + region + }, + }); + const containerImage = new sagemaker.GenericContainerDefinition(); + // create the notebook instance + new sagemaker.Model(stack, 'Model', { + primaryContainer: containerImage + }); + + // THEN + expect(stack).to(haveResource("AWS::SageMaker::Model", { + PrimaryContainer: { + Image: "123456789012.dkr.ecr.us-west-2.amazonaws.com/mymodel:latest", + }, + ExecutionRoleArn: { + "Fn::GetAtt": [ "ModelSagemakerRole321FBBBD", "Arn" ] + }, + Tags: [ + { Key: "Name", Value: "Model" } + ] + })); + + test.done(); + }, + "with all avaiable properties set, it correctly creates the needed resources"(test: Test) { + // GIVEN + const region = 'test-region'; // hardcode the region + const stack = new cdk.Stack(undefined, undefined, { + env: { + region + }, + }); + const vpc = new ec2.Vpc(stack, 'Vpc'); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'), + }); + // create the notebook instance + const containerImage = new sagemaker.GenericContainerDefinition({ + amiMap: { 'test-region': "123456789012.dkr.ecr.us-west-2.amazonaws.com/mymodel:latest" }, + modelDataUrl: 's3://modelbucket/model.tar.gz', + environment: { SOMEVAR: "SOMEVALUE" }, + containerHostname: "myhostname" + }); + const model = new sagemaker.Model(stack, 'Model', { + modelName: "MyModel", + primaryContainer: containerImage, + vpc, + role, + }); + model.node.applyAspect(new cdk.Tag("Project", "myproject")); + + // THEN + expect(stack).to(haveResource("AWS::SageMaker::Model", { + ExecutionRoleArn: { + "Fn::GetAtt": [ "Role1ABCC5F0", "Arn" ] + }, + ModelName: "MyModel", + PrimaryContainer: { + ContainerHostname: "myhostname", + Environment: { + SOMEVAR: "SOMEVALUE" + }, + Image: "123456789012.dkr.ecr.us-west-2.amazonaws.com/mymodel:latest", + ModelDataUrl: "s3://modelbucket/model.tar.gz" + }, + VpcConfig: { + SecurityGroupIds: [ { "Fn::GetAtt": [ "ModelModelSecurityGroupF33B0A56", "GroupId" ] } ], + Subnets: [ + { Ref: "VpcPrivateSubnet1Subnet536B997A" }, + { Ref: "VpcPrivateSubnet2Subnet3788AAA1" }, + { Ref: "VpcPrivateSubnet3SubnetF258B56E" }, + ] + }, + Tags: [ + { Key: "Name", Value: "Model" }, + { Key: "Project", Value: "myproject" }, + ] + })); + + test.done(); + }, + } +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sagemaker/test/test.notebook.ts b/packages/@aws-cdk/aws-sagemaker/test/test.notebook.ts index 61e37d7b86b92..46871317472c4 100644 --- a/packages/@aws-cdk/aws-sagemaker/test/test.notebook.ts +++ b/packages/@aws-cdk/aws-sagemaker/test/test.notebook.ts @@ -38,21 +38,19 @@ export = { const instanceType = new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge); // create the notebook instance - new sagemaker.NotebookInstance(stack, 'Notebook', { + const notebook = new sagemaker.NotebookInstance(stack, 'Notebook', { kmsKeyId: key, + vpc, role, instanceType, notebookInstanceName: "mynotebook", - tags: { - Name: "myname", - Project: "myproject" - }, subnet: subnets[0], - securityGroups: [ sg ], enableDirectInternetAccess: true, enableRootAccess: false, volumeSizeInGB: 100, }); + notebook.addSecurityGroup(sg); + notebook.node.applyAspect(new cdk.Tag("Project", "myproject")); // THEN expect(stack).to(haveResource("AWS::SageMaker::NotebookInstance", { @@ -65,13 +63,13 @@ export = { }, NotebookInstanceName: "mynotebook", Tags: [ - { Key: "Name", Value: "myname" }, { Key: "Project", Value: "myproject" }, ], SubnetId: { Ref: "VpcPrivateSubnet1Subnet536B997A" }, SecurityGroupIds: [ + { "Fn::GetAtt": [ "NotebookNotebookSecurityGroup65EB76D3", "GroupId" ] }, { "Fn::GetAtt": [ "SecurityGroupDD263621", "GroupId" ] }, ], VolumeSizeInGB: 100, From 3d448981bf446d34461c946b931e0dbfb1ee8b46 Mon Sep 17 00:00:00 2001 From: Matt McClean Date: Mon, 17 Jun 2019 14:40:48 +0100 Subject: [PATCH 3/6] feat(sagemaker): added more tests and updated README --- packages/@aws-cdk/aws-sagemaker/README.md | 74 +++++++++++++++- .../@aws-cdk/aws-sagemaker/lib/endpoint.ts | 6 ++ packages/@aws-cdk/aws-sagemaker/lib/model.ts | 17 ++++ .../@aws-cdk/aws-sagemaker/test/test.model.ts | 86 +++++++++++++++++++ 4 files changed, 182 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-sagemaker/README.md b/packages/@aws-cdk/aws-sagemaker/README.md index 0fc5689a70b7d..29d73da5a19d7 100644 --- a/packages/@aws-cdk/aws-sagemaker/README.md +++ b/packages/@aws-cdk/aws-sagemaker/README.md @@ -12,6 +12,78 @@ --- +### Notebook instance + +Define a notebook instance. + +```ts +new NotebookInstance(this, 'MyNotebook'); +``` + +Add a KMS encryption key, launch in own VPC, set the instance type to 'ml.p3.xlarge', disable direct internet access and set the EBS volume size to 100 GB. + +```ts +const vpc = new ec2.Vpc(this, "Vpc"); +const key = new kms.Key(this, 'Key'); +new NotebookInstance(this, 'MyNotebook', { + vpc, + kmsKeyId: key, + instanceType = new ec2.InstanceType('p3.2xlarge'), + enableDirectInternetAccess: false, + volumeSizeInGB: 100, +}); +``` + +Add custom scripts when notebook instance is created and started. + +```ts +const notebook =new NotebookInstance(this, 'MyNotebook'); +notebook.addOnCreateScript('echo "Creating Notebook"'); +notebook.addOnStartScript('echo "Starting Notebook"'); +``` + +Add a security group to the notebook instance. + +```ts +const vpc = new ec2.Vpc(this, "Vpc"); +const sg = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc }); +const notebook =new NotebookInstance(this, 'MyNotebook', { + vpc +}); +notebook.addSecurityGroup(sg); +``` + +### Models + +Define a model. + +```ts +new Model(this, 'MyModel', { + primaryContainer: new GenericContainerDefinition( { 'us-west-2': '123456789012.dkr.ecr.us-west-2.amazonaws.com/mymodel:latest' }) +}); +``` + +Define a model with a container inference pipeline. + +```ts +const model = new Model(this, 'MyModel'); +model.addContainer(new GenericContainerDefinition({ 'us-west-2': '123456789012.dkr.ecr.us-west-2.amazonaws.com/mymodel1:latest' })); +model.addContainer(new GenericContainerDefinition({ 'us-west-2': '123456789012.dkr.ecr.us-west-2.amazonaws.com/mymodel2:latest' })); +``` + +### Endpoonts + +Define an endpoint. + ```ts -const sagemaker = require('@aws-cdk/aws-sagemaker'); +const model = new Model(this, 'MyModel', { + primaryContainer: new GenericContainerDefinition( { 'us-west-2': '123456789012.dkr.ecr.us-west-2.amazonaws.com/mymodel:latest' }) +}); +const endpooint = new sagemaker.Endpoint(stack, 'Endpoint', { + productionVariants: [ + { + model, + } + ], +}); ``` diff --git a/packages/@aws-cdk/aws-sagemaker/lib/endpoint.ts b/packages/@aws-cdk/aws-sagemaker/lib/endpoint.ts index 07aa5949141a4..d5553cdd840a3 100644 --- a/packages/@aws-cdk/aws-sagemaker/lib/endpoint.ts +++ b/packages/@aws-cdk/aws-sagemaker/lib/endpoint.ts @@ -11,6 +11,8 @@ const NAME_TAG: string = 'Name'; /** * Construction properties for a SageMaker Endpoint. + * + * @experimental */ export interface EndpointProps { @@ -46,6 +48,8 @@ export interface EndpointProps { /** * Construction properties for the Production Variant. + * + * @experimental */ export interface ProductionVariant { @@ -102,6 +106,8 @@ export enum AcceleratorType { /** * Defines a SageMaker Endpoint and associated configuration resource. + * + * @experimental */ export class Endpoint extends Resource { diff --git a/packages/@aws-cdk/aws-sagemaker/lib/model.ts b/packages/@aws-cdk/aws-sagemaker/lib/model.ts index 39a43fddca7f5..99318a21d4847 100644 --- a/packages/@aws-cdk/aws-sagemaker/lib/model.ts +++ b/packages/@aws-cdk/aws-sagemaker/lib/model.ts @@ -10,6 +10,8 @@ const NAME_TAG: string = 'Name'; /** * Interface that defines a Model resource. + * + * @experimental */ export interface IModel extends IResource { /** @@ -29,6 +31,8 @@ export interface IModel extends IResource { /** * Construction properties for a generic container image. + * + * @experimental */ export interface GenericContainerProps { @@ -61,6 +65,8 @@ export interface GenericContainerProps { /** * Interface that defines a container definition. + * + * @experimental */ export interface IContainerDefinition { /** @@ -91,6 +97,7 @@ export interface IContainerDefinition { /** * Construct an ECR Container Image URI from a map of region names to ECR container URIs. * + * @experimental */ export class GenericContainerDefinition implements IContainerDefinition { @@ -131,6 +138,8 @@ export class GenericContainerDefinition implements IContainerDefinition { /** * Construction properties for a SageMaker Model. + * + * @experimental */ export interface ModelProps { @@ -172,6 +181,8 @@ export interface ModelProps { /** * Defines a SageMaker Model. + * + * @experimental */ export class Model extends Resource implements IModel, ec2.IConnectable { @@ -281,9 +292,15 @@ export class Model extends Resource implements IModel, ec2.IConnectable { protected validate(): string[] { const result = super.validate(); + // check that either primary container or list of containers defined if (!(this.primaryContainer) && (this.containers.length === 0)) { result.push("Must define either Primary Container or list of inference containers"); } + + // check that container list is not greater than 5 + if (this.containers.length > 5) { + result.push("Cannot have more than 5 containers in inference pipeline"); + } return result; } diff --git a/packages/@aws-cdk/aws-sagemaker/test/test.model.ts b/packages/@aws-cdk/aws-sagemaker/test/test.model.ts index 0cc937c4de138..39a726ec199c4 100644 --- a/packages/@aws-cdk/aws-sagemaker/test/test.model.ts +++ b/packages/@aws-cdk/aws-sagemaker/test/test.model.ts @@ -4,6 +4,7 @@ import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import sagemaker = require('../lib'); +import { GenericContainerDefinition } from '../lib'; export = { "When creating Sagemaker Model": { @@ -93,5 +94,90 @@ export = { test.done(); }, + "use list of containers in inference pipeline"(test: Test) { + // GIVEN + const region = 'us-west-2'; // hardcode the region + const stack = new cdk.Stack(undefined, undefined, { + env: { + region + }, + }); + // create the notebook instance + const containerImage1 = new sagemaker.GenericContainerDefinition({ + amiMap: { 'us-west-2': "123456789012.dkr.ecr.us-west-2.amazonaws.com/mymodel1:latest" }, + }); + const containerImage2 = new sagemaker.GenericContainerDefinition({ + amiMap: { 'us-west-2': "123456789012.dkr.ecr.us-west-2.amazonaws.com/mymodel2:latest" }, + }); + const model = new sagemaker.Model(stack, 'Model', { + modelName: "MyModel", + }); + model.addContainer(containerImage1); + model.addContainer(containerImage2); + + // THEN + expect(stack).to(haveResource("AWS::SageMaker::Model", { + Containers: [ + { Image: "123456789012.dkr.ecr.us-west-2.amazonaws.com/mymodel1:latest" }, + { Image: "123456789012.dkr.ecr.us-west-2.amazonaws.com/mymodel2:latest" }, + ], + Tags: [ + { Key: "Name", Value: "Model" }, + ] + })); + + test.done(); + }, + "it throws error when no primary nor containers defined"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + // create the model + const model = new sagemaker.Model(stack, 'Model'); + + // WHEN + const errors = validate(stack); + + test.equal(errors.length, 1); + const error = errors[0]; + test.same(error.source, model); + test.equal(error.message, "Must define either Primary Container or list of inference containers"); + + test.done(); + }, + "it throws error when more than 5 containers defined"(test: Test) { + // GIVEN + const region = 'test-region'; // hardcode the region + const stack = new cdk.Stack(undefined, undefined, { + env: { + region + }, + }); + // create the model + const model = new sagemaker.Model(stack, 'Model'); + const container = new GenericContainerDefinition(); + times (6) (() => model.addContainer(container)); + + // WHEN + const errors = validate(stack); + + test.equal(errors.length, 1); + const error = errors[0]; + test.same(error.source, model); + test.equal(error.message, "Cannot have more than 5 containers in inference pipeline"); + + test.done(); + }, + } +}; + +function validate(construct: cdk.IConstruct): cdk.ValidationError[] { + cdk.ConstructNode.prepare(construct.node); + return cdk.ConstructNode.validate(construct.node); +} + +const times = (x: number) => (f: () => void) => { + if (x > 0) { + f(); + times (x - 1) (f); } }; \ No newline at end of file From dd105e35c210ff419a4d92cd9eb232728185fe54 Mon Sep 17 00:00:00 2001 From: Matt McClean Date: Mon, 17 Jun 2019 14:43:21 +0100 Subject: [PATCH 4/6] feat(sagemaker): updated README --- packages/@aws-cdk/aws-sagemaker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-sagemaker/README.md b/packages/@aws-cdk/aws-sagemaker/README.md index 29d73da5a19d7..22d5643902731 100644 --- a/packages/@aws-cdk/aws-sagemaker/README.md +++ b/packages/@aws-cdk/aws-sagemaker/README.md @@ -79,7 +79,7 @@ Define an endpoint. const model = new Model(this, 'MyModel', { primaryContainer: new GenericContainerDefinition( { 'us-west-2': '123456789012.dkr.ecr.us-west-2.amazonaws.com/mymodel:latest' }) }); -const endpooint = new sagemaker.Endpoint(stack, 'Endpoint', { +const endpooint = new Endpoint(stack, 'MyEndpoint', { productionVariants: [ { model, From 92e3ff0fb1c5c9a913ff46615c3f2931603fbc6e Mon Sep 17 00:00:00 2001 From: Matt McClean Date: Mon, 17 Jun 2019 15:32:12 +0100 Subject: [PATCH 5/6] fix(sagemaker) fix change in iam.Role props declaration --- packages/@aws-cdk/aws-sagemaker/lib/model.ts | 4 +--- packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-sagemaker/lib/model.ts b/packages/@aws-cdk/aws-sagemaker/lib/model.ts index 99318a21d4847..80bbc68e5c95c 100644 --- a/packages/@aws-cdk/aws-sagemaker/lib/model.ts +++ b/packages/@aws-cdk/aws-sagemaker/lib/model.ts @@ -252,9 +252,7 @@ export class Model extends Resource implements IModel, ec2.IConnectable { // set the sagemaker role or create new one this.role = props.role || new iam.Role(this, 'SagemakerRole', { assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'), - managedPolicyArns: [ - new iam.AwsManagedPolicy('AmazonSageMakerFullAccess', scope).policyArn - ] + managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonSageMakerFullAccess')], }); this.node.applyAspect(new Tag(NAME_TAG, this.node.path)); diff --git a/packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts b/packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts index 036c5d8ff7f36..c9bca3a0b3d75 100644 --- a/packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts +++ b/packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts @@ -158,9 +158,7 @@ export class NotebookInstance extends Resource implements ec2.IConnectable { // set the sagemaker role or create new one this.role = props.role || new iam.Role(this, 'SagemakerRole', { assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'), - managedPolicyArns: [ - new iam.AwsManagedPolicy('AmazonSageMakerFullAccess', scope).policyArn - ] + managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonSageMakerFullAccess')], }); // create the Lifecycle Config resource From 895288b88c1a5e2aee2d882d81e94855a9e22867 Mon Sep 17 00:00:00 2001 From: Matt McClean Date: Wed, 26 Jun 2019 11:30:22 +0100 Subject: [PATCH 6/6] feat(sagemaker): updated to cdk v0.36 baseline --- .../@aws-cdk/aws-sagemaker/lib/endpoint.ts | 16 +++++++------- packages/@aws-cdk/aws-sagemaker/lib/model.ts | 21 ++++++++++--------- .../aws-sagemaker/lib/notebook-instance.ts | 16 +++++--------- .../aws-sagemaker/test/test.endpoint.ts | 6 +++--- .../@aws-cdk/aws-sagemaker/test/test.model.ts | 3 +-- .../aws-sagemaker/test/test.notebook.ts | 6 +++--- 6 files changed, 31 insertions(+), 37 deletions(-) diff --git a/packages/@aws-cdk/aws-sagemaker/lib/endpoint.ts b/packages/@aws-cdk/aws-sagemaker/lib/endpoint.ts index d5553cdd840a3..231220b1dc142 100644 --- a/packages/@aws-cdk/aws-sagemaker/lib/endpoint.ts +++ b/packages/@aws-cdk/aws-sagemaker/lib/endpoint.ts @@ -1,6 +1,6 @@ import ec2 = require('@aws-cdk/aws-ec2'); import kms = require('@aws-cdk/aws-kms'); -import { Construct, Lazy, Resource, Tag } from '@aws-cdk/cdk'; +import { Construct, Lazy, Resource, Tag } from '@aws-cdk/core'; import { IModel } from './model'; import { CfnEndpoint, CfnEndpointConfig } from './sagemaker.generated'; @@ -97,11 +97,11 @@ export interface ProductionVariant { export enum AcceleratorType { - Large = 'ml.eia1.large ', + LARGE = 'ml.eia1.large ', - Medium = 'ml.eia1.medium', + MEDIUM = 'ml.eia1.medium', - XLarge = 'ml.eia1.xlarge', + XLARGE = 'ml.eia1.xlarge', } /** @@ -144,14 +144,14 @@ export class Endpoint extends Resource { endpointConfigName: (props.configName) ? props.configName : (props.endpointName) ? props.endpointName + "Config" : undefined, productionVariants: Lazy.anyValue({ produce: () => this.renderProductionVariants(this.productionVariants) }) }); - this.endpointConfigName = endpointConfig.endpointConfigName; + this.endpointConfigName = endpointConfig.attrEndpointConfigName; // create the endpoint resource const endpoint = new CfnEndpoint(this, 'Endpoint', { - endpointConfigName: endpointConfig.endpointConfigName, + endpointConfigName: this.endpointConfigName, endpointName: (props.endpointName) ? props.endpointName : undefined, }); - this.endpointName = endpoint.endpointName; + this.endpointName = endpoint.attrEndpointName; } /** @@ -206,7 +206,7 @@ export class Endpoint extends Resource { initialVariantWeight: (v.initialVariantWeight) ? v.initialVariantWeight : 100, // get the instance type. Set to 'ml.c4.xlarge' if not defined instanceType: 'ml.' + ((v.instanceType) ? (v.instanceType.toString()) : - new ec2.InstanceTypePair(ec2.InstanceClass.C4, ec2.InstanceSize.XLarge)), + ec2.InstanceType.of(ec2.InstanceClass.C4, ec2.InstanceSize.XLARGE)), modelName: v.model.modelName, // get the variant name. Set to the model name if not defined variantName: (v.variantName) ? v.variantName : v.model.modelName, diff --git a/packages/@aws-cdk/aws-sagemaker/lib/model.ts b/packages/@aws-cdk/aws-sagemaker/lib/model.ts index 80bbc68e5c95c..ed516a016b75c 100644 --- a/packages/@aws-cdk/aws-sagemaker/lib/model.ts +++ b/packages/@aws-cdk/aws-sagemaker/lib/model.ts @@ -1,6 +1,6 @@ import ec2 = require('@aws-cdk/aws-ec2'); import iam = require('@aws-cdk/aws-iam'); -import { Construct, Context, IResource, Lazy, Resource, Stack, Tag, Token } from '@aws-cdk/cdk'; +import { Construct, IResource, Lazy, Resource, Stack, Tag } from '@aws-cdk/core'; import { CfnModel } from './sagemaker.generated'; /** @@ -108,10 +108,7 @@ export class GenericContainerDefinition implements IContainerDefinition { } public getImage(scope: Construct): string { - let region = Stack.of(scope).region; - if (Token.isUnresolved(region)) { - region = Context.getDefaultRegion(scope); - } + const region = Stack.of(scope).region; const uri = region !== 'test-region' ? this.amiMap[region] : '123456789012.dkr.ecr.us-west-2.amazonaws.com/mymodel:latest'; if (!uri) { @@ -120,17 +117,17 @@ export class GenericContainerDefinition implements IContainerDefinition { return uri; } - public getEnvironment(_scope: Construct): {[key: string]: string} | undefined { + public getEnvironment(_scope?: Construct): {[key: string]: string} | undefined { if (!(this.props)) { return undefined; } return this.props.environment; } - public getModelDataUrl(_scope: Construct): string | undefined { + public getModelDataUrl(_scope?: Construct): string | undefined { if (!(this.props)) { return undefined; } return this.props.modelDataUrl; } - public getContainerHostname(_scope: Construct): string | undefined { + public getContainerHostname(_scope?: Construct): string | undefined { if (!(this.props)) { return undefined; } return this.props.containerHostname; } @@ -265,8 +262,12 @@ export class Model extends Resource implements IModel, ec2.IConnectable { vpcConfig: Lazy.anyValue({ produce: () => this.renderVpcConfig() }), containers: Lazy.anyValue({ produce: () => this.renderContainerList(scope, this.containers) }) }); - this.modelName = model.modelName; - this.modelArn = model.modelArn; + this.modelName = model.attrModelName; + this.modelArn = Stack.of(this).formatArn({ + service: 'sagemaker', + resource: 'model', + resourceName: this.modelName + }); } /** diff --git a/packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts b/packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts index c9bca3a0b3d75..223f254ca1ee0 100644 --- a/packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts +++ b/packages/@aws-cdk/aws-sagemaker/lib/notebook-instance.ts @@ -1,7 +1,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); -import { Construct, Fn, Lazy, Resource } from '@aws-cdk/cdk'; +import { Construct, Fn, Lazy, Resource } from '@aws-cdk/core'; import { CfnNotebookInstance, CfnNotebookInstanceLifecycleConfig } from './sagemaker.generated'; /** @@ -94,13 +94,6 @@ export class NotebookInstance extends Resource implements ec2.IConnectable { */ public readonly notebookInstanceName: string; - /** - * Notebook instance ARN. - * - * @attribute - */ - public readonly notebookInstanceArn: string; - private readonly vpc: ec2.IVpc; private readonly securityGroup: ec2.ISecurityGroup; private readonly securityGroups: ec2.ISecurityGroup[] = []; @@ -110,6 +103,7 @@ export class NotebookInstance extends Resource implements ec2.IConnectable { private readonly instanceType: string; private readonly onCreateLines = new Array(); private readonly onStartLines = new Array(); + private readonly lifecycleConfigName: string; constructor(scope: Construct, id: string, props: NotebookInstanceProps = {}) { super(scope, id); @@ -168,12 +162,13 @@ export class NotebookInstance extends Resource implements ec2.IConnectable { onCreate: [{content: onCreateToken}], onStart: [{content: onStartToken}], }); + this.lifecycleConfigName = lifecycleConfig.attrNotebookInstanceLifecycleConfigName; // create the CfnNotebookInstance resource const notebook = new CfnNotebookInstance(this, 'NotebookInstance', { roleArn: this.role.roleArn, instanceType: this.instanceType, - lifecycleConfigName: lifecycleConfig.notebookInstanceLifecycleConfigName, + lifecycleConfigName: this.lifecycleConfigName, notebookInstanceName: this.notebookInstanceName, directInternetAccess: props.enableDirectInternetAccess !== undefined ? (props.enableDirectInternetAccess ? 'Enabled' : 'Disabled') : undefined, @@ -184,8 +179,7 @@ export class NotebookInstance extends Resource implements ec2.IConnectable { rootAccess: props.enableRootAccess !== undefined ? (props.enableRootAccess ? 'Enabled' : 'Disabled') : undefined, kmsKeyId: props.kmsKeyId !== undefined ? props.kmsKeyId.keyArn : undefined, }); - this.notebookInstanceName = notebook.notebookInstanceName; - this.notebookInstanceArn = notebook.notebookInstanceArn; + this.notebookInstanceName = notebook.attrNotebookInstanceName; } /** diff --git a/packages/@aws-cdk/aws-sagemaker/test/test.endpoint.ts b/packages/@aws-cdk/aws-sagemaker/test/test.endpoint.ts index e6b9ec7fefad4..4cafcd74723af 100644 --- a/packages/@aws-cdk/aws-sagemaker/test/test.endpoint.ts +++ b/packages/@aws-cdk/aws-sagemaker/test/test.endpoint.ts @@ -1,7 +1,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); import kms = require('@aws-cdk/aws-kms'); -import cdk = require('@aws-cdk/cdk'); +import cdk = require('@aws-cdk/core'); import { Test } from 'nodeunit'; import sagemaker = require('../lib'); @@ -120,7 +120,7 @@ export = { { initialInstanceCount: -1, initialVariantWeight: 100, - instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.C4, ec2.InstanceSize.XLarge), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C4, ec2.InstanceSize.XLARGE), model, variantName: "production", } @@ -146,7 +146,7 @@ export = { { initialInstanceCount: 1, initialVariantWeight: -100, - instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.C4, ec2.InstanceSize.XLarge), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C4, ec2.InstanceSize.XLARGE), model, variantName: "production", } diff --git a/packages/@aws-cdk/aws-sagemaker/test/test.model.ts b/packages/@aws-cdk/aws-sagemaker/test/test.model.ts index 39a726ec199c4..b9195c4adb212 100644 --- a/packages/@aws-cdk/aws-sagemaker/test/test.model.ts +++ b/packages/@aws-cdk/aws-sagemaker/test/test.model.ts @@ -1,7 +1,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); import iam = require('@aws-cdk/aws-iam'); -import cdk = require('@aws-cdk/cdk'); +import cdk = require('@aws-cdk/core'); import { Test } from 'nodeunit'; import sagemaker = require('../lib'); import { GenericContainerDefinition } from '../lib'; @@ -83,7 +83,6 @@ export = { Subnets: [ { Ref: "VpcPrivateSubnet1Subnet536B997A" }, { Ref: "VpcPrivateSubnet2Subnet3788AAA1" }, - { Ref: "VpcPrivateSubnet3SubnetF258B56E" }, ] }, Tags: [ diff --git a/packages/@aws-cdk/aws-sagemaker/test/test.notebook.ts b/packages/@aws-cdk/aws-sagemaker/test/test.notebook.ts index 46871317472c4..c40b5f191d9ff 100644 --- a/packages/@aws-cdk/aws-sagemaker/test/test.notebook.ts +++ b/packages/@aws-cdk/aws-sagemaker/test/test.notebook.ts @@ -2,7 +2,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); -import cdk = require('@aws-cdk/cdk'); +import cdk = require('@aws-cdk/core'); import { Test } from 'nodeunit'; import sagemaker = require('../lib'); @@ -35,7 +35,7 @@ export = { const sg = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc, allowAllOutbound: true }); const key = new kms.Key(stack, 'Key'); const role = new iam.Role(stack, 'Role', { assumedBy: new iam.ServicePrincipal("sagemaker.amazonaws.com") } ); - const instanceType = new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge); + const instanceType = ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.XLARGE); // create the notebook instance const notebook = new sagemaker.NotebookInstance(stack, 'Notebook', { @@ -121,7 +121,7 @@ export = { const stack = new cdk.Stack(); // WHEN test.throws(() => new sagemaker.NotebookInstance(stack, 'Notebook', { - instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.X1, ec2.InstanceSize.XLarge32) + instanceType: ec2.InstanceType.of(ec2.InstanceClass.X1, ec2.InstanceSize.XLARGE32) }), /Invalid instance type/); test.done(); },