diff --git a/packages/@aws-cdk/aws-sagemaker/README.md b/packages/@aws-cdk/aws-sagemaker/README.md index 482007cffe7d6..eeb1bc7e9eede 100644 --- a/packages/@aws-cdk/aws-sagemaker/README.md +++ b/packages/@aws-cdk/aws-sagemaker/README.md @@ -9,31 +9,150 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- -This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +Amazon SageMaker provides every developer and data scientist with the ability to build, train, and +deploy machine learning models quickly. Amazon SageMaker is a fully-managed service that covers the +entire machine learning workflow to label and prepare your data, choose an algorithm, train the +model, tune and optimize it for deployment, make predictions, and take action. Your models get to +production faster with much less effort and lower cost. + +## Installation + +Install the module: + +```console +$ npm i @aws-cdk/aws-sagemaker +``` + +Import it into your code: + +```typescript +import * as sagemaker from '@aws-cdk/aws-sagemaker'; +``` + +## Model + +To create a machine learning model with Amazon Sagemaker, use the `Model` construct. This construct +includes properties that can be configured to define model components, including the model inference +code as a Docker image and an optional set of separate model data artifacts. See the [AWS +documentation](https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-marketplace-develop.html) +to learn more about SageMaker models. + +### Single Container Model -```ts nofixture +In the event that a single container is sufficient for your inference use-case, you can define a +single-container model: + +```typescript import * as sagemaker from '@aws-cdk/aws-sagemaker'; +import * as path from 'path'; + +const image = sagemaker.ContainerImage.fromAsset(path.join('path', 'to', 'Dockerfile', 'directory')); +const modelData = sagemaker.ModelData.fromAsset(path.join('path', 'to', 'artifact', 'file.tar.gz')); + +const model = new sagemaker.Model(this, 'PrimaryContainerModel', { + containers: [ + { + image: image, + modelData: modelData, + } + ] +}); ``` - +### Inference Pipeline Model + +An inference pipeline is an Amazon SageMaker model that is composed of a linear sequence of multiple +containers that process requests for inferences on data. See the [AWS +documentation](https://docs.aws.amazon.com/sagemaker/latest/dg/inference-pipelines.html) to learn +more about SageMaker inference pipelines. To define an inference pipeline, you can provide +additional containers for your model: -There are no official hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. Here are some suggestions on how to proceed: +```typescript +import * as sagemaker from '@aws-cdk/aws-sagemaker'; -- Search [Construct Hub for SageMaker construct libraries](https://constructs.dev/search?q=sagemaker) -- Use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, in the same way you would use [the CloudFormation AWS::SageMaker resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_SageMaker.html) directly. +declare const image1: sagemaker.ContainerImage; +declare const modelData1: sagemaker.ModelData; +declare const image2: sagemaker.ContainerImage; +declare const modelData2: sagemaker.ModelData; +declare const image3: sagemaker.ContainerImage; +declare const modelData3: sagemaker.ModelData; + +const model = new sagemaker.Model(this, 'InferencePipelineModel', { + containers: [ + { image: image1, modelData: modelData1 }, + { image: image2, modelData: modelData2 }, + { image: image3, modelData: modelData3 } + ], +}); +``` +### Container Images - +Inference code can be stored in the Amazon EC2 Container Registry (Amazon ECR), which is specified +via `ContainerDefinition`'s `image` property which accepts a class that extends the `ContainerImage` +abstract base class. -There are no hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. -However, you can still use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, and use this service exactly as you would using CloudFormation directly. +#### Asset Image -For more information on the resources and properties available for this service, see the [CloudFormation documentation for AWS::SageMaker](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_SageMaker.html). +Reference a local directory containing a Dockerfile: -(Read the [CDK Contributing Guide](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and submit an RFC if you are interested in contributing to this construct library.) +```typescript +import * as sagemaker from '@aws-cdk/aws-sagemaker'; +import * as path from 'path'; + +const image = sagemaker.ContainerImage.fromAsset(path.join('path', 'to', 'Dockerfile', 'directory')); +``` - +#### ECR Image + +Reference an image available within ECR: + +```typescript +import * as ecr from '@aws-cdk/aws-ecr'; +import * as sagemaker from '@aws-cdk/aws-sagemaker'; + +const repository = ecr.Repository.fromRepositoryName(this, 'Repository', 'repo'); +const image = sagemaker.ContainerImage.fromEcrRepository(repository, 'tag'); +``` + +### Model Artifacts + +If you choose to decouple your model artifacts from your inference code (as is natural given +different rates of change between inference code and model artifacts), the artifacts can be +specified via the `modelData` property which accepts a class that extends the `ModelData` abstract +base class. The default is to have no model artifacts associated with a model. + +#### Asset Model Data + +Reference local model data: + +```typescript +import * as sagemaker from '@aws-cdk/aws-sagemaker'; +import * as path from 'path'; + +const modelData = sagemaker.ModelData.fromAsset(path.join('path', 'to', 'artifact', 'file.tar.gz')); +``` + +#### S3 Model Data + +Reference an S3 bucket and object key as the artifacts for a model: + +```typescript +import * as s3 from '@aws-cdk/aws-s3'; +import * as sagemaker from '@aws-cdk/aws-sagemaker'; + +const bucket = new s3.Bucket(this, 'MyBucket'); +const modelData = sagemaker.ModelData.fromBucket(bucket, 'path/to/artifact/file.tar.gz'); +``` diff --git a/packages/@aws-cdk/aws-sagemaker/lib/container-image.ts b/packages/@aws-cdk/aws-sagemaker/lib/container-image.ts new file mode 100644 index 0000000000000..d803ca82578eb --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/lib/container-image.ts @@ -0,0 +1,83 @@ +import * as ecr from '@aws-cdk/aws-ecr'; +import * as assets from '@aws-cdk/aws-ecr-assets'; +import { Construct } from 'constructs'; +import { Model } from './model'; +import { hashcode } from './private/util'; + +/** + * The configuration for creating a container image. + */ +export interface ContainerImageConfig { + /** + * The image name. Images in Amazon ECR repositories can be specified by either using the full registry/repository:tag or + * registry/repository@digest. + * + * For example, `012345678910.dkr.ecr..amazonaws.com/:latest` or + * `012345678910.dkr.ecr..amazonaws.com/@sha256:94afd1f2e64d908bc90dbca0035a5b567EXAMPLE`. + */ + readonly imageName: string; +} + +/** + * Constructs for types of container images + */ +export abstract class ContainerImage { + /** + * Reference an image in an ECR repository + */ + public static fromEcrRepository(repository: ecr.IRepository, tag: string = 'latest'): ContainerImage { + return new EcrImage(repository, tag); + } + + /** + * Reference an image that's constructed directly from sources on disk + * @param directory The directory where the Dockerfile is stored + * @param options The options to further configure the selected image + */ + public static fromAsset(directory: string, options: assets.DockerImageAssetOptions = {}): ContainerImage { + return new AssetImage(directory, options); + } + + /** + * Called when the image is used by a Model + */ + public abstract bind(scope: Construct, model: Model): ContainerImageConfig; +} + +class EcrImage extends ContainerImage { + constructor(private readonly repository: ecr.IRepository, private readonly tag: string) { + super(); + } + + public bind(_scope: Construct, model: Model): ContainerImageConfig { + this.repository.grantPull(model); + + return { + imageName: this.repository.repositoryUriForTag(this.tag), + }; + } +} + +class AssetImage extends ContainerImage { + private asset?: assets.DockerImageAsset; + + constructor(private readonly directory: string, private readonly options: assets.DockerImageAssetOptions = {}) { + super(); + } + + public bind(scope: Construct, model: Model): ContainerImageConfig { + // Retain the first instantiation of this asset + if (!this.asset) { + this.asset = new assets.DockerImageAsset(scope, `ModelImage${hashcode(this.directory)}`, { + directory: this.directory, + ...this.options, + }); + } + + this.asset.repository.grantPull(model); + + return { + imageName: this.asset.imageUri, + }; + } +} diff --git a/packages/@aws-cdk/aws-sagemaker/lib/index.ts b/packages/@aws-cdk/aws-sagemaker/lib/index.ts index 4c40a31057568..576ff2f199e4b 100644 --- a/packages/@aws-cdk/aws-sagemaker/lib/index.ts +++ b/packages/@aws-cdk/aws-sagemaker/lib/index.ts @@ -1,2 +1,6 @@ +export * from './container-image'; +export * from './model'; +export * from './model-data'; + // AWS::SageMaker CloudFormation Resources: export * from './sagemaker.generated'; diff --git a/packages/@aws-cdk/aws-sagemaker/lib/model-data.ts b/packages/@aws-cdk/aws-sagemaker/lib/model-data.ts new file mode 100644 index 0000000000000..2d46a4d8762ad --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/lib/model-data.ts @@ -0,0 +1,93 @@ +import * as s3 from '@aws-cdk/aws-s3'; +import * as assets from '@aws-cdk/aws-s3-assets'; +import { Construct } from 'constructs'; +import { IModel } from './model'; +import { hashcode } from './private/util'; + +// The only supported extension for local asset model data +// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-model-containerdefinition.html#cfn-sagemaker-model-containerdefinition-modeldataurl +const ARTIFACT_EXTENSION = '.tar.gz'; + +/** + * The configuration needed to reference model artifacts. + */ +export interface ModelDataConfig { + /** + * The S3 path where the model artifacts, which result from model training, are stored. This path + * must point to a single gzip compressed tar archive (.tar.gz suffix). + */ + readonly uri: string; +} + +/** + * Model data represents the source of model artifacts, which will ultimately be loaded from an S3 + * location. + */ +export abstract class ModelData { + /** + * Constructs model data which is already available within S3. + * @param bucket The S3 bucket within which the model artifacts are stored + * @param objectKey The S3 object key at which the model artifacts are stored + */ + public static fromBucket(bucket: s3.IBucket, objectKey: string): ModelData { + return new S3ModelData(bucket, objectKey); + } + + /** + * Constructs model data that will be uploaded to S3 as part of the CDK app deployment. + * @param path The local path to a model artifact file as a gzipped tar file + * @param options The options to further configure the selected asset + */ + public static fromAsset(path: string, options: assets.AssetOptions = {}): ModelData { + return new AssetModelData(path, options); + } + + /** + * This method is invoked by the SageMaker Model construct when it needs to resolve the model + * data to a URI. + * @param scope The scope within which the model data is resolved + * @param model The Model construct performing the URI resolution + */ + public abstract bind(scope: Construct, model: IModel): ModelDataConfig; +} + +class S3ModelData extends ModelData { + constructor(private readonly bucket: s3.IBucket, private readonly objectKey: string) { + super(); + } + + public bind(_scope: Construct, model: IModel): ModelDataConfig { + this.bucket.grantRead(model); + + return { + uri: this.bucket.urlForObject(this.objectKey), + }; + } +} + +class AssetModelData extends ModelData { + private asset?: assets.Asset; + + constructor(private readonly path: string, private readonly options: assets.AssetOptions) { + super(); + if (!path.toLowerCase().endsWith(ARTIFACT_EXTENSION)) { + throw new Error(`Asset must be a gzipped tar file with extension ${ARTIFACT_EXTENSION} (${this.path})`); + } + } + + public bind(scope: Construct, model: IModel): ModelDataConfig { + // Retain the first instantiation of this asset + if (!this.asset) { + this.asset = new assets.Asset(scope, `ModelData${hashcode(this.path)}`, { + path: this.path, + ...this.options, + }); + } + + this.asset.grantRead(model); + + return { + uri: this.asset.httpUrl, + }; + } +} 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..78b952647c235 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/lib/model.ts @@ -0,0 +1,411 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { ContainerImage } from './container-image'; +import { ModelData } from './model-data'; +import { CfnModel } from './sagemaker.generated'; + +/** + * Interface that defines a Model resource. + */ +export interface IModel extends cdk.IResource, iam.IGrantable, ec2.IConnectable { + /** + * Returns the ARN of this model. + * + * @attribute + */ + readonly modelArn: string; + + /** + * Returns the name of this model. + * + * @attribute + */ + readonly modelName: string; + + /** + * The IAM role associated with this Model. + */ + readonly role?: iam.IRole; + + /** + * Adds a statement to the IAM role assumed by the instance. + */ + addToRolePolicy(statement: iam.PolicyStatement): void; +} + +/** + * Represents a Model resource defined outside this stack. + */ +export interface ModelAttributes { + /** + * The ARN of this model. + */ + readonly modelArn: string; + + /** + * The IAM execution role associated with this model. + * + * @default - When not provided, any role-related operations will no-op. + */ + readonly role?: iam.IRole; + + /** + * The security groups for this model, if in a VPC. + * + * @default - When not provided, the connections to/from this model cannot be managed. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; +} + +abstract class ModelBase extends cdk.Resource implements IModel { + /** + * Returns the ARN of this model. + * @attribute + */ + public abstract readonly modelArn: string; + /** + * Returns the name of the model. + * @attribute + */ + public abstract readonly modelName: string; + /** + * Execution role for SageMaker Model + */ + public abstract readonly role?: iam.IRole; + /** + * The principal this Model is running as + */ + public abstract readonly grantPrincipal: iam.IPrincipal; + /** + * An accessor for the Connections object that will fail if this Model does not have a VPC + * configured. + */ + public get connections(): ec2.Connections { + if (!this._connections) { + throw new Error('Cannot manage network access without configuring a VPC'); + } + return this._connections; + } + /** + * The actual Connections object for this Model. This may be unset in the event that a VPC has not + * been configured. + * @internal + */ + protected _connections: ec2.Connections | undefined; + + /** + * Adds a statement to the IAM role assumed by the instance. + */ + public addToRolePolicy(statement: iam.PolicyStatement) { + if (!this.role) { + return; + } + + this.role.addToPolicy(statement); + } +} + +/** + * Describes the container, as part of model definition. + */ +export interface ContainerDefinition { + /** + * The image used to start a container. + */ + readonly image: ContainerImage; + + /** + * A map of environment variables to pass into the container. + * + * @default - none + */ + readonly environment?: {[key: string]: string}; + + /** + * Hostname of the container within an inference pipeline. For single container models, this field + * is ignored. When specifying a hostname for one ContainerDefinition in a pipeline, hostnames + * must be specified for all other ContainerDefinitions in that pipeline. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-model-containerdefinition.html#cfn-sagemaker-model-containerdefinition-containerhostname + * + * @default - Amazon SageMaker will automatically assign a unique name based on the position of + * this ContainerDefinition in an inference pipeline. + */ + readonly containerHostname?: string; + + /** + * S3 path to the model artifacts. + * + * @default - none + */ + readonly modelData?: ModelData; +} + +/** + * Construction properties for a SageMaker Model. + */ +export interface ModelProps { + + /** + * The IAM role that the Amazon SageMaker service assumes. + * + * @see https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-roles.html#sagemaker-roles-createmodel-perms + * + * @default - a new IAM role will be created with the `AmazonSageMakerFullAccess` policy attached. + */ + readonly role?: iam.IRole; + + /** + * Name of the SageMaker Model. + * + * @default - AWS CloudFormation generates a unique physical ID and uses that ID for the model's + * name. + */ + readonly modelName?: string; + + /** + * The VPC to deploy model containers to. + * + * @default - none + */ + readonly vpc?: ec2.IVpc; + + /** + * The VPC subnets to use when deploying model containers. + * + * @default - none + */ + readonly vpcSubnets?: ec2.SubnetSelection; + + /** + * The security groups to associate to the Model. If no security groups are provided and 'vpc' is + * configured, one security group will be created automatically. + * + * @default - A security group will be automatically created if 'vpc' is supplied + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * Specifies the container definitions for this model, consisting of either a single primary + * container or an inference pipeline of multiple containers. + * + * @default - none + */ + readonly containers?: ContainerDefinition[]; + + /** + * Whether to allow the SageMaker Model to send all network traffic + * + * If set to false, you must individually add traffic rules to allow the + * SageMaker Model to connect to network targets. + * + * Only used if 'vpc' is supplied. + * + * @default true + */ + readonly allowAllOutbound?: boolean; +} + +/** + * Defines a SageMaker Model. + */ +export class Model extends ModelBase { + /** + * Imports a Model defined either outside the CDK or in a different CDK stack. + * @param scope the Construct scope. + * @param id the resource id. + * @param modelArn the ARN of the model. + */ + public static fromModelArn(scope: Construct, id: string, modelArn: string): IModel { + return Model.fromModelAttributes(scope, id, { modelArn }); + } + + /** + * Imports a Model defined either outside the CDK or in a different CDK stack. + * @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 { + const modelArn = cdk.Stack.of(scope).formatArn({ + service: 'sagemaker', + resource: 'model', + resourceName: modelName, + }); + return Model.fromModelAttributes(scope, id, { modelArn }); + } + + /** + * Imports a Model defined either outside the CDK or in a different CDK stack. + * @param scope the Construct scope. + * @param id the resource id. + * @param attrs the attributes of the model to import. + */ + public static fromModelAttributes(scope: Construct, id: string, attrs: ModelAttributes): IModel { + const modelArn = attrs.modelArn; + const modelName = cdk.Stack.of(scope).splitArn(modelArn, cdk.ArnFormat.SLASH_RESOURCE_NAME).resourceName!; + const role = attrs.role; + + class Import extends ModelBase { + public readonly modelArn = modelArn; + public readonly modelName = modelName; + public readonly role = role; + public readonly grantPrincipal: iam.IPrincipal; + + constructor(s: Construct, i: string) { + super(s, i, { + environmentFromArn: attrs.modelArn, + }); + + this.grantPrincipal = role || new iam.UnknownPrincipal({ resource: this }); + if (attrs.securityGroups) { + this._connections = new ec2.Connections({ + securityGroups: attrs.securityGroups, + }); + } + } + } + + 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; + /** + * Execution role for SageMaker Model + */ + public readonly role?: iam.IRole; + /** + * The principal this Model is running as + */ + public readonly grantPrincipal: iam.IPrincipal; + private readonly subnets: ec2.SelectedSubnets | undefined; + private readonly containers: CfnModel.ContainerDefinitionProperty[] = []; + + constructor(scope: Construct, id: string, props: ModelProps = {}) { + super(scope, id, { + physicalName: props.modelName, + }); + + this._connections = this.configureNetworking(props); + this.subnets = (props.vpc) ? props.vpc.selectSubnets(props.vpcSubnets) : undefined; + + // set the sagemaker role or create new one + this.role = props.role || this.createSageMakerRole(); + this.grantPrincipal = this.role; + + (props.containers || []).map(c => this.addContainer(c)); + + const model = new CfnModel(this, 'Model', { + executionRoleArn: this.role.roleArn, + modelName: this.physicalName, + primaryContainer: cdk.Lazy.any({ produce: () => this.renderPrimaryContainer() }), + vpcConfig: cdk.Lazy.any({ produce: () => this.renderVpcConfig() }), + containers: cdk.Lazy.any({ produce: () => this.renderContainers() }), + }); + this.modelName = this.getResourceNameAttribute(model.attrModelName); + this.modelArn = this.getResourceArnAttribute(model.ref, { + service: 'sagemaker', + resource: 'model', + resourceName: this.physicalName, + }); + + /* + * SageMaker model creation will fail if the model's execution role does not have read access to + * its model data in S3. Since the CDK uses a separate AWS::IAM::Policy CloudFormation resource + * to attach inline policies to IAM roles, the following line ensures that the role and its + * AWS::IAM::Policy resource are deployed prior to model creation. + */ + model.node.addDependency(this.role); + } + + /** + * Add containers to the model. + * + * @param container The container definition to add. + */ + public addContainer(container: ContainerDefinition): void { + this.containers.push(this.renderContainer(container)); + } + + private validateContainers(): void { + // validate number of containers + if (this.containers.length < 1) { + throw new Error('Must configure at least 1 container for model'); + } else if (this.containers.length > 15) { + throw new Error('Cannot have more than 15 containers in inference pipeline'); + } + } + + private renderPrimaryContainer(): CfnModel.ContainerDefinitionProperty | undefined { + return (this.containers.length === 1) ? this.containers[0] : undefined; + } + + private renderContainers(): CfnModel.ContainerDefinitionProperty[] | undefined { + this.validateContainers(); + return (this.containers.length === 1) ? undefined : this.containers; + } + + private renderContainer(container: ContainerDefinition): CfnModel.ContainerDefinitionProperty { + return { + image: container.image.bind(this, this).imageName, + containerHostname: container.containerHostname, + environment: container.environment, + modelDataUrl: container.modelData ? container.modelData.bind(this, this).uri : undefined, + }; + } + + private configureNetworking(props: ModelProps): ec2.Connections | undefined { + if ((props.securityGroups || props.allowAllOutbound !== undefined) && !props.vpc) { + throw new Error('Cannot configure \'securityGroups\' or \'allowAllOutbound\' without configuring a VPC'); + } + + if (!props.vpc) { return undefined; } + + if ((props.securityGroups && props.securityGroups.length > 0) && props.allowAllOutbound !== undefined) { + throw new Error('Configure \'allowAllOutbound\' directly on the supplied SecurityGroups'); + } + + let securityGroups: ec2.ISecurityGroup[]; + if (props.securityGroups && props.securityGroups.length > 0) { + securityGroups = props.securityGroups; + } else { + const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { + vpc: props.vpc, + allowAllOutbound: props.allowAllOutbound, + }); + securityGroups = [securityGroup]; + } + + return new ec2.Connections({ securityGroups }); + } + + private renderVpcConfig(): CfnModel.VpcConfigProperty | undefined { + if (!this._connections) { return undefined; } + + return { + subnets: this.subnets!.subnetIds, + securityGroupIds: this.connections.securityGroups.map(s => s.securityGroupId), + }; + } + + private createSageMakerRole(): iam.IRole { + const sagemakerRole = new iam.Role(this, 'Role', { + assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'), + }); + // Grant SageMaker FullAccess + sagemakerRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSageMakerFullAccess')); + + return sagemakerRole; + } +} diff --git a/packages/@aws-cdk/aws-sagemaker/lib/private/util.ts b/packages/@aws-cdk/aws-sagemaker/lib/private/util.ts new file mode 100644 index 0000000000000..4057c92a2d717 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/lib/private/util.ts @@ -0,0 +1,13 @@ +import * as crypto from 'crypto'; + +/** + * Generates a hash from the provided string for the purposes of avoiding construct ID collision + * for models with multiple distinct sets of model data. + * @param s A string for which to generate a hash + * @returns A hex string representing the hash of the provided string + */ +export function hashcode(s: string): string { + const hash = crypto.createHash('md5'); + hash.update(s); + return hash.digest('hex'); +} diff --git a/packages/@aws-cdk/aws-sagemaker/package.json b/packages/@aws-cdk/aws-sagemaker/package.json index 37739b75d69b8..c5783894fb7f8 100644 --- a/packages/@aws-cdk/aws-sagemaker/package.json +++ b/packages/@aws-cdk/aws-sagemaker/package.json @@ -84,14 +84,31 @@ "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", + "@aws-cdk/integ-runner": "0.0.0", + "@aws-cdk/integ-tests": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.5.2" + "@types/jest": "^27.5.2", + "jest": "^27.5.1" }, "dependencies": { + "@aws-cdk/assets": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-ecr": "0.0.0", + "@aws-cdk/aws-ecr-assets": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^10.0.0" }, "peerDependencies": { + "@aws-cdk/assets": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-ecr": "0.0.0", + "@aws-cdk/aws-ecr-assets": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^10.0.0" }, @@ -99,7 +116,7 @@ "node": ">= 14.15.0" }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false }, diff --git a/packages/@aws-cdk/aws-sagemaker/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-sagemaker/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..739d5078a0c5b --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/rosetta/default.ts-fixture @@ -0,0 +1,11 @@ +// Fixture with packages imported, but nothing else +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +class Fixture extends cdk.Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/asset.126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.tar.gz b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/asset.126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.tar.gz new file mode 100644 index 0000000000000..af6ae9c76e414 Binary files /dev/null and b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/asset.126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.tar.gz differ diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/Dockerfile b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/Dockerfile new file mode 100644 index 0000000000000..7eb03f499c9c6 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/Dockerfile @@ -0,0 +1,19 @@ +FROM --platform=linux/amd64 python:3 + +# The following label allows this image to deployed within an inference pipeline +LABEL com.amazonaws.sagemaker.capabilities.accept-bind-to-port=true + +# Avoid buffering output to expedite log delivery +ENV PYTHONUNBUFFERED=TRUE + +# Default to port 8080 unless SageMaker has passed in its own port (for use within an inference pipline) +ENV SAGEMAKER_BIND_TO_PORT=${SAGEMAKER_BIND_TO_PORT:-8080} +EXPOSE $SAGEMAKER_BIND_TO_PORT + +# Set up the server application +ENV PROGRAM_DIRECTORY=/opt/program +RUN mkdir -p $PROGRAM_DIRECTORY +COPY index.py $PROGRAM_DIRECTORY +ENV PATH="${PROGRAM_DIRECTORY}:${PATH}" +WORKDIR $PROGRAM_DIRECTORY +ENTRYPOINT ["python3", "index.py"] diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.html b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.html new file mode 100644 index 0000000000000..3b18e512dba79 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.html @@ -0,0 +1 @@ +hello world diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.py b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.py new file mode 100644 index 0000000000000..f5ab0458f1cb1 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.py @@ -0,0 +1,48 @@ +#!/usr/bin/python + +""" +This script stands up a lightweight HTTP server listening at the port specified in the +SAGEMAKER_BIND_TO_PORT environment variable. It loads an optional artifact from the path +/opt/ml/model/artifact.txt and returns information about the artifact in response to every +invocation request. Ping requests will always succeed. +""" + +import http.server +import os +import socketserver + +class SimpleSageMakerServer(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + if self.path == '/ping': + self.respond(200, 'Healthy') + else: + self.respond(404, 'Not Found') + + def do_POST(self): + if self.path == '/invocations': + self.respond(200, 'Artifact info: {}'.format(ARTIFACT)) + else: + self.respond(404, 'Not Found') + + def respond(self, status, response): + self.send_response(status) + self.send_header('Content-type', 'text/plain') + self.end_headers() + self.wfile.write(bytes('{}\n'.format(response), 'utf-8')) + + +PORT = int(os.environ['SAGEMAKER_BIND_TO_PORT']) +ARTIFACT_PATH = '/opt/ml/model/artifact.txt' + +print('Looking for model artifacts') +if (os.path.isfile(ARTIFACT_PATH)): + print('Loading model artifact from {}'.format(ARTIFACT_PATH)) + with open(ARTIFACT_PATH, 'r') as artifact_file: + ARTIFACT = artifact_file.read().splitlines() +else: + print('No model artifact present at {}'.format(ARTIFACT_PATH)) + ARTIFACT = 'No artifacts are present' + +with socketserver.TCPServer(('', PORT), SimpleSageMakerServer) as httpd: + print('Serving requests at port {}'.format(PORT)) + httpd.serve_forever() diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/aws-cdk-sagemaker-model.assets.json b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/aws-cdk-sagemaker-model.assets.json new file mode 100644 index 0000000000000..98d2d2f08ae16 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/aws-cdk-sagemaker-model.assets.json @@ -0,0 +1,45 @@ +{ + "version": "21.0.0", + "files": { + "126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916": { + "source": { + "path": "asset.126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.tar.gz", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.gz", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "eebf956b730b436212d72ff616d7ab38ac8a7c128b71f54c4e9d1f0d29b36604": { + "source": { + "path": "aws-cdk-sagemaker-model.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "eebf956b730b436212d72ff616d7ab38ac8a7c128b71f54c4e9d1f0d29b36604.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": { + "442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a": { + "source": { + "directory": "asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + }, + "destinations": { + "current_account-current_region": { + "repositoryName": "cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}", + "imageTag": "442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-image-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/aws-cdk-sagemaker-model.template.json b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/aws-cdk-sagemaker-model.template.json new file mode 100644 index 0000000000000..b8bc0cdb2db5e --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/aws-cdk-sagemaker-model.template.json @@ -0,0 +1,809 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-sagemaker-model/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.0.0/18", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-sagemaker-model/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-sagemaker-model/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociation0B0896DC": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-sagemaker-model/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-sagemaker-model/VPC/PublicSubnet1" + } + ] + }, + "DependsOn": [ + "VPCPublicSubnet1DefaultRoute91CEF279", + "VPCPublicSubnet1RouteTableAssociation0B0896DC" + ] + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": { + "Fn::Select": [ + 1, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.64.0/18", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-sagemaker-model/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-sagemaker-model/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTableAssociation5A808732": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-sagemaker-model/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-sagemaker-model/VPC/PublicSubnet2" + } + ] + }, + "DependsOn": [ + "VPCPublicSubnet2DefaultRouteB7481BBA", + "VPCPublicSubnet2RouteTableAssociation5A808732" + ] + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.128.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-sagemaker-model/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-sagemaker-model/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociation347902D1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": { + "Fn::Select": [ + 1, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.192.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-sagemaker-model/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-sagemaker-model/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociation0C73D413": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-sagemaker-model/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "PrimaryContainerModelSecurityGroup06F42014": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-cdk-sagemaker-model/PrimaryContainerModel/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "PrimaryContainerModelRole8762F8B9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "sagemaker.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonSageMakerFullAccess" + ] + ] + } + ] + } + }, + "PrimaryContainerModelRoleDefaultPolicy56D1C738": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Sub": "cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PrimaryContainerModelRoleDefaultPolicy56D1C738", + "Roles": [ + { + "Ref": "PrimaryContainerModelRole8762F8B9" + } + ] + } + }, + "PrimaryContainerModel389512BE": { + "Type": "AWS::SageMaker::Model", + "Properties": { + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "PrimaryContainerModelRole8762F8B9", + "Arn" + ] + }, + "PrimaryContainer": { + "Image": { + "Fn::Join": [ + "", + [ + { + "Ref": "AWS::AccountId" + }, + ".dkr.ecr.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Fn::Sub": "cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}" + }, + ":442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + ] + ] + }, + "ModelDataUrl": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.gz" + ] + ] + } + }, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "PrimaryContainerModelSecurityGroup06F42014", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + ] + } + }, + "DependsOn": [ + "PrimaryContainerModelRoleDefaultPolicy56D1C738", + "PrimaryContainerModelRole8762F8B9" + ] + }, + "InferencePipelineModelRole6A99C5B3": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "sagemaker.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonSageMakerFullAccess" + ] + ] + } + ] + } + }, + "InferencePipelineModelRoleDefaultPolicy2AF0CDCF": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Sub": "cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "InferencePipelineModelRoleDefaultPolicy2AF0CDCF", + "Roles": [ + { + "Ref": "InferencePipelineModelRole6A99C5B3" + } + ] + } + }, + "InferencePipelineModel3564F141": { + "Type": "AWS::SageMaker::Model", + "Properties": { + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "InferencePipelineModelRole6A99C5B3", + "Arn" + ] + }, + "Containers": [ + { + "Image": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + } + }, + { + "Image": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + }, + "ModelDataUrl": { + "Fn::Sub": "https://s3.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.gz" + } + }, + { + "Image": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + }, + "ModelDataUrl": { + "Fn::Sub": "https://s3.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.gz" + } + } + ] + }, + "DependsOn": [ + "InferencePipelineModelRoleDefaultPolicy2AF0CDCF", + "InferencePipelineModelRole6A99C5B3" + ] + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/cdk.out b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/cdk.out new file mode 100644 index 0000000000000..8ecc185e9dbee --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"21.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/integ.json b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/integ.json new file mode 100644 index 0000000000000..16cfd2d691811 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "21.0.0", + "testCases": { + "integtest-model/DefaultTest": { + "stacks": [ + "aws-cdk-sagemaker-model" + ], + "assertionStack": "integtest-model/DefaultTest/DeployAssert", + "assertionStackName": "integtestmodelDefaultTestDeployAssertCF40BD53" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/integtestmodelDefaultTestDeployAssertCF40BD53.assets.json b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/integtestmodelDefaultTestDeployAssertCF40BD53.assets.json new file mode 100644 index 0000000000000..1f9670b51c1ae --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/integtestmodelDefaultTestDeployAssertCF40BD53.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "integtestmodelDefaultTestDeployAssertCF40BD53.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/integtestmodelDefaultTestDeployAssertCF40BD53.template.json b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/integtestmodelDefaultTestDeployAssertCF40BD53.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/integtestmodelDefaultTestDeployAssertCF40BD53.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/manifest.json b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/manifest.json new file mode 100644 index 0000000000000..879c4bf441966 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/manifest.json @@ -0,0 +1,285 @@ +{ + "version": "21.0.0", + "artifacts": { + "aws-cdk-sagemaker-model.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-sagemaker-model.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-sagemaker-model": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-sagemaker-model.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/eebf956b730b436212d72ff616d7ab38ac8a7c128b71f54c4e9d1f0d29b36604.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-sagemaker-model.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-cdk-sagemaker-model.assets" + ], + "metadata": { + "/aws-cdk-sagemaker-model/VPC/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCB9E5F0B4" + } + ], + "/aws-cdk-sagemaker-model/VPC/PublicSubnet1/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet1SubnetB4246D30" + } + ], + "/aws-cdk-sagemaker-model/VPC/PublicSubnet1/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet1RouteTableFEE4B781" + } + ], + "/aws-cdk-sagemaker-model/VPC/PublicSubnet1/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet1RouteTableAssociation0B0896DC" + } + ], + "/aws-cdk-sagemaker-model/VPC/PublicSubnet1/DefaultRoute": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet1DefaultRoute91CEF279" + } + ], + "/aws-cdk-sagemaker-model/VPC/PublicSubnet1/EIP": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet1EIP6AD938E8" + } + ], + "/aws-cdk-sagemaker-model/VPC/PublicSubnet1/NATGateway": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet1NATGatewayE0556630" + } + ], + "/aws-cdk-sagemaker-model/VPC/PublicSubnet2/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet2Subnet74179F39" + } + ], + "/aws-cdk-sagemaker-model/VPC/PublicSubnet2/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet2RouteTable6F1A15F1" + } + ], + "/aws-cdk-sagemaker-model/VPC/PublicSubnet2/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet2RouteTableAssociation5A808732" + } + ], + "/aws-cdk-sagemaker-model/VPC/PublicSubnet2/DefaultRoute": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet2DefaultRouteB7481BBA" + } + ], + "/aws-cdk-sagemaker-model/VPC/PublicSubnet2/EIP": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet2EIP4947BC00" + } + ], + "/aws-cdk-sagemaker-model/VPC/PublicSubnet2/NATGateway": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPublicSubnet2NATGateway3C070193" + } + ], + "/aws-cdk-sagemaker-model/VPC/PrivateSubnet1/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet1Subnet8BCA10E0" + } + ], + "/aws-cdk-sagemaker-model/VPC/PrivateSubnet1/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet1RouteTableBE8A6027" + } + ], + "/aws-cdk-sagemaker-model/VPC/PrivateSubnet1/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet1RouteTableAssociation347902D1" + } + ], + "/aws-cdk-sagemaker-model/VPC/PrivateSubnet1/DefaultRoute": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet1DefaultRouteAE1D6490" + } + ], + "/aws-cdk-sagemaker-model/VPC/PrivateSubnet2/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + ], + "/aws-cdk-sagemaker-model/VPC/PrivateSubnet2/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet2RouteTable0A19E10E" + } + ], + "/aws-cdk-sagemaker-model/VPC/PrivateSubnet2/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet2RouteTableAssociation0C73D413" + } + ], + "/aws-cdk-sagemaker-model/VPC/PrivateSubnet2/DefaultRoute": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCPrivateSubnet2DefaultRouteF4F5CFD2" + } + ], + "/aws-cdk-sagemaker-model/VPC/IGW": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCIGWB7E252D3" + } + ], + "/aws-cdk-sagemaker-model/VPC/VPCGW": [ + { + "type": "aws:cdk:logicalId", + "data": "VPCVPCGW99B986DC" + } + ], + "/aws-cdk-sagemaker-model/PrimaryContainerModel/SecurityGroup/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "PrimaryContainerModelSecurityGroup06F42014" + } + ], + "/aws-cdk-sagemaker-model/PrimaryContainerModel/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "PrimaryContainerModelRole8762F8B9" + } + ], + "/aws-cdk-sagemaker-model/PrimaryContainerModel/Role/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "PrimaryContainerModelRoleDefaultPolicy56D1C738" + } + ], + "/aws-cdk-sagemaker-model/PrimaryContainerModel/Model": [ + { + "type": "aws:cdk:logicalId", + "data": "PrimaryContainerModel389512BE" + } + ], + "/aws-cdk-sagemaker-model/InferencePipelineModel/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "InferencePipelineModelRole6A99C5B3" + } + ], + "/aws-cdk-sagemaker-model/InferencePipelineModel/Role/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "InferencePipelineModelRoleDefaultPolicy2AF0CDCF" + } + ], + "/aws-cdk-sagemaker-model/InferencePipelineModel/Model": [ + { + "type": "aws:cdk:logicalId", + "data": "InferencePipelineModel3564F141" + } + ], + "/aws-cdk-sagemaker-model/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-sagemaker-model/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-sagemaker-model" + }, + "integtestmodelDefaultTestDeployAssertCF40BD53.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integtestmodelDefaultTestDeployAssertCF40BD53.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integtestmodelDefaultTestDeployAssertCF40BD53": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integtestmodelDefaultTestDeployAssertCF40BD53.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integtestmodelDefaultTestDeployAssertCF40BD53.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "integtestmodelDefaultTestDeployAssertCF40BD53.assets" + ], + "metadata": { + "/integtest-model/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integtest-model/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integtest-model/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/tree.json b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/tree.json new file mode 100644 index 0000000000000..3303c44644e9a --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.model.js.snapshot/tree.json @@ -0,0 +1,1346 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-cdk-sagemaker-model": { + "id": "aws-cdk-sagemaker-model", + "path": "aws-cdk-sagemaker-model", + "children": { + "EcrImage": { + "id": "EcrImage", + "path": "aws-cdk-sagemaker-model/EcrImage", + "children": { + "Staging": { + "id": "Staging", + "path": "aws-cdk-sagemaker-model/EcrImage/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Repository": { + "id": "Repository", + "path": "aws-cdk-sagemaker-model/EcrImage/Repository", + "constructInfo": { + "fqn": "@aws-cdk/aws-ecr.RepositoryBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ecr-assets.DockerImageAsset", + "version": "0.0.0" + } + }, + "S3ModelData": { + "id": "S3ModelData", + "path": "aws-cdk-sagemaker-model/S3ModelData", + "children": { + "Stage": { + "id": "Stage", + "path": "aws-cdk-sagemaker-model/S3ModelData/Stage", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "aws-cdk-sagemaker-model/S3ModelData/AssetBucket", + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3-assets.Asset", + "version": "0.0.0" + } + }, + "VPC": { + "id": "VPC", + "path": "aws-cdk-sagemaker-model/VPC", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-model/VPC/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::VPC", + "aws:cdk:cloudformation:props": { + "cidrBlock": "10.0.0.0/16", + "enableDnsHostnames": true, + "enableDnsSupport": true, + "instanceTenancy": "default", + "tags": [ + { + "key": "Name", + "value": "aws-cdk-sagemaker-model/VPC" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnVPC", + "version": "0.0.0" + } + }, + "PublicSubnet1": { + "id": "PublicSubnet1", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet1", + "children": { + "Subnet": { + "id": "Subnet", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet1/Subnet", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "availabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "" + } + ] + }, + "cidrBlock": "10.0.0.0/18", + "mapPublicIpOnLaunch": true, + "tags": [ + { + "key": "aws-cdk:subnet-name", + "value": "Public" + }, + { + "key": "aws-cdk:subnet-type", + "value": "Public" + }, + { + "key": "Name", + "value": "aws-cdk-sagemaker-model/VPC/PublicSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnet", + "version": "0.0.0" + } + }, + "Acl": { + "id": "Acl", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet1/Acl", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "RouteTable": { + "id": "RouteTable", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet1/RouteTable", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "tags": [ + { + "key": "Name", + "value": "aws-cdk-sagemaker-model/VPC/PublicSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRouteTable", + "version": "0.0.0" + } + }, + "RouteTableAssociation": { + "id": "RouteTableAssociation", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet1/RouteTableAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::SubnetRouteTableAssociation", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "subnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnetRouteTableAssociation", + "version": "0.0.0" + } + }, + "DefaultRoute": { + "id": "DefaultRoute", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet1/DefaultRoute", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Route", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "destinationCidrBlock": "0.0.0.0/0", + "gatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRoute", + "version": "0.0.0" + } + }, + "EIP": { + "id": "EIP", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet1/EIP", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::EIP", + "aws:cdk:cloudformation:props": { + "domain": "vpc", + "tags": [ + { + "key": "Name", + "value": "aws-cdk-sagemaker-model/VPC/PublicSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnEIP", + "version": "0.0.0" + } + }, + "NATGateway": { + "id": "NATGateway", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet1/NATGateway", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::NatGateway", + "aws:cdk:cloudformation:props": { + "subnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "allocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "tags": [ + { + "key": "Name", + "value": "aws-cdk-sagemaker-model/VPC/PublicSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnNatGateway", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.PublicSubnet", + "version": "0.0.0" + } + }, + "PublicSubnet2": { + "id": "PublicSubnet2", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet2", + "children": { + "Subnet": { + "id": "Subnet", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet2/Subnet", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "availabilityZone": { + "Fn::Select": [ + 1, + { + "Fn::GetAZs": "" + } + ] + }, + "cidrBlock": "10.0.64.0/18", + "mapPublicIpOnLaunch": true, + "tags": [ + { + "key": "aws-cdk:subnet-name", + "value": "Public" + }, + { + "key": "aws-cdk:subnet-type", + "value": "Public" + }, + { + "key": "Name", + "value": "aws-cdk-sagemaker-model/VPC/PublicSubnet2" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnet", + "version": "0.0.0" + } + }, + "Acl": { + "id": "Acl", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet2/Acl", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "RouteTable": { + "id": "RouteTable", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet2/RouteTable", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "tags": [ + { + "key": "Name", + "value": "aws-cdk-sagemaker-model/VPC/PublicSubnet2" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRouteTable", + "version": "0.0.0" + } + }, + "RouteTableAssociation": { + "id": "RouteTableAssociation", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet2/RouteTableAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::SubnetRouteTableAssociation", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "subnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnetRouteTableAssociation", + "version": "0.0.0" + } + }, + "DefaultRoute": { + "id": "DefaultRoute", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet2/DefaultRoute", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Route", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "destinationCidrBlock": "0.0.0.0/0", + "gatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRoute", + "version": "0.0.0" + } + }, + "EIP": { + "id": "EIP", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet2/EIP", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::EIP", + "aws:cdk:cloudformation:props": { + "domain": "vpc", + "tags": [ + { + "key": "Name", + "value": "aws-cdk-sagemaker-model/VPC/PublicSubnet2" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnEIP", + "version": "0.0.0" + } + }, + "NATGateway": { + "id": "NATGateway", + "path": "aws-cdk-sagemaker-model/VPC/PublicSubnet2/NATGateway", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::NatGateway", + "aws:cdk:cloudformation:props": { + "subnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "allocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "tags": [ + { + "key": "Name", + "value": "aws-cdk-sagemaker-model/VPC/PublicSubnet2" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnNatGateway", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.PublicSubnet", + "version": "0.0.0" + } + }, + "PrivateSubnet1": { + "id": "PrivateSubnet1", + "path": "aws-cdk-sagemaker-model/VPC/PrivateSubnet1", + "children": { + "Subnet": { + "id": "Subnet", + "path": "aws-cdk-sagemaker-model/VPC/PrivateSubnet1/Subnet", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "availabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "" + } + ] + }, + "cidrBlock": "10.0.128.0/18", + "mapPublicIpOnLaunch": false, + "tags": [ + { + "key": "aws-cdk:subnet-name", + "value": "Private" + }, + { + "key": "aws-cdk:subnet-type", + "value": "Private" + }, + { + "key": "Name", + "value": "aws-cdk-sagemaker-model/VPC/PrivateSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnet", + "version": "0.0.0" + } + }, + "Acl": { + "id": "Acl", + "path": "aws-cdk-sagemaker-model/VPC/PrivateSubnet1/Acl", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "RouteTable": { + "id": "RouteTable", + "path": "aws-cdk-sagemaker-model/VPC/PrivateSubnet1/RouteTable", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "tags": [ + { + "key": "Name", + "value": "aws-cdk-sagemaker-model/VPC/PrivateSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRouteTable", + "version": "0.0.0" + } + }, + "RouteTableAssociation": { + "id": "RouteTableAssociation", + "path": "aws-cdk-sagemaker-model/VPC/PrivateSubnet1/RouteTableAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::SubnetRouteTableAssociation", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "subnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnetRouteTableAssociation", + "version": "0.0.0" + } + }, + "DefaultRoute": { + "id": "DefaultRoute", + "path": "aws-cdk-sagemaker-model/VPC/PrivateSubnet1/DefaultRoute", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Route", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "destinationCidrBlock": "0.0.0.0/0", + "natGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRoute", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.PrivateSubnet", + "version": "0.0.0" + } + }, + "PrivateSubnet2": { + "id": "PrivateSubnet2", + "path": "aws-cdk-sagemaker-model/VPC/PrivateSubnet2", + "children": { + "Subnet": { + "id": "Subnet", + "path": "aws-cdk-sagemaker-model/VPC/PrivateSubnet2/Subnet", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "availabilityZone": { + "Fn::Select": [ + 1, + { + "Fn::GetAZs": "" + } + ] + }, + "cidrBlock": "10.0.192.0/18", + "mapPublicIpOnLaunch": false, + "tags": [ + { + "key": "aws-cdk:subnet-name", + "value": "Private" + }, + { + "key": "aws-cdk:subnet-type", + "value": "Private" + }, + { + "key": "Name", + "value": "aws-cdk-sagemaker-model/VPC/PrivateSubnet2" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnet", + "version": "0.0.0" + } + }, + "Acl": { + "id": "Acl", + "path": "aws-cdk-sagemaker-model/VPC/PrivateSubnet2/Acl", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "RouteTable": { + "id": "RouteTable", + "path": "aws-cdk-sagemaker-model/VPC/PrivateSubnet2/RouteTable", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "tags": [ + { + "key": "Name", + "value": "aws-cdk-sagemaker-model/VPC/PrivateSubnet2" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRouteTable", + "version": "0.0.0" + } + }, + "RouteTableAssociation": { + "id": "RouteTableAssociation", + "path": "aws-cdk-sagemaker-model/VPC/PrivateSubnet2/RouteTableAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::SubnetRouteTableAssociation", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "subnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSubnetRouteTableAssociation", + "version": "0.0.0" + } + }, + "DefaultRoute": { + "id": "DefaultRoute", + "path": "aws-cdk-sagemaker-model/VPC/PrivateSubnet2/DefaultRoute", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Route", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "destinationCidrBlock": "0.0.0.0/0", + "natGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnRoute", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.PrivateSubnet", + "version": "0.0.0" + } + }, + "IGW": { + "id": "IGW", + "path": "aws-cdk-sagemaker-model/VPC/IGW", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::InternetGateway", + "aws:cdk:cloudformation:props": { + "tags": [ + { + "key": "Name", + "value": "aws-cdk-sagemaker-model/VPC" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnInternetGateway", + "version": "0.0.0" + } + }, + "VPCGW": { + "id": "VPCGW", + "path": "aws-cdk-sagemaker-model/VPC/VPCGW", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::VPCGatewayAttachment", + "aws:cdk:cloudformation:props": { + "vpcId": { + "Ref": "VPCB9E5F0B4" + }, + "internetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnVPCGatewayAttachment", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.Vpc", + "version": "0.0.0" + } + }, + "PrimaryContainerModel": { + "id": "PrimaryContainerModel", + "path": "aws-cdk-sagemaker-model/PrimaryContainerModel", + "children": { + "SecurityGroup": { + "id": "SecurityGroup", + "path": "aws-cdk-sagemaker-model/PrimaryContainerModel/SecurityGroup", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-model/PrimaryContainerModel/SecurityGroup/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::SecurityGroup", + "aws:cdk:cloudformation:props": { + "groupDescription": "aws-cdk-sagemaker-model/PrimaryContainerModel/SecurityGroup", + "securityGroupEgress": [ + { + "cidrIp": "0.0.0.0/0", + "description": "Allow all outbound traffic by default", + "ipProtocol": "-1" + } + ], + "vpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.CfnSecurityGroup", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ec2.SecurityGroup", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "aws-cdk-sagemaker-model/PrimaryContainerModel/Role", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-model/PrimaryContainerModel/Role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "sagemaker.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonSageMakerFullAccess" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-cdk-sagemaker-model/PrimaryContainerModel/Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-model/PrimaryContainerModel/Role/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Sub": "cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "policyName": "PrimaryContainerModelRoleDefaultPolicy56D1C738", + "roles": [ + { + "Ref": "PrimaryContainerModelRole8762F8B9" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Model": { + "id": "Model", + "path": "aws-cdk-sagemaker-model/PrimaryContainerModel/Model", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SageMaker::Model", + "aws:cdk:cloudformation:props": { + "executionRoleArn": { + "Fn::GetAtt": [ + "PrimaryContainerModelRole8762F8B9", + "Arn" + ] + }, + "primaryContainer": { + "image": { + "Fn::Join": [ + "", + [ + { + "Ref": "AWS::AccountId" + }, + ".dkr.ecr.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Fn::Sub": "cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}" + }, + ":442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + ] + ] + }, + "modelDataUrl": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.gz" + ] + ] + } + }, + "vpcConfig": { + "subnets": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + ], + "securityGroupIds": [ + { + "Fn::GetAtt": [ + "PrimaryContainerModelSecurityGroup06F42014", + "GroupId" + ] + } + ] + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker.CfnModel", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker.Model", + "version": "0.0.0" + } + }, + "InferencePipelineModel": { + "id": "InferencePipelineModel", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel", + "children": { + "Role": { + "id": "Role", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/Role", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/Role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "sagemaker.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonSageMakerFullAccess" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/Role/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Sub": "cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "policyName": "InferencePipelineModelRoleDefaultPolicy2AF0CDCF", + "roles": [ + { + "Ref": "InferencePipelineModelRole6A99C5B3" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "ModelImage7ffa8020b99fe9d130a903251c36866d": { + "id": "ModelImage7ffa8020b99fe9d130a903251c36866d", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelImage7ffa8020b99fe9d130a903251c36866d", + "children": { + "Staging": { + "id": "Staging", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelImage7ffa8020b99fe9d130a903251c36866d/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Repository": { + "id": "Repository", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelImage7ffa8020b99fe9d130a903251c36866d/Repository", + "constructInfo": { + "fqn": "@aws-cdk/aws-ecr.RepositoryBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-ecr-assets.DockerImageAsset", + "version": "0.0.0" + } + }, + "ModelData412d61f9c984d1aff5ee358daf994d58": { + "id": "ModelData412d61f9c984d1aff5ee358daf994d58", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelData412d61f9c984d1aff5ee358daf994d58", + "children": { + "Stage": { + "id": "Stage", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelData412d61f9c984d1aff5ee358daf994d58/Stage", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelData412d61f9c984d1aff5ee358daf994d58/AssetBucket", + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3-assets.Asset", + "version": "0.0.0" + } + }, + "Model": { + "id": "Model", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/Model", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SageMaker::Model", + "aws:cdk:cloudformation:props": { + "executionRoleArn": { + "Fn::GetAtt": [ + "InferencePipelineModelRole6A99C5B3", + "Arn" + ] + }, + "containers": [ + { + "image": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + } + }, + { + "image": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + }, + "modelDataUrl": { + "Fn::Sub": "https://s3.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.gz" + } + }, + { + "image": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + }, + "modelDataUrl": { + "Fn::Sub": "https://s3.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.gz" + } + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker.CfnModel", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker.Model", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-cdk-sagemaker-model/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-cdk-sagemaker-model/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "integtest-model": { + "id": "integtest-model", + "path": "integtest-model", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "integtest-model/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "integtest-model/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.140" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "integtest-model/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integtest-model/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integtest-model/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.140" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.model.ts b/packages/@aws-cdk/aws-sagemaker/test/integ.model.ts new file mode 100644 index 0000000000000..60b7eab698472 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.model.ts @@ -0,0 +1,123 @@ +import * as path from 'path'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecr_assets from '@aws-cdk/aws-ecr-assets'; +import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import * as cdk from '@aws-cdk/core'; +import { IntegTest } from '@aws-cdk/integ-tests'; +import * as sagemaker from '../lib'; + +/* + * Stack verification steps: + * aws sagemaker describe-model --model-name + * + * For the single container model, the output will resemble: + * + * { + * "ModelName": "PrimaryContainerModel...", + * "PrimaryContainer": { + * "Image": "...", + * "ModelDataUrl": "https://s3..." + * }, + * "ExecutionRoleArn": "arn:aws:iam::...", + * "VpcConfig": { + * "SecurityGroupIds": [ + * "sg-..." + * ], + * "Subnets": [ + * "subnet-...", + * "subnet-..." + * ] + * }, + * "CreationTime": ..., + * "ModelArn": "arn:aws:sagemaker:...", + * "EnableNetworkIsolation": false + * } + * + * For the inference pipeline model, the output will resemble: + * + * { + * "ModelName": "InferencePipelineModel...", + * "Containers": [ + * { + * "Image": "..." + * }, + * { + * "Image": "...", + * "ModelDataUrl": "https://s3..." + * }, + * { + * "Image": "...", + * "ModelDataUrl": "https://s3..." + * } + * ], + * "ExecutionRoleArn": "arn:aws:iam::...", + * "CreationTime": ..., + * "ModelArn": "arn:aws:sagemaker:...", + * "EnableNetworkIsolation": false + * } + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-sagemaker-model'); + +// Paths to local assets +const dockerfileDirectory = path.join(__dirname, 'test-image'); +const artifactFilePath = path.join(__dirname, 'test-artifacts', 'valid-artifact.tar.gz'); + +/* + * Exercise different variations of ContainerImages, including: + * - EcrImage as vended by ContainerImage.fromEcrRepository + * - AssetImage as vended by ContainerImage.fromAsset + */ + +const ecrImageAsset = new ecr_assets.DockerImageAsset(stack, 'EcrImage', { + directory: dockerfileDirectory, +}); +const ecrImage = sagemaker.ContainerImage.fromEcrRepository( + ecrImageAsset.repository, + ecrImageAsset.imageTag, +); + +const localImage = sagemaker.ContainerImage.fromAsset(dockerfileDirectory); + +/* + * Exercise different variations of ModelData, including: + * - S3ModelData as vended by ModelData.fromBucket + * - AssetModelData as vended by ModelData.fromAsset + */ + +const artifactAsset = new s3_assets.Asset(stack, 'S3ModelData', { + path: artifactFilePath, +}); +const s3ModelData = sagemaker.ModelData.fromBucket( + artifactAsset.bucket, + artifactAsset.s3ObjectKey, +); + +const localModelData = sagemaker.ModelData.fromAsset(artifactFilePath); + +/* + * Use the above images and model data instances to create SageMaker models, including: + * - A single-container model, with a VPC, using the "remote" image and model data references + * - An inference pipeline model, without a VPC, using the "local" image and model data references + */ + +new sagemaker.Model(stack, 'PrimaryContainerModel', { + containers: [{ + image: ecrImage, + modelData: s3ModelData, + }], + vpc: new ec2.Vpc(stack, 'VPC'), +}); + +new sagemaker.Model(stack, 'InferencePipelineModel', { + containers: [ + { image: localImage }, + { image: localImage, modelData: localModelData }, + { image: localImage, modelData: localModelData }, + ], +}); + +new IntegTest(app, 'integtest-model', { + testCases: [stack], +}); diff --git a/packages/@aws-cdk/aws-sagemaker/test/model-data.test.ts b/packages/@aws-cdk/aws-sagemaker/test/model-data.test.ts new file mode 100644 index 0000000000000..28eb40b468f61 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/model-data.test.ts @@ -0,0 +1,28 @@ +import * as path from 'path'; +import * as sagemaker from '../lib'; + +describe('When creating model data from a local asset', () => { + test('by supplying a directory, an exception is thrown', () => { + // WHEN + const when = () => sagemaker.ModelData.fromAsset(path.join(__dirname, 'test-artifacts')); + + // THEN + expect(when).toThrow(/Asset must be a gzipped tar file with extension .tar.gz/); + }); + + test('by supplying a zip file, an exception is thrown', () => { + // WHEN + const when = () => sagemaker.ModelData.fromAsset(path.join(__dirname, 'test-artifacts', 'invalid-artifact.zip')); + + // THEN + expect(when).toThrow(/Asset must be a gzipped tar file with extension .tar.gz/); + }); + + test('by supplying a file with an unsupported extension, an exception is thrown', () => { + // WHEN + const when = () => sagemaker.ModelData.fromAsset(path.join(__dirname, 'test-artifacts', 'invalid-artifact.tar')); + + // THEN + expect(when).toThrow(/Asset must be a gzipped tar file with extension .tar.gz/); + }); +}); diff --git a/packages/@aws-cdk/aws-sagemaker/test/model.test.ts b/packages/@aws-cdk/aws-sagemaker/test/model.test.ts new file mode 100644 index 0000000000000..043d6e858d489 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/model.test.ts @@ -0,0 +1,297 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { Match, Template } from '@aws-cdk/assertions'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecr from '@aws-cdk/aws-ecr'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import * as constructs from 'constructs'; +import * as sagemaker from '../lib'; + +describe('When instantiating SageMaker Model', () => { + test('with more than 15 containers, an exception is thrown on synthesis', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app); + const testRepo = ecr.Repository.fromRepositoryName(stack, 'testRepo', '123456789012.dkr.ecr.us-west-2.amazonaws.com/mymodel'); + const containers = [{ image: sagemaker.ContainerImage.fromEcrRepository(testRepo) }]; + for (let i = 0; i < 15; i++) { + const containerDefinition = { + image: sagemaker.ContainerImage.fromEcrRepository(testRepo), + }; + containers.push(containerDefinition); + } + new sagemaker.Model(stack, 'Model', { containers }); + + // WHEN + const when = () => app.synth(); + + // THEN + expect(when).toThrow(/Cannot have more than 15 containers in inference pipeline/); + }); + + test('with no containers, an exception is thrown on synthesis', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app); + new sagemaker.Model(stack, 'Model'); + + // WHEN + const when = () => app.synth(); + + // THEN + expect(when).toThrow(/Must configure at least 1 container for model/); + }); + + test('with a ContainerImage implementation which adds constructs of its own, the new constructs are present', () => { + // GIVEN + const stack = new cdk.Stack(); + class ConstructCreatingContainerImage extends sagemaker.ContainerImage { + public bind(scope: constructs.Construct, _model: sagemaker.Model): sagemaker.ContainerImageConfig { + new iam.User(scope, 'User', { + userName: 'ExtraConstructUserName', + }); + return { + imageName: 'anything', + }; + } + } + + // WHEN + new sagemaker.Model(stack, 'Model', { + containers: [{ + image: new ConstructCreatingContainerImage(), + }], + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::SageMaker::Model', {}); + template.hasResourceProperties('AWS::IAM::User', { + UserName: 'ExtraConstructUserName', + }); + }); + + test('with 2 identical ContainerDefinitions, each file-based asset is instantiated once', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app); + const image = sagemaker.ContainerImage.fromAsset(path.join(__dirname, 'test-image')); + const modelData = sagemaker.ModelData.fromAsset(path.join(__dirname, 'test-artifacts', 'valid-artifact.tar.gz')); + const container: sagemaker.ContainerDefinition = { image, modelData }; + new sagemaker.Model(stack, 'Model', { + containers: [ + container, + container, + ], + }); + + // WHEN + const assembly = app.synth(); + + // THEN + const manifest = JSON.parse(fs.readFileSync(path.join(assembly.directory, `${stack.stackName}.assets.json`), 'utf-8')); + // The assembly asset manifest should include: + // - Two file assets for the model data asset and the stack template + // - One Docker image asset + expect(Object.entries(manifest.files)).toHaveLength(2); + expect(Object.entries(manifest.dockerImages)).toHaveLength(1); + }); + + describe('with a VPC', () => { + test('and security groups, no security group is created', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'testVPC'); + + // WHEN + new sagemaker.Model(stack, 'Model', { + containers: [{ image: sagemaker.ContainerImage.fromEcrRepository(new ecr.Repository(stack, 'Repo')) }], + vpc, + securityGroups: [new ec2.SecurityGroup(stack, 'SG', { vpc })], + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', Match.not({ + GroupDescription: 'Default/Model/SecurityGroup', + })); + }); + + test('but no security groups, a security group is created', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new sagemaker.Model(stack, 'Model', { + containers: [{ image: sagemaker.ContainerImage.fromEcrRepository(new ecr.Repository(stack, 'Repo')) }], + vpc: new ec2.Vpc(stack, 'testVPC'), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroup', { + GroupDescription: 'Default/Model/SecurityGroup', + }); + }); + + test('and both security groups and allowAllOutbound are specified, an exception is thrown', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'testVPC'); + + // WHEN + const when = () => + new sagemaker.Model(stack, 'Model', { + containers: [{ image: sagemaker.ContainerImage.fromEcrRepository(new ecr.Repository(stack, 'Repo')) }], + vpc, + securityGroups: [new ec2.SecurityGroup(stack, 'SG', { vpc })], + allowAllOutbound: false, + }); + + // THEN + expect(when).toThrow(/Configure 'allowAllOutbound' directly on the supplied SecurityGroups/); + }); + }); + + describe('without a VPC', () => { + test('but security groups are specified, an exception is thrown', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpcNotSpecified = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const when = () => + new sagemaker.Model(stack, 'Model', { + containers: [{ image: sagemaker.ContainerImage.fromEcrRepository(new ecr.Repository(stack, 'Repo')) }], + securityGroups: [new ec2.SecurityGroup(stack, 'SG', { vpc: vpcNotSpecified })], + }); + + // THEN + expect(when).toThrow(/Cannot configure 'securityGroups' or 'allowAllOutbound' without configuring a VPC/); + }); + + test('but allowAllOutbound is specified, an exception is thrown', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const when = () => + new sagemaker.Model(stack, 'Model', { + containers: [{ image: sagemaker.ContainerImage.fromEcrRepository(new ecr.Repository(stack, 'Repo')) }], + allowAllOutbound: false, + }); + + // THEN + expect(when).toThrow(/Cannot configure 'securityGroups' or 'allowAllOutbound' without configuring a VPC/); + }); + }); +}); + +describe('When accessing Connections object', () => { + test('from a model with no VPC, an exception is thrown', () => { + // GIVEN + const stack = new cdk.Stack(); + const modelWithoutVpc = new sagemaker.Model(stack, 'Model', { + containers: [{ image: sagemaker.ContainerImage.fromEcrRepository(new ecr.Repository(stack, 'Repo')) }], + }); + + // WHEN + const when = () => modelWithoutVpc.connections; + + // THEN + expect(when).toThrow(/Cannot manage network access without configuring a VPC/); + }); + + test('from an imported model with no security groups specified, an exception is thrown', () => { + // GIVEN + const stack = new cdk.Stack(); + const importedModel = sagemaker.Model.fromModelAttributes(stack, 'Model', { + modelArn: 'arn:aws:sagemaker:us-west-2:123456789012:model/MyModel', + }); + + // WHEN + const when = () => importedModel.connections; + + // THEN + expect(when).toThrow(/Cannot manage network access without configuring a VPC/); + }); +}); + +test('When adding security group after model instantiation, it is reflected in VpcConfig of Model', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'testVPC'); + const model = new sagemaker.Model(stack, 'Model', { + containers: [{ image: sagemaker.ContainerImage.fromEcrRepository(new ecr.Repository(stack, 'Repo')) }], + vpc, + }); + + // WHEN + model.connections.addSecurityGroup(new ec2.SecurityGroup(stack, 'AdditionalGroup', { vpc })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::SageMaker::Model', { + VpcConfig: { + SecurityGroupIds: [ + { + 'Fn::GetAtt': [ + 'ModelSecurityGroup2A7C9E10', + 'GroupId', + ], + }, + { + 'Fn::GetAtt': [ + 'AdditionalGroup4973CFAA', + 'GroupId', + ], + }, + ], + }, + }); +}); + +test('When allowing traffic from an imported model with a security group, an S3 egress rule should be present', () => { + // GIVEN + const stack = new cdk.Stack(); + const model = sagemaker.Model.fromModelAttributes(stack, 'Model', { + modelArn: 'arn:aws:sagemaker:us-west-2:123456789012:model/MyModel', + securityGroups: [ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { + allowAllOutbound: false, + })], + }); + + // WHEN + model.connections.allowToAnyIpv4(ec2.Port.tcp(443)); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SecurityGroupEgress', { + GroupId: 'sg-123456789', + }); +}); + +test('When importing a model by ARN, the model name is determined correctly', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const model = sagemaker.Model.fromModelArn(stack, 'Model', 'arn:aws:sagemaker:us-west-2:123456789012:model/MyModel'); + + // THEN + expect(model.modelName).toEqual('MyModel'); +}); + +test('When importing a model by name, the ARN is constructed correctly', () => { + // GIVEN + const stack = new cdk.Stack(undefined, undefined, { + env: + { + region: 'us-west-2', + account: '123456789012', + }, + }); + + // WHEN + const model = sagemaker.Model.fromModelName(stack, 'Model', 'my-name'); + + // THEN + expect(model.modelArn).toMatch(/arn:.+:sagemaker:us-west-2:123456789012:model\/my-name/); +}); diff --git a/packages/@aws-cdk/aws-sagemaker/test/sagemaker.test.ts b/packages/@aws-cdk/aws-sagemaker/test/sagemaker.test.ts deleted file mode 100644 index 465c7bdea0693..0000000000000 --- a/packages/@aws-cdk/aws-sagemaker/test/sagemaker.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assertions'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/aws-sagemaker/test/test-artifacts/invalid-artifact.tar b/packages/@aws-cdk/aws-sagemaker/test/test-artifacts/invalid-artifact.tar new file mode 100644 index 0000000000000..b3dd0bd4c1846 Binary files /dev/null and b/packages/@aws-cdk/aws-sagemaker/test/test-artifacts/invalid-artifact.tar differ diff --git a/packages/@aws-cdk/aws-sagemaker/test/test-artifacts/invalid-artifact.zip b/packages/@aws-cdk/aws-sagemaker/test/test-artifacts/invalid-artifact.zip new file mode 100644 index 0000000000000..8ab041df79875 Binary files /dev/null and b/packages/@aws-cdk/aws-sagemaker/test/test-artifacts/invalid-artifact.zip differ diff --git a/packages/@aws-cdk/aws-sagemaker/test/test-artifacts/valid-artifact.tar.gz b/packages/@aws-cdk/aws-sagemaker/test/test-artifacts/valid-artifact.tar.gz new file mode 100644 index 0000000000000..af6ae9c76e414 Binary files /dev/null and b/packages/@aws-cdk/aws-sagemaker/test/test-artifacts/valid-artifact.tar.gz differ diff --git a/packages/@aws-cdk/aws-sagemaker/test/test-image/Dockerfile b/packages/@aws-cdk/aws-sagemaker/test/test-image/Dockerfile new file mode 100644 index 0000000000000..7eb03f499c9c6 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/test-image/Dockerfile @@ -0,0 +1,19 @@ +FROM --platform=linux/amd64 python:3 + +# The following label allows this image to deployed within an inference pipeline +LABEL com.amazonaws.sagemaker.capabilities.accept-bind-to-port=true + +# Avoid buffering output to expedite log delivery +ENV PYTHONUNBUFFERED=TRUE + +# Default to port 8080 unless SageMaker has passed in its own port (for use within an inference pipline) +ENV SAGEMAKER_BIND_TO_PORT=${SAGEMAKER_BIND_TO_PORT:-8080} +EXPOSE $SAGEMAKER_BIND_TO_PORT + +# Set up the server application +ENV PROGRAM_DIRECTORY=/opt/program +RUN mkdir -p $PROGRAM_DIRECTORY +COPY index.py $PROGRAM_DIRECTORY +ENV PATH="${PROGRAM_DIRECTORY}:${PATH}" +WORKDIR $PROGRAM_DIRECTORY +ENTRYPOINT ["python3", "index.py"] diff --git a/packages/@aws-cdk/aws-sagemaker/test/test-image/index.html b/packages/@aws-cdk/aws-sagemaker/test/test-image/index.html new file mode 100644 index 0000000000000..3b18e512dba79 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/test-image/index.html @@ -0,0 +1 @@ +hello world diff --git a/packages/@aws-cdk/aws-sagemaker/test/test-image/index.py b/packages/@aws-cdk/aws-sagemaker/test/test-image/index.py new file mode 100644 index 0000000000000..f5ab0458f1cb1 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/test-image/index.py @@ -0,0 +1,48 @@ +#!/usr/bin/python + +""" +This script stands up a lightweight HTTP server listening at the port specified in the +SAGEMAKER_BIND_TO_PORT environment variable. It loads an optional artifact from the path +/opt/ml/model/artifact.txt and returns information about the artifact in response to every +invocation request. Ping requests will always succeed. +""" + +import http.server +import os +import socketserver + +class SimpleSageMakerServer(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + if self.path == '/ping': + self.respond(200, 'Healthy') + else: + self.respond(404, 'Not Found') + + def do_POST(self): + if self.path == '/invocations': + self.respond(200, 'Artifact info: {}'.format(ARTIFACT)) + else: + self.respond(404, 'Not Found') + + def respond(self, status, response): + self.send_response(status) + self.send_header('Content-type', 'text/plain') + self.end_headers() + self.wfile.write(bytes('{}\n'.format(response), 'utf-8')) + + +PORT = int(os.environ['SAGEMAKER_BIND_TO_PORT']) +ARTIFACT_PATH = '/opt/ml/model/artifact.txt' + +print('Looking for model artifacts') +if (os.path.isfile(ARTIFACT_PATH)): + print('Loading model artifact from {}'.format(ARTIFACT_PATH)) + with open(ARTIFACT_PATH, 'r') as artifact_file: + ARTIFACT = artifact_file.read().splitlines() +else: + print('No model artifact present at {}'.format(ARTIFACT_PATH)) + ARTIFACT = 'No artifacts are present' + +with socketserver.TCPServer(('', PORT), SimpleSageMakerServer) as httpd: + print('Serving requests at port {}'.format(PORT)) + httpd.serve_forever()