diff --git a/packages/@aws-cdk/aws-sagemaker/README.md b/packages/@aws-cdk/aws-sagemaker/README.md index 1fb468e4e760b..8da7717d96783 100644 --- a/packages/@aws-cdk/aws-sagemaker/README.md +++ b/packages/@aws-cdk/aws-sagemaker/README.md @@ -195,3 +195,70 @@ const endpointConfig = new sagemaker.EndpointConfig(this, 'EndpointConfig', { ] }); ``` + +### Endpoint + +When you create an endpoint from an `EndpointConfig`, Amazon SageMaker launches the ML compute +instances and deploys the model or models as specified in the configuration. To get inferences from +the model, client applications send requests to the Amazon SageMaker Runtime HTTPS endpoint. For +more information about the API, see the +[InvokeEndpoint](https://docs.aws.amazon.com/sagemaker/latest/dg/API_runtime_InvokeEndpoint.html) +API. Defining an endpoint requires at minimum the associated endpoint configuration: + +```typescript +import * as sagemaker from '@aws-cdk/aws-sagemaker'; + +declare const endpointConfig: sagemaker.EndpointConfig; + +const endpoint = new sagemaker.Endpoint(this, 'Endpoint', { endpointConfig }); +``` + +### AutoScaling + +To enable autoscaling on the production variant, use the `autoScaleInstanceCount` method: + +```typescript +import * as sagemaker from '@aws-cdk/aws-sagemaker'; + +declare const model: sagemaker.Model; + +const variantName = 'my-variant'; +const endpointConfig = new sagemaker.EndpointConfig(this, 'EndpointConfig', { + instanceProductionVariants: [ + { + model: model, + variantName: variantName, + }, + ] +}); + +const endpoint = new sagemaker.Endpoint(this, 'Endpoint', { endpointConfig }); +const productionVariant = endpoint.findInstanceProductionVariant(variantName); +const instanceCount = productionVariant.autoScaleInstanceCount({ + maxCapacity: 3 +}); +instanceCount.scaleOnInvocations('LimitRPS', { + maxRequestsPerSecond: 30, +}); +``` + +For load testing guidance on determining the maximum requests per second per instance, please see +this [documentation](https://docs.aws.amazon.com/sagemaker/latest/dg/endpoint-scaling-loadtest.html). + +### Metrics + +To monitor CloudWatch metrics for a production variant, use one or more of the metric convenience +methods: + +```typescript +import * as sagemaker from '@aws-cdk/aws-sagemaker'; + +declare const endpointConfig: sagemaker.EndpointConfig; + +const endpoint = new sagemaker.Endpoint(this, 'Endpoint', { endpointConfig }); +const productionVariant = endpoint.findInstanceProductionVariant('my-variant'); +productionVariant.metricModelLatency().createAlarm(this, 'ModelLatencyAlarm', { + threshold: 100000, + evaluationPeriods: 3, +}); +``` diff --git a/packages/@aws-cdk/aws-sagemaker/lib/endpoint.ts b/packages/@aws-cdk/aws-sagemaker/lib/endpoint.ts new file mode 100644 index 0000000000000..ab6debccd1695 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/lib/endpoint.ts @@ -0,0 +1,476 @@ +import { EOL } from 'os'; +import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +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 { EndpointConfig, IEndpointConfig, InstanceProductionVariant } from './endpoint-config'; +import { InstanceType } from './instance-type'; +import { sameEnv } from './private/util'; +import { CfnEndpoint } from './sagemaker.generated'; +import { ScalableInstanceCount } from './scalable-instance-count'; + +/* + * Amazon SageMaker automatic scaling doesn't support automatic scaling for burstable instances such + * as T2, because they already allow for increased capacity under increased workloads. + * https://docs.aws.amazon.com/sagemaker/latest/dg/endpoint-auto-scaling-add-console.html + */ +const BURSTABLE_INSTANCE_TYPE_PREFIXES = Object.entries(ec2.InstanceClass) + .filter(([name, _]) => name.startsWith('T')) + .map(([_, prefix]) => `ml.${prefix}.`); + +/** + * The interface for a SageMaker Endpoint resource. + */ +export interface IEndpoint extends cdk.IResource { + /** + * The ARN of the endpoint. + * + * @attribute + */ + readonly endpointArn: string; + + /** + * The name of the endpoint. + * + * @attribute + */ + readonly endpointName: string; + + /** + * Permits an IAM principal to invoke this endpoint + * @param grantee The principal to grant access to + */ + grantInvoke(grantee: iam.IGrantable): iam.Grant; +} + +/** + * Represents the features common to all production variant types (e.g., instance, serverless) that + * have been associated with an endpoint. + */ +interface IEndpointProductionVariant { + /** + * The name of the production variant. + */ + readonly variantName: string; + /** + * Return the given named metric for Endpoint + * + * @default - sum over 5 minutes + */ + metric(namespace: string, metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; +} + +/** + * Represents an instance production variant that has been associated with an endpoint. + */ +export interface IEndpointInstanceProductionVariant extends IEndpointProductionVariant { + /** + * Metric for the number of invocations + * + * @default - sum over 5 minutes + */ + metricInvocations(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the number of invocations per instance + * + * @default - sum over 5 minutes + */ + metricInvocationsPerInstance(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for model latency + * + * @default - average over 5 minutes + */ + metricModelLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for overhead latency + * + * @default - average over 5 minutes + */ + metricOverheadLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the number of invocations by HTTP response code + * + * @default - sum over 5 minutes + */ + metricInvocationResponseCode(responseCode: InvocationHttpResponseCode, props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for disk utilization + * + * @default - average over 5 minutes + */ + metricDiskUtilization(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for CPU utilization + * + * @default - average over 5 minutes + */ + metricCpuUtilization(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for memory utilization + * + * @default - average over 5 minutes + */ + metricMemoryUtilization(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for GPU utilization + * + * @default - average over 5 minutes + */ + metricGpuUtilization(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for GPU memory utilization + * + * @default - average over 5 minutes + */ + metricGpuMemoryUtilization(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Enable autoscaling for SageMaker Endpoint production variant + * + * @param scalingProps EnableScalingProps + */ + autoScaleInstanceCount(scalingProps: appscaling.EnableScalingProps): ScalableInstanceCount; +} + +class EndpointInstanceProductionVariant implements IEndpointInstanceProductionVariant { + public readonly variantName: string; + private readonly endpoint: Endpoint; + private readonly initialInstanceCount: number; + private readonly instanceType: InstanceType; + private scalableInstanceCount?: ScalableInstanceCount; + + constructor(endpoint: Endpoint, variant: InstanceProductionVariant) { + this.initialInstanceCount = variant.initialInstanceCount; + this.instanceType = variant.instanceType; + this.variantName = variant.variantName; + this.endpoint = endpoint; + } + + public metric( + namespace: string, + metricName: string, + props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace, + metricName, + dimensions: { + EndpointName: this.endpoint.endpointName, + VariantName: this.variantName, + }, + statistic: 'Sum', + ...props, + }); + } + + public metricInvocations(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('AWS/SageMaker', 'Invocations', props); + } + + public metricInvocationsPerInstance(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('AWS/SageMaker', 'InvocationsPerInstance', props); + } + + public metricModelLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('AWS/SageMaker', 'ModelLatency', { + statistic: 'Average', + ...props, + }); + } + + public metricOverheadLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('AWS/SageMaker', 'OverheadLatency', { + statistic: 'Average', + ...props, + }); + } + + public metricInvocationResponseCode( + responseCode: InvocationHttpResponseCode, + props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('AWS/SageMaker', responseCode, props); + } + + public metricDiskUtilization(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('/aws/sagemaker/Endpoints', 'DiskUtilization', { + statistic: 'Average', + ...props, + }); + } + + public metricCpuUtilization(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('/aws/sagemaker/Endpoints', 'CPUUtilization', { + statistic: 'Average', + ...props, + }); + } + + public metricMemoryUtilization(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('/aws/sagemaker/Endpoints', 'MemoryUtilization', { + statistic: 'Average', + ...props, + }); + } + + public metricGpuUtilization(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('/aws/sagemaker/Endpoints', 'GPUUtilization', { + statistic: 'Average', + ...props, + }); + } + + public metricGpuMemoryUtilization(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('/aws/sagemaker/Endpoints', 'GPUMemoryUtilization', { + statistic: 'Average', + ...props, + }); + } + + public autoScaleInstanceCount(scalingProps: appscaling.EnableScalingProps): ScalableInstanceCount { + const errors: string[] = []; + if (scalingProps.minCapacity && scalingProps.minCapacity > this.initialInstanceCount) { + errors.push(`minCapacity cannot be greater than initial instance count: ${this.initialInstanceCount}`); + } + if (scalingProps.maxCapacity && scalingProps.maxCapacity < this.initialInstanceCount) { + errors.push(`maxCapacity cannot be less than initial instance count: ${this.initialInstanceCount}`); + } + if (BURSTABLE_INSTANCE_TYPE_PREFIXES.some(prefix => this.instanceType.toString().startsWith(prefix))) { + errors.push(`AutoScaling not supported for burstable instance types like ${this.instanceType}`); + } + if (this.scalableInstanceCount) { + errors.push('AutoScaling of task count already enabled for this service'); + } + + if (errors.length > 0) { + throw new Error(`Invalid Application Auto Scaling configuration: ${errors.join(EOL)}`); + } + + return this.scalableInstanceCount = new ScalableInstanceCount(this.endpoint, 'InstanceCount', { + serviceNamespace: appscaling.ServiceNamespace.SAGEMAKER, + resourceId: `endpoint/${this.endpoint.endpointName}/variant/${this.variantName}`, + dimension: 'sagemaker:variant:DesiredInstanceCount', + role: this.makeScalingRole(), + minCapacity: scalingProps.minCapacity || this.initialInstanceCount, + maxCapacity: scalingProps.maxCapacity || this.initialInstanceCount, + }); + } + + /** + * Return the service linked role which will automatically be created by Application Auto Scaling + * for scaling purposes. + * + * @see https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-service-linked-roles.html + */ + private makeScalingRole(): iam.IRole { + // Use a Service Linked Role. + return iam.Role.fromRoleArn(this.endpoint, 'ScalingRole', cdk.Stack.of(this.endpoint).formatArn({ + service: 'iam', + region: '', + resource: 'role/aws-service-role/sagemaker.application-autoscaling.amazonaws.com', + resourceName: 'AWSServiceRoleForApplicationAutoScaling_SageMakerEndpoint', + })); + } +} + +/** + * Represents an Endpoint resource defined outside this stack. + */ +export interface EndpointAttributes { + /** + * The ARN of this endpoint. + */ + readonly endpointArn: string; +} + +abstract class EndpointBase extends cdk.Resource implements IEndpoint { + /** + * The ARN of the endpoint. + * + * @attribute + */ + public abstract readonly endpointArn: string; + + /** + * The name of the endpoint. + * + * @attribute + */ + public abstract readonly endpointName: string; + + /** + * Permits an IAM principal to invoke this endpoint + * @param grantee The principal to grant access to + */ + public grantInvoke(grantee: iam.IGrantable) { + return iam.Grant.addToPrincipal({ + grantee, + actions: ['sagemaker:InvokeEndpoint'], + resourceArns: [this.endpointArn], + }); + } +} + +/** + * Construction properties for a SageMaker Endpoint. + */ +export interface EndpointProps { + /** + * Name of the endpoint. + * + * @default - AWS CloudFormation generates a unique physical ID and uses that ID for the + * endpoint's name. + */ + readonly endpointName?: string; + + /** + * The endpoint configuration to use for this endpoint. + */ + readonly endpointConfig: IEndpointConfig; +} + +/** + * HTTP response codes for Endpoint invocations + */ +export enum InvocationHttpResponseCode { + /** + * 4xx response codes from Endpoint invocations + */ + INVOCATION_4XX_ERRORS = 'Invocation4XXErrors', + + /** + * 5xx response codes from Endpoint invocations + */ + INVOCATION_5XX_ERRORS = 'Invocation5XXErrors', +} + +/** + * Defines a SageMaker endpoint. + */ +export class Endpoint extends EndpointBase { + /** + * Imports an Endpoint defined either outside the CDK or in a different CDK stack. + * @param scope the Construct scope. + * @param id the resource id. + * @param endpointArn the ARN of the endpoint. + */ + public static fromEndpointArn(scope: Construct, id: string, endpointArn: string): IEndpoint { + return Endpoint.fromEndpointAttributes(scope, id, { endpointArn }); + } + + /** + * Imports an Endpoint defined either outside the CDK or in a different CDK stack. + * @param scope the Construct scope. + * @param id the resource id. + * @param endpointName the name of the endpoint. + */ + public static fromEndpointName(scope: Construct, id: string, endpointName: string): IEndpoint { + const endpointArn = cdk.Stack.of(scope).formatArn({ + service: 'sagemaker', + resource: 'endpoint', + resourceName: endpointName, + }); + return Endpoint.fromEndpointAttributes(scope, id, { endpointArn }); + } + + /** + * Imports an Endpoint 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 endpoint to import. + */ + public static fromEndpointAttributes(scope: Construct, id: string, attrs: EndpointAttributes): IEndpoint { + const endpointArn = attrs.endpointArn; + const endpointName = cdk.Stack.of(scope).splitArn(endpointArn, cdk.ArnFormat.SLASH_RESOURCE_NAME).resourceName!; + + class Import extends EndpointBase { + public readonly endpointArn = endpointArn; + public readonly endpointName = endpointName; + + constructor(s: Construct, i: string) { + super(s, i, { + environmentFromArn: endpointArn, + }); + } + } + + return new Import(scope, id); + } + + /** + * The ARN of the endpoint. + * + * @attribute + */ + public readonly endpointArn: string; + /** + * The name of the endpoint. + * + * @attribute + */ + public readonly endpointName: string; + private readonly endpointConfig: IEndpointConfig; + + constructor(scope: Construct, id: string, props: EndpointProps) { + super(scope, id, { + physicalName: props.endpointName, + }); + + this.validateEnvironmentCompatibility(props.endpointConfig); + this.endpointConfig = props.endpointConfig; + + // create the endpoint resource + const endpoint = new CfnEndpoint(this, 'Endpoint', { + endpointConfigName: props.endpointConfig.endpointConfigName, + endpointName: this.physicalName, + }); + this.endpointName = this.getResourceNameAttribute(endpoint.attrEndpointName); + this.endpointArn = this.getResourceArnAttribute(endpoint.ref, { + service: 'sagemaker', + resource: 'endpoint', + resourceName: this.physicalName, + }); + } + + private validateEnvironmentCompatibility(endpointConfig: IEndpointConfig): void { + if (!sameEnv(endpointConfig.env.account, this.env.account)) { + throw new Error(`Cannot use endpoint configuration in account ${endpointConfig.env.account} for endpoint in account ${this.env.account}`); + } else if (!sameEnv(endpointConfig.env.region, this.env.region)) { + throw new Error(`Cannot use endpoint configuration in region ${endpointConfig.env.region} for endpoint in region ${this.env.region}`); + } + } + + /** + * Get instance production variants associated with endpoint. + */ + public get instanceProductionVariants(): IEndpointInstanceProductionVariant[] { + if (this.endpointConfig instanceof EndpointConfig) { + return this.endpointConfig._instanceProductionVariants.map(v => new EndpointInstanceProductionVariant(this, v)); + } + + throw new Error('Production variant lookup is not supported for an imported IEndpointConfig'); + } + + /** + * Find instance production variant based on variant name + * @param name Variant name from production variant + */ + public findInstanceProductionVariant(name: string): IEndpointInstanceProductionVariant { + if (this.endpointConfig instanceof EndpointConfig) { + const variant = this.endpointConfig._findInstanceProductionVariant(name); + return new EndpointInstanceProductionVariant(this, variant); + } + + throw new Error('Production variant lookup is not supported for an imported IEndpointConfig'); + } +} diff --git a/packages/@aws-cdk/aws-sagemaker/lib/index.ts b/packages/@aws-cdk/aws-sagemaker/lib/index.ts index a764740e5ab70..7e1c8abccbacd 100644 --- a/packages/@aws-cdk/aws-sagemaker/lib/index.ts +++ b/packages/@aws-cdk/aws-sagemaker/lib/index.ts @@ -1,9 +1,11 @@ export * from './accelerator-type'; export * from './container-image'; +export * from './endpoint'; export * from './endpoint-config'; export * from './instance-type'; export * from './model'; export * from './model-data'; +export * from './scalable-instance-count'; // AWS::SageMaker CloudFormation Resources: export * from './sagemaker.generated'; diff --git a/packages/@aws-cdk/aws-sagemaker/lib/scalable-instance-count.ts b/packages/@aws-cdk/aws-sagemaker/lib/scalable-instance-count.ts new file mode 100644 index 0000000000000..1bd2cb5f80158 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/lib/scalable-instance-count.ts @@ -0,0 +1,69 @@ +import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; +import { Construct } from 'constructs'; + +/** + * The properties of a scalable attribute representing task count. + */ +export interface ScalableInstanceCountProps extends appscaling.BaseScalableAttributeProps { +} + +/** + * A scalable sagemaker endpoint attribute + */ +export class ScalableInstanceCount extends appscaling.BaseScalableAttribute { + /** + * Constructs a new instance of the ScalableInstanceCount class. + */ + constructor(scope: Construct, id: string, props: ScalableInstanceCountProps) { + super(scope, id, props); + } + + /** + * Scales in or out to achieve a target requests per second per instance. + */ + public scaleOnInvocations(id: string, props: InvocationsScalingProps) { + const predefinedMetric = appscaling.PredefinedMetric.SAGEMAKER_VARIANT_INVOCATIONS_PER_INSTANCE; + + super.doScaleToTrackMetric(id, { + policyName: props.policyName, + disableScaleIn: props.disableScaleIn, + scaleInCooldown: props.scaleInCooldown, + scaleOutCooldown: props.scaleOutCooldown, + targetValue: this.calculateScalingTarget(props), + predefinedMetric, + }); + } + + /** + * Calculate target value based on a ScalableProductionVariant + * + * Documentation for the equation is here: https://docs.aws.amazon.com/sagemaker/latest/dg/endpoint-scaling-loadtest.html + * @param props the scaling properties. + */ + private calculateScalingTarget(props: InvocationsScalingProps): number { + const safetyFactor = props.safetyFactor ?? 0.5; + if (safetyFactor <= 0.0 || safetyFactor > 1.0) { + throw new Error(`Safety factor (${safetyFactor}) must be greater than 0.0 and less than or equal 1.0`); + } + return safetyFactor * props.maxRequestsPerSecond * 60; + } +} + +/** + * Properties for enabling SageMaker Endpoint utilization tracking + */ +export interface InvocationsScalingProps extends appscaling.BaseTargetTrackingProps { + /** + * Max RPS per instance used for calculating the target SageMaker variant invocation per instance + * + * More documentation available here: https://docs.aws.amazon.com/sagemaker/latest/dg/endpoint-scaling-loadtest.html + */ + readonly maxRequestsPerSecond: number; + /** + * Safty factor for calculating the target SageMaker variant invocation per instance + * + * More documentation available here: https://docs.aws.amazon.com/sagemaker/latest/dg/endpoint-scaling-loadtest.html + * @default 0.5 + */ + readonly safetyFactor?: number; +} diff --git a/packages/@aws-cdk/aws-sagemaker/package.json b/packages/@aws-cdk/aws-sagemaker/package.json index 55e81fd8a056f..4de9941f99eec 100644 --- a/packages/@aws-cdk/aws-sagemaker/package.json +++ b/packages/@aws-cdk/aws-sagemaker/package.json @@ -92,6 +92,8 @@ }, "dependencies": { "@aws-cdk/assets": "0.0.0", + "@aws-cdk/aws-applicationautoscaling": "0.0.0", + "@aws-cdk/aws-cloudwatch": "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", @@ -104,6 +106,8 @@ }, "peerDependencies": { "@aws-cdk/assets": "0.0.0", + "@aws-cdk/aws-applicationautoscaling": "0.0.0", + "@aws-cdk/aws-cloudwatch": "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", diff --git a/packages/@aws-cdk/aws-sagemaker/test/endpoint.test.ts b/packages/@aws-cdk/aws-sagemaker/test/endpoint.test.ts new file mode 100644 index 0000000000000..95cb9c97685bb --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/endpoint.test.ts @@ -0,0 +1,431 @@ +import * as cdk from '@aws-cdk/core'; +import * as sagemaker from '../lib'; + +describe('When searching an Endpoint for a production variant', () => { + test('that exists, the variant is returned', () => { + // GIVEN + const stack = new cdk.Stack(); + const model = sagemaker.Model.fromModelName(stack, 'Model', 'model'); + const endpointConfig = new sagemaker.EndpointConfig(stack, 'EndpointConfig', { instanceProductionVariants: [{ variantName: 'variant', model }] }); + const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { endpointConfig }); + + // WHEN + const variant = endpoint.findInstanceProductionVariant('variant'); + + // THEN + expect(variant.variantName).toEqual('variant'); + }); + + test('that does not exist, an exception is thrown', () => { + // GIVEN + const stack = new cdk.Stack(); + const model = sagemaker.Model.fromModelName(stack, 'Model', 'model'); + const endpointConfig = new sagemaker.EndpointConfig(stack, 'EndpointConfig', { instanceProductionVariants: [{ variantName: 'variant', model }] }); + const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { endpointConfig }); + + // WHEN + const when = () => endpoint.findInstanceProductionVariant('missing-variant'); + + // THEN + expect(when).toThrow(/No variant with name: 'missing-variant'/); + }); + + test('from an imported IEndpointConfig, an exception is thrown', () => { + // GIVEN + const stack = new cdk.Stack(); + const endpointConfig = sagemaker.EndpointConfig.fromEndpointConfigName(stack, 'EndpointConfig', 'MyEndpointConfig'); + const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { endpointConfig }); + + // WHEN + const when = () => endpoint.findInstanceProductionVariant('variant'); + + // THEN + expect(when).toThrow(/Production variant lookup is not supported for an imported IEndpointConfig/); + }); +}); + +describe('When fetching production variants from an Endpoint', () => { + test('with one production variant, the variant is returned', () => { + // GIVEN + const stack = new cdk.Stack(); + const model = sagemaker.Model.fromModelName(stack, 'Model', 'model'); + const endpointConfig = new sagemaker.EndpointConfig(stack, 'EndpointConfig', { instanceProductionVariants: [{ variantName: 'variant', model }] }); + const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { endpointConfig }); + + // WHEN + const variants: sagemaker.IEndpointInstanceProductionVariant[] = endpoint.instanceProductionVariants; + + // THEN + expect(variants.length).toEqual(1); + expect(variants[0].variantName).toEqual('variant'); + }); + + test('with an imported IEndpointConfig, an exception is thrown', () => { + // GIVEN + const stack = new cdk.Stack(); + const endpointConfig = sagemaker.EndpointConfig.fromEndpointConfigName(stack, 'EndpointConfig', 'MyEndpointConfig'); + const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { endpointConfig }); + + // WHEN + const when = () => endpoint.instanceProductionVariants; + + // THEN + expect(when).toThrow(/Production variant lookup is not supported for an imported IEndpointConfig/); + }); +}); + +describe('When importing an endpoint by ARN, the name is determined correctly', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const endpoint = sagemaker.Endpoint.fromEndpointArn(stack, 'Endpoint', 'arn:aws:sagemaker:us-west-2:123456789012:endpoint/my-name'); + + // THEN + expect(endpoint.endpointName).toEqual('my-name'); +}); + +describe('When importing an endpoint by name, the ARN is constructed correctly', () => { + // GIVEN + const stack = new cdk.Stack(undefined, undefined, { + env: + { + region: 'us-west-2', + account: '123456789012', + }, + }); + + // WHEN + const endpoint = sagemaker.Endpoint.fromEndpointName(stack, 'Endpoint', 'my-name'); + + // THEN + expect(endpoint.endpointArn).toMatch(/arn:.+:sagemaker:us-west-2:123456789012:endpoint\/my-name/); +}); + +describe('When sharing an endpoint config from an origin stack with a destination stack', () => { + describe('which represents an owned EndpointConfig instance', () => { + test('across stack account boundaries, synthesis fails', () => { + // GIVEN + const app = new cdk.App(); + const originStack = new cdk.Stack(app, 'OriginStack', { + env: + { + region: 'us-west-2', + account: '123456789012', + }, + }); + const originStackEndpointConfig = new sagemaker.EndpointConfig(originStack, 'MyEndpointConfig', { + endpointConfigName: 'explicit-name', + instanceProductionVariants: [{ + variantName: 'my-variant', + model: sagemaker.Model.fromModelName(originStack, 'MyModel', 'my-model'), + }], + }); + const destinationStack = new cdk.Stack(app, 'DestinationStack', { + env: + { + region: 'us-west-2', + account: '234567890123', + }, + }); + + // WHEN + const when = () => + new sagemaker.Endpoint(destinationStack, 'MyEndpoint', { + endpointConfig: originStackEndpointConfig, + }); + + // THEN + expect(when).toThrow(/Cannot use endpoint configuration in account 123456789012 for endpoint in account 234567890123/); + }); + + test('across stack region boundaries, synthesis fails', () => { + // GIVEN + const app = new cdk.App(); + const originStack = new cdk.Stack(app, 'OriginStack', { + env: + { + region: 'us-west-2', + account: '123456789012', + }, + }); + const originStackEndpointConfig = new sagemaker.EndpointConfig(originStack, 'MyEndpointConfig', { + endpointConfigName: 'explicit-name', + instanceProductionVariants: [{ + variantName: 'my-variant', + model: sagemaker.Model.fromModelName(originStack, 'MyModel', 'my-model'), + }], + }); + const destinationStack = new cdk.Stack(app, 'DestinationStack', { + env: + { + region: 'us-east-1', + account: '123456789012', + }, + }); + + // WHEN + const when = () => + new sagemaker.Endpoint(destinationStack, 'MyEndpoint', { + endpointConfig: originStackEndpointConfig, + }); + + // THEN + expect(when).toThrow(/Cannot use endpoint configuration in region us-west-2 for endpoint in region us-east-1/); + }); + }); + + describe('which represents an unowned IEndpointConfig instance', () => { + describe('imported by name', () => { + test('across stack account boundaries, synthesis fails', () => { + // GIVEN + const app = new cdk.App(); + const originStack = new cdk.Stack(app, 'OriginStack', { + env: + { + region: 'us-west-2', + account: '123456789012', + }, + }); + const originStackEndpointConfig = sagemaker.EndpointConfig.fromEndpointConfigName(originStack, 'MyEndpointConfig', 'explicit-name'); + const destinationStack = new cdk.Stack(app, 'DestinationStack', { + env: + { + region: 'us-west-2', + account: '234567890123', + }, + }); + + // WHEN + const when = () => + new sagemaker.Endpoint(destinationStack, 'MyEndpoint', { + endpointConfig: originStackEndpointConfig, + }); + + // THEN + expect(when).toThrow(/Cannot use endpoint configuration in account 123456789012 for endpoint in account 234567890123/); + }); + + test('across stack region boundaries, synthesis fails', () => { + // GIVEN + const app = new cdk.App(); + const originStack = new cdk.Stack(app, 'OriginStack', { + env: + { + region: 'us-west-2', + account: '123456789012', + }, + }); + const originStackEndpointConfig = sagemaker.EndpointConfig.fromEndpointConfigName(originStack, 'MyEndpointConfig', 'explicit-name'); + const destinationStack = new cdk.Stack(app, 'DestinationStack', { + env: + { + region: 'us-east-1', + account: '123456789012', + }, + }); + + // WHEN + const when = () => + new sagemaker.Endpoint(destinationStack, 'MyEndpoint', { + endpointConfig: originStackEndpointConfig, + }); + + // THEN + expect(when).toThrow(/Cannot use endpoint configuration in region us-west-2 for endpoint in region us-east-1/); + }); + }); + + describe('imported by ARN', () => { + test('in a different account than both stacks, synthesis fails', () => { + // GIVEN + const app = new cdk.App(); + const originStack = new cdk.Stack(app, 'OriginStack', { + env: + { + region: 'us-west-2', + account: '234567890123', + }, + }); + const originStackEndpointConfig = sagemaker.EndpointConfig.fromEndpointConfigArn(originStack, 'MyEndpointConfig', 'arn:aws:sagemaker:us-west-2:123456789012:endpoint/explicit-name'); + const destinationStack = new cdk.Stack(app, 'DestinationStack', { + env: + { + region: 'us-west-2', + account: '234567890123', + }, + }); + + // WHEN + const when = () => + new sagemaker.Endpoint(destinationStack, 'MyEndpoint', { + endpointConfig: originStackEndpointConfig, + }); + + // THEN + expect(when).toThrow(/Cannot use endpoint configuration in account 123456789012 for endpoint in account 234567890123/); + }); + + test('in a different region than both stacks, synthesis fails', () => { + // GIVEN + const app = new cdk.App(); + const originStack = new cdk.Stack(app, 'OriginStack', { + env: + { + region: 'us-east-1', + account: '123456789012', + }, + }); + const originStackEndpointConfig = sagemaker.EndpointConfig.fromEndpointConfigArn(originStack, 'MyEndpointConfig', 'arn:aws:sagemaker:us-west-2:123456789012:endpoint/explicit-name'); + const destinationStack = new cdk.Stack(app, 'DestinationStack', { + env: + { + region: 'us-east-1', + account: '123456789012', + }, + }); + + // WHEN + const when = () => + new sagemaker.Endpoint(destinationStack, 'MyEndpoint', { + endpointConfig: originStackEndpointConfig, + }); + + // THEN + expect(when).toThrow(/Cannot use endpoint configuration in region us-west-2 for endpoint in region us-east-1/); + }); + }); + }); +}); + +describe('When auto-scaling a production variant\'s instance count', () => { + test('with minimum capacity greater than initial instance count, an exception is thrown', () => { + // GIVEN + const stack = new cdk.Stack(); + const model = sagemaker.Model.fromModelName(stack, 'Model', 'model'); + const endpointConfig = new sagemaker.EndpointConfig(stack, 'EndpointConfig', { instanceProductionVariants: [{ variantName: 'variant', model }] }); + const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { endpointConfig }); + const variant = endpoint.findInstanceProductionVariant('variant'); + + // WHEN + const when = () => variant.autoScaleInstanceCount({ + minCapacity: 2, + maxCapacity: 3, + }); + + // THEN + expect(when).toThrow(/minCapacity cannot be greater than initial instance count: 1/); + }); + + test('with maximum capacity less than initial instance count, an exception is thrown', () => { + // GIVEN + const stack = new cdk.Stack(); + const model = sagemaker.Model.fromModelName(stack, 'Model', 'model'); + const endpointConfig = new sagemaker.EndpointConfig(stack, 'EndpointConfig', { instanceProductionVariants: [{ variantName: 'variant', model, initialInstanceCount: 2 }] }); + const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { endpointConfig }); + const variant = endpoint.findInstanceProductionVariant('variant'); + + // WHEN + const when = () => variant.autoScaleInstanceCount({ maxCapacity: 1 }); + + // THEN + expect(when).toThrow(/maxCapacity cannot be less than initial instance count: 2/); + }); + + test('with burstable instance type, an exception is thrown', () => { + // GIVEN + const stack = new cdk.Stack(); + const model = sagemaker.Model.fromModelName(stack, 'Model', 'model'); + const endpointConfig = new sagemaker.EndpointConfig(stack, 'EndpointConfig', { + instanceProductionVariants: [{ + variantName: 'variant', + model, + instanceType: sagemaker.InstanceType.T2_MEDIUM, + }], + }); + const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { endpointConfig }); + const variant = endpoint.findInstanceProductionVariant('variant'); + + // WHEN + const when = () => variant.autoScaleInstanceCount({ maxCapacity: 3 }); + + // THEN + expect(when).toThrow(/AutoScaling not supported for burstable instance types like ml.t2.medium/); + }); + + test('which already has auto-scaling enabled, an exception is thrown', () => { + // GIVEN + const stack = new cdk.Stack(); + const model = sagemaker.Model.fromModelName(stack, 'Model', 'model'); + const endpointConfig = new sagemaker.EndpointConfig(stack, 'EndpointConfig', { + instanceProductionVariants: [{ + variantName: 'variant', + model, + instanceType: sagemaker.InstanceType.M5_LARGE, + }], + }); + const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { endpointConfig }); + const variant = endpoint.findInstanceProductionVariant('variant'); + variant.autoScaleInstanceCount({ maxCapacity: 3 }); + + // WHEN + const when = () => variant.autoScaleInstanceCount({ maxCapacity: 3 }); + + // THEN + expect(when).toThrow(/AutoScaling of task count already enabled for this service/); + }); + + test('with a safety factor of zero, an exception is thrown', () => { + // GIVEN + const stack = new cdk.Stack(); + const model = sagemaker.Model.fromModelName(stack, 'Model', 'model'); + const endpointConfig = new sagemaker.EndpointConfig(stack, 'EndpointConfig', { + instanceProductionVariants: [{ + variantName: 'variant', + model, + initialInstanceCount: 2, + instanceType: sagemaker.InstanceType.M5_LARGE, + }], + }); + const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { endpointConfig }); + const variant = endpoint.findInstanceProductionVariant('variant'); + const instanceCount = variant.autoScaleInstanceCount({ maxCapacity: 3 }); + + // WHEN + const when = () => + instanceCount.scaleOnInvocations('LimitRPS', { + maxRequestsPerSecond: 30, + safetyFactor: 0, + }); + + // THEN + expect(when).toThrow(/Safety factor \(0\) must be greater than 0.0 and less than or equal 1\.0/); + }); + + test('with a safety factor greater than one, an exception is thrown', () => { + // GIVEN + const stack = new cdk.Stack(); + const model = sagemaker.Model.fromModelName(stack, 'Model', 'model'); + const endpointConfig = new sagemaker.EndpointConfig(stack, 'EndpointConfig', { + instanceProductionVariants: [{ + variantName: 'variant', + model, + initialInstanceCount: 2, + instanceType: sagemaker.InstanceType.M5_LARGE, + }], + }); + const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { endpointConfig }); + const variant = endpoint.findInstanceProductionVariant('variant'); + const instanceCount = variant.autoScaleInstanceCount({ maxCapacity: 3 }); + + // WHEN + const when = () => + instanceCount.scaleOnInvocations('LimitRPS', { + maxRequestsPerSecond: 30, + safetyFactor: 1.1, + }); + + // THEN + expect(when).toThrow(/Safety factor \(1\.1\) must be greater than 0.0 and less than or equal 1\.0/); + }); +}); diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/asset.126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.tar.gz b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.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.endpoint.alarms.js.snapshot/asset.126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.tar.gz differ diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/Dockerfile b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/Dockerfile new file mode 100644 index 0000000000000..7eb03f499c9c6 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.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.endpoint.alarms.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.html b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.html new file mode 100644 index 0000000000000..3b18e512dba79 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.html @@ -0,0 +1 @@ +hello world diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.py b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.py new file mode 100644 index 0000000000000..f5ab0458f1cb1 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.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.endpoint.alarms.js.snapshot/aws-cdk-sagemaker-endpoint-alarms.assets.json b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/aws-cdk-sagemaker-endpoint-alarms.assets.json new file mode 100644 index 0000000000000..aacfba8577e26 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/aws-cdk-sagemaker-endpoint-alarms.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}" + } + } + }, + "a8773958f8215d1c73afd5170e32c721525ba4ae099b316e6d7818e6943ac8b9": { + "source": { + "path": "aws-cdk-sagemaker-endpoint-alarms.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "a8773958f8215d1c73afd5170e32c721525ba4ae099b316e6d7818e6943ac8b9.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.endpoint.alarms.js.snapshot/aws-cdk-sagemaker-endpoint-alarms.template.json b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/aws-cdk-sagemaker-endpoint-alarms.template.json new file mode 100644 index 0000000000000..3aa1523256cb9 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/aws-cdk-sagemaker-endpoint-alarms.template.json @@ -0,0 +1,540 @@ +{ + "Resources": { + "ModelWithArtifactRole854ED848": { + "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" + ] + ] + } + ] + } + }, + "ModelWithArtifactRoleDefaultPolicy63A6F076": { + "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": "ModelWithArtifactRoleDefaultPolicy63A6F076", + "Roles": [ + { + "Ref": "ModelWithArtifactRole854ED848" + } + ] + } + }, + "ModelWithArtifactModel7EBC6421": { + "Type": "AWS::SageMaker::Model", + "Properties": { + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "ModelWithArtifactRole854ED848", + "Arn" + ] + }, + "PrimaryContainer": { + "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": [ + "ModelWithArtifactRoleDefaultPolicy63A6F076", + "ModelWithArtifactRole854ED848" + ] + }, + "EndpointConfigFD7B6F91": { + "Type": "AWS::SageMaker::EndpointConfig", + "Properties": { + "ProductionVariants": [ + { + "InitialInstanceCount": 1, + "InitialVariantWeight": 1, + "InstanceType": "ml.t2.medium", + "ModelName": { + "Fn::GetAtt": [ + "ModelWithArtifactModel7EBC6421", + "ModelName" + ] + }, + "VariantName": "firstVariant" + } + ] + } + }, + "Endpoint8024A810": { + "Type": "AWS::SageMaker::Endpoint", + "Properties": { + "EndpointConfigName": { + "Fn::GetAtt": [ + "EndpointConfigFD7B6F91", + "EndpointConfigName" + ] + } + } + }, + "Invoker060A9026": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "InvokerDefaultPolicy3FF8208D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sagemaker:InvokeEndpoint", + "Effect": "Allow", + "Resource": { + "Ref": "Endpoint8024A810" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "InvokerDefaultPolicy3FF8208D", + "Roles": [ + { + "Ref": "Invoker060A9026" + } + ] + } + }, + "InvocationsAlarmBC3830BD": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 2, + "Dimensions": [ + { + "Name": "EndpointName", + "Value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "Name": "VariantName", + "Value": "firstVariant" + } + ], + "MetricName": "Invocations", + "Namespace": "AWS/SageMaker", + "Period": 300, + "Statistic": "Sum", + "Threshold": 1 + } + }, + "InvocationsPerInstanceAlarm82CBCF29": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 5, + "Dimensions": [ + { + "Name": "EndpointName", + "Value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "Name": "VariantName", + "Value": "firstVariant" + } + ], + "MetricName": "InvocationsPerInstance", + "Namespace": "AWS/SageMaker", + "Period": 300, + "Statistic": "Sum", + "Threshold": 4 + } + }, + "ModelLatencyAlarm96AC7D24": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 8, + "Dimensions": [ + { + "Name": "EndpointName", + "Value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "Name": "VariantName", + "Value": "firstVariant" + } + ], + "MetricName": "ModelLatency", + "Namespace": "AWS/SageMaker", + "Period": 300, + "Statistic": "Average", + "Threshold": 7 + } + }, + "OverheadLatencyAlarm10D8981B": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 11, + "Dimensions": [ + { + "Name": "EndpointName", + "Value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "Name": "VariantName", + "Value": "firstVariant" + } + ], + "MetricName": "OverheadLatency", + "Namespace": "AWS/SageMaker", + "Period": 300, + "Statistic": "Average", + "Threshold": 10 + } + }, + "Invocation5XXErrorsAlarmF9BAF026": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 14, + "Dimensions": [ + { + "Name": "EndpointName", + "Value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "Name": "VariantName", + "Value": "firstVariant" + } + ], + "MetricName": "Invocation5XXErrors", + "Namespace": "AWS/SageMaker", + "Period": 300, + "Statistic": "Sum", + "Threshold": 13 + } + }, + "DiskUtilizationAlarmE19E4184": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 17, + "Dimensions": [ + { + "Name": "EndpointName", + "Value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "Name": "VariantName", + "Value": "firstVariant" + } + ], + "MetricName": "DiskUtilization", + "Namespace": "/aws/sagemaker/Endpoints", + "Period": 300, + "Statistic": "Average", + "Threshold": 16 + } + }, + "CPUUtilizationAlarm4D91B4D0": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 20, + "Dimensions": [ + { + "Name": "EndpointName", + "Value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "Name": "VariantName", + "Value": "firstVariant" + } + ], + "MetricName": "CPUUtilization", + "Namespace": "/aws/sagemaker/Endpoints", + "Period": 300, + "Statistic": "Average", + "Threshold": 19 + } + }, + "MemoryUtilizationAlarm544270BF": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 23, + "Dimensions": [ + { + "Name": "EndpointName", + "Value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "Name": "VariantName", + "Value": "firstVariant" + } + ], + "MetricName": "MemoryUtilization", + "Namespace": "/aws/sagemaker/Endpoints", + "Period": 300, + "Statistic": "Average", + "Threshold": 22 + } + }, + "GPUUtilizationAlarmEC9BEC6F": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 26, + "Dimensions": [ + { + "Name": "EndpointName", + "Value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "Name": "VariantName", + "Value": "firstVariant" + } + ], + "MetricName": "GPUUtilization", + "Namespace": "/aws/sagemaker/Endpoints", + "Period": 300, + "Statistic": "Average", + "Threshold": 25 + } + }, + "GPUMemoryUtilizationAlarmD03813BC": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 29, + "Dimensions": [ + { + "Name": "EndpointName", + "Value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "Name": "VariantName", + "Value": "firstVariant" + } + ], + "MetricName": "GPUMemoryUtilization", + "Namespace": "/aws/sagemaker/Endpoints", + "Period": 300, + "Statistic": "Average", + "Threshold": 28 + } + } + }, + "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.endpoint.alarms.js.snapshot/cdk.out b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/cdk.out new file mode 100644 index 0000000000000..8ecc185e9dbee --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.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.endpoint.alarms.js.snapshot/integ.json b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/integ.json new file mode 100644 index 0000000000000..078424138cc8c --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "21.0.0", + "testCases": { + "integtest-endpoint-alarms/DefaultTest": { + "stacks": [ + "aws-cdk-sagemaker-endpoint-alarms" + ], + "assertionStack": "integtest-endpoint-alarms/DefaultTest/DeployAssert", + "assertionStackName": "integtestendpointalarmsDefaultTestDeployAssert25A3073D" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/integtestendpointalarmsDefaultTestDeployAssert25A3073D.assets.json b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/integtestendpointalarmsDefaultTestDeployAssert25A3073D.assets.json new file mode 100644 index 0000000000000..d3d6e6165f6de --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/integtestendpointalarmsDefaultTestDeployAssert25A3073D.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "integtestendpointalarmsDefaultTestDeployAssert25A3073D.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.endpoint.alarms.js.snapshot/integtestendpointalarmsDefaultTestDeployAssert25A3073D.template.json b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/integtestendpointalarmsDefaultTestDeployAssert25A3073D.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/integtestendpointalarmsDefaultTestDeployAssert25A3073D.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.endpoint.alarms.js.snapshot/manifest.json b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/manifest.json new file mode 100644 index 0000000000000..41e4801448461 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/manifest.json @@ -0,0 +1,207 @@ +{ + "version": "21.0.0", + "artifacts": { + "aws-cdk-sagemaker-endpoint-alarms.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-sagemaker-endpoint-alarms.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-sagemaker-endpoint-alarms": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-sagemaker-endpoint-alarms.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}/a8773958f8215d1c73afd5170e32c721525ba4ae099b316e6d7818e6943ac8b9.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-sagemaker-endpoint-alarms.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-endpoint-alarms.assets" + ], + "metadata": { + "/aws-cdk-sagemaker-endpoint-alarms/ModelWithArtifact/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ModelWithArtifactRole854ED848" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/ModelWithArtifact/Role/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ModelWithArtifactRoleDefaultPolicy63A6F076" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/ModelWithArtifact/Model": [ + { + "type": "aws:cdk:logicalId", + "data": "ModelWithArtifactModel7EBC6421" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/EndpointConfig/EndpointConfig": [ + { + "type": "aws:cdk:logicalId", + "data": "EndpointConfigFD7B6F91" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/Endpoint/Endpoint": [ + { + "type": "aws:cdk:logicalId", + "data": "Endpoint8024A810" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/Invoker/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Invoker060A9026" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/Invoker/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "InvokerDefaultPolicy3FF8208D" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/InvocationsAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "InvocationsAlarmBC3830BD" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/InvocationsPerInstanceAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "InvocationsPerInstanceAlarm82CBCF29" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/ModelLatencyAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ModelLatencyAlarm96AC7D24" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/OverheadLatencyAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "OverheadLatencyAlarm10D8981B" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/Invocation5XXErrorsAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Invocation5XXErrorsAlarmF9BAF026" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/DiskUtilizationAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "DiskUtilizationAlarmE19E4184" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/CPUUtilizationAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CPUUtilizationAlarm4D91B4D0" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/MemoryUtilizationAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MemoryUtilizationAlarm544270BF" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/GPUUtilizationAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "GPUUtilizationAlarmEC9BEC6F" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/GPUMemoryUtilizationAlarm/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "GPUMemoryUtilizationAlarmD03813BC" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-sagemaker-endpoint-alarms/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-sagemaker-endpoint-alarms" + }, + "integtestendpointalarmsDefaultTestDeployAssert25A3073D.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integtestendpointalarmsDefaultTestDeployAssert25A3073D.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integtestendpointalarmsDefaultTestDeployAssert25A3073D": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integtestendpointalarmsDefaultTestDeployAssert25A3073D.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": [ + "integtestendpointalarmsDefaultTestDeployAssert25A3073D.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": [ + "integtestendpointalarmsDefaultTestDeployAssert25A3073D.assets" + ], + "metadata": { + "/integtest-endpoint-alarms/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integtest-endpoint-alarms/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integtest-endpoint-alarms/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.endpoint.alarms.js.snapshot/tree.json b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/tree.json new file mode 100644 index 0000000000000..310dfe05e044e --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.js.snapshot/tree.json @@ -0,0 +1,973 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-cdk-sagemaker-endpoint-alarms": { + "id": "aws-cdk-sagemaker-endpoint-alarms", + "path": "aws-cdk-sagemaker-endpoint-alarms", + "children": { + "ModelWithArtifact": { + "id": "ModelWithArtifact", + "path": "aws-cdk-sagemaker-endpoint-alarms/ModelWithArtifact", + "children": { + "Role": { + "id": "Role", + "path": "aws-cdk-sagemaker-endpoint-alarms/ModelWithArtifact/Role", + "children": { + "ImportRole": { + "id": "ImportRole", + "path": "aws-cdk-sagemaker-endpoint-alarms/ModelWithArtifact/Role/ImportRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint-alarms/ModelWithArtifact/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-endpoint-alarms/ModelWithArtifact/Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint-alarms/ModelWithArtifact/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": "ModelWithArtifactRoleDefaultPolicy63A6F076", + "roles": [ + { + "Ref": "ModelWithArtifactRole854ED848" + } + ] + } + }, + "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-endpoint-alarms/ModelWithArtifact/ModelImage7ffa8020b99fe9d130a903251c36866d", + "children": { + "Staging": { + "id": "Staging", + "path": "aws-cdk-sagemaker-endpoint-alarms/ModelWithArtifact/ModelImage7ffa8020b99fe9d130a903251c36866d/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Repository": { + "id": "Repository", + "path": "aws-cdk-sagemaker-endpoint-alarms/ModelWithArtifact/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-endpoint-alarms/ModelWithArtifact/ModelData412d61f9c984d1aff5ee358daf994d58", + "children": { + "Stage": { + "id": "Stage", + "path": "aws-cdk-sagemaker-endpoint-alarms/ModelWithArtifact/ModelData412d61f9c984d1aff5ee358daf994d58/Stage", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "aws-cdk-sagemaker-endpoint-alarms/ModelWithArtifact/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-endpoint-alarms/ModelWithArtifact/Model", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SageMaker::Model", + "aws:cdk:cloudformation:props": { + "executionRoleArn": { + "Fn::GetAtt": [ + "ModelWithArtifactRole854ED848", + "Arn" + ] + }, + "primaryContainer": { + "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" + } + }, + "EndpointConfig": { + "id": "EndpointConfig", + "path": "aws-cdk-sagemaker-endpoint-alarms/EndpointConfig", + "children": { + "EndpointConfig": { + "id": "EndpointConfig", + "path": "aws-cdk-sagemaker-endpoint-alarms/EndpointConfig/EndpointConfig", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SageMaker::EndpointConfig", + "aws:cdk:cloudformation:props": { + "productionVariants": [ + { + "initialInstanceCount": 1, + "initialVariantWeight": 1, + "instanceType": "ml.t2.medium", + "modelName": { + "Fn::GetAtt": [ + "ModelWithArtifactModel7EBC6421", + "ModelName" + ] + }, + "variantName": "firstVariant" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker.CfnEndpointConfig", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker.EndpointConfig", + "version": "0.0.0" + } + }, + "Endpoint": { + "id": "Endpoint", + "path": "aws-cdk-sagemaker-endpoint-alarms/Endpoint", + "children": { + "Endpoint": { + "id": "Endpoint", + "path": "aws-cdk-sagemaker-endpoint-alarms/Endpoint/Endpoint", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SageMaker::Endpoint", + "aws:cdk:cloudformation:props": { + "endpointConfigName": { + "Fn::GetAtt": [ + "EndpointConfigFD7B6F91", + "EndpointConfigName" + ] + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker.CfnEndpoint", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker.Endpoint", + "version": "0.0.0" + } + }, + "Invoker": { + "id": "Invoker", + "path": "aws-cdk-sagemaker-endpoint-alarms/Invoker", + "children": { + "ImportInvoker": { + "id": "ImportInvoker", + "path": "aws-cdk-sagemaker-endpoint-alarms/Invoker/ImportInvoker", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint-alarms/Invoker/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-cdk-sagemaker-endpoint-alarms/Invoker/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint-alarms/Invoker/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": "sagemaker:InvokeEndpoint", + "Effect": "Allow", + "Resource": { + "Ref": "Endpoint8024A810" + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "InvokerDefaultPolicy3FF8208D", + "roles": [ + { + "Ref": "Invoker060A9026" + } + ] + } + }, + "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" + } + }, + "InvocationsAlarm": { + "id": "InvocationsAlarm", + "path": "aws-cdk-sagemaker-endpoint-alarms/InvocationsAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint-alarms/InvocationsAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 2, + "dimensions": [ + { + "name": "EndpointName", + "value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "name": "VariantName", + "value": "firstVariant" + } + ], + "metricName": "Invocations", + "namespace": "AWS/SageMaker", + "period": 300, + "statistic": "Sum", + "threshold": 1 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.Alarm", + "version": "0.0.0" + } + }, + "InvocationsPerInstanceAlarm": { + "id": "InvocationsPerInstanceAlarm", + "path": "aws-cdk-sagemaker-endpoint-alarms/InvocationsPerInstanceAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint-alarms/InvocationsPerInstanceAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 5, + "dimensions": [ + { + "name": "EndpointName", + "value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "name": "VariantName", + "value": "firstVariant" + } + ], + "metricName": "InvocationsPerInstance", + "namespace": "AWS/SageMaker", + "period": 300, + "statistic": "Sum", + "threshold": 4 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.Alarm", + "version": "0.0.0" + } + }, + "ModelLatencyAlarm": { + "id": "ModelLatencyAlarm", + "path": "aws-cdk-sagemaker-endpoint-alarms/ModelLatencyAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint-alarms/ModelLatencyAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 8, + "dimensions": [ + { + "name": "EndpointName", + "value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "name": "VariantName", + "value": "firstVariant" + } + ], + "metricName": "ModelLatency", + "namespace": "AWS/SageMaker", + "period": 300, + "statistic": "Average", + "threshold": 7 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.Alarm", + "version": "0.0.0" + } + }, + "OverheadLatencyAlarm": { + "id": "OverheadLatencyAlarm", + "path": "aws-cdk-sagemaker-endpoint-alarms/OverheadLatencyAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint-alarms/OverheadLatencyAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 11, + "dimensions": [ + { + "name": "EndpointName", + "value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "name": "VariantName", + "value": "firstVariant" + } + ], + "metricName": "OverheadLatency", + "namespace": "AWS/SageMaker", + "period": 300, + "statistic": "Average", + "threshold": 10 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.Alarm", + "version": "0.0.0" + } + }, + "Invocation5XXErrorsAlarm": { + "id": "Invocation5XXErrorsAlarm", + "path": "aws-cdk-sagemaker-endpoint-alarms/Invocation5XXErrorsAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint-alarms/Invocation5XXErrorsAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 14, + "dimensions": [ + { + "name": "EndpointName", + "value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "name": "VariantName", + "value": "firstVariant" + } + ], + "metricName": "Invocation5XXErrors", + "namespace": "AWS/SageMaker", + "period": 300, + "statistic": "Sum", + "threshold": 13 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.Alarm", + "version": "0.0.0" + } + }, + "DiskUtilizationAlarm": { + "id": "DiskUtilizationAlarm", + "path": "aws-cdk-sagemaker-endpoint-alarms/DiskUtilizationAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint-alarms/DiskUtilizationAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 17, + "dimensions": [ + { + "name": "EndpointName", + "value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "name": "VariantName", + "value": "firstVariant" + } + ], + "metricName": "DiskUtilization", + "namespace": "/aws/sagemaker/Endpoints", + "period": 300, + "statistic": "Average", + "threshold": 16 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.Alarm", + "version": "0.0.0" + } + }, + "CPUUtilizationAlarm": { + "id": "CPUUtilizationAlarm", + "path": "aws-cdk-sagemaker-endpoint-alarms/CPUUtilizationAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint-alarms/CPUUtilizationAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 20, + "dimensions": [ + { + "name": "EndpointName", + "value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "name": "VariantName", + "value": "firstVariant" + } + ], + "metricName": "CPUUtilization", + "namespace": "/aws/sagemaker/Endpoints", + "period": 300, + "statistic": "Average", + "threshold": 19 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.Alarm", + "version": "0.0.0" + } + }, + "MemoryUtilizationAlarm": { + "id": "MemoryUtilizationAlarm", + "path": "aws-cdk-sagemaker-endpoint-alarms/MemoryUtilizationAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint-alarms/MemoryUtilizationAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 23, + "dimensions": [ + { + "name": "EndpointName", + "value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "name": "VariantName", + "value": "firstVariant" + } + ], + "metricName": "MemoryUtilization", + "namespace": "/aws/sagemaker/Endpoints", + "period": 300, + "statistic": "Average", + "threshold": 22 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.Alarm", + "version": "0.0.0" + } + }, + "GPUUtilizationAlarm": { + "id": "GPUUtilizationAlarm", + "path": "aws-cdk-sagemaker-endpoint-alarms/GPUUtilizationAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint-alarms/GPUUtilizationAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 26, + "dimensions": [ + { + "name": "EndpointName", + "value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "name": "VariantName", + "value": "firstVariant" + } + ], + "metricName": "GPUUtilization", + "namespace": "/aws/sagemaker/Endpoints", + "period": 300, + "statistic": "Average", + "threshold": 25 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.Alarm", + "version": "0.0.0" + } + }, + "GPUMemoryUtilizationAlarm": { + "id": "GPUMemoryUtilizationAlarm", + "path": "aws-cdk-sagemaker-endpoint-alarms/GPUMemoryUtilizationAlarm", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint-alarms/GPUMemoryUtilizationAlarm/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudWatch::Alarm", + "aws:cdk:cloudformation:props": { + "comparisonOperator": "GreaterThanOrEqualToThreshold", + "evaluationPeriods": 29, + "dimensions": [ + { + "name": "EndpointName", + "value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + } + }, + { + "name": "VariantName", + "value": "firstVariant" + } + ], + "metricName": "GPUMemoryUtilization", + "namespace": "/aws/sagemaker/Endpoints", + "period": 300, + "statistic": "Average", + "threshold": 28 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.CfnAlarm", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-cloudwatch.Alarm", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-cdk-sagemaker-endpoint-alarms/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-cdk-sagemaker-endpoint-alarms/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "integtest-endpoint-alarms": { + "id": "integtest-endpoint-alarms", + "path": "integtest-endpoint-alarms", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "integtest-endpoint-alarms/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "integtest-endpoint-alarms/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.161" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "integtest-endpoint-alarms/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integtest-endpoint-alarms/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integtest-endpoint-alarms/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.161" + } + } + }, + "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.endpoint.alarms.ts b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.ts new file mode 100644 index 0000000000000..fd2bd60d80a75 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.alarms.ts @@ -0,0 +1,136 @@ +import * as path from 'path'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { IntegTest } from '@aws-cdk/integ-tests'; +import * as sagemaker from '../lib'; + +/* + * Stack verification steps: + * aws cloudwatch describe-alarms --query "MetricAlarms[?Namespace=='/aws/sagemaker/Endpoints' || Namespace=='AWS/SageMaker'].{MetricName: MetricName, Namespace: Namespace, Dimension: join(\`, \`, Dimensions[].Value)}" + * + * The above command will result in the following: + * + * [ + * { + * "MetricName": "CPUUtilization", + * "Namespace": "/aws/sagemaker/Endpoints", + * "Dimension": "Endpoint..., firstVariant" + * }, + * { + * "MetricName": "DiskUtilization", + * "Namespace": "/aws/sagemaker/Endpoints", + * "Dimension": "Endpoint..., firstVariant" + * }, + * { + * "MetricName": "GPUMemoryUtilization", + * "Namespace": "/aws/sagemaker/Endpoints", + * "Dimension": "Endpoint..., firstVariant" + * }, + * { + * "MetricName": "GPUUtilization", + * "Namespace": "/aws/sagemaker/Endpoints", + * "Dimension": "Endpoint..., firstVariant" + * }, + * { + * "MetricName": "Invocation5XXErrors", + * "Namespace": "AWS/SageMaker", + * "Dimension": "Endpoint..., firstVariant" + * }, + * { + * "MetricName": "Invocations", + * "Namespace": "AWS/SageMaker", + * "Dimension": "Endpoint..., firstVariant" + * }, + * { + * "MetricName": "InvocationsPerInstance", + * "Namespace": "AWS/SageMaker", + * "Dimension": "Endpoint..., firstVariant" + * }, + * { + * "MetricName": "MemoryUtilization", + * "Namespace": "/aws/sagemaker/Endpoints", + * "Dimension": "Endpoint..., firstVariant" + * }, + * { + * "MetricName": "ModelLatency", + * "Namespace": "AWS/SageMaker", + * "Dimension": "Endpoint..., firstVariant" + * }, + * { + * "MetricName": "OverheadLatency", + * "Namespace": "AWS/SageMaker", + * "Dimension": "Endpoint..., firstVariant" + * } + * ] + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-sagemaker-endpoint-alarms'); + +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 modelWithArtifact = new sagemaker.Model(stack, 'ModelWithArtifact', { + containers: [{ image, modelData }], +}); + +const endpointConfig = new sagemaker.EndpointConfig(stack, 'EndpointConfig', { + instanceProductionVariants: [ + { + model: modelWithArtifact, + variantName: 'firstVariant', + }, + ], +}); +const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { endpointConfig }); + +const invokerRole = new iam.Role(stack, 'Invoker', { + assumedBy: new iam.AccountRootPrincipal(), +}); +endpoint.grantInvoke(invokerRole); + +const productionVariant = endpoint.findInstanceProductionVariant('firstVariant'); +productionVariant.metricInvocations().createAlarm(stack, 'InvocationsAlarm', { + threshold: 1, + evaluationPeriods: 2, +}); +productionVariant.metricInvocationsPerInstance().createAlarm(stack, 'InvocationsPerInstanceAlarm', { + threshold: 4, + evaluationPeriods: 5, +}); +productionVariant.metricModelLatency().createAlarm(stack, 'ModelLatencyAlarm', { + threshold: 7, + evaluationPeriods: 8, +}); +productionVariant.metricOverheadLatency().createAlarm(stack, 'OverheadLatencyAlarm', { + threshold: 10, + evaluationPeriods: 11, +}); +productionVariant.metricInvocationResponseCode(sagemaker.InvocationHttpResponseCode.INVOCATION_5XX_ERRORS).createAlarm(stack, 'Invocation5XXErrorsAlarm', { + threshold: 13, + evaluationPeriods: 14, +}); +productionVariant.metricDiskUtilization().createAlarm(stack, 'DiskUtilizationAlarm', { + threshold: 16, + evaluationPeriods: 17, +}); +productionVariant.metricCpuUtilization().createAlarm(stack, 'CPUUtilizationAlarm', { + threshold: 19, + evaluationPeriods: 20, +}); +productionVariant.metricMemoryUtilization().createAlarm(stack, 'MemoryUtilizationAlarm', { + threshold: 22, + evaluationPeriods: 23, +}); +productionVariant.metricGpuUtilization().createAlarm(stack, 'GPUUtilizationAlarm', { + threshold: 25, + evaluationPeriods: 26, +}); +productionVariant.metricGpuMemoryUtilization().createAlarm(stack, 'GPUMemoryUtilizationAlarm', { + threshold: 28, + evaluationPeriods: 29, +}); + +new IntegTest(app, 'integtest-endpoint-alarms', { + testCases: [stack], +}); diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/asset.126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.tar.gz b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.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.endpoint.js.snapshot/asset.126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.tar.gz differ diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/Dockerfile b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/Dockerfile new file mode 100644 index 0000000000000..7eb03f499c9c6 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.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.endpoint.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.html b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.html new file mode 100644 index 0000000000000..3b18e512dba79 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.html @@ -0,0 +1 @@ +hello world diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.py b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/asset.442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a/index.py new file mode 100644 index 0000000000000..f5ab0458f1cb1 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.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.endpoint.js.snapshot/asset.b54b99043c35bd080b9d9d1afce31e3541cf15b679799ba980ed40c837dcb03b.bundle/index.js b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/asset.b54b99043c35bd080b9d9d1afce31e3541cf15b679799ba980ed40c837dcb03b.bundle/index.js new file mode 100644 index 0000000000000..2d6c2f0e85497 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/asset.b54b99043c35bd080b9d9d1afce31e3541cf15b679799ba980ed40c837dcb03b.bundle/index.js @@ -0,0 +1,768 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// lib/assertions/providers/lambda-handler/index.ts +var lambda_handler_exports = {}; +__export(lambda_handler_exports, { + handler: () => handler, + isComplete: () => isComplete, + onTimeout: () => onTimeout +}); +module.exports = __toCommonJS(lambda_handler_exports); + +// ../assertions/lib/matcher.ts +var Matcher = class { + static isMatcher(x) { + return x && x instanceof Matcher; + } +}; +var MatchResult = class { + constructor(target) { + this.failures = []; + this.captures = /* @__PURE__ */ new Map(); + this.finalized = false; + this.target = target; + } + push(matcher, path, message) { + return this.recordFailure({ matcher, path, message }); + } + recordFailure(failure) { + this.failures.push(failure); + return this; + } + hasFailed() { + return this.failures.length !== 0; + } + get failCount() { + return this.failures.length; + } + compose(id, inner) { + const innerF = inner.failures; + this.failures.push(...innerF.map((f) => { + return { path: [id, ...f.path], message: f.message, matcher: f.matcher }; + })); + inner.captures.forEach((vals, capture) => { + vals.forEach((value) => this.recordCapture({ capture, value })); + }); + return this; + } + finished() { + if (this.finalized) { + return this; + } + if (this.failCount === 0) { + this.captures.forEach((vals, cap) => cap._captured.push(...vals)); + } + this.finalized = true; + return this; + } + toHumanStrings() { + return this.failures.map((r) => { + const loc = r.path.length === 0 ? "" : ` at ${r.path.join("")}`; + return "" + r.message + loc + ` (using ${r.matcher.name} matcher)`; + }); + } + recordCapture(options) { + let values = this.captures.get(options.capture); + if (values === void 0) { + values = []; + } + values.push(options.value); + this.captures.set(options.capture, values); + } +}; + +// ../assertions/lib/private/matchers/absent.ts +var AbsentMatch = class extends Matcher { + constructor(name) { + super(); + this.name = name; + } + test(actual) { + const result = new MatchResult(actual); + if (actual !== void 0) { + result.recordFailure({ + matcher: this, + path: [], + message: `Received ${actual}, but key should be absent` + }); + } + return result; + } +}; + +// ../assertions/lib/private/type.ts +function getType(obj) { + return Array.isArray(obj) ? "array" : typeof obj; +} + +// ../assertions/lib/match.ts +var Match = class { + static absent() { + return new AbsentMatch("absent"); + } + static arrayWith(pattern) { + return new ArrayMatch("arrayWith", pattern); + } + static arrayEquals(pattern) { + return new ArrayMatch("arrayEquals", pattern, { subsequence: false }); + } + static exact(pattern) { + return new LiteralMatch("exact", pattern, { partialObjects: false }); + } + static objectLike(pattern) { + return new ObjectMatch("objectLike", pattern); + } + static objectEquals(pattern) { + return new ObjectMatch("objectEquals", pattern, { partial: false }); + } + static not(pattern) { + return new NotMatch("not", pattern); + } + static serializedJson(pattern) { + return new SerializedJson("serializedJson", pattern); + } + static anyValue() { + return new AnyMatch("anyValue"); + } + static stringLikeRegexp(pattern) { + return new StringLikeRegexpMatch("stringLikeRegexp", pattern); + } +}; +var LiteralMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.partialObjects = options.partialObjects ?? false; + if (Matcher.isMatcher(this.pattern)) { + throw new Error("LiteralMatch cannot directly contain another matcher. Remove the top-level matcher or nest it more deeply."); + } + } + test(actual) { + if (Array.isArray(this.pattern)) { + return new ArrayMatch(this.name, this.pattern, { subsequence: false, partialObjects: this.partialObjects }).test(actual); + } + if (typeof this.pattern === "object") { + return new ObjectMatch(this.name, this.pattern, { partial: this.partialObjects }).test(actual); + } + const result = new MatchResult(actual); + if (typeof this.pattern !== typeof actual) { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected type ${typeof this.pattern} but received ${getType(actual)}` + }); + return result; + } + if (actual !== this.pattern) { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected ${this.pattern} but received ${actual}` + }); + } + return result; + } +}; +var ArrayMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.subsequence = options.subsequence ?? true; + this.partialObjects = options.partialObjects ?? false; + } + test(actual) { + if (!Array.isArray(actual)) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected type array but received ${getType(actual)}` + }); + } + if (!this.subsequence && this.pattern.length !== actual.length) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected array of length ${this.pattern.length} but received ${actual.length}` + }); + } + let patternIdx = 0; + let actualIdx = 0; + const result = new MatchResult(actual); + while (patternIdx < this.pattern.length && actualIdx < actual.length) { + const patternElement = this.pattern[patternIdx]; + const matcher = Matcher.isMatcher(patternElement) ? patternElement : new LiteralMatch(this.name, patternElement, { partialObjects: this.partialObjects }); + const matcherName = matcher.name; + if (this.subsequence && (matcherName == "absent" || matcherName == "anyValue")) { + throw new Error(`The Matcher ${matcherName}() cannot be nested within arrayWith()`); + } + const innerResult = matcher.test(actual[actualIdx]); + if (!this.subsequence || !innerResult.hasFailed()) { + result.compose(`[${actualIdx}]`, innerResult); + patternIdx++; + actualIdx++; + } else { + actualIdx++; + } + } + for (; patternIdx < this.pattern.length; patternIdx++) { + const pattern = this.pattern[patternIdx]; + const element = Matcher.isMatcher(pattern) || typeof pattern === "object" ? " " : ` [${pattern}] `; + result.recordFailure({ + matcher: this, + path: [], + message: `Missing element${element}at pattern index ${patternIdx}` + }); + } + return result; + } +}; +var ObjectMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.partial = options.partial ?? true; + } + test(actual) { + if (typeof actual !== "object" || Array.isArray(actual)) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected type object but received ${getType(actual)}` + }); + } + const result = new MatchResult(actual); + if (!this.partial) { + for (const a of Object.keys(actual)) { + if (!(a in this.pattern)) { + result.recordFailure({ + matcher: this, + path: [`/${a}`], + message: "Unexpected key" + }); + } + } + } + for (const [patternKey, patternVal] of Object.entries(this.pattern)) { + if (!(patternKey in actual) && !(patternVal instanceof AbsentMatch)) { + result.recordFailure({ + matcher: this, + path: [`/${patternKey}`], + message: `Missing key '${patternKey}' among {${Object.keys(actual).join(",")}}` + }); + continue; + } + const matcher = Matcher.isMatcher(patternVal) ? patternVal : new LiteralMatch(this.name, patternVal, { partialObjects: this.partial }); + const inner = matcher.test(actual[patternKey]); + result.compose(`/${patternKey}`, inner); + } + return result; + } +}; +var SerializedJson = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const result = new MatchResult(actual); + if (getType(actual) !== "string") { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected JSON as a string but found ${getType(actual)}` + }); + return result; + } + let parsed; + try { + parsed = JSON.parse(actual); + } catch (err) { + if (err instanceof SyntaxError) { + result.recordFailure({ + matcher: this, + path: [], + message: `Invalid JSON string: ${actual}` + }); + return result; + } else { + throw err; + } + } + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + const innerResult = matcher.test(parsed); + result.compose(`(${this.name})`, innerResult); + return result; + } +}; +var NotMatch = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + const innerResult = matcher.test(actual); + const result = new MatchResult(actual); + if (innerResult.failCount === 0) { + result.recordFailure({ + matcher: this, + path: [], + message: `Found unexpected match: ${JSON.stringify(actual, void 0, 2)}` + }); + } + return result; + } +}; +var AnyMatch = class extends Matcher { + constructor(name) { + super(); + this.name = name; + } + test(actual) { + const result = new MatchResult(actual); + if (actual == null) { + result.recordFailure({ + matcher: this, + path: [], + message: "Expected a value but found none" + }); + } + return result; + } +}; +var StringLikeRegexpMatch = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const result = new MatchResult(actual); + const regex = new RegExp(this.pattern, "gm"); + if (typeof actual !== "string") { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected a string, but got '${typeof actual}'` + }); + } + if (!regex.test(actual)) { + result.recordFailure({ + matcher: this, + path: [], + message: `String '${actual}' did not match pattern '${this.pattern}'` + }); + } + return result; + } +}; + +// lib/assertions/providers/lambda-handler/base.ts +var https = __toESM(require("https")); +var url = __toESM(require("url")); +var AWS = __toESM(require("aws-sdk")); +var CustomResourceHandler = class { + constructor(event, context) { + this.event = event; + this.context = context; + this.timedOut = false; + this.timeout = setTimeout(async () => { + await this.respond({ + status: "FAILED", + reason: "Lambda Function Timeout", + data: this.context.logStreamName + }); + this.timedOut = true; + }, context.getRemainingTimeInMillis() - 1200); + this.event = event; + this.physicalResourceId = extractPhysicalResourceId(event); + } + async handle() { + try { + if ("stateMachineArn" in this.event.ResourceProperties) { + const req = { + stateMachineArn: this.event.ResourceProperties.stateMachineArn, + name: this.event.RequestId, + input: JSON.stringify(this.event) + }; + await this.startExecution(req); + return; + } else { + const response = await this.processEvent(this.event.ResourceProperties); + return response; + } + } catch (e) { + console.log(e); + throw e; + } finally { + clearTimeout(this.timeout); + } + } + async handleIsComplete() { + try { + const result = await this.processEvent(this.event.ResourceProperties); + return result; + } catch (e) { + console.log(e); + return; + } finally { + clearTimeout(this.timeout); + } + } + async startExecution(req) { + try { + const sfn = new AWS.StepFunctions(); + await sfn.startExecution(req).promise(); + } finally { + clearTimeout(this.timeout); + } + } + respond(response) { + if (this.timedOut) { + return; + } + const cfResponse = { + Status: response.status, + Reason: response.reason, + PhysicalResourceId: this.physicalResourceId, + StackId: this.event.StackId, + RequestId: this.event.RequestId, + LogicalResourceId: this.event.LogicalResourceId, + NoEcho: false, + Data: response.data + }; + const responseBody = JSON.stringify(cfResponse); + console.log("Responding to CloudFormation", responseBody); + const parsedUrl = url.parse(this.event.ResponseURL); + const requestOptions = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: "PUT", + headers: { "content-type": "", "content-length": responseBody.length } + }; + return new Promise((resolve, reject) => { + try { + const request2 = https.request(requestOptions, resolve); + request2.on("error", reject); + request2.write(responseBody); + request2.end(); + } catch (e) { + reject(e); + } finally { + clearTimeout(this.timeout); + } + }); + } +}; +function extractPhysicalResourceId(event) { + switch (event.RequestType) { + case "Create": + return event.LogicalResourceId; + case "Update": + case "Delete": + return event.PhysicalResourceId; + } +} + +// lib/assertions/providers/lambda-handler/assertion.ts +var AssertionHandler = class extends CustomResourceHandler { + async processEvent(request2) { + let actual = decodeCall(request2.actual); + const expected = decodeCall(request2.expected); + let result; + const matcher = new MatchCreator(expected).getMatcher(); + console.log(`Testing equality between ${JSON.stringify(request2.actual)} and ${JSON.stringify(request2.expected)}`); + const matchResult = matcher.test(actual); + matchResult.finished(); + if (matchResult.hasFailed()) { + result = { + failed: true, + assertion: JSON.stringify({ + status: "fail", + message: [ + ...matchResult.toHumanStrings(), + JSON.stringify(matchResult.target, void 0, 2) + ].join("\n") + }) + }; + if (request2.failDeployment) { + throw new Error(result.assertion); + } + } else { + result = { + assertion: JSON.stringify({ + status: "success" + }) + }; + } + return result; + } +}; +var MatchCreator = class { + constructor(obj) { + this.parsedObj = { + matcher: obj + }; + } + getMatcher() { + try { + const final = JSON.parse(JSON.stringify(this.parsedObj), function(_k, v) { + const nested = Object.keys(v)[0]; + switch (nested) { + case "$ArrayWith": + return Match.arrayWith(v[nested]); + case "$ObjectLike": + return Match.objectLike(v[nested]); + case "$StringLike": + return Match.stringLikeRegexp(v[nested]); + default: + return v; + } + }); + if (Matcher.isMatcher(final.matcher)) { + return final.matcher; + } + return Match.exact(final.matcher); + } catch { + return Match.exact(this.parsedObj.matcher); + } + } +}; +function decodeCall(call) { + if (!call) { + return void 0; + } + try { + const parsed = JSON.parse(call); + return parsed; + } catch (e) { + return call; + } +} + +// lib/assertions/providers/lambda-handler/utils.ts +function decode(object) { + return JSON.parse(JSON.stringify(object), (_k, v) => { + switch (v) { + case "TRUE:BOOLEAN": + return true; + case "FALSE:BOOLEAN": + return false; + default: + return v; + } + }); +} + +// lib/assertions/providers/lambda-handler/sdk.ts +function flatten(object) { + return Object.assign( + {}, + ...function _flatten(child, path = []) { + return [].concat(...Object.keys(child).map((key) => { + let childKey = Buffer.isBuffer(child[key]) ? child[key].toString("utf8") : child[key]; + if (typeof childKey === "string") { + childKey = isJsonString(childKey); + } + return typeof childKey === "object" && childKey !== null ? _flatten(childKey, path.concat([key])) : { [path.concat([key]).join(".")]: childKey }; + })); + }(object) + ); +} +var AwsApiCallHandler = class extends CustomResourceHandler { + async processEvent(request2) { + const AWS2 = require("aws-sdk"); + console.log(`AWS SDK VERSION: ${AWS2.VERSION}`); + if (!Object.prototype.hasOwnProperty.call(AWS2, request2.service)) { + throw Error(`Service ${request2.service} does not exist in AWS SDK version ${AWS2.VERSION}.`); + } + const service = new AWS2[request2.service](); + const response = await service[request2.api](request2.parameters && decode(request2.parameters)).promise(); + console.log(`SDK response received ${JSON.stringify(response)}`); + delete response.ResponseMetadata; + const respond = { + apiCallResponse: response + }; + const flatData = { + ...flatten(respond) + }; + const resp = request2.flattenResponse === "true" ? flatData : respond; + console.log(`Returning result ${JSON.stringify(resp)}`); + return resp; + } +}; +function isJsonString(value) { + try { + return JSON.parse(value); + } catch { + return value; + } +} + +// lib/assertions/providers/lambda-handler/types.ts +var ASSERT_RESOURCE_TYPE = "Custom::DeployAssert@AssertEquals"; +var SDK_RESOURCE_TYPE_PREFIX = "Custom::DeployAssert@SdkCall"; + +// lib/assertions/providers/lambda-handler/index.ts +async function handler(event, context) { + console.log(`Event: ${JSON.stringify({ ...event, ResponseURL: "..." })}`); + const provider = createResourceHandler(event, context); + try { + if (event.RequestType === "Delete") { + await provider.respond({ + status: "SUCCESS", + reason: "OK" + }); + return; + } + const result = await provider.handle(); + if ("stateMachineArn" in event.ResourceProperties) { + console.info('Found "stateMachineArn", waiter statemachine started'); + return; + } else if ("expected" in event.ResourceProperties) { + console.info('Found "expected", testing assertions'); + const actualPath = event.ResourceProperties.actualPath; + const actual = actualPath ? result[`apiCallResponse.${actualPath}`] : result.apiCallResponse; + const assertion = new AssertionHandler({ + ...event, + ResourceProperties: { + ServiceToken: event.ServiceToken, + actual, + expected: event.ResourceProperties.expected + } + }, context); + try { + const assertionResult = await assertion.handle(); + await provider.respond({ + status: "SUCCESS", + reason: "OK", + data: { + ...assertionResult, + ...result + } + }); + return; + } catch (e) { + await provider.respond({ + status: "FAILED", + reason: e.message ?? "Internal Error" + }); + return; + } + } + await provider.respond({ + status: "SUCCESS", + reason: "OK", + data: result + }); + } catch (e) { + await provider.respond({ + status: "FAILED", + reason: e.message ?? "Internal Error" + }); + return; + } + return; +} +async function onTimeout(timeoutEvent) { + const isCompleteRequest = JSON.parse(JSON.parse(timeoutEvent.Cause).errorMessage); + const provider = createResourceHandler(isCompleteRequest, standardContext); + await provider.respond({ + status: "FAILED", + reason: "Operation timed out: " + JSON.stringify(isCompleteRequest) + }); +} +async function isComplete(event, context) { + console.log(`Event: ${JSON.stringify({ ...event, ResponseURL: "..." })}`); + const provider = createResourceHandler(event, context); + try { + const result = await provider.handleIsComplete(); + const actualPath = event.ResourceProperties.actualPath; + if (result) { + const actual = actualPath ? result[`apiCallResponse.${actualPath}`] : result.apiCallResponse; + if ("expected" in event.ResourceProperties) { + const assertion = new AssertionHandler({ + ...event, + ResourceProperties: { + ServiceToken: event.ServiceToken, + actual, + expected: event.ResourceProperties.expected + } + }, context); + const assertionResult = await assertion.handleIsComplete(); + if (!(assertionResult == null ? void 0 : assertionResult.failed)) { + await provider.respond({ + status: "SUCCESS", + reason: "OK", + data: { + ...assertionResult, + ...result + } + }); + return; + } else { + console.log(`Assertion Failed: ${JSON.stringify(assertionResult)}`); + throw new Error(JSON.stringify(event)); + } + } + await provider.respond({ + status: "SUCCESS", + reason: "OK", + data: result + }); + } else { + console.log("No result"); + throw new Error(JSON.stringify(event)); + } + return; + } catch (e) { + console.log(e); + throw new Error(JSON.stringify(event)); + } +} +function createResourceHandler(event, context) { + if (event.ResourceType.startsWith(SDK_RESOURCE_TYPE_PREFIX)) { + return new AwsApiCallHandler(event, context); + } else if (event.ResourceType.startsWith(ASSERT_RESOURCE_TYPE)) { + return new AssertionHandler(event, context); + } else { + throw new Error(`Unsupported resource type "${event.ResourceType}`); + } +} +var standardContext = { + getRemainingTimeInMillis: () => 9e4 +}; +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + handler, + isComplete, + onTimeout +}); diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/aws-cdk-sagemaker-endpoint.assets.json b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/aws-cdk-sagemaker-endpoint.assets.json new file mode 100644 index 0000000000000..d0a6a22b53815 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/aws-cdk-sagemaker-endpoint.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}" + } + } + }, + "2b5895573703338c7a63f348fee88fea1fbc97fde8e8eac9970b0cf433455b78": { + "source": { + "path": "aws-cdk-sagemaker-endpoint.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "2b5895573703338c7a63f348fee88fea1fbc97fde8e8eac9970b0cf433455b78.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.endpoint.js.snapshot/aws-cdk-sagemaker-endpoint.template.json b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/aws-cdk-sagemaker-endpoint.template.json new file mode 100644 index 0000000000000..c8ee42be60b0f --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/aws-cdk-sagemaker-endpoint.template.json @@ -0,0 +1,466 @@ +{ + "Resources": { + "ModelWithArtifactRole854ED848": { + "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" + ] + ] + } + ] + } + }, + "ModelWithArtifactRoleDefaultPolicy63A6F076": { + "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": "ModelWithArtifactRoleDefaultPolicy63A6F076", + "Roles": [ + { + "Ref": "ModelWithArtifactRole854ED848" + } + ] + } + }, + "ModelWithArtifactModel7EBC6421": { + "Type": "AWS::SageMaker::Model", + "Properties": { + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "ModelWithArtifactRole854ED848", + "Arn" + ] + }, + "PrimaryContainer": { + "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": [ + "ModelWithArtifactRoleDefaultPolicy63A6F076", + "ModelWithArtifactRole854ED848" + ] + }, + "ModelWithoutArtifactRoleB9EF84FD": { + "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" + ] + ] + } + ] + } + }, + "ModelWithoutArtifactRoleDefaultPolicyCFDF486F": { + "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": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ModelWithoutArtifactRoleDefaultPolicyCFDF486F", + "Roles": [ + { + "Ref": "ModelWithoutArtifactRoleB9EF84FD" + } + ] + } + }, + "ModelWithoutArtifactModel9AB56FA5": { + "Type": "AWS::SageMaker::Model", + "Properties": { + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "ModelWithoutArtifactRoleB9EF84FD", + "Arn" + ] + }, + "PrimaryContainer": { + "Image": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + } + } + }, + "DependsOn": [ + "ModelWithoutArtifactRoleDefaultPolicyCFDF486F", + "ModelWithoutArtifactRoleB9EF84FD" + ] + }, + "EndpointConfigFD7B6F91": { + "Type": "AWS::SageMaker::EndpointConfig", + "Properties": { + "ProductionVariants": [ + { + "InitialInstanceCount": 1, + "InitialVariantWeight": 1, + "InstanceType": "ml.m5.large", + "ModelName": { + "Fn::GetAtt": [ + "ModelWithArtifactModel7EBC6421", + "ModelName" + ] + }, + "VariantName": "firstVariant" + }, + { + "InitialInstanceCount": 1, + "InitialVariantWeight": 1, + "InstanceType": "ml.t2.medium", + "ModelName": { + "Fn::GetAtt": [ + "ModelWithArtifactModel7EBC6421", + "ModelName" + ] + }, + "VariantName": "secondVariant" + }, + { + "InitialInstanceCount": 1, + "InitialVariantWeight": 2, + "InstanceType": "ml.t2.medium", + "ModelName": { + "Fn::GetAtt": [ + "ModelWithoutArtifactModel9AB56FA5", + "ModelName" + ] + }, + "VariantName": "thirdVariant" + } + ] + } + }, + "Endpoint8024A810": { + "Type": "AWS::SageMaker::Endpoint", + "Properties": { + "EndpointConfigName": { + "Fn::GetAtt": [ + "EndpointConfigFD7B6F91", + "EndpointConfigName" + ] + } + } + }, + "EndpointInstanceCountTargetDA8C8EDB": { + "Type": "AWS::ApplicationAutoScaling::ScalableTarget", + "Properties": { + "MaxCapacity": 3, + "MinCapacity": 1, + "ResourceId": { + "Fn::Join": [ + "", + [ + "endpoint/", + { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + }, + "/variant/firstVariant" + ] + ] + }, + "RoleARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/aws-service-role/sagemaker.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_SageMakerEndpoint" + ] + ] + }, + "ScalableDimension": "sagemaker:variant:DesiredInstanceCount", + "ServiceNamespace": "sagemaker" + } + }, + "EndpointInstanceCountTargetLimitRPSE1D92DB6": { + "Type": "AWS::ApplicationAutoScaling::ScalingPolicy", + "Properties": { + "PolicyName": "awscdksagemakerendpointEndpointInstanceCountTargetLimitRPSCC857664", + "PolicyType": "TargetTrackingScaling", + "ScalingTargetId": { + "Ref": "EndpointInstanceCountTargetDA8C8EDB" + }, + "TargetTrackingScalingPolicyConfiguration": { + "PredefinedMetricSpecification": { + "PredefinedMetricType": "SageMakerVariantInvocationsPerInstance" + }, + "TargetValue": 900 + } + } + }, + "Invoker060A9026": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "InvokerDefaultPolicy3FF8208D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sagemaker:InvokeEndpoint", + "Effect": "Allow", + "Resource": { + "Ref": "Endpoint8024A810" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "InvokerDefaultPolicy3FF8208D", + "Roles": [ + { + "Ref": "Invoker060A9026" + } + ] + } + } + }, + "Outputs": { + "ExportsOutputFnGetAttEndpoint8024A810EndpointNameC77E4EFF": { + "Value": { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + }, + "Export": { + "Name": "aws-cdk-sagemaker-endpoint:ExportsOutputFnGetAttEndpoint8024A810EndpointNameC77E4EFF" + } + } + }, + "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.endpoint.js.snapshot/cdk.out b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/cdk.out new file mode 100644 index 0000000000000..8ecc185e9dbee --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.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.endpoint.js.snapshot/integ.json b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/integ.json new file mode 100644 index 0000000000000..aa84d13a7cc58 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "21.0.0", + "testCases": { + "integtest-endpoint/DefaultTest": { + "stacks": [ + "aws-cdk-sagemaker-endpoint" + ], + "assertionStack": "integtest-endpoint/DefaultTest/DeployAssert", + "assertionStackName": "integtestendpointDefaultTestDeployAssert02E18E07" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/integtestendpointDefaultTestDeployAssert02E18E07.assets.json b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/integtestendpointDefaultTestDeployAssert02E18E07.assets.json new file mode 100644 index 0000000000000..626a1b069e165 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/integtestendpointDefaultTestDeployAssert02E18E07.assets.json @@ -0,0 +1,32 @@ +{ + "version": "21.0.0", + "files": { + "b54b99043c35bd080b9d9d1afce31e3541cf15b679799ba980ed40c837dcb03b": { + "source": { + "path": "asset.b54b99043c35bd080b9d9d1afce31e3541cf15b679799ba980ed40c837dcb03b.bundle", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "b54b99043c35bd080b9d9d1afce31e3541cf15b679799ba980ed40c837dcb03b.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "2267da2b834d1285fe97eee4e496aac42962dd72c087eecb46cc7edb4a4c3b82": { + "source": { + "path": "integtestendpointDefaultTestDeployAssert02E18E07.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "2267da2b834d1285fe97eee4e496aac42962dd72c087eecb46cc7edb4a4c3b82.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.endpoint.js.snapshot/integtestendpointDefaultTestDeployAssert02E18E07.template.json b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/integtestendpointDefaultTestDeployAssert02E18E07.template.json new file mode 100644 index 0000000000000..70058c739fa45 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/integtestendpointDefaultTestDeployAssert02E18E07.template.json @@ -0,0 +1,140 @@ +{ + "Resources": { + "AwsApiCallSageMakerRuntimeinvokeEndpoint": { + "Type": "Custom::DeployAssert@SdkCallSageMakerRuntimeinvokeEndpoint", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "service": "SageMakerRuntime", + "api": "invokeEndpoint", + "expected": "{\"$StringLike\":\"Artifact info:.*This file exists for test purposes only and is not a real SageMaker Model artifact\"}", + "actualPath": "Body", + "parameters": { + "EndpointName": { + "Fn::ImportValue": "aws-cdk-sagemaker-endpoint:ExportsOutputFnGetAttEndpoint8024A810EndpointNameC77E4EFF" + }, + "TargetVariant": "firstVariant", + "Body": "any string" + }, + "flattenResponse": "true", + "salt": "1669239283795" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "runtime.sagemaker:InvokeEndpoint" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + }, + { + "Effect": "Allow", + "Action": "sagemaker:InvokeEndpoint", + "Resource": "*" + } + ] + } + } + ] + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "b54b99043c35bd080b9d9d1afce31e3541cf15b679799ba980ed40c837dcb03b.zip" + }, + "Timeout": 120, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73", + "Arn" + ] + } + } + } + }, + "Outputs": { + "AssertionResultsAwsApiCallSageMakerRuntimeinvokeEndpoint": { + "Value": { + "Fn::GetAtt": [ + "AwsApiCallSageMakerRuntimeinvokeEndpoint", + "assertion" + ] + } + } + }, + "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.endpoint.js.snapshot/manifest.json b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/manifest.json new file mode 100644 index 0000000000000..f4404b75c88c7 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/manifest.json @@ -0,0 +1,208 @@ +{ + "version": "21.0.0", + "artifacts": { + "aws-cdk-sagemaker-endpoint.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-sagemaker-endpoint.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-sagemaker-endpoint": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-sagemaker-endpoint.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}/2b5895573703338c7a63f348fee88fea1fbc97fde8e8eac9970b0cf433455b78.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-sagemaker-endpoint.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-endpoint.assets" + ], + "metadata": { + "/aws-cdk-sagemaker-endpoint/ModelWithArtifact/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ModelWithArtifactRole854ED848" + } + ], + "/aws-cdk-sagemaker-endpoint/ModelWithArtifact/Role/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ModelWithArtifactRoleDefaultPolicy63A6F076" + } + ], + "/aws-cdk-sagemaker-endpoint/ModelWithArtifact/Model": [ + { + "type": "aws:cdk:logicalId", + "data": "ModelWithArtifactModel7EBC6421" + } + ], + "/aws-cdk-sagemaker-endpoint/ModelWithoutArtifact/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ModelWithoutArtifactRoleB9EF84FD" + } + ], + "/aws-cdk-sagemaker-endpoint/ModelWithoutArtifact/Role/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ModelWithoutArtifactRoleDefaultPolicyCFDF486F" + } + ], + "/aws-cdk-sagemaker-endpoint/ModelWithoutArtifact/Model": [ + { + "type": "aws:cdk:logicalId", + "data": "ModelWithoutArtifactModel9AB56FA5" + } + ], + "/aws-cdk-sagemaker-endpoint/EndpointConfig/EndpointConfig": [ + { + "type": "aws:cdk:logicalId", + "data": "EndpointConfigFD7B6F91" + } + ], + "/aws-cdk-sagemaker-endpoint/Endpoint/Endpoint": [ + { + "type": "aws:cdk:logicalId", + "data": "Endpoint8024A810" + } + ], + "/aws-cdk-sagemaker-endpoint/Endpoint/InstanceCount/Target/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "EndpointInstanceCountTargetDA8C8EDB" + } + ], + "/aws-cdk-sagemaker-endpoint/Endpoint/InstanceCount/Target/LimitRPS/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "EndpointInstanceCountTargetLimitRPSE1D92DB6" + } + ], + "/aws-cdk-sagemaker-endpoint/Invoker/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Invoker060A9026" + } + ], + "/aws-cdk-sagemaker-endpoint/Invoker/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "InvokerDefaultPolicy3FF8208D" + } + ], + "/aws-cdk-sagemaker-endpoint/Exports/Output{\"Fn::GetAtt\":[\"Endpoint8024A810\",\"EndpointName\"]}": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportsOutputFnGetAttEndpoint8024A810EndpointNameC77E4EFF" + } + ], + "/aws-cdk-sagemaker-endpoint/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-sagemaker-endpoint/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-sagemaker-endpoint" + }, + "integtestendpointDefaultTestDeployAssert02E18E07.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integtestendpointDefaultTestDeployAssert02E18E07.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integtestendpointDefaultTestDeployAssert02E18E07": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integtestendpointDefaultTestDeployAssert02E18E07.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}/2267da2b834d1285fe97eee4e496aac42962dd72c087eecb46cc7edb4a4c3b82.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integtestendpointDefaultTestDeployAssert02E18E07.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-endpoint", + "integtestendpointDefaultTestDeployAssert02E18E07.assets" + ], + "metadata": { + "/integtest-endpoint/DefaultTest/DeployAssert/AwsApiCallSageMakerRuntimeinvokeEndpoint/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsApiCallSageMakerRuntimeinvokeEndpoint" + } + ], + "/integtest-endpoint/DefaultTest/DeployAssert/AwsApiCallSageMakerRuntimeinvokeEndpoint/AssertionResults": [ + { + "type": "aws:cdk:logicalId", + "data": "AssertionResultsAwsApiCallSageMakerRuntimeinvokeEndpoint" + } + ], + "/integtest-endpoint/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73" + } + ], + "/integtest-endpoint/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F" + } + ], + "/integtest-endpoint/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integtest-endpoint/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integtest-endpoint/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.endpoint.js.snapshot/tree.json b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/tree.json new file mode 100644 index 0000000000000..c31cd13af4156 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.js.snapshot/tree.json @@ -0,0 +1,924 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-cdk-sagemaker-endpoint": { + "id": "aws-cdk-sagemaker-endpoint", + "path": "aws-cdk-sagemaker-endpoint", + "children": { + "ModelWithArtifact": { + "id": "ModelWithArtifact", + "path": "aws-cdk-sagemaker-endpoint/ModelWithArtifact", + "children": { + "Role": { + "id": "Role", + "path": "aws-cdk-sagemaker-endpoint/ModelWithArtifact/Role", + "children": { + "ImportRole": { + "id": "ImportRole", + "path": "aws-cdk-sagemaker-endpoint/ModelWithArtifact/Role/ImportRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint/ModelWithArtifact/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-endpoint/ModelWithArtifact/Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint/ModelWithArtifact/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": "ModelWithArtifactRoleDefaultPolicy63A6F076", + "roles": [ + { + "Ref": "ModelWithArtifactRole854ED848" + } + ] + } + }, + "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-endpoint/ModelWithArtifact/ModelImage7ffa8020b99fe9d130a903251c36866d", + "children": { + "Staging": { + "id": "Staging", + "path": "aws-cdk-sagemaker-endpoint/ModelWithArtifact/ModelImage7ffa8020b99fe9d130a903251c36866d/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Repository": { + "id": "Repository", + "path": "aws-cdk-sagemaker-endpoint/ModelWithArtifact/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-endpoint/ModelWithArtifact/ModelData412d61f9c984d1aff5ee358daf994d58", + "children": { + "Stage": { + "id": "Stage", + "path": "aws-cdk-sagemaker-endpoint/ModelWithArtifact/ModelData412d61f9c984d1aff5ee358daf994d58/Stage", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "aws-cdk-sagemaker-endpoint/ModelWithArtifact/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-endpoint/ModelWithArtifact/Model", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SageMaker::Model", + "aws:cdk:cloudformation:props": { + "executionRoleArn": { + "Fn::GetAtt": [ + "ModelWithArtifactRole854ED848", + "Arn" + ] + }, + "primaryContainer": { + "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" + } + }, + "ModelWithoutArtifact": { + "id": "ModelWithoutArtifact", + "path": "aws-cdk-sagemaker-endpoint/ModelWithoutArtifact", + "children": { + "Role": { + "id": "Role", + "path": "aws-cdk-sagemaker-endpoint/ModelWithoutArtifact/Role", + "children": { + "ImportRole": { + "id": "ImportRole", + "path": "aws-cdk-sagemaker-endpoint/ModelWithoutArtifact/Role/ImportRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint/ModelWithoutArtifact/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-endpoint/ModelWithoutArtifact/Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint/ModelWithoutArtifact/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": "*" + } + ], + "Version": "2012-10-17" + }, + "policyName": "ModelWithoutArtifactRoleDefaultPolicyCFDF486F", + "roles": [ + { + "Ref": "ModelWithoutArtifactRoleB9EF84FD" + } + ] + } + }, + "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-endpoint/ModelWithoutArtifact/Model", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SageMaker::Model", + "aws:cdk:cloudformation:props": { + "executionRoleArn": { + "Fn::GetAtt": [ + "ModelWithoutArtifactRoleB9EF84FD", + "Arn" + ] + }, + "primaryContainer": { + "image": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + } + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker.CfnModel", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker.Model", + "version": "0.0.0" + } + }, + "EndpointConfig": { + "id": "EndpointConfig", + "path": "aws-cdk-sagemaker-endpoint/EndpointConfig", + "children": { + "EndpointConfig": { + "id": "EndpointConfig", + "path": "aws-cdk-sagemaker-endpoint/EndpointConfig/EndpointConfig", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SageMaker::EndpointConfig", + "aws:cdk:cloudformation:props": { + "productionVariants": [ + { + "initialInstanceCount": 1, + "initialVariantWeight": 1, + "instanceType": "ml.m5.large", + "modelName": { + "Fn::GetAtt": [ + "ModelWithArtifactModel7EBC6421", + "ModelName" + ] + }, + "variantName": "firstVariant" + }, + { + "initialInstanceCount": 1, + "initialVariantWeight": 1, + "instanceType": "ml.t2.medium", + "modelName": { + "Fn::GetAtt": [ + "ModelWithArtifactModel7EBC6421", + "ModelName" + ] + }, + "variantName": "secondVariant" + }, + { + "initialInstanceCount": 1, + "initialVariantWeight": 2, + "instanceType": "ml.t2.medium", + "modelName": { + "Fn::GetAtt": [ + "ModelWithoutArtifactModel9AB56FA5", + "ModelName" + ] + }, + "variantName": "thirdVariant" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker.CfnEndpointConfig", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker.EndpointConfig", + "version": "0.0.0" + } + }, + "Endpoint": { + "id": "Endpoint", + "path": "aws-cdk-sagemaker-endpoint/Endpoint", + "children": { + "Endpoint": { + "id": "Endpoint", + "path": "aws-cdk-sagemaker-endpoint/Endpoint/Endpoint", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SageMaker::Endpoint", + "aws:cdk:cloudformation:props": { + "endpointConfigName": { + "Fn::GetAtt": [ + "EndpointConfigFD7B6F91", + "EndpointConfigName" + ] + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker.CfnEndpoint", + "version": "0.0.0" + } + }, + "ScalingRole": { + "id": "ScalingRole", + "path": "aws-cdk-sagemaker-endpoint/Endpoint/ScalingRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "InstanceCount": { + "id": "InstanceCount", + "path": "aws-cdk-sagemaker-endpoint/Endpoint/InstanceCount", + "children": { + "Target": { + "id": "Target", + "path": "aws-cdk-sagemaker-endpoint/Endpoint/InstanceCount/Target", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint/Endpoint/InstanceCount/Target/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApplicationAutoScaling::ScalableTarget", + "aws:cdk:cloudformation:props": { + "maxCapacity": 3, + "minCapacity": 1, + "resourceId": { + "Fn::Join": [ + "", + [ + "endpoint/", + { + "Fn::GetAtt": [ + "Endpoint8024A810", + "EndpointName" + ] + }, + "/variant/firstVariant" + ] + ] + }, + "roleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/aws-service-role/sagemaker.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_SageMakerEndpoint" + ] + ] + }, + "scalableDimension": "sagemaker:variant:DesiredInstanceCount", + "serviceNamespace": "sagemaker" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-applicationautoscaling.CfnScalableTarget", + "version": "0.0.0" + } + }, + "LimitRPS": { + "id": "LimitRPS", + "path": "aws-cdk-sagemaker-endpoint/Endpoint/InstanceCount/Target/LimitRPS", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint/Endpoint/InstanceCount/Target/LimitRPS/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApplicationAutoScaling::ScalingPolicy", + "aws:cdk:cloudformation:props": { + "policyName": "awscdksagemakerendpointEndpointInstanceCountTargetLimitRPSCC857664", + "policyType": "TargetTrackingScaling", + "scalingTargetId": { + "Ref": "EndpointInstanceCountTargetDA8C8EDB" + }, + "targetTrackingScalingPolicyConfiguration": { + "predefinedMetricSpecification": { + "predefinedMetricType": "SageMakerVariantInvocationsPerInstance" + }, + "targetValue": 900 + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-applicationautoscaling.CfnScalingPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-applicationautoscaling.TargetTrackingScalingPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-applicationautoscaling.ScalableTarget", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker.ScalableInstanceCount", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker.Endpoint", + "version": "0.0.0" + } + }, + "Invoker": { + "id": "Invoker", + "path": "aws-cdk-sagemaker-endpoint/Invoker", + "children": { + "ImportInvoker": { + "id": "ImportInvoker", + "path": "aws-cdk-sagemaker-endpoint/Invoker/ImportInvoker", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint/Invoker/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-cdk-sagemaker-endpoint/Invoker/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-endpoint/Invoker/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": "sagemaker:InvokeEndpoint", + "Effect": "Allow", + "Resource": { + "Ref": "Endpoint8024A810" + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "InvokerDefaultPolicy3FF8208D", + "roles": [ + { + "Ref": "Invoker060A9026" + } + ] + } + }, + "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" + } + }, + "Exports": { + "id": "Exports", + "path": "aws-cdk-sagemaker-endpoint/Exports", + "children": { + "Output{\"Fn::GetAtt\":[\"Endpoint8024A810\",\"EndpointName\"]}": { + "id": "Output{\"Fn::GetAtt\":[\"Endpoint8024A810\",\"EndpointName\"]}", + "path": "aws-cdk-sagemaker-endpoint/Exports/Output{\"Fn::GetAtt\":[\"Endpoint8024A810\",\"EndpointName\"]}", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.161" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-cdk-sagemaker-endpoint/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-cdk-sagemaker-endpoint/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "integtest-endpoint": { + "id": "integtest-endpoint", + "path": "integtest-endpoint", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "integtest-endpoint/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "integtest-endpoint/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.161" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "integtest-endpoint/DefaultTest/DeployAssert", + "children": { + "AwsApiCallSageMakerRuntimeinvokeEndpoint": { + "id": "AwsApiCallSageMakerRuntimeinvokeEndpoint", + "path": "integtest-endpoint/DefaultTest/DeployAssert/AwsApiCallSageMakerRuntimeinvokeEndpoint", + "children": { + "SdkProvider": { + "id": "SdkProvider", + "path": "integtest-endpoint/DefaultTest/DeployAssert/AwsApiCallSageMakerRuntimeinvokeEndpoint/SdkProvider", + "children": { + "AssertionsProvider": { + "id": "AssertionsProvider", + "path": "integtest-endpoint/DefaultTest/DeployAssert/AwsApiCallSageMakerRuntimeinvokeEndpoint/SdkProvider/AssertionsProvider", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.161" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AssertionsProvider", + "version": "0.0.0" + } + }, + "Default": { + "id": "Default", + "path": "integtest-endpoint/DefaultTest/DeployAssert/AwsApiCallSageMakerRuntimeinvokeEndpoint/Default", + "children": { + "Default": { + "id": "Default", + "path": "integtest-endpoint/DefaultTest/DeployAssert/AwsApiCallSageMakerRuntimeinvokeEndpoint/Default/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + }, + "AssertionResults": { + "id": "AssertionResults", + "path": "integtest-endpoint/DefaultTest/DeployAssert/AwsApiCallSageMakerRuntimeinvokeEndpoint/AssertionResults", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AwsApiCall", + "version": "0.0.0" + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81": { + "id": "SingletonFunction1488541a7b23466481b69b4408076b81", + "path": "integtest-endpoint/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81", + "children": { + "Staging": { + "id": "Staging", + "path": "integtest-endpoint/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "integtest-endpoint/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Role", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "integtest-endpoint/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Handler", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.161" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integtest-endpoint/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integtest-endpoint/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.161" + } + } + }, + "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.endpoint.ts b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.ts new file mode 100644 index 0000000000000..e605096385bcc --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/test/integ.endpoint.ts @@ -0,0 +1,98 @@ +import * as path from 'path'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { ExpectedResult, IntegTest } from '@aws-cdk/integ-tests'; +import * as sagemaker from '../lib'; + +/* + * Stack verification steps: + * aws sagemaker-runtime invoke-endpoint --endpoint-name --body "any string" --cli-binary-format raw-in-base64-out /tmp/inference.txt && cat /tmp/inference.txt + * + * The above command will result in one of the following outputs (based on relative variant weight). + * + * Roughly 25% of the time: + * + * { + * "ContentType": "text/plain", + * "InvokedProductionVariant": "firstVariant" + * } + * Artifact info: ['This file exists for test purposes only and is not a real SageMaker Model artifact'] + * + * Roughly 25% of the time: + * + * { + * "ContentType": "text/plain", + * "InvokedProductionVariant": "secondVariant" + * } + * Artifact info: ['This file exists for test purposes only and is not a real SageMaker Model artifact'] + * + * Roughly 50% of the time: + * + * { + * "ContentType": "text/plain", + * "InvokedProductionVariant": "thirdVariant" + * } + * Artifact info: No artifacts are present + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-sagemaker-endpoint'); + +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 modelWithArtifact = new sagemaker.Model(stack, 'ModelWithArtifact', { + containers: [{ image, modelData }], +}); +const modelWithoutArtifact = new sagemaker.Model(stack, 'ModelWithoutArtifact', { + containers: [{ image }], +}); + +const endpointConfig = new sagemaker.EndpointConfig(stack, 'EndpointConfig', { + instanceProductionVariants: [ + { + model: modelWithArtifact, + variantName: 'firstVariant', + instanceType: sagemaker.InstanceType.M5_LARGE, + }, + { + model: modelWithArtifact, + variantName: 'secondVariant', + }, + ], +}); +endpointConfig.addInstanceProductionVariant({ + model: modelWithoutArtifact, + variantName: 'thirdVariant', + initialVariantWeight: 2.0, +}); +const endpoint = new sagemaker.Endpoint(stack, 'Endpoint', { endpointConfig }); + +const invokerRole = new iam.Role(stack, 'Invoker', { + assumedBy: new iam.AccountRootPrincipal(), +}); +endpoint.grantInvoke(invokerRole); + +const productionVariant = endpoint.findInstanceProductionVariant('firstVariant'); +const instanceCount = productionVariant.autoScaleInstanceCount({ + maxCapacity: 3, +}); +instanceCount.scaleOnInvocations('LimitRPS', { + maxRequestsPerSecond: 30, +}); + +const integ = new IntegTest(app, 'integtest-endpoint', { + testCases: [stack], +}); + +const invoke = integ.assertions.awsApiCall('SageMakerRuntime', 'invokeEndpoint', { + EndpointName: endpoint.endpointName, + TargetVariant: 'firstVariant', + Body: 'any string', +}); +invoke.provider.addToRolePolicy({ + Effect: 'Allow', + Action: 'sagemaker:InvokeEndpoint', + Resource: '*', +}); +invoke.assertAtPath('Body', ExpectedResult.stringLikeRegexp('Artifact info:.*This file exists for test purposes only and is not a real SageMaker Model artifact'));