diff --git a/packages/@aws-cdk/aws-sagemaker-alpha/README.md b/packages/@aws-cdk/aws-sagemaker-alpha/README.md index cdddec2ec8103..f4798faf19036 100644 --- a/packages/@aws-cdk/aws-sagemaker-alpha/README.md +++ b/packages/@aws-cdk/aws-sagemaker-alpha/README.md @@ -78,6 +78,31 @@ const model = new sagemaker.Model(this, 'InferencePipelineModel', { }); ``` +### Model Properties + +#### Network Isolation + +If you enable [network isolation](https://docs.aws.amazon.com/sagemaker/latest/dg/mkt-algo-model-internet-free.html), the containers can't make any outbound network calls, even to other AWS services such as Amazon S3. Additionally, no AWS credentials are made available to the container runtime environment. + +To enable network isolation, set the `networkIsolation` property to `true`: + +```typescript +import * as sagemaker from '@aws-cdk/aws-sagemaker-alpha'; + +declare const image: sagemaker.ContainerImage; +declare const modelData: sagemaker.ModelData; + +const model = new sagemaker.Model(this, 'ContainerModel', { + containers: [ + { + image, + modelData, + } + ], + networkIsolation: true, +}); +``` + ### Container Images Inference code can be stored in the Amazon EC2 Container Registry (Amazon ECR), which is specified diff --git a/packages/@aws-cdk/aws-sagemaker-alpha/lib/model.ts b/packages/@aws-cdk/aws-sagemaker-alpha/lib/model.ts index 589caed2e9556..28f88d4934b1d 100644 --- a/packages/@aws-cdk/aws-sagemaker-alpha/lib/model.ts +++ b/packages/@aws-cdk/aws-sagemaker-alpha/lib/model.ts @@ -206,6 +206,17 @@ export interface ModelProps { * @default true */ readonly allowAllOutbound?: boolean; + + /** + * Whether to enable network isolation for the model container. + * + * When enabled, no inbound or outbound network calls can be made to or from the model container. + * + * @see https://docs.aws.amazon.com/sagemaker/latest/dg/mkt-algo-model-internet-free.html + * + * @default false + */ + readonly networkIsolation?: boolean; } /** @@ -312,6 +323,7 @@ export class Model extends ModelBase { primaryContainer: cdk.Lazy.any({ produce: () => this.renderPrimaryContainer() }), vpcConfig: cdk.Lazy.any({ produce: () => this.renderVpcConfig() }), containers: cdk.Lazy.any({ produce: () => this.renderContainers() }), + enableNetworkIsolation: props.networkIsolation, }); this.modelName = this.getResourceNameAttribute(model.attrModelName); this.modelArn = this.getResourceArnAttribute(model.ref, { diff --git a/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/aws-cdk-sagemaker-model.assets.json b/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/aws-cdk-sagemaker-model.assets.json index d350b0f8b2887..b325edd0bba0b 100644 --- a/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/aws-cdk-sagemaker-model.assets.json +++ b/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/aws-cdk-sagemaker-model.assets.json @@ -14,7 +14,7 @@ } } }, - "3091b68a4482354b22fb86be9dfa04e7f07fd597c15825ad5b16b32f8fdaaf6a": { + "2bf7be4479c7f2590f4e7436df08e6f6516adf0e7234bfcfd7ea7115c7e6dfb6": { "source": { "path": "aws-cdk-sagemaker-model.template.json", "packaging": "file" @@ -22,7 +22,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "3091b68a4482354b22fb86be9dfa04e7f07fd597c15825ad5b16b32f8fdaaf6a.json", + "objectKey": "2bf7be4479c7f2590f4e7436df08e6f6516adf0e7234bfcfd7ea7115c7e6dfb6.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/aws-cdk-sagemaker-model.template.json b/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/aws-cdk-sagemaker-model.template.json index 51f06e115d3f8..1a783d4262dce 100644 --- a/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/aws-cdk-sagemaker-model.template.json +++ b/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/aws-cdk-sagemaker-model.template.json @@ -898,6 +898,169 @@ "HuggingFaceModelRoleDefaultPolicy50587D35", "HuggingFaceModelRoleDA17DA00" ] + }, + "NetworkIsolationModelRole562D6C7F": { + "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" + ] + ] + } + ] + } + }, + "NetworkIsolationModelRoleDefaultPolicy84ACFE88": { + "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": "NetworkIsolationModelRoleDefaultPolicy84ACFE88", + "Roles": [ + { + "Ref": "NetworkIsolationModelRole562D6C7F" + } + ] + } + }, + "NetworkIsolationModel29FE9107": { + "Type": "AWS::SageMaker::Model", + "Properties": { + "Containers": [ + { + "Image": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + } + }, + { + "Image": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + }, + "ModelDataUrl": { + "Fn::Sub": "https://s3.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.gz" + } + }, + { + "Image": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + }, + "ModelDataUrl": { + "Fn::Sub": "https://s3.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.gz" + } + } + ], + "EnableNetworkIsolation": true, + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "NetworkIsolationModelRole562D6C7F", + "Arn" + ] + } + }, + "DependsOn": [ + "NetworkIsolationModelRoleDefaultPolicy84ACFE88", + "NetworkIsolationModelRole562D6C7F" + ] } }, "Mappings": { diff --git a/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/manifest.json b/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/manifest.json index 0991dc132bb8b..9411f22bfb458 100644 --- a/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/manifest.json @@ -18,7 +18,7 @@ "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}/3091b68a4482354b22fb86be9dfa04e7f07fd597c15825ad5b16b32f8fdaaf6a.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/2bf7be4479c7f2590f4e7436df08e6f6516adf0e7234bfcfd7ea7115c7e6dfb6.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -238,6 +238,24 @@ "data": "DlcRepositoryAccountMap" } ], + "/aws-cdk-sagemaker-model/NetworkIsolationModel/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "NetworkIsolationModelRole562D6C7F" + } + ], + "/aws-cdk-sagemaker-model/NetworkIsolationModel/Role/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "NetworkIsolationModelRoleDefaultPolicy84ACFE88" + } + ], + "/aws-cdk-sagemaker-model/NetworkIsolationModel/Model": [ + { + "type": "aws:cdk:logicalId", + "data": "NetworkIsolationModel29FE9107" + } + ], "/aws-cdk-sagemaker-model/BootstrapVersion": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/tree.json b/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/tree.json index 660e3efff302b..60a1416e86bb8 100644 --- a/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.js.snapshot/tree.json @@ -1169,13 +1169,13 @@ "version": "0.0.0" } }, - "ModelImage4351027d8888cb0133eeba7ae4ab91c5": { - "id": "ModelImage4351027d8888cb0133eeba7ae4ab91c5", - "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelImage4351027d8888cb0133eeba7ae4ab91c5", + "ModelImage1df1fb2d6963bb25c0be68fe5b4e5a62": { + "id": "ModelImage1df1fb2d6963bb25c0be68fe5b4e5a62", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelImage1df1fb2d6963bb25c0be68fe5b4e5a62", "children": { "Staging": { "id": "Staging", - "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelImage4351027d8888cb0133eeba7ae4ab91c5/Staging", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelImage1df1fb2d6963bb25c0be68fe5b4e5a62/Staging", "constructInfo": { "fqn": "aws-cdk-lib.AssetStaging", "version": "0.0.0" @@ -1183,7 +1183,7 @@ }, "Repository": { "id": "Repository", - "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelImage4351027d8888cb0133eeba7ae4ab91c5/Repository", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelImage1df1fb2d6963bb25c0be68fe5b4e5a62/Repository", "constructInfo": { "fqn": "aws-cdk-lib.aws_ecr.RepositoryBase", "version": "0.0.0" @@ -1195,13 +1195,13 @@ "version": "0.0.0" } }, - "ModelDatab93b3e254f66541093e95be708719bbd": { - "id": "ModelDatab93b3e254f66541093e95be708719bbd", - "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelDatab93b3e254f66541093e95be708719bbd", + "ModelDataac735c034334b02fb2f240145313a846": { + "id": "ModelDataac735c034334b02fb2f240145313a846", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelDataac735c034334b02fb2f240145313a846", "children": { "Stage": { "id": "Stage", - "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelDatab93b3e254f66541093e95be708719bbd/Stage", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelDataac735c034334b02fb2f240145313a846/Stage", "constructInfo": { "fqn": "aws-cdk-lib.AssetStaging", "version": "0.0.0" @@ -1209,7 +1209,7 @@ }, "AssetBucket": { "id": "AssetBucket", - "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelDatab93b3e254f66541093e95be708719bbd/AssetBucket", + "path": "aws-cdk-sagemaker-model/InferencePipelineModel/ModelDataac735c034334b02fb2f240145313a846/AssetBucket", "constructInfo": { "fqn": "aws-cdk-lib.aws_s3.BucketBase", "version": "0.0.0" @@ -1471,6 +1471,227 @@ "version": "0.0.0" } }, + "NetworkIsolationModel": { + "id": "NetworkIsolationModel", + "path": "aws-cdk-sagemaker-model/NetworkIsolationModel", + "children": { + "Role": { + "id": "Role", + "path": "aws-cdk-sagemaker-model/NetworkIsolationModel/Role", + "children": { + "ImportRole": { + "id": "ImportRole", + "path": "aws-cdk-sagemaker-model/NetworkIsolationModel/Role/ImportRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-model/NetworkIsolationModel/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-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-cdk-sagemaker-model/NetworkIsolationModel/Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sagemaker-model/NetworkIsolationModel/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": "NetworkIsolationModelRoleDefaultPolicy84ACFE88", + "roles": [ + { + "Ref": "NetworkIsolationModelRole562D6C7F" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "Model": { + "id": "Model", + "path": "aws-cdk-sagemaker-model/NetworkIsolationModel/Model", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SageMaker::Model", + "aws:cdk:cloudformation:props": { + "containers": [ + { + "image": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + } + }, + { + "image": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + }, + "modelDataUrl": { + "Fn::Sub": "https://s3.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.gz" + } + }, + { + "image": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:442a71de95281cb26bd41da567c79060206108b97bdde93cb4ce5f213f50013a" + }, + "modelDataUrl": { + "Fn::Sub": "https://s3.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/126d48fa0e32fbef5078b9d88658b35ad29d4291eb86675a64c75fa4f1338916.gz" + } + } + ], + "enableNetworkIsolation": true, + "executionRoleArn": { + "Fn::GetAtt": [ + "NetworkIsolationModelRole562D6C7F", + "Arn" + ] + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_sagemaker.CfnModel", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sagemaker-alpha.Model", + "version": "0.0.0" + } + }, "BootstrapVersion": { "id": "BootstrapVersion", "path": "aws-cdk-sagemaker-model/BootstrapVersion", diff --git a/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.ts b/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.ts index 4de64b4b6dafe..e52785a2b35a0 100644 --- a/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.ts +++ b/packages/@aws-cdk/aws-sagemaker-alpha/test/integ.model.ts @@ -131,6 +131,15 @@ new sagemaker.Model(stack, 'HuggingFaceModel', { ], }); +new sagemaker.Model(stack, 'NetworkIsolationModel', { + containers: [ + { image: localImage }, + { image: localImage, modelData: localModelData }, + { image: localImage, modelData: localModelData }, + ], + networkIsolation: true, +}); + new IntegTest(app, 'integtest-model', { testCases: [stack], }); diff --git a/packages/@aws-cdk/aws-sagemaker-alpha/test/model.test.ts b/packages/@aws-cdk/aws-sagemaker-alpha/test/model.test.ts index f4e64ec6131fc..d08ac389ed5e4 100644 --- a/packages/@aws-cdk/aws-sagemaker-alpha/test/model.test.ts +++ b/packages/@aws-cdk/aws-sagemaker-alpha/test/model.test.ts @@ -98,6 +98,22 @@ describe('When instantiating SageMaker Model', () => { expect(Object.entries(manifest.dockerImages)).toHaveLength(1); }); + test('set network isolation', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new sagemaker.Model(stack, 'Model', { + containers: [{ image: sagemaker.ContainerImage.fromEcrRepository(new ecr.Repository(stack, 'Repo')) }], + networkIsolation: true, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::SageMaker::Model', { + EnableNetworkIsolation: true, + }); + }); + describe('with a VPC', () => { test('and security groups, no security group is created', () => { // GIVEN